This commit is contained in:
mtvare6
2025-09-01 16:21:28 +05:30
488 changed files with 27610 additions and 32159 deletions

View File

@@ -11,7 +11,7 @@ The graph that is presented to users in the editor is known as the document grap
```rs
pub struct DocumentNode {
pub inputs: Vec<NodeInput>,
pub manual_composition: Option<Type>,
pub call_argument: Type,
pub implementation: DocumentNodeImplementation,
pub skip_deduplication: bool,
pub visible: bool,
@@ -157,7 +157,7 @@ raster_node!(graphene_core::raster::OpacityNode<_>, params: [f64]),
There is also the more general `register_node!` for nodes that do not need to run per pixel.
```rs
register_node!(graphene_core::transform_nodes::SetTransformNode<_>, input: VectorData, params: [DAffine2]),
register_node!(graphene_core::transform_nodes::SetTransformNode<_>, input: Vector, params: [DAffine2]),
```
## Debugging

View File

@@ -42,7 +42,7 @@ pub trait Size {
fn size(&self) -> UVec2;
}
#[cfg(target_arch = "wasm32")]
#[cfg(target_family = "wasm")]
impl Size for web_sys::HtmlCanvasElement {
fn size(&self) -> UVec2 {
UVec2::new(self.width(), self.height())
@@ -52,11 +52,20 @@ impl Size for web_sys::HtmlCanvasElement {
#[derive(Debug, Clone)]
pub struct ImageTexture {
#[cfg(feature = "wgpu")]
pub texture: Arc<wgpu::Texture>,
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) {
@@ -106,9 +115,9 @@ pub struct SurfaceHandle<Surface> {
pub surface: Surface,
}
// #[cfg(target_arch = "wasm32")]
// #[cfg(target_family = "wasm")]
// unsafe impl<T: dyn_any::WasmNotSend> Send for SurfaceHandle<T> {}
// #[cfg(target_arch = "wasm32")]
// #[cfg(target_family = "wasm")]
// unsafe impl<T: dyn_any::WasmNotSync> Sync for SurfaceHandle<T> {}
impl<S: Size> Size for SurfaceHandle<S> {
@@ -144,9 +153,9 @@ impl<'a, Surface> Drop for SurfaceHandle<'a, Surface> {
}
}*/
#[cfg(target_arch = "wasm32")]
#[cfg(target_family = "wasm")]
pub type ResourceFuture = Pin<Box<dyn Future<Output = Result<Arc<[u8]>, ApplicationError>>>>;
#[cfg(not(target_arch = "wasm32"))]
#[cfg(not(target_family = "wasm"))]
pub type ResourceFuture = Pin<Box<dyn Future<Output = Result<Arc<[u8]>, ApplicationError>> + Send>>;
pub trait ApplicationIo {
@@ -240,7 +249,7 @@ struct Logger;
impl NodeGraphUpdateSender for Logger {
fn send(&self, message: NodeGraphUpdateMessage) {
log::warn!("dispatching message with fallback node graph update sender {:?}", message);
log::warn!("dispatching message with fallback node graph update sender {message:?}");
}
}

View File

@@ -2,19 +2,19 @@ use crate::brush_cache::BrushCache;
use crate::brush_stroke::{BrushStroke, BrushStyle};
use glam::{DAffine2, DVec2};
use graphene_core::blending::BlendMode;
use graphene_core::bounds::BoundingBox;
use graphene_core::bounds::{BoundingBox, RenderBoundingBox};
use graphene_core::color::{Alpha, Color, Pixel, Sample};
use graphene_core::generic::FnNode;
use graphene_core::instances::Instance;
use graphene_core::math::bbox::{AxisAlignedBbox, Bbox};
use graphene_core::raster::BitmapMut;
use graphene_core::raster::image::Image;
use graphene_core::raster_types::{CPU, Raster, RasterDataTable};
use graphene_core::raster_types::{CPU, Raster};
use graphene_core::registry::FutureWrapperNode;
use graphene_core::table::{Table, TableRow};
use graphene_core::transform::Transform;
use graphene_core::value::ClonedNode;
use graphene_core::{Ctx, Node};
use graphene_raster_nodes::adjustments::blend_colors;
use graphene_raster_nodes::blending_nodes::blend_colors;
use graphene_raster_nodes::std_nodes::{empty_image, extend_image_to_bounds};
#[derive(Clone, Copy, Debug, PartialEq)]
@@ -77,7 +77,7 @@ fn brush_stamp_generator(#[unit(" px")] diameter: f64, color: Color, hardness: f
}
#[node_macro::node(skip_impl)]
fn blit<BlendFn>(mut target: RasterDataTable<CPU>, texture: Raster<CPU>, positions: Vec<DVec2>, blend_mode: BlendFn) -> RasterDataTable<CPU>
fn blit<BlendFn>(mut target: Table<Raster<CPU>>, texture: Raster<CPU>, positions: Vec<DVec2>, blend_mode: BlendFn) -> Table<Raster<CPU>>
where
BlendFn: for<'any_input> Node<'any_input, (Color, Color), Output = Color>,
{
@@ -85,14 +85,14 @@ where
return target;
}
for target_instance in target.instance_mut_iter() {
let target_width = target_instance.instance.width;
let target_height = target_instance.instance.height;
for table_row in target.iter_mut() {
let target_width = table_row.element.width;
let target_height = table_row.element.height;
let target_size = DVec2::new(target_width as f64, target_height as f64);
let texture_size = DVec2::new(texture.width as f64, texture.height as f64);
let document_to_target = DAffine2::from_translation(-texture_size / 2.) * DAffine2::from_scale(target_size) * target_instance.transform.inverse();
let document_to_target = DAffine2::from_translation(-texture_size / 2.) * DAffine2::from_scale(target_size) * table_row.transform.inverse();
for position in &positions {
let start = document_to_target.transform_point2(*position).round();
@@ -112,12 +112,12 @@ where
let max_y = (blit_area_offset.y + blit_area_dimensions.y).saturating_sub(1);
let max_x = (blit_area_offset.x + blit_area_dimensions.x).saturating_sub(1);
assert!(texture_index(max_x, max_y) < texture.data.len());
assert!(target_index(max_x, max_y) < target_instance.instance.data.len());
assert!(target_index(max_x, max_y) < table_row.element.data.len());
for y in blit_area_offset.y..blit_area_offset.y + blit_area_dimensions.y {
for x in blit_area_offset.x..blit_area_offset.x + blit_area_dimensions.x {
let src_pixel = texture.data[texture_index(x, y)];
let dst_pixel = &mut target_instance.instance.data_mut().data[target_index(x + clamp_start.x, y + clamp_start.y)];
let dst_pixel = &mut table_row.element.data_mut().data[target_index(x + clamp_start.x, y + clamp_start.y)];
*dst_pixel = blend_mode.eval((src_pixel, *dst_pixel));
}
}
@@ -130,14 +130,14 @@ where
pub async fn create_brush_texture(brush_style: &BrushStyle) -> Raster<CPU> {
let stamp = brush_stamp_generator(brush_style.diameter, brush_style.color, brush_style.hardness, brush_style.flow);
let transform = DAffine2::from_scale_angle_translation(DVec2::splat(brush_style.diameter), 0., -DVec2::splat(brush_style.diameter / 2.));
let blank_texture = empty_image((), transform, Color::TRANSPARENT).instance_iter().next().unwrap_or_default();
let blank_texture = empty_image((), transform, Table::new_from_element(Color::TRANSPARENT)).into_iter().next().unwrap_or_default();
let image = blend_stamp_closure(stamp, blank_texture, |a, b| blend_colors(a, b, BlendMode::Normal, 1.));
image.instance
image.element
}
pub fn blend_with_mode(background: Instance<Raster<CPU>>, foreground: Instance<Raster<CPU>>, blend_mode: BlendMode, opacity: f64) -> Instance<Raster<CPU>> {
let opacity = opacity / 100.;
pub fn blend_with_mode(background: TableRow<Raster<CPU>>, foreground: TableRow<Raster<CPU>>, blend_mode: BlendMode, opacity: f64) -> TableRow<Raster<CPU>> {
let opacity = opacity as f32 / 100.;
match std::hint::black_box(blend_mode) {
// Normal group
BlendMode::Normal => blend_image_closure(foreground, background, |a, b| blend_colors(a, b, BlendMode::Normal, opacity)),
@@ -179,14 +179,15 @@ pub fn blend_with_mode(background: Instance<Raster<CPU>>, foreground: Instance<R
}
#[node_macro::node(category("Raster"))]
async fn brush(_: impl Ctx, mut image_frame_table: RasterDataTable<CPU>, strokes: Vec<BrushStroke>, cache: BrushCache) -> RasterDataTable<CPU> {
async fn brush(_: impl Ctx, mut image_frame_table: Table<Raster<CPU>>, strokes: Vec<BrushStroke>, cache: BrushCache) -> Table<Raster<CPU>> {
if image_frame_table.is_empty() {
image_frame_table.push(Instance::default());
image_frame_table.push(TableRow::default());
}
// TODO: Find a way to handle more than one instance
let image_frame_instance = image_frame_table.instance_ref_iter().next().expect("Expected the one instance we just pushed").to_instance_cloned();
// TODO: Find a way to handle more than one row
let table_row = image_frame_table.iter().next().expect("Expected the one row we just pushed").into_cloned();
let [start, end] = image_frame_instance.clone().to_table().bounding_box(DAffine2::IDENTITY, false).unwrap_or([DVec2::ZERO, DVec2::ZERO]);
let bounds = Table::new_from_row(table_row.clone()).bounding_box(DAffine2::IDENTITY, false);
let [start, end] = if let RenderBoundingBox::Rectangle(rect) = bounds { rect } else { [DVec2::ZERO, DVec2::ZERO] };
let image_bbox = AxisAlignedBbox { start, end };
let stroke_bbox = strokes.iter().map(|s| s.bounding_box()).reduce(|a, b| a.union(&b)).unwrap_or(AxisAlignedBbox::ZERO);
let bbox = if image_bbox.size().length() < 0.1 { stroke_bbox } else { stroke_bbox.union(&image_bbox) };
@@ -195,11 +196,11 @@ async fn brush(_: impl Ctx, mut image_frame_table: RasterDataTable<CPU>, strokes
let mut draw_strokes: Vec<_> = strokes.iter().filter(|&s| !matches!(s.style.blend_mode, BlendMode::Erase | BlendMode::Restore)).cloned().collect();
let erase_restore_strokes: Vec<_> = strokes.iter().filter(|&s| matches!(s.style.blend_mode, BlendMode::Erase | BlendMode::Restore)).cloned().collect();
let mut brush_plan = cache.compute_brush_plan(image_frame_instance, &draw_strokes);
let mut brush_plan = cache.compute_brush_plan(table_row, &draw_strokes);
// TODO: Find a way to handle more than one instance
let Some(mut actual_image) = extend_image_to_bounds((), brush_plan.background.to_table(), background_bounds).instance_iter().next() else {
return RasterDataTable::default();
// TODO: Find a way to handle more than one row
let Some(mut actual_image) = extend_image_to_bounds((), Table::new_from_row(brush_plan.background), background_bounds).into_iter().next() else {
return Table::new();
};
let final_stroke_idx = brush_plan.strokes.len().saturating_sub(1);
@@ -229,7 +230,6 @@ async fn brush(_: impl Ctx, mut image_frame_table: RasterDataTable<CPU>, strokes
let stroke_origin_in_layer = bbox.start - snap_offset - DVec2::splat(stroke.style.diameter / 2.);
let stroke_to_layer = DAffine2::from_translation(stroke_origin_in_layer) * DAffine2::from_scale(stroke_size);
// let normal_blend = BlendColorPairNode::new(ValueNode::new(CopiedNode::new(BlendMode::Normal)), ValueNode::new(CopiedNode::new(100.)));
let normal_blend = FnNode::new(|(a, b)| blend_colors(a, b, BlendMode::Normal, 1.));
let blit_node = BlitNode::new(
FutureWrapperNode::new(ClonedNode::new(brush_texture)),
@@ -238,15 +238,15 @@ async fn brush(_: impl Ctx, mut image_frame_table: RasterDataTable<CPU>, strokes
);
let blit_target = if idx == 0 {
let target = core::mem::take(&mut brush_plan.first_stroke_texture);
extend_image_to_bounds((), target.to_table(), stroke_to_layer)
extend_image_to_bounds((), Table::new_from_row(target), stroke_to_layer)
} else {
empty_image((), stroke_to_layer, Color::TRANSPARENT)
empty_image((), stroke_to_layer, Table::new_from_element(Color::TRANSPARENT))
// EmptyImageNode::new(CopiedNode::new(stroke_to_layer), CopiedNode::new(Color::TRANSPARENT)).eval(())
};
let instances = blit_node.eval(blit_target).await;
assert_eq!(instances.len(), 1);
instances.instance_iter().next().unwrap_or_default()
let table = blit_node.eval(blit_target).await;
assert_eq!(table.len(), 1);
table.into_iter().next().unwrap_or_default()
};
// Cache image before doing final blend, and store final stroke texture.
@@ -261,8 +261,8 @@ async fn brush(_: impl Ctx, mut image_frame_table: RasterDataTable<CPU>, strokes
let has_erase_strokes = strokes.iter().any(|s| s.style.blend_mode == BlendMode::Erase);
if has_erase_strokes {
let opaque_image = Image::new(bbox.size().x as u32, bbox.size().y as u32, Color::WHITE);
let mut erase_restore_mask = Instance {
instance: Raster::new_cpu(opaque_image),
let mut erase_restore_mask = TableRow {
element: Raster::new_cpu(opaque_image),
transform: background_bounds,
..Default::default()
};
@@ -285,7 +285,7 @@ async fn brush(_: impl Ctx, mut image_frame_table: RasterDataTable<CPU>, strokes
FutureWrapperNode::new(ClonedNode::new(positions)),
FutureWrapperNode::new(ClonedNode::new(blend_params)),
);
erase_restore_mask = blit_node.eval(erase_restore_mask.to_table()).await.instance_iter().next().unwrap_or_default();
erase_restore_mask = blit_node.eval(Table::new_from_row(erase_restore_mask)).await.into_iter().next().unwrap_or_default();
}
// Yes, this is essentially the same as the above, but we duplicate to inline the blend mode.
BlendMode::Restore => {
@@ -295,7 +295,7 @@ async fn brush(_: impl Ctx, mut image_frame_table: RasterDataTable<CPU>, strokes
FutureWrapperNode::new(ClonedNode::new(positions)),
FutureWrapperNode::new(ClonedNode::new(blend_params)),
);
erase_restore_mask = blit_node.eval(erase_restore_mask.to_table()).await.instance_iter().next().unwrap_or_default();
erase_restore_mask = blit_node.eval(Table::new_from_row(erase_restore_mask)).await.into_iter().next().unwrap_or_default();
}
_ => unreachable!(),
}
@@ -305,8 +305,8 @@ async fn brush(_: impl Ctx, mut image_frame_table: RasterDataTable<CPU>, strokes
actual_image = blend_image_closure(erase_restore_mask, actual_image, |a, b| blend_params.eval((a, b)));
}
let first_row = image_frame_table.instance_mut_iter().next().unwrap();
*first_row.instance = actual_image.instance;
let first_row = image_frame_table.iter_mut().next().unwrap();
*first_row.element = actual_image.element;
*first_row.transform = actual_image.transform;
*first_row.alpha_blending = actual_image.alpha_blending;
*first_row.source_node_id = actual_image.source_node_id;
@@ -314,9 +314,9 @@ async fn brush(_: impl Ctx, mut image_frame_table: RasterDataTable<CPU>, strokes
image_frame_table
}
pub fn blend_image_closure(foreground: Instance<Raster<CPU>>, mut background: Instance<Raster<CPU>>, map_fn: impl Fn(Color, Color) -> Color) -> Instance<Raster<CPU>> {
let foreground_size = DVec2::new(foreground.instance.width as f64, foreground.instance.height as f64);
let background_size = DVec2::new(background.instance.width as f64, background.instance.height as f64);
pub fn blend_image_closure(foreground: TableRow<Raster<CPU>>, mut background: TableRow<Raster<CPU>>, map_fn: impl Fn(Color, Color) -> Color) -> TableRow<Raster<CPU>> {
let foreground_size = DVec2::new(foreground.element.width as f64, foreground.element.height as f64);
let background_size = DVec2::new(background.element.width as f64, background.element.height as f64);
// Transforms a point from the background image to the foreground image
let background_to_foreground = DAffine2::from_scale(foreground_size) * foreground.transform.inverse() * background.transform * DAffine2::from_scale(1. / background_size);
@@ -333,8 +333,8 @@ pub fn blend_image_closure(foreground: Instance<Raster<CPU>>, mut background: In
let background_point = DVec2::new(x as f64, y as f64);
let foreground_point = background_to_foreground.transform_point2(background_point);
let source_pixel = foreground.instance.sample(foreground_point);
let Some(destination_pixel) = background.instance.data_mut().get_pixel_mut(x, y) else { continue };
let source_pixel = foreground.element.sample(foreground_point);
let Some(destination_pixel) = background.element.data_mut().get_pixel_mut(x, y) else { continue };
*destination_pixel = map_fn(source_pixel, *destination_pixel);
}
@@ -343,8 +343,8 @@ pub fn blend_image_closure(foreground: Instance<Raster<CPU>>, mut background: In
background
}
pub fn blend_stamp_closure(foreground: BrushStampGenerator<Color>, mut background: Instance<Raster<CPU>>, map_fn: impl Fn(Color, Color) -> Color) -> Instance<Raster<CPU>> {
let background_size = DVec2::new(background.instance.width as f64, background.instance.height as f64);
pub fn blend_stamp_closure(foreground: BrushStampGenerator<Color>, mut background: TableRow<Raster<CPU>>, map_fn: impl Fn(Color, Color) -> Color) -> TableRow<Raster<CPU>> {
let background_size = DVec2::new(background.element.width as f64, background.element.height as f64);
// Transforms a point from the background image to the foreground image
let background_to_foreground = background.transform * DAffine2::from_scale(1. / background_size);
@@ -363,7 +363,7 @@ pub fn blend_stamp_closure(foreground: BrushStampGenerator<Color>, mut backgroun
let foreground_point = background_to_foreground.transform_point2(background_point);
let Some(source_pixel) = foreground.sample(foreground_point, area) else { continue };
let Some(destination_pixel) = background.instance.data_mut().get_pixel_mut(x, y) else { continue };
let Some(destination_pixel) = background.element.data_mut().get_pixel_mut(x, y) else { continue };
*destination_pixel = map_fn(source_pixel, *destination_pixel);
}
@@ -391,7 +391,7 @@ mod test {
async fn test_brush_output_size() {
let image = brush(
(),
RasterDataTable::<CPU>::new(Raster::new_cpu(Image::<Color>::default())),
Table::new_from_element(Raster::new_cpu(Image::<Color>::default())),
vec![BrushStroke {
trace: vec![crate::brush_stroke::BrushInputSample { position: DVec2::ZERO }],
style: BrushStyle {
@@ -406,6 +406,6 @@ mod test {
BrushCache::default(),
)
.await;
assert_eq!(image.instance_ref_iter().next().unwrap().instance.width, 20);
assert_eq!(image.iter().next().unwrap().element.width, 20);
}
}

View File

@@ -1,9 +1,9 @@
use crate::brush_stroke::BrushStroke;
use crate::brush_stroke::BrushStyle;
use dyn_any::DynAny;
use graphene_core::instances::Instance;
use graphene_core::raster_types::CPU;
use graphene_core::raster_types::Raster;
use graphene_core::table::TableRow;
use std::collections::HashMap;
use std::hash::Hash;
use std::hash::Hasher;
@@ -20,12 +20,12 @@ struct BrushCacheImpl {
prev_input: Vec<BrushStroke>,
// The strokes that have been fully processed and blended into the background.
#[serde(deserialize_with = "graphene_core::raster::image::migrate_image_frame_instance")]
background: Instance<Raster<CPU>>,
#[serde(deserialize_with = "graphene_core::raster::image::migrate_image_frame_instance")]
blended_image: Instance<Raster<CPU>>,
#[serde(deserialize_with = "graphene_core::raster::image::migrate_image_frame_instance")]
last_stroke_texture: Instance<Raster<CPU>>,
#[serde(deserialize_with = "graphene_core::raster::image::migrate_image_frame_row")]
background: TableRow<Raster<CPU>>,
#[serde(deserialize_with = "graphene_core::raster::image::migrate_image_frame_row")]
blended_image: TableRow<Raster<CPU>>,
#[serde(deserialize_with = "graphene_core::raster::image::migrate_image_frame_row")]
last_stroke_texture: TableRow<Raster<CPU>>,
// A cache for brush textures.
#[serde(skip)]
@@ -33,7 +33,7 @@ struct BrushCacheImpl {
}
impl BrushCacheImpl {
fn compute_brush_plan(&mut self, mut background: Instance<Raster<CPU>>, input: &[BrushStroke]) -> BrushPlan {
fn compute_brush_plan(&mut self, mut background: TableRow<Raster<CPU>>, input: &[BrushStroke]) -> BrushPlan {
// Do background invalidation.
if background != self.background {
self.background = background.clone();
@@ -60,8 +60,8 @@ impl BrushCacheImpl {
background = std::mem::take(&mut self.blended_image);
// Check if the first non-blended stroke is an extension of the last one.
let mut first_stroke_texture = Instance {
instance: Raster::<CPU>::default(),
let mut first_stroke_texture = TableRow {
element: Raster::<CPU>::default(),
transform: glam::DAffine2::ZERO,
..Default::default()
};
@@ -88,7 +88,7 @@ impl BrushCacheImpl {
}
}
pub fn cache_results(&mut self, input: Vec<BrushStroke>, blended_image: Instance<Raster<CPU>>, last_stroke_texture: Instance<Raster<CPU>>) {
pub fn cache_results(&mut self, input: Vec<BrushStroke>, blended_image: TableRow<Raster<CPU>>, last_stroke_texture: TableRow<Raster<CPU>>) {
self.prev_input = input;
self.blended_image = blended_image;
self.last_stroke_texture = last_stroke_texture;
@@ -123,8 +123,8 @@ impl Hash for BrushCacheImpl {
#[derive(Clone, Debug, Default)]
pub struct BrushPlan {
pub strokes: Vec<BrushStroke>,
pub background: Instance<Raster<CPU>>,
pub first_stroke_texture: Instance<Raster<CPU>>,
pub background: TableRow<Raster<CPU>>,
pub first_stroke_texture: TableRow<Raster<CPU>>,
pub first_stroke_point_skip: usize,
}
@@ -160,12 +160,12 @@ impl Hash for BrushCache {
}
impl BrushCache {
pub fn compute_brush_plan(&self, background: Instance<Raster<CPU>>, input: &[BrushStroke]) -> BrushPlan {
pub fn compute_brush_plan(&self, background: TableRow<Raster<CPU>>, input: &[BrushStroke]) -> BrushPlan {
let mut inner = self.0.lock().unwrap();
inner.compute_brush_plan(background, input)
}
pub fn cache_results(&self, input: Vec<BrushStroke>, blended_image: Instance<Raster<CPU>>, last_stroke_texture: Instance<Raster<CPU>>) {
pub fn cache_results(&self, input: Vec<BrushStroke>, blended_image: TableRow<Raster<CPU>>, last_stroke_texture: TableRow<Raster<CPU>>) {
let mut inner = self.0.lock().unwrap();
inner.cache_results(input, blended_image, last_stroke_texture)
}

View File

@@ -0,0 +1,38 @@
[package]
name = "graphene-core-shaders"
version = "0.1.0"
edition = "2024"
description = "no_std API definitions for Graphene"
authors = ["Graphite Authors <contact@graphite.rs>"]
license = "MIT OR Apache-2.0"
[features]
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"]
[dependencies]
# 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 }
# Workspace std dependencies
serde = { workspace = true, optional = true }
specta = { workspace = true, optional = true }
log = { workspace = true, optional = true }
[dev-dependencies]
graphene-core = { 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 = ["graphene-core"]

View File

@@ -1,8 +1,11 @@
use dyn_any::DynAny;
use std::hash::Hash;
use core::fmt::Display;
use core::hash::{Hash, Hasher};
#[cfg(not(feature = "std"))]
use num_traits::float::Float;
#[derive(Copy, Clone, Debug, PartialEq, DynAny, specta::Type, serde::Serialize, serde::Deserialize)]
#[serde(default)]
#[derive(Debug, Clone, Copy, PartialEq)]
#[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,
@@ -15,15 +18,15 @@ impl Default for AlphaBlending {
}
}
impl Hash for AlphaBlending {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
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 std::fmt::Display for AlphaBlending {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
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,
@@ -56,11 +59,15 @@ impl AlphaBlending {
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 }
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, DynAny, Hash, specta::Type)]
#[repr(i32)]
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "std", derive(dyn_any::DynAny, specta::Type, serde::Serialize, serde::Deserialize))]
pub enum BlendMode {
// Basic group
#[default]
@@ -185,19 +192,20 @@ impl BlendMode {
}
/// 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(|| {
warn!("Unsupported blend mode {self:?}");
log::warn!("Unsupported blend mode {self:?}");
"normal"
})
)
}
}
impl std::fmt::Display for BlendMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl Display for BlendMode {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
// Normal group
BlendMode::Normal => write!(f, "Normal"),

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

@@ -1,11 +1,10 @@
use bytemuck::{Pod, Zeroable};
use glam::DVec2;
use std::fmt::Debug;
#[cfg(target_arch = "spirv")]
use spirv_std::num_traits::float::Float;
pub use crate::blending::*;
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;
@@ -15,9 +14,9 @@ pub trait Linear {
fn lerp(self, other: Self, value: Self) -> Self
where
Self: Sized + Copy,
Self: std::ops::Sub<Self, Output = Self>,
Self: std::ops::Mul<Self, Output = Self>,
Self: std::ops::Add<Self, Output = Self>,
Self: core::ops::Sub<Self, Output = Self>,
Self: core::ops::Mul<Self, Output = Self>,
Self: core::ops::Add<Self, Output = Self>,
{
self + (other - self) * value
}
@@ -64,7 +63,6 @@ impl<T: Linear + Debug + Copy> Channel for T {
impl<T: Linear + Debug + Copy> LinearChannel for T {}
use num_derive::*;
#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Num, NumCast, NumOps, One, Zero, ToPrimitive, FromPrimitive)]
pub struct SRGBGammaFloat(f32);
@@ -97,17 +95,9 @@ impl<T: Rec709Primaries> RGBPrimaries for T {
pub trait SRGB: Rec709Primaries {}
pub trait Serde: serde::Serialize + for<'a> serde::Deserialize<'a> {}
#[cfg(not(feature = "serde"))]
pub trait Serde {}
impl<T: serde::Serialize + for<'a> serde::Deserialize<'a>> Serde for T {}
#[cfg(not(feature = "serde"))]
impl<T> Serde for T {}
// TODO: Come up with a better name for this trait
pub trait Pixel: Clone + Pod + Zeroable + Default {
#[cfg(not(target_arch = "spirv"))]
#[cfg(feature = "std")]
fn to_bytes(&self) -> Vec<u8> {
bytemuck::bytes_of(self).to_vec()
}

View File

@@ -1,16 +1,18 @@
use super::color_traits::{Alpha, AlphaMut, AssociatedAlpha, Luminance, LuminanceMut, Pixel, RGB, RGBMut, Rec709Primaries, SRGB};
use super::discrete_srgb::{float_to_srgb_u8, srgb_u8_to_float};
use bytemuck::{Pod, Zeroable};
use dyn_any::DynAny;
use core::fmt::Debug;
use core::hash::Hash;
use half::f16;
#[cfg(target_arch = "spirv")]
use spirv_std::num_traits::Euclid;
#[cfg(target_arch = "spirv")]
use spirv_std::num_traits::float::Float;
use std::hash::Hash;
#[cfg(not(feature = "std"))]
use num_traits::Euclid;
#[cfg(not(feature = "std"))]
use num_traits::float::Float;
#[repr(C)]
#[derive(Debug, Default, Clone, Copy, PartialEq, DynAny, Pod, Zeroable, serde::Serialize, serde::Deserialize)]
#[derive(Default, Clone, Copy, PartialEq, Pod, Zeroable)]
#[cfg_attr(not(target_arch = "spirv"), derive(Debug))]
#[cfg_attr(feature = "std", derive(dyn_any::DynAny, serde::Serialize, serde::Deserialize))]
pub struct RGBA16F {
red: f16,
green: f16,
@@ -18,6 +20,14 @@ pub struct RGBA16F {
alpha: f16,
}
/// hack around half still masking out impl Debug for f16 on spirv
#[cfg(target_arch = "spirv")]
impl core::fmt::Debug for RGBA16F {
fn fmt(&self, _f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
Ok(())
}
}
impl From<Color> for RGBA16F {
#[inline(always)]
fn from(c: Color) -> Self {
@@ -82,7 +92,8 @@ impl Alpha for RGBA16F {
impl Pixel for RGBA16F {}
#[repr(C)]
#[derive(Debug, Default, Clone, Copy, PartialEq, DynAny, Pod, Zeroable, specta::Type, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Default, Clone, Copy, PartialEq, Pod, Zeroable)]
#[cfg_attr(feature = "std", derive(dyn_any::DynAny, specta::Type, serde::Serialize, serde::Deserialize))]
pub struct SRGBA8 {
red: u8,
green: u8,
@@ -162,7 +173,8 @@ impl Alpha for SRGBA8 {
impl Pixel for SRGBA8 {}
#[repr(C)]
#[derive(Debug, Default, Clone, Copy, PartialEq, DynAny, Pod, Zeroable, specta::Type, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Default, Clone, Copy, PartialEq, Pod, Zeroable)]
#[cfg_attr(feature = "std", derive(dyn_any::DynAny, specta::Type, serde::Serialize, serde::Deserialize))]
pub struct Luma(pub f32);
impl Luminance for Luma {
@@ -202,7 +214,8 @@ impl Pixel for Luma {}
/// The other components (RGB) are stored as `f32` that range from `0.0` up to `f32::MAX`,
/// the values encode the brightness of each channel proportional to the light intensity in cd/m² (nits) in HDR, and `0.0` (black) to `1.0` (white) in SDR color.
#[repr(C)]
#[derive(Debug, Default, Clone, Copy, PartialEq, DynAny, Pod, Zeroable, specta::Type, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Default, Clone, Copy, PartialEq, Pod, Zeroable)]
#[cfg_attr(feature = "std", derive(dyn_any::DynAny, specta::Type, serde::Serialize, serde::Deserialize))]
pub struct Color {
red: f32,
green: f32,
@@ -212,7 +225,7 @@ pub struct Color {
#[allow(clippy::derived_hash_with_manual_eq)]
impl Hash for Color {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.red.to_bits().hash(state);
self.green.to_bits().hash(state);
self.blue.to_bits().hash(state);
@@ -253,7 +266,7 @@ impl AlphaMut for Color {
}
impl Pixel for Color {
#[cfg(not(target_arch = "spirv"))]
#[cfg(feature = "std")]
fn to_bytes(&self) -> Vec<u8> {
self.to_rgba8_srgb().to_vec()
}
@@ -426,9 +439,9 @@ impl Color {
lightness + saturation - lightness * saturation
};
let temp2 = 2. * lightness - temp1;
#[cfg(not(target_arch = "spirv"))]
#[cfg(feature = "std")]
let rem = |x: f32| x.rem_euclid(1.);
#[cfg(target_arch = "spirv")]
#[cfg(not(feature = "std"))]
let rem = |x: f32| x.rem_euclid(&1.);
let mut red = rem(hue + 1. / 3.);
@@ -687,7 +700,7 @@ impl Color {
if c_s <= 0.5 {
c_b - (1. - 2. * c_s) * c_b * (1. - c_b)
} else {
let d: fn(f32) -> f32 = |x| if x <= 0.25 { ((16. * x - 12.) * x + 4.) * x } else { x.sqrt() };
let d = |x: f32| if x <= 0.25 { ((16. * x - 12.) * x + 4.) * x } else { x.sqrt() };
c_b + (2. * c_s - 1.) * (d(c_b) - c_b)
}
}
@@ -790,6 +803,7 @@ impl Color {
/// let color = Color::from_rgba8_srgb(0x52, 0x67, 0xFA, 0x61); // Premultiplied alpha
/// assert_eq!("3240a261", color.to_rgba_hex_srgb()); // Equivalent hex incorporating premultiplied alpha
/// ```
#[cfg(feature = "std")]
pub fn to_rgba_hex_srgb(&self) -> String {
let gamma = self.to_gamma_srgb();
format!(
@@ -807,6 +821,7 @@ impl Color {
/// let color = Color::from_rgba8_srgb(0x52, 0x67, 0xFA, 0x61); // Premultiplied alpha
/// assert_eq!("3240a2", color.to_rgb_hex_srgb()); // Equivalent hex incorporating premultiplied alpha
/// ```
#[cfg(feature = "std")]
pub fn to_rgb_hex_srgb(&self) -> String {
self.to_gamma_srgb().to_rgb_hex_srgb_from_gamma()
}
@@ -817,6 +832,7 @@ impl Color {
/// let color = Color::from_rgba8_srgb(0x52, 0x67, 0xFA, 0x61); // Premultiplied alpha
/// assert_eq!("3240a2", color.to_rgb_hex_srgb()); // Equivalent hex incorporating premultiplied alpha
/// ```
#[cfg(feature = "std")]
pub fn to_rgb_hex_srgb_from_gamma(&self) -> String {
format!("{:02x?}{:02x?}{:02x?}", (self.r() * 255.) as u8, (self.g() * 255.) as u8, (self.b() * 255.) as u8)
}
@@ -876,9 +892,9 @@ impl Color {
} else {
4. + (self.red - self.green) / (max_channel - min_channel)
} / 6.;
#[cfg(not(target_arch = "spirv"))]
#[cfg(feature = "std")]
let hue = hue.rem_euclid(1.);
#[cfg(target_arch = "spirv")]
#[cfg(not(feature = "std"))]
let hue = hue.rem_euclid(&1.);
[hue, saturation, lightness, self.alpha]

View File

@@ -69,7 +69,7 @@ pub fn float_to_srgb_u8(mut f: f32) -> u8 {
// We clamped f to [0, 1], and the integer representations
// of the positive finite non-NaN floats are monotonic.
// This makes the later LUT lookup panicless.
unsafe { std::hint::unreachable_unchecked() }
unsafe { core::hint::unreachable_unchecked() }
}
// Compute a piecewise linear interpolation that is always

View File

@@ -1,7 +1,7 @@
mod color;
mod color_traits;
mod color_types;
mod discrete_srgb;
pub use color::*;
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,19 @@
#![cfg_attr(not(feature = "std"), no_std)]
pub mod blending;
pub mod choice_type;
pub mod color;
pub mod context;
pub mod registry;
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

@@ -14,10 +14,12 @@ wgpu = ["dep:wgpu"]
dealloc_nodes = []
[dependencies]
# Local dependencies
graphene-core-shaders = { workspace = true, features = ["std"] }
# Workspace dependencies
bytemuck = { workspace = true }
node-macro = { workspace = true }
num-derive = { workspace = true }
num-traits = { workspace = true }
rand = { workspace = true }
glam = { workspace = true }
@@ -27,16 +29,16 @@ rustc-hash = { workspace = true }
dyn-any = { workspace = true }
ctor = { workspace = true }
rand_chacha = { workspace = true }
bezier-rs = { workspace = true }
specta = { workspace = true }
image = { workspace = true }
half = { workspace = true }
tinyvec = { workspace = true }
parley = { workspace = true }
skrifa = { workspace = true }
kurbo = { workspace = true }
lyon_geom = { workspace = true }
log = { workspace = true }
base64 = { workspace = true }
poly-cool = { workspace = true }
# Optional workspace dependencies
serde = { workspace = true, optional = true }
@@ -46,9 +48,3 @@ wgpu = { workspace = true, optional = true }
# Workspace dependencies
tokio = { workspace = true }
serde_json = { workspace = true }
[lints.rust]
# the spirv target is not in the list of common cfgs so must be added manually
unexpected_cfgs = { level = "warn", check-cfg = [
'cfg(target_arch, values("spirv"))',
] }

View File

@@ -0,0 +1,140 @@
use crate::blending::AlphaBlending;
use crate::bounds::{BoundingBox, RenderBoundingBox};
use crate::gradient::GradientStops;
use crate::math::quad::Quad;
use crate::raster_types::{CPU, GPU, Raster};
use crate::table::{Table, TableRow};
use crate::transform::TransformMut;
use crate::uuid::NodeId;
use crate::vector::Vector;
use crate::{CloneVarArgs, Color, Context, Ctx, ExtractAll, Graphic, OwnedContextImpl};
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,
}
}
}
// 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 EitherFormat {
ArtboardGroup(ArtboardGroup),
ArtboardTable(Table<Artboard>),
}
Ok(match EitherFormat::deserialize(deserializer)? {
EitherFormat::ArtboardGroup(artboard_group) => {
let mut table = Table::new();
for (artboard, source_node_id) in artboard_group.artboards {
table.push(TableRow {
element: artboard,
mask: None,
transform: DAffine2::IDENTITY,
alpha_blending: AlphaBlending::default(),
source_node_id,
});
}
table
}
EitherFormat::ArtboardTable(artboard_table) => artboard_table,
})
}
#[node_macro::node(category(""))]
async fn create_artboard<T: Into<Table<Graphic>> + 'n>(
ctx: impl ExtractAll + CloneVarArgs + Ctx,
#[implementations(
Context -> Table<Graphic>,
Context -> Table<Vector>,
Context -> Table<Raster<CPU>>,
Context -> Table<Raster<GPU>>,
Context -> Table<Color>,
Context -> Table<GradientStops>,
Context -> DAffine2,
)]
content: impl Node<Context<'static>, Output = T>,
label: String,
location: DVec2,
dimensions: DVec2,
background: Table<Color>,
clip: bool,
) -> Table<Artboard> {
let location = location.as_ivec2();
let footprint = ctx.try_footprint().copied();
let mut new_ctx = OwnedContextImpl::from(ctx);
if let Some(mut footprint) = footprint {
footprint.translate(location.as_dvec2());
new_ctx = new_ctx.with_footprint(footprint);
}
let content = content.eval(new_ctx.into_context()).await.into();
let dimensions = dimensions.as_ivec2().max(IVec2::ONE);
let location = location.min(location + dimensions);
let dimensions = dimensions.abs();
let background: Option<Color> = background.into();
let background = background.unwrap_or(Color::WHITE);
Table::new_from_element(Artboard {
content,
label,
location,
dimensions,
background,
clip,
})
}

View File

@@ -1,8 +1,9 @@
use crate::raster::Image;
use crate::raster_types::{CPU, RasterDataTable};
use crate::gradient::GradientStops;
use crate::raster_types::{CPU, Raster};
use crate::registry::types::Percentage;
use crate::vector::VectorDataTable;
use crate::{BlendMode, Color, Ctx, GraphicElement, GraphicGroupTable};
use crate::table::Table;
use crate::vector::Vector;
use crate::{BlendMode, Color, Ctx, Graphic};
pub(super) trait MultiplyAlpha {
fn multiply_alpha(&mut self, factor: f64);
@@ -13,27 +14,38 @@ impl MultiplyAlpha for Color {
*self = Color::from_rgbaf32_unchecked(self.r(), self.g(), self.b(), (self.a() * factor as f32).clamp(0., 1.))
}
}
impl MultiplyAlpha for VectorDataTable {
impl MultiplyAlpha for Table<Vector> {
fn multiply_alpha(&mut self, factor: f64) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.opacity *= factor as f32;
for row in self.iter_mut() {
row.alpha_blending.opacity *= factor as f32;
}
}
}
impl MultiplyAlpha for GraphicGroupTable {
impl MultiplyAlpha for Table<Graphic> {
fn multiply_alpha(&mut self, factor: f64) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.opacity *= factor as f32;
for row in self.iter_mut() {
row.alpha_blending.opacity *= factor as f32;
}
}
}
impl MultiplyAlpha for RasterDataTable<CPU>
where
GraphicElement: From<Image<Color>>,
{
impl MultiplyAlpha for Table<Raster<CPU>> {
fn multiply_alpha(&mut self, factor: f64) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.opacity *= factor as f32;
for row in self.iter_mut() {
row.alpha_blending.opacity *= factor as f32;
}
}
}
impl MultiplyAlpha for Table<Color> {
fn multiply_alpha(&mut self, factor: f64) {
for row in self.iter_mut() {
row.alpha_blending.opacity *= factor as f32;
}
}
}
impl MultiplyAlpha for Table<GradientStops> {
fn multiply_alpha(&mut self, factor: f64) {
for row in self.iter_mut() {
row.alpha_blending.opacity *= factor as f32;
}
}
}
@@ -46,24 +58,38 @@ impl MultiplyFill for Color {
*self = Color::from_rgbaf32_unchecked(self.r(), self.g(), self.b(), (self.a() * factor as f32).clamp(0., 1.))
}
}
impl MultiplyFill for VectorDataTable {
impl MultiplyFill for Table<Vector> {
fn multiply_fill(&mut self, factor: f64) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.fill *= factor as f32;
for row in self.iter_mut() {
row.alpha_blending.fill *= factor as f32;
}
}
}
impl MultiplyFill for GraphicGroupTable {
impl MultiplyFill for Table<Graphic> {
fn multiply_fill(&mut self, factor: f64) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.fill *= factor as f32;
for row in self.iter_mut() {
row.alpha_blending.fill *= factor as f32;
}
}
}
impl MultiplyFill for RasterDataTable<CPU> {
impl MultiplyFill for Table<Raster<CPU>> {
fn multiply_fill(&mut self, factor: f64) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.fill *= factor as f32;
for row in self.iter_mut() {
row.alpha_blending.fill *= factor as f32;
}
}
}
impl MultiplyFill for Table<Color> {
fn multiply_fill(&mut self, factor: f64) {
for row in self.iter_mut() {
row.alpha_blending.fill *= factor as f32;
}
}
}
impl MultiplyFill for Table<GradientStops> {
fn multiply_fill(&mut self, factor: f64) {
for row in self.iter_mut() {
row.alpha_blending.fill *= factor as f32;
}
}
}
@@ -72,24 +98,38 @@ trait SetBlendMode {
fn set_blend_mode(&mut self, blend_mode: BlendMode);
}
impl SetBlendMode for VectorDataTable {
impl SetBlendMode for Table<Vector> {
fn set_blend_mode(&mut self, blend_mode: BlendMode) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.blend_mode = blend_mode;
for row in self.iter_mut() {
row.alpha_blending.blend_mode = blend_mode;
}
}
}
impl SetBlendMode for GraphicGroupTable {
impl SetBlendMode for Table<Graphic> {
fn set_blend_mode(&mut self, blend_mode: BlendMode) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.blend_mode = blend_mode;
for row in self.iter_mut() {
row.alpha_blending.blend_mode = blend_mode;
}
}
}
impl SetBlendMode for RasterDataTable<CPU> {
impl SetBlendMode for Table<Raster<CPU>> {
fn set_blend_mode(&mut self, blend_mode: BlendMode) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.blend_mode = blend_mode;
for row in self.iter_mut() {
row.alpha_blending.blend_mode = blend_mode;
}
}
}
impl SetBlendMode for Table<Color> {
fn set_blend_mode(&mut self, blend_mode: BlendMode) {
for row in self.iter_mut() {
row.alpha_blending.blend_mode = blend_mode;
}
}
}
impl SetBlendMode for Table<GradientStops> {
fn set_blend_mode(&mut self, blend_mode: BlendMode) {
for row in self.iter_mut() {
row.alpha_blending.blend_mode = blend_mode;
}
}
}
@@ -98,24 +138,38 @@ trait SetClip {
fn set_clip(&mut self, clip: bool);
}
impl SetClip for VectorDataTable {
impl SetClip for Table<Vector> {
fn set_clip(&mut self, clip: bool) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.clip = clip;
for row in self.iter_mut() {
row.alpha_blending.clip = clip;
}
}
}
impl SetClip for GraphicGroupTable {
impl SetClip for Table<Graphic> {
fn set_clip(&mut self, clip: bool) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.clip = clip;
for row in self.iter_mut() {
row.alpha_blending.clip = clip;
}
}
}
impl SetClip for RasterDataTable<CPU> {
impl SetClip for Table<Raster<CPU>> {
fn set_clip(&mut self, clip: bool) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.clip = clip;
for row in self.iter_mut() {
row.alpha_blending.clip = clip;
}
}
}
impl SetClip for Table<Color> {
fn set_clip(&mut self, clip: bool) {
for row in self.iter_mut() {
row.alpha_blending.clip = clip;
}
}
}
impl SetClip for Table<GradientStops> {
fn set_clip(&mut self, clip: bool) {
for row in self.iter_mut() {
row.alpha_blending.clip = clip;
}
}
}
@@ -124,14 +178,16 @@ impl SetClip for RasterDataTable<CPU> {
fn blend_mode<T: SetBlendMode>(
_: impl Ctx,
#[implementations(
GraphicGroupTable,
VectorDataTable,
RasterDataTable<CPU>,
Table<Graphic>,
Table<Vector>,
Table<Raster<CPU>>,
Table<Color>,
Table<GradientStops>,
)]
mut value: T,
blend_mode: BlendMode,
) -> T {
// TODO: Find a way to make this apply once to the table's parent (i.e. its row in its parent table or Instance<T>) rather than applying to each row in its own table, which produces the undesired result
// TODO: Find a way to make this apply once to the table's parent (i.e. its row in its parent table or TableRow<T>) rather than applying to each row in its own table, which produces the undesired result
value.set_blend_mode(blend_mode);
value
}
@@ -140,14 +196,16 @@ fn blend_mode<T: SetBlendMode>(
fn opacity<T: MultiplyAlpha>(
_: impl Ctx,
#[implementations(
GraphicGroupTable,
VectorDataTable,
RasterDataTable<CPU>,
Table<Graphic>,
Table<Vector>,
Table<Raster<CPU>>,
Table<Color>,
Table<GradientStops>,
)]
mut value: T,
#[default(100.)] opacity: Percentage,
) -> T {
// TODO: Find a way to make this apply once to the table's parent (i.e. its row in its parent table or Instance<T>) rather than applying to each row in its own table, which produces the undesired result
// TODO: Find a way to make this apply once to the table's parent (i.e. its row in its parent table or TableRow<T>) rather than applying to each row in its own table, which produces the undesired result
value.multiply_alpha(opacity / 100.);
value
}
@@ -156,9 +214,11 @@ fn opacity<T: MultiplyAlpha>(
fn blending<T: SetBlendMode + MultiplyAlpha + MultiplyFill + SetClip>(
_: impl Ctx,
#[implementations(
GraphicGroupTable,
VectorDataTable,
RasterDataTable<CPU>,
Table<Graphic>,
Table<Vector>,
Table<Raster<CPU>>,
Table<Color>,
Table<GradientStops>,
)]
mut value: T,
blend_mode: BlendMode,
@@ -166,7 +226,7 @@ fn blending<T: SetBlendMode + MultiplyAlpha + MultiplyFill + SetClip>(
#[default(100.)] fill: Percentage,
#[default(false)] clip: bool,
) -> T {
// TODO: Find a way to make this apply once to the table's parent (i.e. its row in its parent table or Instance<T>) rather than applying to each row in its own table, which produces the undesired result
// TODO: Find a way to make this apply once to the table's parent (i.e. its row in its parent table or TableRow<T>) rather than applying to each row in its own table, which produces the undesired result
value.set_blend_mode(blend_mode);
value.multiply_alpha(opacity / 100.);
value.multiply_fill(fill / 100.);

View File

@@ -1,24 +1,40 @@
use crate::Color;
use crate::{Color, gradient::GradientStops};
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) -> Option<[DVec2; 2]>;
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) -> Option<[DVec2; 2]> {
None
fn bounding_box(&self, _transform: DAffine2, _include_stroke: bool) -> RenderBoundingBox {
RenderBoundingBox::None
}
}
};
}
none_impl!(String);
none_impl!(bool);
none_impl!(f32);
none_impl!(f64);
none_impl!(DVec2);
none_impl!(Option<Color>);
none_impl!(Vec<Color>);
none_impl!(String);
impl BoundingBox for Color {
fn bounding_box(&self, _transform: DAffine2, _include_stroke: bool) -> RenderBoundingBox {
RenderBoundingBox::Infinite
}
}
impl BoundingBox for GradientStops {
fn bounding_box(&self, _transform: DAffine2, _include_stroke: bool) -> RenderBoundingBox {
RenderBoundingBox::Infinite
}
}

View File

@@ -1,11 +1,10 @@
use crate::transform::Footprint;
pub use graphene_core_shaders::context::{ArcCtx, Ctx};
use std::any::Any;
use std::borrow::Borrow;
use std::panic::Location;
use std::sync::Arc;
pub trait Ctx: Clone + Send {}
pub trait ExtractFootprint {
#[track_caller]
fn try_footprint(&self) -> Option<&Footprint>;
@@ -51,9 +50,6 @@ pub enum VarArgsResult {
IndexOutOfBounds,
NoVarArgs,
}
impl<T: Ctx> Ctx for Option<T> {}
impl<T: Ctx + Sync> Ctx for &T {}
impl Ctx for () {}
impl Ctx for Footprint {}
impl ExtractFootprint for () {
fn try_footprint(&self) -> Option<&Footprint> {
@@ -157,7 +153,7 @@ impl<T: CloneVarArgs + Sync> CloneVarArgs for Arc<T> {
}
impl Ctx for ContextImpl<'_> {}
impl Ctx for Arc<OwnedContextImpl> {}
impl ArcCtx for OwnedContextImpl {}
impl ExtractFootprint for ContextImpl<'_> {
fn try_footprint(&self) -> Option<&Footprint> {

View File

@@ -1,12 +1,12 @@
use crate::raster_types::{CPU, RasterDataTable};
use crate::vector::VectorDataTable;
use crate::{Color, Ctx};
use crate::Ctx;
use crate::raster_types::{CPU, Raster};
use crate::table::Table;
use glam::{DAffine2, DVec2};
#[node_macro::node(category("Debug"), name("Log to Console"))]
fn log_to_console<T: std::fmt::Debug>(_: impl Ctx, #[implementations(String, bool, f64, u32, u64, DVec2, VectorDataTable, DAffine2, Color, Option<Color>)] value: T) -> T {
fn log_to_console<T: std::fmt::Debug>(_: impl Ctx, #[implementations(bool, f64, u32, u64, DVec2, DAffine2, String)] value: T) -> T {
// KEEP THIS `debug!()` - It acts as the output for the debug node itself
log::debug!("{:#?}", value);
log::debug!("{value:#?}");
value
}
@@ -18,18 +18,18 @@ fn size_of(_: impl Ctx, ty: crate::Type) -> Option<usize> {
/// Meant for debugging purposes, not general use. Wraps the input value in the Some variant of an Option.
#[node_macro::node(category("Debug"))]
fn some<T>(_: impl Ctx, #[implementations(f64, f32, u32, u64, String, Color)] input: T) -> Option<T> {
fn some<T>(_: impl Ctx, #[implementations(f64, f32, u32, u64, String)] input: T) -> Option<T> {
Some(input)
}
/// Meant for debugging purposes, not general use. Unwraps the input value from an Option, returning the default value if the input is None.
#[node_macro::node(category("Debug"))]
fn unwrap<T: Default>(_: impl Ctx, #[implementations(Option<f64>, Option<f32>, Option<u32>, Option<u64>, Option<String>, Option<Color>)] input: Option<T>) -> T {
fn unwrap_option<T: Default>(_: impl Ctx, #[implementations(Option<f64>, Option<u32>, Option<u64>, Option<String>)] input: Option<T>) -> T {
input.unwrap_or_default()
}
/// Meant for debugging purposes, not general use. Clones the input value.
#[node_macro::node(category("Debug"))]
fn clone<'i, T: Clone + 'i>(_: impl Ctx, #[implementations(&RasterDataTable<CPU>)] value: &'i T) -> T {
fn clone<'i, T: Clone + 'i>(_: impl Ctx, #[implementations(&Table<Raster<CPU>>)] value: &'i T) -> T {
value.clone()
}

View File

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

View File

@@ -126,7 +126,6 @@ pub struct Gradient {
pub gradient_type: GradientType,
pub start: DVec2,
pub end: DVec2,
pub transform: DAffine2,
}
impl Default for Gradient {
@@ -136,7 +135,6 @@ impl Default for Gradient {
gradient_type: GradientType::Linear,
start: DVec2::new(0., 0.5),
end: DVec2::new(1., 0.5),
transform: DAffine2::IDENTITY,
}
}
}
@@ -147,7 +145,6 @@ impl std::hash::Hash for Gradient {
[].iter()
.chain(self.start.to_array().iter())
.chain(self.end.to_array().iter())
.chain(self.transform.to_cols_array().iter())
.chain(self.stops.0.iter().map(|(position, _)| position))
.for_each(|x| x.to_bits().hash(state));
self.stops.0.iter().for_each(|(_, color)| color.hash(state));
@@ -171,20 +168,15 @@ impl std::fmt::Display for Gradient {
impl Gradient {
/// Constructs a new gradient with the colors at 0 and 1 specified.
pub fn new(start: DVec2, start_color: Color, end: DVec2, end_color: Color, transform: DAffine2, gradient_type: GradientType) -> Self {
Gradient {
start,
end,
stops: GradientStops::new(vec![(0., start_color.to_gamma_srgb()), (1., end_color.to_gamma_srgb())]),
transform,
gradient_type,
}
pub fn 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 transform = self.transform;
let stops = self
.stops
.0
@@ -199,13 +191,7 @@ impl Gradient {
let stops = GradientStops::new(stops);
let gradient_type = if time < 0.5 { self.gradient_type } else { other.gradient_type };
Self {
start,
end,
transform,
stops,
gradient_type,
}
Self { start, end, stops, gradient_type }
}
/// Insert a stop into the gradient, the index if successful

View File

@@ -0,0 +1,556 @@
use crate::blending::AlphaBlending;
use crate::bounds::{BoundingBox, RenderBoundingBox};
use crate::gradient::GradientStops;
use crate::raster_types::{CPU, GPU, Raster};
use crate::table::{Table, TableRow};
use crate::uuid::NodeId;
use crate::vector::Vector;
use crate::{Artboard, Color, Ctx};
use dyn_any::DynAny;
use glam::{DAffine2, DVec2};
use std::hash::Hash;
/// 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)
}
}
impl From<Vector> for Table<Graphic> {
fn from(vector: Vector) -> Self {
Table::new_from_element(Graphic::Vector(Table::new_from_element(vector)))
}
}
impl From<Table<Vector>> for Table<Graphic> {
fn from(vector: Table<Vector>) -> Self {
Table::new_from_element(Graphic::Vector(vector))
}
}
// 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)
}
}
impl From<Raster<CPU>> for Table<Graphic> {
fn from(raster: Raster<CPU>) -> Self {
Table::new_from_element(Graphic::RasterCPU(Table::new_from_element(raster)))
}
}
impl From<Table<Raster<CPU>>> for Table<Graphic> {
fn from(raster: Table<Raster<CPU>>) -> Self {
Table::new_from_element(Graphic::RasterCPU(raster))
}
}
// 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)
}
}
impl From<Raster<GPU>> for Table<Graphic> {
fn from(raster: Raster<GPU>) -> Self {
Table::new_from_element(Graphic::RasterGPU(Table::new_from_element(raster)))
}
}
impl From<Table<Raster<GPU>>> for Table<Graphic> {
fn from(raster: Table<Raster<GPU>>) -> Self {
Table::new_from_element(Graphic::RasterGPU(raster))
}
}
// 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)
}
}
impl From<Color> for Table<Graphic> {
fn from(color: Color) -> Self {
Table::new_from_element(Graphic::Color(Table::new_from_element(color)))
}
}
impl From<Table<Color>> for Table<Graphic> {
fn from(color: Table<Color>) -> Self {
Table::new_from_element(Graphic::Color(color))
}
}
// 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()
}
}
}
impl From<Option<Color>> for Table<Graphic> {
fn from(color: Option<Color>) -> Self {
if let Some(color) = color {
Table::new_from_element(Graphic::Color(Table::new_from_element(color)))
} else {
Table::new()
}
}
}
impl From<Table<Color>> for Option<Color> {
fn from(color: Table<Color>) -> Self {
color.into_iter().next().map(|row| row.element)
}
}
// 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)
}
}
impl From<GradientStops> for Table<Graphic> {
fn from(gradient: GradientStops) -> Self {
Table::new_from_element(Graphic::Gradient(Table::new_from_element(gradient)))
}
}
impl From<Table<GradientStops>> for Table<Graphic> {
fn from(gradient: Table<GradientStops>) -> Self {
Table::new_from_element(Graphic::Gradient(gradient))
}
}
// DAffine2
impl From<DAffine2> for Graphic {
fn from(_: DAffine2) -> Self {
Graphic::default()
}
}
impl From<DAffine2> for Table<Graphic> {
fn from(_: DAffine2) -> Self {
Table::new()
}
}
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),
}
}
}
#[node_macro::node(category(""))]
async fn source_node_id<I: 'n + Send + Clone>(
_: impl Ctx,
#[implementations(Table<Artboard>, Table<Graphic>, Table<Vector>, Table<Raster<CPU>>, Table<Raster<GPU>>, Table<Color>, Table<GradientStops>)] content: Table<I>,
node_path: Vec<NodeId>,
) -> Table<I> {
// Get the penultimate element of the node path, or None if the path is too short
// This is used to get the ID of the user-facing parent layer-style node (which encapsulates this internal node).
let source_node_id = node_path.get(node_path.len().wrapping_sub(2)).copied();
let mut content = content;
for row in content.iter_mut() {
*row.source_node_id = source_node_id;
}
content
}
/// Joins two tables of the same type, extending the base table with the rows of the new table.
#[node_macro::node(category("General"))]
async fn extend<I: 'n + Send + Clone>(
_: impl Ctx,
/// The table whose rows will appear at the start of the extended table.
#[implementations(Table<Artboard>, Table<Graphic>, Table<Vector>, Table<Raster<CPU>>, Table<Raster<GPU>>, Table<Color>, Table<GradientStops>)]
base: Table<I>,
/// The table whose rows will appear at the end of the extended table.
#[expose]
#[implementations(Table<Artboard>, Table<Graphic>, Table<Vector>, Table<Raster<CPU>>, Table<Raster<GPU>>, Table<Color>, Table<GradientStops>)]
new: Table<I>,
) -> Table<I> {
let mut base = base;
base.extend(new);
base
}
// TODO: Eventually remove this document upgrade code
#[node_macro::node(category(""))]
async fn legacy_layer_extend<I: 'n + Send + Clone>(
_: impl Ctx,
#[implementations(Table<Artboard>, Table<Graphic>, Table<Vector>, Table<Raster<CPU>>, Table<Raster<GPU>>, Table<Color>, Table<GradientStops>)] base: Table<I>,
#[expose]
#[implementations(Table<Artboard>, Table<Graphic>, Table<Vector>, Table<Raster<CPU>>, Table<Raster<GPU>>, Table<Color>, Table<GradientStops>)]
new: Table<I>,
nested_node_path: Vec<NodeId>,
) -> Table<I> {
// Get the penultimate element of the node path, or None if the path is too short
// This is used to get the ID of the user-facing parent layer-style node (which encapsulates this internal node).
let source_node_id = nested_node_path.get(nested_node_path.len().wrapping_sub(2)).copied();
let mut base = base;
for row in new.into_iter() {
base.push(TableRow { source_node_id, ..row });
}
base
}
/// Places a table of graphical content into an element of a new wrapper graphic table.
#[node_macro::node(category("General"))]
async fn wrap_graphic<T: Into<Graphic> + 'n>(
_: impl Ctx,
#[implementations(
Table<Graphic>,
Table<Vector>,
Table<Raster<CPU>>,
Table<Raster<GPU>>,
Table<Color>,
Table<GradientStops>,
DAffine2,
)]
content: T,
) -> Table<Graphic> {
Table::new_from_element(content.into())
}
/// Converts a table of graphical content into a graphic table by placing it into an element of a new wrapper graphic table.
/// If it is already a graphic table, it is not wrapped again. Use the 'Wrap Graphic' node if wrapping is always desired.
#[node_macro::node(category("Type Conversion"))]
async fn to_graphic<T: Into<Table<Graphic>> + 'n>(
_: impl Ctx,
#[implementations(
Table<Graphic>,
Table<Vector>,
Table<Raster<CPU>>,
Table<Raster<GPU>>,
Table<Color>,
Table<GradientStops>,
)]
content: T,
) -> Table<Graphic> {
content.into()
}
#[node_macro::node(category("General"))]
async fn flatten_graphic(_: impl Ctx, content: Table<Graphic>, fully_flatten: bool) -> Table<Graphic> {
// TODO: Avoid mutable reference, instead return a new Table<Graphic>?
fn flatten_table(output_graphic_table: &mut Table<Graphic>, current_graphic_table: Table<Graphic>, fully_flatten: bool, recursion_depth: usize) {
for current_row in current_graphic_table.iter() {
let current_element = current_row.element.clone();
let reference = *current_row.source_node_id;
let recurse = fully_flatten || recursion_depth == 0;
match current_element {
// If we're allowed to recurse, flatten any graphics we encounter
Graphic::Graphic(mut current_element) if recurse => {
// Apply the parent graphic's transform to all child elements
for graphic in current_element.iter_mut() {
*graphic.transform = *current_row.transform * *graphic.transform;
}
flatten_table(output_graphic_table, current_element, fully_flatten, recursion_depth + 1);
}
// Push any leaf Graphic elements we encounter, which can be either Graphic table elements beyond the recursion depth, or table elements other than Graphic tables
_ => {
output_graphic_table.push(TableRow {
element: current_element,
mask: current_row.mask.clone(),
transform: *current_row.transform,
alpha_blending: *current_row.alpha_blending,
source_node_id: reference,
});
}
}
}
}
let mut output = Table::new();
flatten_table(&mut output, content, fully_flatten, 0);
output
}
#[node_macro::node(category("Vector"))]
async fn flatten_vector(_: impl Ctx, content: Table<Graphic>) -> Table<Vector> {
// TODO: Avoid mutable reference, instead return a new Table<Graphic>?
fn flatten_table(output_vector_table: &mut Table<Vector>, current_graphic_table: Table<Graphic>) {
for current_graphic_row in current_graphic_table.iter() {
let current_graphic = current_graphic_row.element.clone();
let source_node_id = *current_graphic_row.source_node_id;
match current_graphic {
// If we're allowed to recurse, flatten any tables we encounter
Graphic::Graphic(mut current_graphic_table) => {
// Apply the parent graphic's transform to all child elements
for graphic in current_graphic_table.iter_mut() {
*graphic.transform = *current_graphic_row.transform * *graphic.transform;
}
flatten_table(output_vector_table, current_graphic_table);
}
// Push any leaf Vector elements we encounter
Graphic::Vector(vector_table) => {
for current_vector_row in vector_table.iter() {
output_vector_table.push(TableRow {
element: current_vector_row.element.clone(),
mask: current_vector_row.mask.clone(),
transform: *current_graphic_row.transform * *current_vector_row.transform,
alpha_blending: AlphaBlending {
blend_mode: current_vector_row.alpha_blending.blend_mode,
opacity: current_graphic_row.alpha_blending.opacity * current_vector_row.alpha_blending.opacity,
fill: current_vector_row.alpha_blending.fill,
clip: current_vector_row.alpha_blending.clip,
},
source_node_id,
});
}
}
_ => {}
}
}
}
let mut output = Table::new();
flatten_table(&mut output, content);
output
}
/// Returns the value at the specified index in the collection.
/// If that index has no value, the type's default value is returned.
#[node_macro::node(category("General"))]
fn index<T: AtIndex + Clone + Default>(
_: impl Ctx,
/// The collection of data, such as a list or table.
#[implementations(
Vec<f64>,
Vec<u32>,
Vec<u64>,
Vec<DVec2>,
Table<Artboard>,
Table<Graphic>,
Table<Vector>,
Table<Raster<CPU>>,
Table<Raster<GPU>>,
Table<Color>,
Table<GradientStops>,
)]
collection: T,
/// The index of the item to retrieve, starting from 0 for the first item.
index: u32,
) -> T::Output
where
T::Output: Clone + Default,
{
collection.at_index(index as usize).unwrap_or_default()
}
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(serde::Serialize, serde::Deserialize)]
#[serde(untagged)]
enum EitherFormat {
OldGraphicGroup(OldGraphicGroup),
Table(serde_json::Value),
}
Ok(match EitherFormat::deserialize(deserializer)? {
EitherFormat::OldGraphicGroup(old) => {
let mut graphic_table = Table::new();
for (graphic, source_node_id) in old.elements {
graphic_table.push(TableRow {
element: graphic,
mask: None,
transform: old.transform,
alpha_blending: old.alpha_blending,
source_node_id,
});
}
graphic_table
}
EitherFormat::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(),
mask: None,
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

@@ -1,309 +0,0 @@
use crate::transform::ApplyTransform;
use crate::uuid::NodeId;
use crate::{AlphaBlending, GraphicElement};
use dyn_any::StaticType;
use glam::DAffine2;
use std::hash::Hash;
pub type Mask = Option<GraphicElement>;
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct Instances<T> {
#[serde(alias = "instances")]
instance: Vec<T>,
#[serde(default = "one_mask_default")]
mask: Vec<Mask>,
#[serde(default = "one_daffine2_default")]
transform: Vec<DAffine2>,
#[serde(default = "one_alpha_blending_default")]
alpha_blending: Vec<AlphaBlending>,
#[serde(default = "one_source_node_id_default")]
source_node_id: Vec<Option<NodeId>>,
}
impl<T> Instances<T> {
pub fn new(instance: T) -> Self {
Self {
instance: vec![instance],
mask: vec![None],
transform: vec![DAffine2::IDENTITY],
alpha_blending: vec![AlphaBlending::default()],
source_node_id: vec![None],
}
}
pub fn new_instance(instance: Instance<T>) -> Self {
Self {
instance: vec![instance.instance],
mask: vec![instance.mask],
transform: vec![instance.transform],
alpha_blending: vec![instance.alpha_blending],
source_node_id: vec![instance.source_node_id],
}
}
pub fn with_capacity(capacity: usize) -> Self {
Self {
instance: Vec::with_capacity(capacity),
mask: Vec::with_capacity(capacity),
transform: Vec::with_capacity(capacity),
alpha_blending: Vec::with_capacity(capacity),
source_node_id: Vec::with_capacity(capacity),
}
}
pub fn push(&mut self, instance: Instance<T>) {
self.instance.push(instance.instance);
self.mask.push(instance.mask);
self.transform.push(instance.transform);
self.alpha_blending.push(instance.alpha_blending);
self.source_node_id.push(instance.source_node_id);
}
pub fn extend(&mut self, instances: Instances<T>) {
self.instance.extend(instances.instance);
self.transform.extend(instances.transform);
self.alpha_blending.extend(instances.alpha_blending);
self.source_node_id.extend(instances.source_node_id);
}
pub fn instance_iter(self) -> impl DoubleEndedIterator<Item = Instance<T>> {
self.instance
.into_iter()
.zip(self.mask)
.zip(self.transform)
.zip(self.alpha_blending)
.zip(self.source_node_id)
.map(|((((instance, mask), transform), alpha_blending), source_node_id)| Instance {
instance,
mask,
transform,
alpha_blending,
source_node_id,
})
}
pub fn instance_ref_iter(&self) -> impl DoubleEndedIterator<Item = InstanceRef<'_, T>> + Clone {
self.instance
.iter()
.zip(self.mask.iter())
.zip(self.transform.iter())
.zip(self.alpha_blending.iter())
.zip(self.source_node_id.iter())
.map(|((((instance, mask), transform), alpha_blending), source_node_id)| InstanceRef {
instance,
mask,
transform,
alpha_blending,
source_node_id,
})
}
pub fn instance_mut_iter(&mut self) -> impl DoubleEndedIterator<Item = InstanceMut<'_, T>> {
self.instance
.iter_mut()
.zip(self.mask.iter_mut())
.zip(self.transform.iter_mut())
.zip(self.alpha_blending.iter_mut())
.zip(self.source_node_id.iter_mut())
.map(|((((instance, mask), transform), alpha_blending), source_node_id)| InstanceMut {
instance,
mask,
transform,
alpha_blending,
source_node_id,
})
}
pub fn get(&self, index: usize) -> Option<InstanceRef<'_, T>> {
if index >= self.instance.len() {
return None;
}
Some(InstanceRef {
instance: &self.instance[index],
mask: &self.mask[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<InstanceMut<'_, T>> {
if index >= self.instance.len() {
return None;
}
Some(InstanceMut {
instance: &mut self.instance[index],
mask: &mut self.mask[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.instance.len()
}
pub fn is_empty(&self) -> bool {
self.instance.is_empty()
}
}
impl<T> Default for Instances<T> {
fn default() -> Self {
Self {
instance: Vec::new(),
mask: Vec::new(),
transform: Vec::new(),
alpha_blending: Vec::new(),
source_node_id: Vec::new(),
}
}
}
impl<T: Hash> Hash for Instances<T> {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
for instance in &self.instance {
instance.hash(state);
}
}
}
impl<T> ApplyTransform for Instances<T> {
fn apply_transform(&mut self, modification: &DAffine2) {
for transform in &mut self.transform {
*transform *= *modification;
}
}
fn left_apply_transform(&mut self, modification: &DAffine2) {
for transform in &mut self.transform {
*transform = *modification * *transform;
}
}
}
impl<T: PartialEq> PartialEq for Instances<T> {
fn eq(&self, other: &Self) -> bool {
self.instance.len() == other.instance.len() && { self.instance.iter().zip(other.instance.iter()).all(|(a, b)| a == b) }
}
}
unsafe impl<T: StaticType + 'static> StaticType for Instances<T> {
type Static = Instances<T>;
}
fn one_mask_default() -> Vec<Mask> {
vec![None]
}
impl<T> FromIterator<Instance<T>> for Instances<T> {
fn from_iter<I: IntoIterator<Item = Instance<T>>>(iter: I) -> Self {
let iter = iter.into_iter();
let (lower, _) = iter.size_hint();
let mut instances = Self::with_capacity(lower);
for instance in iter {
instances.push(instance);
}
instances
}
}
fn one_daffine2_default() -> Vec<DAffine2> {
vec![DAffine2::IDENTITY]
}
fn one_alpha_blending_default() -> Vec<AlphaBlending> {
vec![AlphaBlending::default()]
}
fn one_source_node_id_default() -> Vec<Option<NodeId>> {
vec![None]
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct InstanceRef<'a, T> {
pub instance: &'a T,
pub mask: &'a Mask,
pub transform: &'a DAffine2,
pub alpha_blending: &'a AlphaBlending,
pub source_node_id: &'a Option<NodeId>,
}
impl<T> InstanceRef<'_, T> {
pub fn to_instance_cloned(self) -> Instance<T>
where
T: Clone,
{
Instance {
instance: self.instance.clone(),
mask: self.mask.clone(),
transform: *self.transform,
alpha_blending: *self.alpha_blending,
source_node_id: *self.source_node_id,
}
}
}
#[derive(Debug)]
pub struct InstanceMut<'a, T> {
pub instance: &'a mut T,
pub mask: &'a mut Mask,
pub transform: &'a mut DAffine2,
pub alpha_blending: &'a mut AlphaBlending,
pub source_node_id: &'a mut Option<NodeId>,
}
#[derive(Clone, Default, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct Instance<T> {
pub instance: T,
pub mask: Mask,
pub transform: DAffine2,
pub alpha_blending: AlphaBlending,
pub source_node_id: Option<NodeId>,
}
impl<T> Instance<T> {
pub fn to_graphic_element<U>(self) -> Instance<U>
where
T: Into<U>,
{
Instance {
instance: self.instance.into(),
mask: self.mask,
transform: self.transform,
alpha_blending: self.alpha_blending,
source_node_id: self.source_node_id,
}
}
pub fn to_instance_ref(&self) -> InstanceRef<'_, T> {
InstanceRef {
instance: &self.instance,
mask: &self.mask,
transform: &self.transform,
alpha_blending: &self.alpha_blending,
source_node_id: &self.source_node_id,
}
}
pub fn to_instance_mut(&mut self) -> InstanceMut<'_, T> {
InstanceMut {
instance: &mut self.instance,
mask: &mut self.mask,
transform: &mut self.transform,
alpha_blending: &mut self.alpha_blending,
source_node_id: &mut self.source_node_id,
}
}
pub fn to_table(self) -> Instances<T> {
Instances {
instance: vec![self.instance],
mask: vec![self.mask],
transform: vec![self.transform],
alpha_blending: vec![self.alpha_blending],
source_node_id: vec![self.source_node_id],
}
}
}

View File

@@ -2,18 +2,16 @@
extern crate log;
pub mod animation;
pub mod blending;
pub mod artboard;
pub mod blending_nodes;
pub mod bounds;
pub mod color;
pub mod consts;
pub mod context;
pub mod debug;
pub mod extract_xy;
pub mod generic;
pub mod gradient;
pub mod graphic_element;
pub mod instances;
pub mod graphic;
pub mod logic;
pub mod math;
pub mod memo;
@@ -23,7 +21,8 @@ pub mod raster;
pub mod raster_types;
pub mod registry;
pub mod render_complexity;
pub mod structural;
pub mod subpath;
pub mod table;
pub mod text;
pub mod transform;
pub mod transform_nodes;
@@ -32,14 +31,19 @@ pub mod value;
pub mod vector;
pub use crate as graphene_core;
pub use artboard::Artboard;
pub use blending::*;
pub use color::Color;
pub use context::*;
pub use ctor;
pub use dyn_any::{StaticTypeSized, WasmNotSend, WasmNotSync};
pub use graphic_element::{Artboard, ArtboardGroupTable, GraphicElement, GraphicGroupTable};
pub use graphene_core_shaders::AsU32;
pub use graphene_core_shaders::blending;
pub use graphene_core_shaders::choice_type;
pub use graphene_core_shaders::color;
pub use graphic::Graphic;
pub use memo::MemoHash;
pub use num_traits;
pub use raster::Color;
use std::any::TypeId;
use std::future::Future;
use std::pin::Pin;
@@ -165,12 +169,3 @@ pub trait NodeInputDecleration {
fn identifier() -> ProtoNodeIdentifier;
type Result;
}
pub trait AsU32 {
fn as_u32(&self) -> u32;
}
impl AsU32 for u32 {
fn as_u32(&self) -> u32 {
*self
}
}

View File

@@ -1,23 +1,23 @@
use crate::ArtboardGroupTable;
use crate::Artboard;
use crate::Color;
use crate::GraphicElement;
use crate::GraphicGroupTable;
use crate::Graphic;
use crate::gradient::GradientStops;
use crate::graphene_core::registry::types::TextArea;
use crate::raster_types::{CPU, GPU, RasterDataTable};
use crate::vector::VectorDataTable;
use crate::raster_types::{CPU, GPU, Raster};
use crate::table::Table;
use crate::vector::Vector;
use crate::{Context, Ctx};
use glam::{DAffine2, DVec2};
#[node_macro::node(category("Text"))]
fn to_string<T: std::fmt::Debug>(_: impl Ctx, #[implementations(String, bool, f64, u32, u64, DVec2, DAffine2, VectorDataTable)] value: T) -> String {
format!("{:?}", value)
#[node_macro::node(category("Type Conversion"))]
fn to_string<T: std::fmt::Debug>(_: impl Ctx, #[implementations(bool, f64, u32, u64, DVec2, DAffine2, String)] value: T) -> String {
format!("{value:?}")
}
#[node_macro::node(category("Text"))]
fn serialize<T: serde::Serialize>(
_: impl Ctx,
#[implementations(String, bool, f64, u32, u64, DVec2, DAffine2, Color, Option<Color>, GraphicGroupTable, VectorDataTable, RasterDataTable<CPU>)] value: T,
#[implementations(String, bool, f64, u32, u64, DVec2, DAffine2, Table<Artboard>, Table<Graphic>, Table<Vector>, Table<Raster<CPU>>, Table<Color>, Table<GradientStops>)] value: T,
) -> String {
serde_json::to_string(&value).unwrap_or_else(|_| "Serialization Error".to_string())
}
@@ -59,14 +59,12 @@ async fn switch<T, C: Send + 'n + Clone>(
Context -> u64,
Context -> DVec2,
Context -> DAffine2,
Context -> ArtboardGroupTable,
Context -> VectorDataTable,
Context -> GraphicGroupTable,
Context -> RasterDataTable<CPU>,
Context -> RasterDataTable<GPU>,
Context -> GraphicElement,
Context -> Color,
Context -> Option<Color>,
Context -> Table<Artboard>,
Context -> Table<Graphic>,
Context -> Table<Vector>,
Context -> Table<Raster<CPU>>,
Context -> Table<Raster<GPU>>,
Context -> Table<Color>,
Context -> GradientStops,
)]
if_true: impl Node<C, Output = T>,
@@ -80,14 +78,12 @@ async fn switch<T, C: Send + 'n + Clone>(
Context -> u64,
Context -> DVec2,
Context -> DAffine2,
Context -> ArtboardGroupTable,
Context -> VectorDataTable,
Context -> GraphicGroupTable,
Context -> RasterDataTable<CPU>,
Context -> RasterDataTable<GPU>,
Context -> GraphicElement,
Context -> Color,
Context -> Option<Color>,
Context -> Table<Artboard>,
Context -> Table<Graphic>,
Context -> Table<Vector>,
Context -> Table<Raster<CPU>>,
Context -> Table<Raster<GPU>>,
Context -> Table<Color>,
Context -> GradientStops,
)]
if_false: impl Node<C, Output = T>,

View File

@@ -1,16 +1,23 @@
use crate::math::quad::Quad;
use crate::math::rect::Rect;
use bezier_rs::Bezier;
use crate::subpath::Bezier;
use crate::vector::misc::dvec2_to_point;
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 {

View File

@@ -1,4 +1,5 @@
pub mod bbox;
pub mod math_ext;
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

@@ -8,6 +8,21 @@ use std::sync::Arc;
use std::sync::Mutex;
/// Caches the output of a given Node and acts as a proxy
///
/// ```text
/// ┌───────────────┐ ┌───────────────┐
/// │ │◄───┤ │◄─── EVAL (START)
/// │ CacheNode │ │ F │
/// │ ├───►│ │───► RESULT (END)
/// ┌───────────────┐ ├───────────────┤ └───────────────┘
/// │ │◄───┤ │
/// │ G │ │ Cached Data │
/// │ ├───►│ │
/// └───────────────┘ └───────────────┘
/// ```
///
/// The call from `F` directly reaches the `CacheNode` and the `CacheNode` can decide whether to call `G.eval(input_from_f)`
/// in the event of a cache miss or just return the cached data in the event of a cache hit.
#[derive(Default)]
pub struct MemoNode<T, CachedNode> {
cache: Arc<Mutex<Option<(u64, T)>>>,
@@ -50,6 +65,7 @@ impl<T, CachedNode> MemoNode<T, CachedNode> {
}
}
#[allow(clippy::module_inception)]
pub mod memo {
pub const IDENTIFIER: crate::ProtoNodeIdentifier = crate::ProtoNodeIdentifier::new("graphene_core::memo::MemoNode");
}
@@ -155,10 +171,10 @@ pub mod monitor {
pub const IDENTIFIER: crate::ProtoNodeIdentifier = crate::ProtoNodeIdentifier::new("graphene_core::memo::MonitorNode");
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)]
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub struct MemoHash<T: Hash> {
hash: u64,
value: T,
value: Arc<T>,
}
impl<'de, T: serde::Deserialize<'de> + Hash> serde::Deserialize<'de> for MemoHash<T> {
@@ -182,10 +198,10 @@ impl<T: Hash + serde::Serialize> serde::Serialize for MemoHash<T> {
impl<T: Hash> MemoHash<T> {
pub fn new(value: T) -> Self {
let hash = Self::calc_hash(&value);
Self { hash, value }
Self { hash, value: value.into() }
}
pub fn new_with_hash(value: T, hash: u64) -> Self {
Self { hash, value }
Self { hash, value: value.into() }
}
fn calc_hash(data: &T) -> u64 {
@@ -197,7 +213,7 @@ impl<T: Hash> MemoHash<T> {
pub fn inner_mut(&mut self) -> MemoHashGuard<'_, T> {
MemoHashGuard { inner: self }
}
pub fn into_inner(self) -> T {
pub fn into_inner(self) -> Arc<T> {
self.value
}
pub fn hash_code(&self) -> u64 {
@@ -243,8 +259,8 @@ impl<T: Hash> Deref for MemoHashGuard<'_, T> {
}
}
impl<T: Hash> std::ops::DerefMut for MemoHashGuard<'_, T> {
impl<T: Hash + Clone> std::ops::DerefMut for MemoHashGuard<'_, T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.inner.value
Arc::make_mut(&mut self.inner.value)
}
}

View File

@@ -60,3 +60,30 @@ impl Clampable for DVec2 {
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<graphene_core_shaders::color::Color>, D::Error> {
use crate::table::Table;
use graphene_core_shaders::color::Color;
use serde::Deserialize;
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(untagged)]
enum EitherFormat {
Color(Color),
OptionalColor(Option<Color>),
ColorTable(Table<Color>),
}
Ok(match EitherFormat::deserialize(deserializer)? {
EitherFormat::Color(color) => Table::new_from_element(color),
EitherFormat::OptionalColor(color) => {
if let Some(color) = color {
Table::new_from_element(color)
} else {
Table::new()
}
}
EitherFormat::ColorTable(color_table) => color_table,
})
}

View File

@@ -1,3 +1,5 @@
use graphene_core_shaders::Ctx;
use crate::Node;
use std::marker::PhantomData;
@@ -41,28 +43,9 @@ impl<'i, N: for<'a> Node<'a, I> + Clone, I: 'i> Clone for TypeNode<N, I, <N as N
}
impl<'i, N: for<'a> Node<'a, I> + Copy, I: 'i> Copy for TypeNode<N, I, <N as Node<'i, I>>::Output> {}
// Into
pub struct IntoNode<O>(PhantomData<O>);
impl<O> IntoNode<O> {
pub const fn new() -> Self {
Self(PhantomData)
}
}
impl<O> Default for IntoNode<O> {
fn default() -> Self {
Self::new()
}
}
impl<'input, I: 'input, O: 'input> Node<'input, I> for IntoNode<O>
where
I: Into<O> + Sync + Send,
{
type Output = dyn_any::DynFuture<'input, O>;
#[inline]
fn eval(&'input self, input: I) -> Self::Output {
Box::pin(async move { input.into() })
}
#[node_macro::node(skip_impl)]
fn into<'i, T: 'i + Send + Into<O>, O: 'i + Send>(_: impl Ctx, value: T, _out_ty: PhantomData<O>) -> O {
value.into()
}
/// The [`Convert`] trait allows for conversion between Rust primitive numeric types.
@@ -122,25 +105,9 @@ impl_convert!(u128);
impl_convert!(isize);
impl_convert!(usize);
// Convert
pub struct ConvertNode<O>(PhantomData<O>);
impl<_O> ConvertNode<_O> {
pub const fn new() -> Self {
Self(core::marker::PhantomData)
}
}
impl<_O> Default for ConvertNode<_O> {
fn default() -> Self {
Self::new()
}
}
impl<'input, I: 'input + Convert<_O> + Sync + Send, _O: 'input> Node<'input, I> for ConvertNode<_O> {
type Output = ::dyn_any::DynFuture<'input, _O>;
#[inline]
fn eval(&'input self, input: I) -> Self::Output {
Box::pin(async move { input.convert() })
}
#[node_macro::node(skip_impl)]
fn convert<'i, T: 'i + Send + Convert<O>, O: 'i + Send>(_: impl Ctx, value: T, _out_ty: PhantomData<O>) -> O {
value.convert()
}
#[cfg(test)]

View File

@@ -1,12 +1,3 @@
use crate::GraphicGroupTable;
pub use crate::color::*;
use crate::raster_types::{CPU, RasterDataTable};
use crate::vector::VectorDataTable;
use std::fmt::Debug;
#[cfg(target_arch = "spirv")]
use spirv_std::num_traits::float::Float;
/// as to not yet rename all references
pub mod color {
pub use super::*;
@@ -15,6 +6,9 @@ pub mod color {
pub mod image;
pub use self::image::Image;
pub use crate::color::*;
use crate::raster_types::CPU;
use std::fmt::Debug;
pub trait Bitmap {
type Pixel: Pixel;

View File

@@ -1,8 +1,9 @@
use super::Color;
use crate::AlphaBlending;
use crate::color::float_to_srgb_u8;
use crate::instances::{Instance, Instances};
use crate::raster_types::Raster;
use crate::table::{Table, TableRow};
use crate::vector::Vector;
use core::hash::{Hash, Hasher};
use dyn_any::{DynAny, StaticType};
use glam::{DAffine2, DVec2};
@@ -50,6 +51,13 @@ pub struct Image<P: Pixel> {
// TODO: Currently it is always anchored at the top left corner at (0, 0). The bottom right corner of the new origin field would correspond to (1, 1).
}
#[derive(Debug, Clone, dyn_any::DynAny, Default, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
pub struct TransformImage(pub DAffine2);
impl Hash for TransformImage {
fn hash<H: std::hash::Hasher>(&self, _: &mut H) {}
}
impl<P: Pixel + Debug> Debug for Image<P> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let length = self.data.len();
@@ -205,25 +213,23 @@ impl<P: Pixel> IntoIterator for Image<P> {
}
// TODO: Eventually remove this migration document upgrade code
pub fn migrate_image_frame<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<RasterDataTable<CPU>, D::Error> {
pub fn migrate_image_frame<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<Table<Raster<CPU>>, D::Error> {
use serde::Deserialize;
type ImageFrameTable<P> = Instances<Image<P>>;
#[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(ImageFrameTable<Color>),
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(ImageFrameTable::new(Image::deserialize(deserializer)?)))
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(image_instances) => image_instances.serialize(serializer),
RasterFrame::ImageFrame(table) => table.serialize(serializer),
}
}
}
@@ -231,9 +237,9 @@ pub fn migrate_image_frame<'de, D: serde::Deserializer<'de>>(deserializer: D) ->
#[derive(Clone, Debug, Hash, PartialEq, DynAny, serde::Serialize, serde::Deserialize)]
pub enum GraphicElement {
/// Equivalent to the SVG <g> tag: https://developer.mozilla.org/en-US/docs/Web/SVG/Element/g
GraphicGroup(GraphicGroupTable),
GraphicGroup(Table<GraphicElement>),
/// A vector shape, equivalent to the SVG <path> tag: https://developer.mozilla.org/en-US/docs/Web/SVG/Element/path
VectorData(VectorDataTable),
VectorData(Table<Vector>),
RasterFrame(RasterFrame),
}
@@ -243,16 +249,16 @@ pub fn migrate_image_frame<'de, D: serde::Deserializer<'de>>(deserializer: D) ->
}
impl From<ImageFrame<Color>> for GraphicElement {
fn from(image_frame: ImageFrame<Color>) -> Self {
GraphicElement::RasterFrame(RasterFrame::ImageFrame(ImageFrameTable::new(image_frame.image)))
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.instance_ref_iter().next().unwrap().instance.clone(),
image: image.iter().next().unwrap().element.clone(),
},
_ => panic!("Expected Image, found {:?}", element),
_ => panic!("Expected Image, found {element:?}"),
}
}
}
@@ -277,54 +283,51 @@ pub fn migrate_image_frame<'de, D: serde::Deserializer<'de>>(deserializer: D) ->
enum FormatVersions {
Image(Image<Color>),
OldImageFrame(OldImageFrame<Color>),
ImageFrame(Instances<ImageFrame<Color>>),
ImageFrameTable(ImageFrameTable<Color>),
RasterDataTable(RasterDataTable<CPU>),
ImageFrameTable(Table<ImageFrame<Color>>),
ImageTable(Table<Image<Color>>),
RasterTable(Table<Raster<CPU>>),
}
Ok(match FormatVersions::deserialize(deserializer)? {
FormatVersions::Image(image) => RasterDataTable::new(Raster::new_cpu(image)),
FormatVersions::OldImageFrame(image_frame_with_transform_and_blending) => {
let OldImageFrame { image, transform, alpha_blending } = image_frame_with_transform_and_blending;
let mut image_frame_table = RasterDataTable::new(Raster::new_cpu(image));
*image_frame_table.instance_mut_iter().next().unwrap().transform = transform;
*image_frame_table.instance_mut_iter().next().unwrap().alpha_blending = alpha_blending;
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::ImageFrame(image_frame) => RasterDataTable::new(Raster::new_cpu(
FormatVersions::ImageFrameTable(image_frame) => Table::new_from_element(Raster::new_cpu(
image_frame
.instance_ref_iter()
.iter()
.next()
.unwrap_or(Instances::new(ImageFrame::default()).instance_ref_iter().next().unwrap())
.instance
.unwrap_or(Table::new_from_element(ImageFrame::default()).iter().next().unwrap())
.element
.image
.clone(),
)),
FormatVersions::ImageFrameTable(image_frame_table) => RasterDataTable::new(Raster::new_cpu(image_frame_table.instance_ref_iter().next().unwrap().instance.clone())),
FormatVersions::RasterDataTable(raster_data_table) => raster_data_table,
FormatVersions::ImageTable(table) => Table::new_from_element(Raster::new_cpu(table.iter().next().unwrap().element.clone())),
FormatVersions::RasterTable(table) => table,
})
}
// TODO: Eventually remove this migration document upgrade code
pub fn migrate_image_frame_instance<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<Instance<Raster<CPU>>, D::Error> {
pub fn migrate_image_frame_row<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<TableRow<Raster<CPU>>, D::Error> {
use serde::Deserialize;
type ImageFrameTable<P> = Instances<Image<P>>;
#[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(ImageFrameTable<Color>),
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(ImageFrameTable::new(Image::deserialize(deserializer)?)))
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(image_instances) => image_instances.serialize(serializer),
RasterFrame::ImageFrame(table) => table.serialize(serializer),
}
}
}
@@ -332,9 +335,9 @@ pub fn migrate_image_frame_instance<'de, D: serde::Deserializer<'de>>(deserializ
#[derive(Clone, Debug, Hash, PartialEq, DynAny, serde::Serialize, serde::Deserialize)]
pub enum GraphicElement {
/// Equivalent to the SVG <g> tag: https://developer.mozilla.org/en-US/docs/Web/SVG/Element/g
GraphicGroup(GraphicGroupTable),
GraphicGroup(Table<GraphicElement>),
/// A vector shape, equivalent to the SVG <path> tag: https://developer.mozilla.org/en-US/docs/Web/SVG/Element/path
VectorData(VectorDataTable),
VectorData(Table<Vector>),
RasterFrame(RasterFrame),
}
@@ -344,16 +347,16 @@ pub fn migrate_image_frame_instance<'de, D: serde::Deserializer<'de>>(deserializ
}
impl From<ImageFrame<Color>> for GraphicElement {
fn from(image_frame: ImageFrame<Color>) -> Self {
GraphicElement::RasterFrame(RasterFrame::ImageFrame(ImageFrameTable::new(image_frame.image)))
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.instance_ref_iter().next().unwrap().instance.clone(),
image: image.iter().next().unwrap().element.clone(),
},
_ => panic!("Expected Image, found {:?}", element),
_ => panic!("Expected Image, found {element:?}"),
}
}
}
@@ -378,34 +381,32 @@ pub fn migrate_image_frame_instance<'de, D: serde::Deserializer<'de>>(deserializ
enum FormatVersions {
Image(Image<Color>),
OldImageFrame(OldImageFrame<Color>),
ImageFrame(Instances<ImageFrame<Color>>),
RasterDataTable(RasterDataTable<CPU>),
ImageInstance(Instance<Raster<CPU>>),
ImageFrameTable(Table<ImageFrame<Color>>),
RasterTable(Table<Raster<CPU>>),
RasterTableRow(TableRow<Raster<CPU>>),
}
Ok(match FormatVersions::deserialize(deserializer)? {
FormatVersions::Image(image) => Instance {
instance: Raster::new_cpu(image),
FormatVersions::Image(image) => TableRow {
element: Raster::new_cpu(image),
..Default::default()
},
FormatVersions::OldImageFrame(image_frame_with_transform_and_blending) => Instance {
instance: Raster::new_cpu(image_frame_with_transform_and_blending.image),
FormatVersions::OldImageFrame(image_frame_with_transform_and_blending) => TableRow {
element: Raster::new_cpu(image_frame_with_transform_and_blending.image),
mask: None,
transform: image_frame_with_transform_and_blending.transform,
alpha_blending: image_frame_with_transform_and_blending.alpha_blending,
source_node_id: None,
},
FormatVersions::ImageFrame(image_frame) => Instance {
instance: Raster::new_cpu(image_frame.instance_ref_iter().next().unwrap().instance.image.clone()),
FormatVersions::ImageFrameTable(image_frame) => TableRow {
element: Raster::new_cpu(image_frame.iter().next().unwrap().element.image.clone()),
..Default::default()
},
FormatVersions::RasterDataTable(image_frame_table) => image_frame_table.instance_iter().next().unwrap_or_default(),
FormatVersions::ImageInstance(image_instance) => image_instance,
FormatVersions::RasterTable(image_frame_table) => image_frame_table.into_iter().next().unwrap_or_default(),
FormatVersions::RasterTableRow(image_table_row) => image_table_row,
})
}
// pub type RasterDataTable<P> = Instances<Image<P>>;
impl<P: Debug + Copy + Pixel> Sample for Image<P> {
type Pixel = P;
@@ -452,24 +453,6 @@ impl From<Image<Color>> for Image<SRGBA8> {
}
}
// impl From<RasterDataTable<CPU>> for RasterDataTable<SRGBA8> {
// fn from(image_frame_table: RasterDataTable<CPU>) -> Self {
// let mut result_table = RasterDataTable::<SRGBA8>::default();
// for image_frame_instance in image_frame_table.instance_iter() {
// result_table.push(Instance {
// instance: image_frame_instance.instance,
// mask: image_frame_instance.mask,
// transform: image_frame_instance.transform,
// alpha_blending: image_frame_instance.alpha_blending,
// source_node_id: image_frame_instance.source_node_id,
// });
// }
// result_table
// }
// }
impl From<Image<SRGBA8>> for Image<Color> {
fn from(image: Image<SRGBA8>) -> Self {
let data = image.data.into_iter().map(|x| x.into()).collect();
@@ -496,9 +479,9 @@ mod test {
};
let serialized = serde_json::to_string(&image).unwrap();
println!("{}", serialized);
println!("{serialized}");
let deserialized: Image<Color> = serde_json::from_str(&serialized).unwrap();
println!("{:?}", deserialized);
println!("{deserialized:?}");
assert_eq!(image, deserialized);
}

View File

@@ -1,148 +1,213 @@
use crate::Color;
use crate::bounds::BoundingBox;
use crate::instances::Instances;
use crate::bounds::{BoundingBox, RenderBoundingBox};
use crate::math::quad::Quad;
use crate::raster::Image;
use core::ops::Deref;
use dyn_any::DynAny;
use glam::{DAffine2, DVec2};
#[cfg(feature = "wgpu")]
use std::sync::Arc;
use std::fmt::Debug;
use std::ops::DerefMut;
#[derive(Clone, Debug, Hash, PartialEq, Eq, Copy)]
pub struct CPU;
#[derive(Clone, Debug, Hash, PartialEq, Eq, Copy)]
pub struct GPU;
mod __private {
pub trait Sealed {}
}
trait Storage: 'static {}
impl Storage for CPU {}
impl Storage for GPU {}
pub trait Storage: __private::Sealed + Clone + Debug + 'static {
fn is_empty(&self) -> bool;
}
#[derive(Clone, Debug, Hash, PartialEq)]
#[allow(private_bounds)]
pub struct Raster<T: Storage> {
data: RasterStorage,
#[derive(Clone, Debug, PartialEq, Hash, Default)]
pub struct Raster<T>
where
Raster<T>: Storage,
{
storage: T,
}
unsafe impl<T: Storage> dyn_any::StaticType for Raster<T> {
unsafe impl<T> dyn_any::StaticType for Raster<T>
where
Raster<T>: Storage,
{
type Static = Raster<T>;
}
#[derive(Clone, Debug, Hash, PartialEq, DynAny)]
pub enum RasterStorage {
Cpu(Image<Color>),
#[cfg(feature = "wgpu")]
Gpu(Arc<wgpu::Texture>),
#[cfg(not(feature = "wgpu"))]
Gpu(()),
impl<T> Raster<T>
where
Raster<T>: Storage,
{
pub fn new(t: T) -> Self {
Self { storage: t }
}
}
impl RasterStorage {}
impl Raster<CPU> {
pub fn new_cpu(image: Image<Color>) -> Self {
Self {
data: RasterStorage::Cpu(image),
storage: CPU,
}
}
pub fn data(&self) -> &Image<Color> {
let RasterStorage::Cpu(cpu) = &self.data else { unreachable!() };
cpu
}
pub fn data_mut(&mut self) -> &mut Image<Color> {
let RasterStorage::Cpu(cpu) = &mut self.data else { unreachable!() };
cpu
}
pub fn into_data(self) -> Image<Color> {
let RasterStorage::Cpu(cpu) = self.data else { unreachable!() };
cpu
}
pub fn is_empty(&self) -> bool {
let data = self.data();
data.height == 0 || data.width == 0
}
}
impl Default for Raster<CPU> {
fn default() -> Self {
Self {
data: RasterStorage::Cpu(Image::default()),
storage: CPU,
}
}
}
impl Deref for Raster<CPU> {
type Target = Image<Color>;
impl<T> Deref for Raster<T>
where
Raster<T>: Storage,
{
type Target = T;
fn deref(&self) -> &Self::Target {
self.data()
&self.storage
}
}
#[cfg(feature = "wgpu")]
impl Raster<GPU> {
pub fn new_gpu(image: Arc<wgpu::Texture>) -> Self {
Self {
data: RasterStorage::Gpu(image),
storage: GPU,
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
}
}
pub fn data(&self) -> &wgpu::Texture {
let RasterStorage::Gpu(gpu) = &self.data else { unreachable!() };
gpu
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
}
}
pub fn data_mut(&mut self) -> &mut Arc<wgpu::Texture> {
let RasterStorage::Gpu(gpu) = &mut self.data else { unreachable!() };
gpu
impl Deref for CPU {
type Target = Image<Color>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
pub fn data_owned(&self) -> Arc<wgpu::Texture> {
let RasterStorage::Gpu(gpu) = &self.data else { unreachable!() };
gpu.clone()
impl DerefMut for CPU {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl<'de> serde::Deserialize<'de> for Raster<CPU> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
Ok(Raster::new_cpu(Image::deserialize(deserializer)?))
}
}
impl serde::Serialize for Raster<CPU> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.0.serialize(serializer)
}
}
}
impl Raster<GPU> {
#[cfg(feature = "wgpu")]
pub fn is_empty(&self) -> bool {
let data = self.data();
data.width() == 0 || data.height() == 0
}
#[cfg(not(feature = "wgpu"))]
pub fn is_empty(&self) -> bool {
true
}
}
pub use gpu::GPU;
#[cfg(feature = "wgpu")]
impl Deref for Raster<GPU> {
type Target = wgpu::Texture;
mod gpu {
use super::*;
use crate::raster_types::__private::Sealed;
fn deref(&self) -> &Self::Target {
self.data()
#[derive(Clone, Debug, PartialEq, Hash)]
pub struct GPU {
texture: wgpu::Texture,
}
impl Sealed for Raster<GPU> {}
impl Storage for Raster<GPU> {
fn is_empty(&self) -> bool {
self.texture.width() == 0 || self.texture.height() == 0
}
}
impl Raster<GPU> {
pub fn new_gpu(texture: wgpu::Texture) -> Self {
Self::new(GPU { texture })
}
pub fn data(&self) -> &wgpu::Texture {
&self.texture
}
}
}
pub type RasterDataTable<Storage> = Instances<Raster<Storage>>;
#[cfg(not(feature = "wgpu"))]
mod gpu {
use super::*;
use crate::raster_types::__private::Sealed;
// TODO: Make this not dupliated
impl BoundingBox for RasterDataTable<CPU> {
fn bounding_box(&self, transform: DAffine2, _include_stroke: bool) -> Option<[DVec2; 2]> {
self.instance_ref_iter()
.filter(|instance| !instance.instance.is_empty()) // Eliminate empty images
.flat_map(|instance| {
let transform = transform * *instance.transform;
(transform.matrix2.determinant() != 0.).then(|| (transform * Quad::from_box([DVec2::ZERO, DVec2::ONE])).bounding_box())
})
.reduce(Quad::combine_bounds)
#[derive(Clone, Debug, PartialEq, Hash)]
pub struct GPU;
impl Sealed for Raster<GPU> {}
impl Storage for Raster<GPU> {
fn is_empty(&self) -> bool {
true
}
}
}
impl BoundingBox for RasterDataTable<GPU> {
fn bounding_box(&self, transform: DAffine2, _include_stroke: bool) -> Option<[DVec2; 2]> {
self.instance_ref_iter()
.filter(|instance| !instance.instance.is_empty()) // Eliminate empty images
.flat_map(|instance| {
let transform = transform * *instance.transform;
(transform.matrix2.determinant() != 0.).then(|| (transform * Quad::from_box([DVec2::ZERO, DVec2::ONE])).bounding_box())
})
.reduce(Quad::combine_bounds)
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())
}
}

View File

@@ -1,36 +1,12 @@
use crate::{Node, NodeIO, NodeIOTypes, ProtoNodeIdentifier, Type, WasmNotSend};
use dyn_any::{DynAny, StaticType};
use std::borrow::Cow;
use std::collections::HashMap;
use std::marker::PhantomData;
use std::ops::Deref;
use std::pin::Pin;
use std::sync::{LazyLock, Mutex};
pub mod types {
/// 0% - 100%
pub type Percentage = f64;
/// -100% - 100%
pub type SignedPercentage = f64;
/// -180° - 180°
pub type Angle = f64;
/// Ends in the unit of x
pub type Multiplier = f64;
/// Non-negative integer with px unit
pub type PixelLength = f64;
/// Non-negative
pub type Length = f64;
/// 0 to 1
pub type Fraction = f64;
/// Unsigned integer
pub type IntegerCount = u32;
/// Unsigned integer to be used for random seeds
pub type SeedValue = u32;
/// DVec2 with px unit
pub type PixelSize = glam::DVec2;
/// String with one or more than one line
pub type TextArea = String;
}
pub use graphene_core_shaders::registry::types;
// Translation struct between macro and definition
#[derive(Clone)]
@@ -59,33 +35,6 @@ pub struct FieldMetadata {
pub unit: Option<&'static str>,
}
pub trait ChoiceTypeStatic: Sized + Copy + crate::AsU32 + Send + Sync {
const WIDGET_HINT: ChoiceWidgetHint;
const DESCRIPTION: Option<&'static str>;
fn list() -> &'static [&'static [(Self, VariantMetadata)]];
}
pub enum ChoiceWidgetHint {
Dropdown,
RadioButtons,
}
/// Translation struct between macro and definition.
#[derive(Clone, Debug)]
pub struct VariantMetadata {
/// Name as declared in source code.
pub name: Cow<'static, str>,
/// Name to be displayed in UI.
pub label: Cow<'static, str>,
/// User-facing documentation text.
pub docstring: Option<Cow<'static, str>>,
/// Name of icon to display in radio buttons and such.
pub icon: Option<Cow<'static, str>>,
}
#[derive(Clone, Debug)]
pub enum RegistryWidgetOverride {
None,
@@ -107,20 +56,20 @@ pub static NODE_REGISTRY: NodeRegistry = LazyLock::new(|| Mutex::new(HashMap::ne
pub static NODE_METADATA: LazyLock<Mutex<HashMap<ProtoNodeIdentifier, NodeMetadata>>> = LazyLock::new(|| Mutex::new(HashMap::new()));
#[cfg(not(target_arch = "wasm32"))]
#[cfg(not(target_family = "wasm"))]
pub type DynFuture<'n, T> = Pin<Box<dyn Future<Output = T> + 'n + Send>>;
#[cfg(target_arch = "wasm32")]
#[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_arch = "wasm32"))]
#[cfg(not(target_family = "wasm"))]
pub type Any<'n> = Box<dyn DynAny<'n> + 'n + Send>;
#[cfg(target_arch = "wasm32")]
#[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_arch = "wasm32"))]
#[cfg(not(target_family = "wasm"))]
pub type TypeErasedNode<'n> = dyn for<'i> NodeIO<'i, Any<'i>, Output = FutureAny<'i>> + 'n + Send + Sync;
#[cfg(target_arch = "wasm32")]
#[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>;
@@ -204,13 +153,14 @@ where
{
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 Input {e} in: \n{node_name}"));
let out = dyn_any::downcast(future.await).unwrap_or_else(|e| panic!("DowncastBothNode wrong output type: {e} in: \n{node_name}"));
*out
})
}
@@ -285,7 +235,7 @@ where
};
match dyn_any::downcast(input) {
Ok(input) => Box::pin(output(*input)),
Err(e) => panic!("DynAnyNode Input, {0} in:\n{1}", e, node_name),
Err(e) => panic!("DynAnyNode Input, {e} in:\n{node_name}"),
}
}

View File

@@ -1,8 +1,8 @@
use crate::instances::Instances;
use crate::gradient::GradientStops;
use crate::raster_types::{CPU, GPU, Raster};
use crate::vector::VectorData;
use crate::{Artboard, Color, GraphicElement};
use glam::DVec2;
use crate::table::Table;
use crate::vector::Vector;
use crate::{Artboard, Color, Graphic};
pub trait RenderComplexity {
fn render_complexity(&self) -> usize {
@@ -10,30 +10,32 @@ pub trait RenderComplexity {
}
}
impl<T: RenderComplexity> RenderComplexity for Instances<T> {
impl<T: RenderComplexity> RenderComplexity for Table<T> {
fn render_complexity(&self) -> usize {
self.instance_ref_iter().map(|instance| instance.instance.render_complexity()).fold(0, usize::saturating_add)
self.iter().map(|row| row.element.render_complexity()).fold(0, usize::saturating_add)
}
}
impl RenderComplexity for Artboard {
fn render_complexity(&self) -> usize {
self.graphic_group.render_complexity()
self.content.render_complexity()
}
}
impl RenderComplexity for GraphicElement {
impl RenderComplexity for Graphic {
fn render_complexity(&self) -> usize {
match self {
Self::GraphicGroup(instances) => instances.render_complexity(),
Self::VectorData(instances) => instances.render_complexity(),
Self::RasterDataCPU(instances) => instances.render_complexity(),
Self::RasterDataGPU(instances) => instances.render_complexity(),
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(),
}
}
}
impl RenderComplexity for VectorData {
impl RenderComplexity for Vector {
fn render_complexity(&self) -> usize {
self.segment_domain.ids().len()
}
@@ -52,10 +54,14 @@ impl RenderComplexity for Raster<GPU> {
}
}
impl RenderComplexity for String {}
impl RenderComplexity for bool {}
impl RenderComplexity for f32 {}
impl RenderComplexity for f64 {}
impl RenderComplexity for DVec2 {}
impl RenderComplexity for Option<Color> {}
impl RenderComplexity for Vec<Color> {}
impl RenderComplexity for Color {
fn render_complexity(&self) -> usize {
1
}
}
impl RenderComplexity for GradientStops {
fn render_complexity(&self) -> usize {
1
}
}

View File

@@ -1,153 +0,0 @@
use crate::Node;
use std::marker::PhantomData;
/// This is how we can generically define composition of two nodes.
/// This is done generically as shown: <https://files.keavon.com/-/SurprisedGaseousAnhinga/capture.png>
/// A concrete example: <https://files.keavon.com/-/ExcitableGoldRay/capture.png>
/// And showing the direction of data flow: <https://files.keavon.com/-/SoreShimmeringElephantseal/capture.png>
/// ```text
/// ┌────────────────┐
/// T │ │ U
/// ───────────►│ Compose Node ├───────────►
/// │ │
/// └────┬───────────┤
/// ┌──────────┐ │ │
/// │ │ T -> V │ │
/// │ First ├─────────────►│ │
/// │ │ │ │
/// └──────────┘ │ │
/// ┌──────────┐ │ │
/// │ │ V -> U │ │
/// │ Second ├─────────────►│ │
/// │ │ └───────────┘
/// └──────────┘
/// ```
#[derive(Clone, Copy)]
pub struct ComposeNode<First, Second, I> {
first: First,
second: Second,
phantom: PhantomData<I>,
}
impl<'i, Input: 'i, First, Second> Node<'i, Input> for ComposeNode<First, Second, Input>
where
First: Node<'i, Input>,
Second: Node<'i, <First as Node<'i, Input>>::Output> + 'i,
{
type Output = <Second as Node<'i, <First as Node<'i, Input>>::Output>>::Output;
fn eval(&'i self, input: Input) -> Self::Output {
let arg = self.first.eval(input);
let second = &self.second;
second.eval(arg)
}
}
impl<First, Second, Input> ComposeNode<First, Second, Input> {
pub const fn new(first: First, second: Second) -> Self {
ComposeNode::<First, Second, Input> { first, second, phantom: PhantomData }
}
}
#[derive(Clone)]
pub struct AsyncComposeNode<First, Second, I> {
first: First,
second: Second,
phantom: PhantomData<I>,
}
impl<'i, Input: 'static, First, Second> Node<'i, Input> for AsyncComposeNode<First, Second, Input>
where
First: Node<'i, Input>,
First::Output: Future,
Second: Node<'i, <<First as Node<'i, Input>>::Output as Future>::Output> + 'i,
{
type Output = std::pin::Pin<Box<dyn Future<Output = <Second as Node<'i, <<First as Node<'i, Input>>::Output as Future>::Output>>::Output> + 'i>>;
fn eval(&'i self, input: Input) -> Self::Output {
Box::pin(async move {
let arg = self.first.eval(input).await;
self.second.eval(arg)
})
}
}
impl<'i, First, Second, Input: 'i> AsyncComposeNode<First, Second, Input>
where
First: Node<'i, Input>,
First::Output: Future,
Second: Node<'i, <<First as Node<'i, Input>>::Output as Future>::Output> + 'i,
{
pub const fn new(first: First, second: Second) -> Self {
AsyncComposeNode::<First, Second, Input> { first, second, phantom: PhantomData }
}
}
pub trait Then<'i, Input: 'i>: Sized {
fn then<Second>(self, second: Second) -> ComposeNode<Self, Second, Input>
where
Self: Node<'i, Input>,
Second: Node<'i, <Self as Node<'i, Input>>::Output>,
{
ComposeNode::new(self, second)
}
}
impl<'i, First: Node<'i, Input>, Input: 'i> Then<'i, Input> for First {}
pub trait AndThen<'i, Input: 'i>: Sized {
fn and_then<Second>(self, second: Second) -> AsyncComposeNode<Self, Second, Input>
where
Self: Node<'i, Input>,
Self::Output: Future,
Second: Node<'i, <<Self as Node<'i, Input>>::Output as Future>::Output> + 'i,
{
AsyncComposeNode::new(self, second)
}
}
impl<'i, First: Node<'i, Input>, Input: 'i> AndThen<'i, Input> for First {}
pub struct ConsNode<I: From<()>, Root>(pub Root, PhantomData<I>);
impl<'i, Root, Input: 'i, I: 'i + From<()>> Node<'i, Input> for ConsNode<I, Root>
where
Root: Node<'i, I>,
{
type Output = (Input, Root::Output);
fn eval(&'i self, input: Input) -> Self::Output {
let arg = self.0.eval(I::from(()));
(input, arg)
}
}
impl<'i, Root: Node<'i, I>, I: 'i + From<()>> ConsNode<I, Root> {
pub fn new(root: Root) -> Self {
ConsNode(root, PhantomData)
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::generic::FnNode;
use crate::value::ValueNode;
#[test]
fn compose() {
let value = ValueNode::new(4u32);
let compose = value.then(FnNode::new(|x| x));
assert_eq!(compose.eval(()), &4u32);
let type_erased = &compose as &dyn Node<'_, (), Output = &'_ u32>;
assert_eq!(type_erased.eval(()), &4u32);
}
#[test]
fn test_ref_eval() {
let value = ValueNode::new(5);
assert_eq!(value.eval(()), &5);
let id = FnNode::new(|x| x);
let compose = ComposeNode::new(&value, &id);
assert_eq!(compose.eval(()), &5);
}
}

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,318 @@
use super::consts::*;
use super::*;
use crate::vector::misc::point_to_dvec2;
use glam::DVec2;
use kurbo::PathSeg;
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 % 2 == 0 {
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)
}
}

View File

@@ -0,0 +1,114 @@
use super::consts::MAX_ABSOLUTE_DIFFERENCE;
use super::*;
use crate::math::polynomial::pathseg_to_parametric_polynomial;
use crate::vector::algorithms::bezpath_algorithms::pathseg_length_centroid_and_length;
use crate::vector::algorithms::intersection::{filtered_all_segment_intersections, pathseg_self_intersections};
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,338 @@
use crate::bounds::{BoundingBox, RenderBoundingBox};
use crate::transform::ApplyTransform;
use crate::uuid::NodeId;
use crate::Graphic;
use crate::{AlphaBlending, math::quad::Quad};
use dyn_any::StaticType;
use glam::DAffine2;
use std::hash::Hash;
pub type Mask = Option<Graphic>;
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct Table<T> {
#[serde(alias = "instances", alias = "instance")]
element: Vec<T>,
mask: Vec<Mask>,
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),
mask: 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],
mask: vec![None],
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],
mask: vec![row.mask],
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.mask.push(row.mask);
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.mask.extend(table.mask);
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],
mask: &self.mask[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],
mask: &mut self.mask[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.mask.iter())
.zip(self.transform.iter())
.zip(self.alpha_blending.iter())
.zip(self.source_node_id.iter())
.map(|((((element, mask), transform), alpha_blending), source_node_id)| TableRowRef {
element,
mask,
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.mask.iter_mut())
.zip(self.transform.iter_mut())
.zip(self.alpha_blending.iter_mut())
.zip(self.source_node_id.iter_mut())
.map(|((((element, mask), transform), alpha_blending), source_node_id)| TableRowMut {
element,
mask,
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(),
mask: self.mask.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>,
mask: std::vec::IntoIter<Mask>,
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 mask = self.mask.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,
mask,
transform,
alpha_blending,
source_node_id,
})
}
}
impl<T> Default for Table<T> {
fn default() -> Self {
Self {
element: Vec::new(),
mask: 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);
}
}
}
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;
}
}
}
impl<T: PartialEq> PartialEq for Table<T> {
fn eq(&self, other: &Self) -> bool {
self.element.len() == other.element.len() && { self.element.iter().zip(other.element.iter()).all(|(a, b)| a == b) }
}
}
unsafe impl<T: StaticType + 'static> StaticType for Table<T> {
type Static = Table<T>;
}
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(Clone, Default, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct TableRow<T> {
#[serde(alias = "instance")]
pub element: T,
pub mask: Mask,
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,
mask: None,
transform: DAffine2::IDENTITY,
alpha_blending: AlphaBlending::default(),
source_node_id: None,
}
}
pub fn as_ref(&self) -> TableRowRef<'_, T> {
TableRowRef {
element: &self.element,
mask: &self.mask,
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,
mask: &mut self.mask,
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 mask: &'a Mask,
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(),
mask: self.mask.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 mask: &'a mut Mask,
pub transform: &'a mut DAffine2,
pub alpha_blending: &'a mut AlphaBlending,
pub source_node_id: &'a mut Option<NodeId>,
}

View File

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

View File

@@ -1,10 +1,11 @@
use crate::instances::Instance;
use crate::vector::{PointId, VectorData, VectorDataTable};
use bezier_rs::{ManipulatorGroup, Subpath};
use super::TextAlign;
use crate::subpath::{ManipulatorGroup, Subpath};
use crate::table::{Table, TableRow};
use crate::vector::{PointId, Vector};
use core::cell::RefCell;
use glam::{DAffine2, DVec2};
use parley::fontique::Blob;
use parley::{Alignment, AlignmentOptions, FontContext, GlyphRun, Layout, LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty};
use parley::{AlignmentOptions, FontContext, GlyphRun, Layout, LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty};
use skrifa::GlyphId;
use skrifa::instance::{LocationRef, NormalizedCoord, Size};
use skrifa::outline::{DrawSettings, OutlinePen};
@@ -23,7 +24,7 @@ struct PathBuilder {
current_subpath: Subpath<PointId>,
origin: DVec2,
glyph_subpaths: Vec<Subpath<PointId>>,
vector_table: VectorDataTable,
vector_table: Table<Vector>,
scale: f64,
id: PointId,
}
@@ -50,15 +51,15 @@ impl PathBuilder {
}
if per_glyph_instances {
self.vector_table.push(Instance {
instance: VectorData::from_subpaths(core::mem::take(&mut self.glyph_subpaths), false),
self.vector_table.push(TableRow {
element: Vector::from_subpaths(core::mem::take(&mut self.glyph_subpaths), false),
transform: DAffine2::from_translation(glyph_offset),
..Default::default()
});
} else {
for subpath in self.glyph_subpaths.drain(..) {
// Unwrapping here is ok because `self.vector_table` is initialized with a single `VectorData`
self.vector_table.get_mut(0).unwrap().instance.append_subpath(subpath, false);
// Unwrapping here is ok because `self.vector_table` is initialized with a single `Vector` table element
self.vector_table.get_mut(0).unwrap().element.append_subpath(subpath, false);
}
}
}
@@ -103,6 +104,7 @@ pub struct TypesettingConfig {
pub max_width: Option<f64>,
pub max_height: Option<f64>,
pub tilt: f64,
pub align: TextAlign,
}
impl Default for TypesettingConfig {
@@ -114,6 +116,7 @@ impl Default for TypesettingConfig {
max_width: None,
max_height: None,
tilt: 0.,
align: TextAlign::default(),
}
}
}
@@ -197,24 +200,20 @@ fn layout_text(str: &str, font_data: Option<Blob<u8>>, typesetting: TypesettingC
let mut layout: Layout<()> = builder.build(str);
layout.break_all_lines(typesetting.max_width.map(|mw| mw as f32));
layout.align(typesetting.max_width.map(|max_w| max_w as f32), Alignment::Left, AlignmentOptions::default());
layout.align(typesetting.max_width.map(|max_w| max_w as f32), typesetting.align.into(), AlignmentOptions::default());
Some(layout)
}
pub fn to_path(str: &str, font_data: Option<Blob<u8>>, typesetting: TypesettingConfig, per_glyph_instances: bool) -> VectorDataTable {
pub fn to_path(str: &str, font_data: Option<Blob<u8>>, typesetting: TypesettingConfig, per_glyph_instances: bool) -> Table<Vector> {
let Some(layout) = layout_text(str, font_data, typesetting) else {
return VectorDataTable::new(VectorData::default());
return Table::new_from_element(Vector::default());
};
let mut path_builder = PathBuilder {
current_subpath: Subpath::new(Vec::new(), false),
glyph_subpaths: Vec::new(),
vector_table: if per_glyph_instances {
VectorDataTable::default()
} else {
VectorDataTable::new(VectorData::default())
},
vector_table: if per_glyph_instances { Table::new() } else { Table::new_from_element(Vector::default()) },
scale: layout.scale() as f64,
id: PointId::ZERO,
origin: DVec2::default(),
@@ -229,7 +228,7 @@ pub fn to_path(str: &str, font_data: Option<Blob<u8>>, typesetting: TypesettingC
}
if path_builder.vector_table.is_empty() {
path_builder.vector_table = VectorDataTable::new(VectorData::default());
path_builder.vector_table = Table::new_from_element(Vector::default());
}
path_builder.vector_table

View File

@@ -2,7 +2,7 @@ use crate::Artboard;
use crate::math::bbox::AxisAlignedBbox;
pub use crate::vector::ReferencePoint;
use core::f64;
use glam::{DAffine2, DMat2, DVec2};
use glam::{DAffine2, DMat2, DVec2, UVec2};
pub trait Transform {
fn transform(&self) -> DAffine2;
@@ -89,7 +89,7 @@ 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: glam::UVec2,
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,
}
@@ -103,7 +103,7 @@ impl Default for Footprint {
impl Footprint {
pub const DEFAULT: Self = Self {
transform: DAffine2::IDENTITY,
resolution: glam::UVec2::new(1920, 1080),
resolution: UVec2::new(1920, 1080),
quality: RenderQuality::Full,
};
@@ -112,7 +112,7 @@ impl Footprint {
matrix2: DMat2::from_diagonal(DVec2::splat(f64::INFINITY)),
translation: DVec2::ZERO,
},
resolution: glam::UVec2::new(0, 0),
resolution: UVec2::ZERO,
quality: RenderQuality::Full,
};

View File

@@ -1,10 +1,12 @@
use crate::instances::Instances;
use crate::raster_types::{CPU, GPU, RasterDataTable};
use crate::gradient::GradientStops;
use crate::raster_types::{CPU, GPU, Raster};
use crate::table::Table;
use crate::transform::{ApplyTransform, Footprint, Transform};
use crate::vector::VectorDataTable;
use crate::{CloneVarArgs, Context, Ctx, ExtractAll, GraphicGroupTable, OwnedContextImpl};
use crate::vector::Vector;
use crate::{CloneVarArgs, Context, Ctx, ExtractAll, Graphic, OwnedContextImpl};
use core::f64;
use glam::{DAffine2, DVec2};
use graphene_core_shaders::color::Color;
#[node_macro::node(category(""))]
async fn transform<T: ApplyTransform + 'n + 'static>(
@@ -12,10 +14,12 @@ async fn transform<T: ApplyTransform + 'n + 'static>(
#[implementations(
Context -> DAffine2,
Context -> DVec2,
Context -> VectorDataTable,
Context -> GraphicGroupTable,
Context -> RasterDataTable<CPU>,
Context -> RasterDataTable<GPU>,
Context -> Table<Vector>,
Context -> Table<Graphic>,
Context -> Table<Raster<CPU>>,
Context -> Table<Raster<GPU>>,
Context -> Table<Color>,
Context -> Table<GradientStops>,
)]
value: impl Node<Context<'static>, Output = T>,
translate: DVec2,
@@ -43,10 +47,10 @@ async fn transform<T: ApplyTransform + 'n + 'static>(
#[node_macro::node(category(""))]
fn replace_transform<Data, TransformInput: Transform>(
_: impl Ctx,
#[implementations(VectorDataTable, RasterDataTable<CPU>, GraphicGroupTable)] mut data: Instances<Data>,
#[implementations(Table<Vector>, Table<Raster<CPU>>, Table<Graphic>, Table<Color>, Table<GradientStops>)] mut data: Table<Data>,
#[implementations(DAffine2)] transform: TransformInput,
) -> Instances<Data> {
for data_transform in data.instance_mut_iter() {
) -> Table<Data> {
for data_transform in data.iter_mut() {
*data_transform.transform = transform.transform();
}
data
@@ -56,14 +60,16 @@ fn replace_transform<Data, TransformInput: Transform>(
async fn extract_transform<T>(
_: impl Ctx,
#[implementations(
GraphicGroupTable,
VectorDataTable,
RasterDataTable<CPU>,
RasterDataTable<GPU>,
Table<Graphic>,
Table<Vector>,
Table<Raster<CPU>>,
Table<Raster<GPU>>,
Table<Color>,
Table<GradientStops>,
)]
vector_data: Instances<T>,
vector: Table<T>,
) -> DAffine2 {
vector_data.instance_ref_iter().next().map(|vector_data| *vector_data.transform).unwrap_or_default()
vector.iter().next().map(|row| *row.transform).unwrap_or_default()
}
#[node_macro::node(category("Math: Transform"))]
@@ -90,10 +96,12 @@ fn decompose_scale(_: impl Ctx, transform: DAffine2) -> DVec2 {
async fn boundless_footprint<T: 'n + 'static>(
ctx: impl Ctx + CloneVarArgs + ExtractAll,
#[implementations(
Context -> VectorDataTable,
Context -> GraphicGroupTable,
Context -> RasterDataTable<CPU>,
Context -> RasterDataTable<GPU>,
Context -> Table<Vector>,
Context -> Table<Graphic>,
Context -> Table<Raster<CPU>>,
Context -> Table<Raster<GPU>>,
Context -> Table<Color>,
Context -> Table<GradientStops>,
Context -> String,
Context -> f64,
)]
@@ -108,10 +116,12 @@ async fn boundless_footprint<T: 'n + 'static>(
async fn freeze_real_time<T: 'n + 'static>(
ctx: impl Ctx + CloneVarArgs + ExtractAll,
#[implementations(
Context -> VectorDataTable,
Context -> GraphicGroupTable,
Context -> RasterDataTable<CPU>,
Context -> RasterDataTable<GPU>,
Context -> Table<Vector>,
Context -> Table<Graphic>,
Context -> Table<Raster<CPU>>,
Context -> Table<Raster<GPU>>,
Context -> Table<Color>,
Context -> Table<GradientStops>,
Context -> String,
Context -> f64,
)]

View File

@@ -1,6 +1,7 @@
use std::any::TypeId;
pub use std::borrow::Cow;
use std::fmt::{Display, Formatter};
use std::ops::Deref;
#[macro_export]
@@ -160,6 +161,12 @@ impl Deref for ProtoNodeIdentifier {
}
}
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;
@@ -167,14 +174,21 @@ fn migrate_type_descriptor_names<'de, D: serde::Deserializer<'de>>(deserializer:
let name = match name.as_str() {
"f32" => "f64".to_string(),
"graphene_core::transform::Footprint" => "std::option::Option<std::sync::Arc<graphene_core::context::OwnedContextImpl>>".to_string(),
"graphene_core::graphic_element::GraphicGroup" => "graphene_core::instances::Instances<graphene_core::graphic_element::GraphicGroup>".to_string(),
"graphene_core::vector::vector_data::VectorData" => "graphene_core::instances::Instances<graphene_core::vector::vector_data::VectorData>".to_string(),
"graphene_core::graphic_element::GraphicGroup" => "graphene_core::table::Table<graphene_core::graphic::Graphic>".to_string(),
"graphene_core::raster::image::ImageFrame<Color>"
| "graphene_core::raster::image::ImageFrame<graphene_core::raster::color::Color>"
| "graphene_core::instances::Instances<graphene_core::raster::image::ImageFrame<Color>>"
| "graphene_core::instances::Instances<graphene_core::raster::image::ImageFrame<graphene_core::raster::color::Color>>" => {
"graphene_core::instances::Instances<graphene_core::raster::image::Image<graphene_core::raster::color::Color>>".to_string()
| "graphene_core::instances::Instances<graphene_core::raster::image::ImageFrame<graphene_core::raster::color::Color>>"
| "graphene_core::instances::Instances<graphene_core::raster::image::Image<graphene_core::raster::color::Color>>" => {
"graphene_core::table::Table<graphene_core::raster::image::Image<graphene_core::raster::color::Color>>".to_string()
}
"graphene_core::vector::vector_data::VectorData"
| "graphene_core::instances::Instances<graphene_core::vector::vector_data::VectorData>"
| "graphene_core::table::Table<graphene_core::vector::vector_data::VectorData>"
| "graphene_core::table::Table<graphene_core::vector::vector_data::Vector>" => "graphene_core::table::Table<graphene_core::vector::vector_types::Vector>".to_string(),
"graphene_core::instances::Instances<graphene_core::graphic_element::Artboard>" => "graphene_core::table::Table<graphene_core::artboard::Artboard>".to_string(),
"graphene_core::vector::vector_data::modification::VectorModification" => "graphene_core::vector::vector_modification::VectorModification".to_string(),
"graphene_core::table::Table<graphene_core::graphic_element::Graphic>" => "graphene_core::table::Table<graphene_core::graphic::Graphic>".to_string(),
_ => name,
};
@@ -223,7 +237,6 @@ pub enum Type {
/// 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.
/// See the example and explanation in the `ComposeNode` implementation within the node registry for more info.
Fn(Box<Type>, Box<Type>),
/// Represents a future which promises to return the inner type.
Future(Box<Type>),
@@ -360,7 +373,7 @@ impl std::fmt::Debug for Type {
Self::Future(ty) => format!("{ty:?}"),
};
let result = result.replace("Option<Arc<OwnedContextImpl>>", "Context");
write!(f, "{}", result)
write!(f, "{result}")
}
}
@@ -373,6 +386,6 @@ impl std::fmt::Display for Type {
Type::Future(ty) => ty.to_string(),
};
let result = result.replace("Option<Arc<OwnedContextImpl>>", "Context");
write!(f, "{}", result)
write!(f, "{result}")
}
}

View File

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

View File

@@ -1,18 +1,22 @@
use super::intersection::bezpath_intersections;
use super::poisson_disk::poisson_disk_sample;
use super::util::pathseg_tangent;
use crate::math::polynomial::pathseg_to_parametric_polynomial;
use crate::vector::algorithms::offset_subpath::MAX_ABSOLUTE_DIFFERENCE;
use crate::vector::misc::{PointSpacingType, dvec2_to_point};
use glam::DVec2;
use kurbo::{BezPath, CubicBez, DEFAULT_ACCURACY, Line, ParamCurve, ParamCurveDeriv, PathEl, PathSeg, Point, QuadBez, Rect, Shape};
use crate::vector::misc::{PointSpacingType, dvec2_to_point, point_to_dvec2};
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 `t` value which lie in the range of [0, 1].
/// Splits the [`BezPath`] at segment index at `t` value which lie in the range of [0, 1].
/// Returns [`None`] if the given [`BezPath`] has no segments or `t` is within f64::EPSILON of 0 or 1.
pub fn split_bezpath(bezpath: &BezPath, t: f64, euclidian: bool) -> Option<(BezPath, BezPath)> {
pub fn split_bezpath_at_segment(bezpath: &BezPath, segment_index: usize, t: f64) -> Option<(BezPath, BezPath)> {
if t <= f64::EPSILON || (1. - t) <= f64::EPSILON || bezpath.segments().count() == 0 {
return None;
}
// Get the segment which lies at the split.
let (segment_index, t) = t_value_to_parametric(bezpath, t, euclidian, None);
let segment = bezpath.get_seg(segment_index + 1).unwrap();
// Divide the segment.
@@ -53,14 +57,27 @@ pub fn split_bezpath(bezpath: &BezPath, t: f64, euclidian: bool) -> Option<(BezP
Some((first_bezpath, second_bezpath))
}
pub fn position_on_bezpath(bezpath: &BezPath, t: f64, euclidian: bool, segments_length: Option<&[f64]>) -> Point {
let (segment_index, t) = t_value_to_parametric(bezpath, t, euclidian, segments_length);
/// Splits the [`BezPath`] at a `t` value which lies in the range of [0, 1].
/// Returns [`None`] if the given [`BezPath`] has no segments.
pub fn split_bezpath(bezpath: &BezPath, t_value: TValue) -> Option<(BezPath, BezPath)> {
if bezpath.segments().count() == 0 {
return None;
}
// Get the segment which lies at the split.
let (segment_index, t) = eval_bezpath(bezpath, t_value, None);
split_bezpath_at_segment(bezpath, segment_index, t)
}
pub fn evaluate_bezpath(bezpath: &BezPath, t_value: TValue, segments_length: Option<&[f64]>) -> Point {
let (segment_index, t) = eval_bezpath(bezpath, t_value, segments_length);
bezpath.get_seg(segment_index + 1).unwrap().eval(t)
}
pub fn tangent_on_bezpath(bezpath: &BezPath, t: f64, euclidian: bool, segments_length: Option<&[f64]>) -> Point {
let (segment_index, t) = t_value_to_parametric(bezpath, t, euclidian, segments_length);
pub fn tangent_on_bezpath(bezpath: &BezPath, t_value: TValue, segments_length: Option<&[f64]>) -> Point {
let (segment_index, t) = eval_bezpath(bezpath, t_value, segments_length);
let segment = bezpath.get_seg(segment_index + 1).unwrap();
match segment {
PathSeg::Line(line) => line.deriv().eval(t),
PathSeg::Quad(quad_bez) => quad_bez.deriv().eval(t),
@@ -166,23 +183,173 @@ pub fn sample_polyline_on_bezpath(
Some(sample_bezpath)
}
pub fn t_value_to_parametric(bezpath: &BezPath, t: f64, euclidian: bool, segments_length: Option<&[f64]>) -> (usize, f64) {
if euclidian {
let (segment_index, t) = bezpath_t_value_to_parametric(bezpath, BezPathTValue::GlobalEuclidean(t), segments_length);
let segment = bezpath.get_seg(segment_index + 1).unwrap();
return (segment_index, eval_pathseg_euclidean(segment, t, DEFAULT_ACCURACY));
#[derive(Debug, Clone, Copy)]
pub enum TValue {
Parametric(f64),
Euclidean(f64),
}
/// 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 = poly_cool::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)
}
}
bezpath_t_value_to_parametric(bezpath, BezPathTValue::GlobalParametric(t), segments_length)
}
/// Finds the t value of point on the given path segment i.e fractional distance along the segment's total length.
/// It uses a binary search to find the value `t` such that the ratio `length_up_to_t / total_length` approximates the input `distance`.
pub fn eval_pathseg_euclidean(path_segment: PathSeg, distance: f64, accuracy: f64) -> f64 {
pub fn eval_pathseg_euclidean(segment: PathSeg, distance: f64, accuracy: f64) -> f64 {
let mut low_t = 0.;
let mut mid_t = 0.5;
let mut high_t = 1.;
let total_length = path_segment.perimeter(accuracy);
let total_length = segment.perimeter(accuracy);
if !total_length.is_finite() || total_length <= f64::EPSILON {
return 0.;
@@ -191,7 +358,7 @@ pub fn eval_pathseg_euclidean(path_segment: PathSeg, distance: f64, accuracy: f6
let distance = distance.clamp(0., 1.);
while high_t - low_t > accuracy {
let current_length = path_segment.subsegment(0.0..mid_t).perimeter(accuracy);
let current_length = segment.subsegment(0.0..mid_t).perimeter(accuracy);
let current_distance = current_length / total_length;
if current_distance > distance {
@@ -208,7 +375,7 @@ pub fn eval_pathseg_euclidean(path_segment: PathSeg, distance: f64, accuracy: f6
/// Converts from a bezpath (composed of multiple segments) to a point along a certain segment represented.
/// The returned tuple represents the segment index and the `t` value along that segment.
/// Both the input global `t` value and the output `t` value are in euclidean space, meaning there is a constant rate of change along the arc length.
fn global_euclidean_to_local_euclidean(bezpath: &BezPath, global_t: f64, lengths: &[f64], total_length: f64) -> (usize, f64) {
fn eval_bazpath_to_euclidean(bezpath: &BezPath, global_t: f64, lengths: &[f64], total_length: f64) -> (usize, f64) {
let mut accumulator = 0.;
for (index, length) in lengths.iter().enumerate() {
let length_ratio = length / total_length;
@@ -220,19 +387,14 @@ fn global_euclidean_to_local_euclidean(bezpath: &BezPath, global_t: f64, lengths
(bezpath.segments().count() - 1, 1.)
}
enum BezPathTValue {
GlobalEuclidean(f64),
GlobalParametric(f64),
}
/// Convert a [BezPathTValue] to a parametric `(segment_index, t)` tuple.
/// - Asserts that `t` values contained within the `SubpathTValue` argument lie in the range [0, 1].
fn bezpath_t_value_to_parametric(bezpath: &BezPath, t: BezPathTValue, precomputed_segments_length: Option<&[f64]>) -> (usize, f64) {
/// Convert a [TValue] to a parametric `(segment_index, t)` tuple.
/// - Asserts that `t` values contained within the `TValue` argument lie in the range [0, 1].
fn eval_bezpath(bezpath: &BezPath, t: TValue, precomputed_segments_length: Option<&[f64]>) -> (usize, f64) {
let segment_count = bezpath.segments().count();
assert!(segment_count >= 1);
match t {
BezPathTValue::GlobalEuclidean(t) => {
TValue::Euclidean(t) => {
let computed_segments_length;
let segments_length = if let Some(segments_length) = precomputed_segments_length {
@@ -244,16 +406,18 @@ fn bezpath_t_value_to_parametric(bezpath: &BezPath, t: BezPathTValue, precompute
let total_length = segments_length.iter().sum();
global_euclidean_to_local_euclidean(bezpath, t, segments_length, total_length)
let (segment_index, t) = eval_bazpath_to_euclidean(bezpath, t, segments_length, total_length);
let segment = bezpath.get_seg(segment_index + 1).unwrap();
(segment_index, eval_pathseg_euclidean(segment, t, DEFAULT_ACCURACY))
}
BezPathTValue::GlobalParametric(global_t) => {
assert!((0.0..=1.).contains(&global_t));
TValue::Parametric(t) => {
assert!((0.0..=1.).contains(&t));
if global_t == 1. {
if t == 1. {
return (segment_count - 1, 1.);
}
let scaled_t = global_t * segment_count as f64;
let scaled_t = t * segment_count as f64;
let segment_index = scaled_t.floor() as usize;
let t = scaled_t - segment_index as f64;
@@ -328,3 +492,185 @@ pub fn is_linear(segment: &PathSeg) -> bool {
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

@@ -1,33 +1,37 @@
use crate::instances::{InstanceRef, Instances};
use crate::raster_types::{CPU, RasterDataTable};
use crate::vector::VectorDataTable;
use crate::{CloneVarArgs, Context, Ctx, ExtractAll, ExtractIndex, ExtractVarArgs, GraphicElement, GraphicGroupTable, OwnedContextImpl};
use crate::gradient::GradientStops;
use crate::raster_types::{CPU, Raster};
use crate::table::{Table, TableRowRef};
use crate::vector::Vector;
use crate::{CloneVarArgs, Context, Ctx, ExtractAll, ExtractIndex, ExtractVarArgs, Graphic, OwnedContextImpl};
use glam::DVec2;
use graphene_core_shaders::color::Color;
#[node_macro::node(name("Instance on Points"), category("Instancing"), path(graphene_core::vector))]
async fn instance_on_points<T: Into<GraphicElement> + Default + Send + Clone + 'static>(
async fn instance_on_points<T: Into<Graphic> + Default + Send + Clone + 'static>(
ctx: impl ExtractAll + CloneVarArgs + Sync + Ctx,
points: VectorDataTable,
points: Table<Vector>,
#[implementations(
Context -> GraphicGroupTable,
Context -> VectorDataTable,
Context -> RasterDataTable<CPU>
Context -> Table<Graphic>,
Context -> Table<Vector>,
Context -> Table<Raster<CPU>>,
Context -> Table<Color>,
Context -> Table<GradientStops>,
)]
instance: impl Node<'n, Context<'static>, Output = Instances<T>>,
instance: impl Node<'n, Context<'static>, Output = Table<T>>,
reverse: bool,
) -> Instances<T> {
let mut result_table = Instances::<T>::default();
) -> Table<T> {
let mut result_table = Table::new();
for InstanceRef { instance: points, transform, .. } in points.instance_ref_iter() {
for TableRowRef { element: points, transform, .. } in points.iter() {
let mut iteration = async |index, point| {
let transformed_point = transform.transform_point2(point);
let new_ctx = OwnedContextImpl::from(ctx.clone()).with_index(index).with_vararg(Box::new(transformed_point));
let generated_instance = instance.eval(new_ctx.into_context()).await;
for mut instanced in generated_instance.instance_iter() {
instanced.transform.translation = transformed_point;
result_table.push(instanced);
for mut generated_row in generated_instance.into_iter() {
generated_row.transform.translation = transformed_point;
result_table.push(generated_row);
}
};
@@ -47,20 +51,22 @@ async fn instance_on_points<T: Into<GraphicElement> + Default + Send + Clone + '
}
#[node_macro::node(category("Instancing"), path(graphene_core::vector))]
async fn instance_repeat<T: Into<GraphicElement> + Default + Send + Clone + 'static>(
async fn instance_repeat<T: Into<Graphic> + Default + Send + Clone + 'static>(
ctx: impl ExtractAll + CloneVarArgs + Ctx,
#[implementations(
Context -> GraphicGroupTable,
Context -> VectorDataTable,
Context -> RasterDataTable<CPU>
Context -> Table<Graphic>,
Context -> Table<Vector>,
Context -> Table<Raster<CPU>>,
Context -> Table<Color>,
Context -> Table<GradientStops>,
)]
instance: impl Node<'n, Context<'static>, Output = Instances<T>>,
instance: impl Node<'n, Context<'static>, Output = Table<T>>,
#[default(1)] count: u64,
reverse: bool,
) -> Instances<T> {
) -> Table<T> {
let count = count.max(1) as usize;
let mut result_table = Instances::<T>::default();
let mut result_table = Table::new();
for index in 0..count {
let index = if reverse { count - index - 1 } else { index };
@@ -68,8 +74,8 @@ async fn instance_repeat<T: Into<GraphicElement> + Default + Send + Clone + 'sta
let new_ctx = OwnedContextImpl::from(ctx.clone()).with_index(index);
let generated_instance = instance.eval(new_ctx.into_context()).await;
for instanced in generated_instance.instance_iter() {
result_table.push(instanced);
for generated_row in generated_instance.into_iter() {
result_table.push(generated_row);
}
}
@@ -99,8 +105,8 @@ mod test {
use super::*;
use crate::Node;
use crate::extract_xy::{ExtractXyNode, XY};
use crate::vector::VectorData;
use bezier_rs::Subpath;
use crate::subpath::Subpath;
use crate::vector::Vector;
use glam::DVec2;
use std::pin::Pin;
@@ -128,11 +134,11 @@ mod test {
);
let positions = [DVec2::new(40., 20.), DVec2::ONE, DVec2::new(-42., 9.), DVec2::new(10., 345.)];
let points = VectorDataTable::new(VectorData::from_subpath(Subpath::from_anchors_linear(positions, false)));
let repeated = super::instance_on_points(owned, points, &rect, false).await;
assert_eq!(repeated.len(), positions.len());
for (position, instanced) in positions.into_iter().zip(repeated.instance_ref_iter()) {
let bounds = instanced.instance.bounding_box_with_transform(*instanced.transform).unwrap();
let points = Table::new_from_element(Vector::from_subpath(Subpath::from_anchors_linear(positions, false)));
let generated = super::instance_on_points(owned, points, &rect, false).await;
assert_eq!(generated.len(), positions.len());
for (position, generated_row) in positions.into_iter().zip(generated.iter()) {
let bounds = generated_row.element.bounding_box_with_transform(*generated_row.transform).unwrap();
assert!(position.abs_diff_eq((bounds[0] + bounds[1]) / 2., 1e-10));
assert_eq!((bounds[1] - bounds[0]).x, position.y);
}

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

@@ -1,6 +1,8 @@
use crate::vector::{PointDomain, PointId, SegmentDomain, VectorData, VectorDataIndex};
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 {
@@ -9,10 +11,10 @@ pub trait MergeByDistanceExt {
fn merge_by_distance_spatial(&mut self, transform: DAffine2, distance: f64);
}
impl MergeByDistanceExt for VectorData {
impl MergeByDistanceExt for Vector {
fn merge_by_distance_topological(&mut self, distance: f64) {
// Treat self as an undirected graph
let indices = VectorDataIndex::build_from(self);
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
@@ -207,8 +209,94 @@ impl MergeByDistanceExt for VectorData {
}
}
// Create new vector data
// 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(data: &Vector) -> 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(&self, id: PointId, data: &Vector) -> DVec2 {
let offset = self.point_to_offset[&id];
data.point_domain.positions()[offset]
}
}

View File

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

View File

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

View File

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

@@ -1,8 +1,12 @@
use super::algorithms::{bezpath_algorithms::bezpath_is_inside_bezpath, intersection::filtered_segment_intersections};
use super::misc::dvec2_to_point;
use crate::math::math_ext::QuadExt;
use crate::math::quad::Quad;
use crate::subpath::Subpath;
use crate::vector::PointId;
use bezier_rs::Subpath;
use crate::vector::misc::point_to_dvec2;
use glam::{DAffine2, DMat2, DVec2};
use kurbo::{Affine, BezPath, ParamCurve, PathSeg, Shape};
#[derive(Copy, Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct FreePoint {
@@ -99,7 +103,7 @@ impl ClickTarget {
}
/// Does the click target intersect the path
pub fn intersect_path<It: Iterator<Item = bezier_rs::Bezier>>(&self, mut bezier_iter: impl FnMut() -> It, layer_transform: DAffine2) -> bool {
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 {
@@ -107,25 +111,27 @@ impl ClickTarget {
}
let inverse = layer_transform.inverse();
let mut bezier_iter = || bezier_iter().map(|bezier| bezier.apply_transformation(|point| inverse.transform_point2(point)));
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: bezier_rs::Bezier| bezier_iter().any(|line| !path_segment.intersections(&line, None, None).is_empty());
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(bezier.start)) {
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
let any_point_from_subpath = subpath.manipulator_groups().first().map(|group| group.anchor);
any_point_from_subpath.is_some_and(|shape_point| bezier_iter().map(|bezier| bezier.winding(shape_point)).sum::<i32>() != 0)
bezpath_is_inside_bezpath(&subpath.to_bezpath(), &selection, None, None)
}
ClickTargetType::FreePoint(point) => bezier_iter().map(|bezier: bezier_rs::Bezier| bezier.winding(point.position)).sum::<i32>() != 0,
ClickTargetType::FreePoint(point) => bezier_iter().map(|bezier: PathSeg| bezier.winding(dvec2_to_point(point.position))).sum::<i32>() != 0,
}
}
@@ -144,7 +150,7 @@ impl ClickTarget {
// Allows for selecting lines
// TODO: actual intersection of stroke
let inflated_quad = Quad::from_box(target_bounds);
self.intersect_path(|| inflated_quad.bezier_lines(), layer_transform)
self.intersect_path(|| inflated_quad.to_lines(), layer_transform)
}
/// Does the click target intersect the point (not accounting for stroke size)

View File

@@ -2,21 +2,23 @@ use super::misc::{ArcType, AsU64, GridType};
use super::{PointId, SegmentId, StrokeId};
use crate::Ctx;
use crate::registry::types::{Angle, PixelSize};
use crate::vector::{HandleId, VectorData, VectorDataTable};
use bezier_rs::Subpath;
use crate::subpath;
use crate::table::Table;
use crate::vector::Vector;
use crate::vector::misc::HandleId;
use glam::DVec2;
trait CornerRadius {
fn generate(self, size: DVec2, clamped: bool) -> VectorDataTable;
fn generate(self, size: DVec2, clamped: bool) -> Table<Vector>;
}
impl CornerRadius for f64 {
fn generate(self, size: DVec2, clamped: bool) -> VectorDataTable {
fn generate(self, size: DVec2, clamped: bool) -> Table<Vector> {
let clamped_radius = if clamped { self.clamp(0., size.x.min(size.y).max(0.) / 2.) } else { self };
VectorDataTable::new(VectorData::from_subpath(Subpath::new_rounded_rect(size / -2., size / 2., [clamped_radius; 4])))
Table::new_from_element(Vector::from_subpath(subpath::Subpath::new_rounded_rect(size / -2., size / 2., [clamped_radius; 4])))
}
}
impl CornerRadius for [f64; 4] {
fn generate(self, size: DVec2, clamped: bool) -> VectorDataTable {
fn generate(self, size: DVec2, clamped: bool) -> Table<Vector> {
let clamped_radius = if clamped {
// Algorithm follows the CSS spec: <https://drafts.csswg.org/css-backgrounds/#corner-overlap>
@@ -32,7 +34,7 @@ impl CornerRadius for [f64; 4] {
} else {
self
};
VectorDataTable::new(VectorData::from_subpath(Subpath::new_rounded_rect(size / -2., size / 2., clamped_radius)))
Table::new_from_element(Vector::from_subpath(subpath::Subpath::new_rounded_rect(size / -2., size / 2., clamped_radius)))
}
}
@@ -43,9 +45,9 @@ fn circle(
#[unit(" px")]
#[default(50.)]
radius: f64,
) -> VectorDataTable {
) -> Table<Vector> {
let radius = radius.abs();
VectorDataTable::new(VectorData::from_subpath(Subpath::new_ellipse(DVec2::splat(-radius), DVec2::splat(radius))))
Table::new_from_element(Vector::from_subpath(subpath::Subpath::new_ellipse(DVec2::splat(-radius), DVec2::splat(radius))))
}
#[node_macro::node(category("Vector: Shape"))]
@@ -60,15 +62,15 @@ fn arc(
#[range((0., 360.))]
sweep_angle: Angle,
arc_type: ArcType,
) -> VectorDataTable {
VectorDataTable::new(VectorData::from_subpath(Subpath::new_arc(
) -> Table<Vector> {
Table::new_from_element(Vector::from_subpath(subpath::Subpath::new_arc(
radius,
start_angle / 360. * std::f64::consts::TAU,
sweep_angle / 360. * std::f64::consts::TAU,
match arc_type {
ArcType::Open => bezier_rs::ArcType::Open,
ArcType::Closed => bezier_rs::ArcType::Closed,
ArcType::PieSlice => bezier_rs::ArcType::PieSlice,
ArcType::Open => subpath::ArcType::Open,
ArcType::Closed => subpath::ArcType::Closed,
ArcType::PieSlice => subpath::ArcType::PieSlice,
},
)))
}
@@ -83,12 +85,12 @@ fn ellipse(
#[unit(" px")]
#[default(25)]
radius_y: f64,
) -> VectorDataTable {
) -> Table<Vector> {
let radius = DVec2::new(radius_x, radius_y);
let corner1 = -radius;
let corner2 = radius;
let mut ellipse = VectorData::from_subpath(Subpath::new_ellipse(corner1, corner2));
let mut ellipse = Vector::from_subpath(subpath::Subpath::new_ellipse(corner1, corner2));
let len = ellipse.segment_domain.ids().len();
for i in 0..len {
@@ -97,7 +99,7 @@ fn ellipse(
.push([HandleId::end(ellipse.segment_domain.ids()[i]), HandleId::primary(ellipse.segment_domain.ids()[(i + 1) % len])]);
}
VectorDataTable::new(ellipse)
Table::new_from_element(ellipse)
}
#[node_macro::node(category("Vector: Shape"), properties("rectangle_properties"))]
@@ -113,7 +115,7 @@ fn rectangle<T: CornerRadius>(
_individual_corner_radii: bool, // TODO: Move this to the bottom once we have a migration capability
#[implementations(f64, [f64; 4])] corner_radius: T,
#[default(true)] clamped: bool,
) -> VectorDataTable {
) -> Table<Vector> {
corner_radius.generate(DVec2::new(width, height), clamped)
}
@@ -128,10 +130,10 @@ fn regular_polygon<T: AsU64>(
#[unit(" px")]
#[default(50)]
radius: f64,
) -> VectorDataTable {
) -> Table<Vector> {
let points = sides.as_u64();
let radius: f64 = radius * 2.;
VectorDataTable::new(VectorData::from_subpath(Subpath::new_regular_polygon(DVec2::splat(-radius), points, radius)))
Table::new_from_element(Vector::from_subpath(subpath::Subpath::new_regular_polygon(DVec2::splat(-radius), points, radius)))
}
#[node_macro::node(category("Vector: Shape"))]
@@ -148,17 +150,17 @@ fn star<T: AsU64>(
#[unit(" px")]
#[default(25)]
radius_2: f64,
) -> VectorDataTable {
) -> Table<Vector> {
let points = sides.as_u64();
let diameter: f64 = radius_1 * 2.;
let inner_diameter = radius_2 * 2.;
VectorDataTable::new(VectorData::from_subpath(Subpath::new_star_polygon(DVec2::splat(-diameter), points, diameter, inner_diameter)))
Table::new_from_element(Vector::from_subpath(subpath::Subpath::new_star_polygon(DVec2::splat(-diameter), points, diameter, inner_diameter)))
}
#[node_macro::node(category("Vector: Shape"))]
fn line(_: impl Ctx, _primary: (), #[default(0., 0.)] start: PixelSize, #[default(100., 100.)] end: PixelSize) -> VectorDataTable {
VectorDataTable::new(VectorData::from_subpath(Subpath::new_line(start, end)))
fn line(_: impl Ctx, _primary: (), #[default(0., 0.)] start: PixelSize, #[default(100., 100.)] end: PixelSize) -> Table<Vector> {
Table::new_from_element(Vector::from_subpath(subpath::Subpath::new_line(start, end)))
}
trait GridSpacing {
@@ -188,11 +190,11 @@ fn grid<T: GridSpacing>(
#[default(10)] columns: u32,
#[default(10)] rows: u32,
#[default(30., 30.)] angles: DVec2,
) -> VectorDataTable {
) -> Table<Vector> {
let (x_spacing, y_spacing) = spacing.as_dvec2().into();
let (angle_a, angle_b) = angles.into();
let mut vector_data = VectorData::default();
let mut vector = Vector::default();
let mut segment_id = SegmentId::ZERO;
let mut point_id = PointId::ZERO;
@@ -202,15 +204,15 @@ fn grid<T: GridSpacing>(
for y in 0..rows {
for x in 0..columns {
// Add current point to the grid
let current_index = vector_data.point_domain.ids().len();
vector_data.point_domain.push(point_id.next_id(), DVec2::new(x_spacing * x as f64, y_spacing * y as f64));
let current_index = vector.point_domain.ids().len();
vector.point_domain.push(point_id.next_id(), DVec2::new(x_spacing * x as f64, y_spacing * y as f64));
// Helper function to connect points with line segments
let mut push_segment = |to_index: Option<usize>| {
if let Some(other_index) = to_index {
vector_data
vector
.segment_domain
.push(segment_id.next_id(), other_index, current_index, bezier_rs::BezierHandles::Linear, StrokeId::ZERO);
.push(segment_id.next_id(), other_index, current_index, subpath::BezierHandles::Linear, StrokeId::ZERO);
}
};
@@ -232,7 +234,7 @@ fn grid<T: GridSpacing>(
for y in 0..rows {
for x in 0..columns {
// Add current point to the grid with offset for odd columns
let current_index = vector_data.point_domain.ids().len();
let current_index = vector.point_domain.ids().len();
let a_angles_eaten = x.div_ceil(2) as f64;
let b_angles_eaten = (x / 2) as f64;
@@ -240,14 +242,14 @@ fn grid<T: GridSpacing>(
let offset_y_fraction = b_angles_eaten * tan_b - a_angles_eaten * tan_a;
let position = DVec2::new(spacing.x * x as f64, spacing.y * y as f64 + offset_y_fraction * spacing.x);
vector_data.point_domain.push(point_id.next_id(), position);
vector.point_domain.push(point_id.next_id(), position);
// Helper function to connect points with line segments
let mut push_segment = |to_index: Option<usize>| {
if let Some(other_index) = to_index {
vector_data
vector
.segment_domain
.push(segment_id.next_id(), other_index, current_index, bezier_rs::BezierHandles::Linear, StrokeId::ZERO);
.push(segment_id.next_id(), other_index, current_index, subpath::BezierHandles::Linear, StrokeId::ZERO);
}
};
@@ -270,7 +272,7 @@ fn grid<T: GridSpacing>(
}
}
VectorDataTable::new(vector_data)
Table::new_from_element(vector)
}
#[cfg(test)]
@@ -284,10 +286,10 @@ mod tests {
// Works properly
let grid = grid((), (), GridType::Isometric, 10., 5, 5, (30., 30.).into());
assert_eq!(grid.instance_ref_iter().next().unwrap().instance.point_domain.ids().len(), 5 * 5);
assert_eq!(grid.instance_ref_iter().next().unwrap().instance.segment_bezier_iter().count(), 4 * 5 + 4 * 9);
for (_, bezier, _, _) in grid.instance_ref_iter().next().unwrap().instance.segment_bezier_iter() {
assert_eq!(bezier.handles, bezier_rs::BezierHandles::Linear);
assert_eq!(grid.iter().next().unwrap().element.point_domain.ids().len(), 5 * 5);
assert_eq!(grid.iter().next().unwrap().element.segment_bezier_iter().count(), 4 * 5 + 4 * 9);
for (_, bezier, _, _) in grid.iter().next().unwrap().element.segment_bezier_iter() {
assert_eq!(bezier.handles, subpath::BezierHandles::Linear);
assert!(
((bezier.start - bezier.end).length() - 10.).abs() < 1e-5,
"Length of {} should be 10",
@@ -299,13 +301,13 @@ mod tests {
#[test]
fn skew_isometric_grid_test() {
let grid = grid((), (), GridType::Isometric, 10., 5, 5, (40., 30.).into());
assert_eq!(grid.instance_ref_iter().next().unwrap().instance.point_domain.ids().len(), 5 * 5);
assert_eq!(grid.instance_ref_iter().next().unwrap().instance.segment_bezier_iter().count(), 4 * 5 + 4 * 9);
for (_, bezier, _, _) in grid.instance_ref_iter().next().unwrap().instance.segment_bezier_iter() {
assert_eq!(bezier.handles, bezier_rs::BezierHandles::Linear);
assert_eq!(grid.iter().next().unwrap().element.point_domain.ids().len(), 5 * 5);
assert_eq!(grid.iter().next().unwrap().element.segment_bezier_iter().count(), 4 * 5 + 4 * 9);
for (_, bezier, _, _) in grid.iter().next().unwrap().element.segment_bezier_iter() {
assert_eq!(bezier.handles, subpath::BezierHandles::Linear);
let vector = bezier.start - bezier.end;
let angle = (vector.angle_to(DVec2::X).to_degrees() + 180.) % 180.;
assert!([90., 150., 40.].into_iter().any(|target| (target - angle).abs() < 1e-10), "unexpected angle of {}", angle)
assert!([90., 150., 40.].into_iter().any(|target| (target - angle).abs() < 1e-10), "unexpected angle of {angle}")
}
}
}

View File

@@ -1,9 +1,11 @@
use bezier_rs::{BezierHandles, ManipulatorGroup, Subpath};
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, PathSeg, Point, QuadBez};
use super::PointId;
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)]
@@ -67,7 +69,7 @@ pub enum GridType {
#[widget(Radio)]
pub enum ArcType {
#[default]
Open,
Open = 0,
Closed,
PieSlice,
}
@@ -113,18 +115,18 @@ pub fn segment_to_handles(segment: &PathSeg) -> BezierHandles {
pub fn handles_to_segment(start: DVec2, handles: BezierHandles, end: DVec2) -> PathSeg {
match handles {
bezier_rs::BezierHandles::Linear => {
BezierHandles::Linear => {
let p0 = dvec2_to_point(start);
let p1 = dvec2_to_point(end);
PathSeg::Line(Line::new(p0, p1))
}
bezier_rs::BezierHandles::Quadratic { handle } => {
BezierHandles::Quadratic { handle } => {
let p0 = dvec2_to_point(start);
let p1 = dvec2_to_point(handle);
let p2 = dvec2_to_point(end);
PathSeg::Quad(QuadBez::new(p0, p1, p2))
}
bezier_rs::BezierHandles::Cubic { handle_start, handle_end } => {
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);
@@ -134,12 +136,6 @@ pub fn handles_to_segment(start: DVec2, handles: BezierHandles, end: DVec2) -> P
}
}
pub fn subpath_to_kurbo_bezpath(subpath: Subpath<PointId>) -> BezPath {
let maniputor_groups = subpath.manipulator_groups();
let closed = subpath.closed();
bezpath_from_manipulator_groups(maniputor_groups, closed)
}
pub fn bezpath_from_manipulator_groups(manipulator_groups: &[ManipulatorGroup<PointId>], closed: bool) -> BezPath {
let mut bezpath = kurbo::BezPath::new();
let mut out_handle;
@@ -169,3 +165,249 @@ pub fn bezpath_from_manipulator_groups(manipulator_groups: &[ManipulatorGroup<Po
}
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(&self, vector: &Vector) -> 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(&self, vector: &Vector) -> 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(self, vector: &Vector) -> 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(self, vector: &Vector) -> 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(self, vector: &Vector) -> 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(self, vector: &Vector) -> 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),
}
}
}

View File

@@ -4,11 +4,12 @@ pub mod generator_nodes;
pub mod misc;
mod reference_point;
pub mod style;
mod vector_data;
mod vector_attributes;
mod vector_modification;
mod vector_nodes;
mod vector_types;
pub use bezier_rs;
pub use reference_point::*;
pub use style::PathStyle;
pub use vector_data::*;
pub use vector_nodes::*;
pub use vector_types::*;

View File

@@ -2,6 +2,7 @@
use crate::Color;
pub use crate::gradient::*;
use crate::table::Table;
use dyn_any::DynAny;
use glam::DAffine2;
@@ -24,7 +25,7 @@ impl std::fmt::Display for Fill {
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),
Self::Gradient(gradient) => write!(f, "{gradient}"),
}
}
}
@@ -120,6 +121,21 @@ impl From<Option<Color>> for Fill {
}
}
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)
@@ -309,17 +325,6 @@ impl std::hash::Hash for Stroke {
}
}
impl From<Color> for Stroke {
fn from(color: Color) -> Self {
Self::new(Some(color), 1.)
}
}
impl From<Option<Color>> for Stroke {
fn from(color: Option<Color>) -> Self {
Self::new(color, 1.)
}
}
impl Stroke {
pub const fn new(color: Option<Color>, weight: f64) -> Self {
Self {
@@ -366,6 +371,16 @@ impl Stroke {
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()

View File

@@ -1,8 +1,9 @@
use crate::vector::misc::dvec2_to_point;
use crate::vector::vector_data::{HandleId, VectorData};
use bezier_rs::{BezierHandles, ManipulatorGroup};
use crate::subpath::{Bezier, BezierHandles, Identifier, ManipulatorGroup, Subpath};
use crate::vector::misc::{HandleId, dvec2_to_point};
use crate::vector::vector_types::Vector;
use dyn_any::DynAny;
use glam::{DAffine2, DVec2};
use kurbo::{CubicBez, Line, PathSeg, QuadBez};
use std::collections::HashMap;
use std::hash::{Hash, Hasher};
use std::iter::zip;
@@ -47,7 +48,7 @@ macro_rules! create_ids {
};
}
create_ids! { InstanceId, PointId, SegmentId, RegionId, StrokeId, FillId }
create_ids! { PointId, SegmentId, RegionId, StrokeId, FillId }
/// A no-op hasher that allows writing u64s (the id type).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
@@ -304,7 +305,7 @@ impl SegmentDomain {
&self.stroke
}
pub(crate) fn push(&mut self, id: SegmentId, start: usize, end: usize, handles: BezierHandles, stroke: StrokeId) {
pub fn push(&mut self, id: SegmentId, start: usize, end: usize, handles: BezierHandles, stroke: StrokeId) {
debug_assert!(!self.id.contains(&id), "Tried to push an existing point to a point domain");
self.id.push(id);
@@ -441,11 +442,7 @@ impl SegmentDomain {
zip(ids, zip(start_point, zip(end_point, handles))).map(|(id, (start_point, (end_point, handles)))| (id, start_point, end_point, handles))
}
pub(crate) fn pair_handles_and_points_mut_by_index(
&mut self,
index1: usize,
index2: usize,
) -> (&mut bezier_rs::BezierHandles, &mut usize, &mut usize, &mut bezier_rs::BezierHandles, &mut usize, &mut usize) {
pub(crate) fn pair_handles_and_points_mut_by_index(&mut self, index1: usize, index2: usize) -> (&mut BezierHandles, &mut usize, &mut usize, &mut BezierHandles, &mut usize, &mut usize) {
// Use split_at_mut to avoid multiple mutable borrows of the same slice
let (handles_first, handles_second) = self.handles.split_at_mut(index2.max(index1));
let (start_first, start_second) = self.start_point.split_at_mut(index2.max(index1));
@@ -672,26 +669,38 @@ impl FoundSubpath {
}
}
impl VectorData {
/// Construct a [`bezier_rs::Bezier`] curve spanning from the resolved position of the start and end points with the specified handles.
fn segment_to_bezier_with_index(&self, start: usize, end: usize, handles: BezierHandles) -> bezier_rs::Bezier {
let start = self.point_domain.positions()[start];
let end = self.point_domain.positions()[end];
bezier_rs::Bezier { start, end, handles }
impl Vector {
/// Construct a [`kurbo::PathSeg`] by resolving the points from their ids.
fn path_segment_from_index(&self, start: usize, end: usize, handles: BezierHandles) -> PathSeg {
let start = dvec2_to_point(self.point_domain.positions()[start]);
let end = dvec2_to_point(self.point_domain.positions()[end]);
match handles {
BezierHandles::Linear => PathSeg::Line(Line::new(start, end)),
BezierHandles::Quadratic { handle } => PathSeg::Quad(QuadBez::new(start, dvec2_to_point(handle), end)),
BezierHandles::Cubic { handle_start, handle_end } => PathSeg::Cubic(CubicBez::new(start, dvec2_to_point(handle_start), dvec2_to_point(handle_end), end)),
}
}
/// Tries to convert a segment with the specified id to a [`bezier_rs::Bezier`], returning None if the id is invalid.
pub fn segment_from_id(&self, id: SegmentId) -> Option<bezier_rs::Bezier> {
/// Construct a [`Bezier`] curve spanning from the resolved position of the start and end points with the specified handles.
fn segment_to_bezier_with_index(&self, start: usize, end: usize, handles: BezierHandles) -> Bezier {
let start = self.point_domain.positions()[start];
let end = self.point_domain.positions()[end];
Bezier { start, end, handles }
}
/// Tries to convert a segment with the specified id to a [`Bezier`], returning None if the id is invalid.
pub fn segment_from_id(&self, id: SegmentId) -> Option<Bezier> {
self.segment_points_from_id(id).map(|(_, _, bezier)| bezier)
}
/// Tries to convert a segment with the specified id to the start and end points and a [`bezier_rs::Bezier`], returning None if the id is invalid.
pub fn segment_points_from_id(&self, id: SegmentId) -> Option<(PointId, PointId, bezier_rs::Bezier)> {
/// Tries to convert a segment with the specified id to the start and end points and a [`Bezier`], returning None if the id is invalid.
pub fn segment_points_from_id(&self, id: SegmentId) -> Option<(PointId, PointId, Bezier)> {
Some(self.segment_points_from_index(self.segment_domain.id_to_index(id)?))
}
/// Tries to convert a segment with the specified index to the start and end points and a [`bezier_rs::Bezier`].
pub fn segment_points_from_index(&self, index: usize) -> (PointId, PointId, bezier_rs::Bezier) {
/// Tries to convert a segment with the specified index to the start and end points and a [`Bezier`].
pub fn segment_points_from_index(&self, index: usize) -> (PointId, PointId, Bezier) {
let start = self.segment_domain.start_point[index];
let end = self.segment_domain.end_point[index];
let start_id = self.point_domain.ids()[start];
@@ -699,8 +708,21 @@ impl VectorData {
(start_id, end_id, self.segment_to_bezier_with_index(start, end, self.segment_domain.handles[index]))
}
/// Iterator over all of the [`bezier_rs::Bezier`] following the order that they are stored in the segment domain, skipping invalid segments.
pub fn segment_bezier_iter(&self) -> impl Iterator<Item = (SegmentId, bezier_rs::Bezier, PointId, PointId)> + '_ {
/// Iterator over all of the [`Bezier`] following the order that they are stored in the segment domain, skipping invalid segments.
pub fn segment_iter(&self) -> impl Iterator<Item = (SegmentId, PathSeg, PointId, PointId)> {
let to_segment = |(((&handles, &id), &start), &end)| (id, self.path_segment_from_index(start, end, handles), self.point_domain.ids()[start], self.point_domain.ids()[end]);
self.segment_domain
.handles
.iter()
.zip(&self.segment_domain.id)
.zip(self.segment_domain.start_point())
.zip(self.segment_domain.end_point())
.map(to_segment)
}
/// Iterator over all of the [`Bezier`] following the order that they are stored in the segment domain, skipping invalid segments.
pub fn segment_bezier_iter(&self) -> impl Iterator<Item = (SegmentId, Bezier, PointId, PointId)> + '_ {
let to_bezier = |(((&handles, &id), &start), &end)| (id, self.segment_to_bezier_with_index(start, end, handles), self.point_domain.ids()[start], self.point_domain.ids()[end]);
self.segment_domain
.handles
@@ -782,16 +804,16 @@ impl VectorData {
}
}
/// Construct a [`bezier_rs::Bezier`] curve from an iterator of segments with (handles, start point, end point) independently of discontinuities.
pub fn subpath_from_segments_ignore_discontinuities(&self, segments: impl Iterator<Item = (BezierHandles, usize, usize)>) -> Option<bezier_rs::Subpath<PointId>> {
/// Construct a [`Bezier`] curve from an iterator of segments with (handles, start point, end point) independently of discontinuities.
pub fn subpath_from_segments_ignore_discontinuities(&self, segments: impl Iterator<Item = (BezierHandles, usize, usize)>) -> Option<Subpath<PointId>> {
let mut first_point = None;
let mut groups = Vec::new();
let mut manipulators_list = Vec::new();
let mut last: Option<(usize, BezierHandles)> = None;
for (handle, start, end) in segments {
first_point = Some(first_point.unwrap_or(start));
groups.push(ManipulatorGroup {
manipulators_list.push(ManipulatorGroup {
anchor: self.point_domain.positions()[start],
in_handle: last.and_then(|(_, handle)| handle.end()),
out_handle: handle.start(),
@@ -801,13 +823,13 @@ impl VectorData {
last = Some((end, handle));
}
let closed = groups.len() > 1 && last.map(|(point, _)| point) == first_point;
let closed = manipulators_list.len() > 1 && last.map(|(point, _)| point) == first_point;
if let Some((end, last_handle)) = last {
if closed {
groups[0].in_handle = last_handle.end();
manipulators_list[0].in_handle = last_handle.end();
} else {
groups.push(ManipulatorGroup {
manipulators_list.push(ManipulatorGroup {
anchor: self.point_domain.positions()[end],
in_handle: last_handle.end(),
out_handle: None,
@@ -816,51 +838,11 @@ impl VectorData {
}
}
Some(bezier_rs::Subpath::new(groups, closed))
Some(Subpath::new(manipulators_list, closed))
}
/// Construct a [`bezier_rs::Bezier`] curve from an iterator of segments with (handles, start point, end point). Returns None if any ids are invalid or if the segments are not continuous.
fn subpath_from_segments(&self, segments: impl Iterator<Item = (BezierHandles, usize, usize)>) -> Option<bezier_rs::Subpath<PointId>> {
let mut first_point = None;
let mut groups = Vec::new();
let mut last: Option<(usize, BezierHandles)> = None;
for (handle, start, end) in segments {
if last.is_some_and(|(previous_end, _)| previous_end != start) {
warn!("subpath_from_segments that were not continuous");
return None;
}
first_point = Some(first_point.unwrap_or(start));
groups.push(ManipulatorGroup {
anchor: self.point_domain.positions()[start],
in_handle: last.and_then(|(_, handle)| handle.end()),
out_handle: handle.start(),
id: self.point_domain.ids()[start],
});
last = Some((end, handle));
}
let closed = groups.len() > 1 && last.map(|(point, _)| point) == first_point;
if let Some((end, last_handle)) = last {
if closed {
groups[0].in_handle = last_handle.end();
} else {
groups.push(ManipulatorGroup {
anchor: self.point_domain.positions()[end],
in_handle: last_handle.end(),
out_handle: None,
id: self.point_domain.ids()[end],
});
}
}
Some(bezier_rs::Subpath::new(groups, closed))
}
/// Construct a [`bezier_rs::Bezier`] curve for each region, skipping invalid regions.
pub fn region_bezier_paths(&self) -> impl Iterator<Item = (RegionId, bezier_rs::Subpath<PointId>)> + '_ {
/// Construct a [`Bezier`] curve for each region, skipping invalid regions.
pub fn region_manipulator_groups(&self) -> impl Iterator<Item = (RegionId, Vec<ManipulatorGroup<PointId>>)> + '_ {
self.region_domain
.id
.iter()
@@ -876,7 +858,29 @@ impl VectorData {
.zip(self.segment_domain.end_point.get(range)?)
.map(|((&handles, &start), &end)| (handles, start, end));
self.subpath_from_segments(segments_iter).map(|subpath| (id, subpath))
let mut manipulator_groups = Vec::new();
let mut in_handle = None;
for segment in segments_iter {
let (handles, start_point_index, _end_point_index) = segment;
let start_point_id = self.point_domain.id[start_point_index];
let start_point = self.point_domain.position[start_point_index];
let (manipulator_group, next_in_handle) = match handles {
BezierHandles::Linear => (ManipulatorGroup::new_with_id(start_point, in_handle, None, start_point_id), None),
BezierHandles::Quadratic { handle } => (ManipulatorGroup::new_with_id(start_point, in_handle, Some(handle), start_point_id), None),
BezierHandles::Cubic { handle_start, handle_end } => (ManipulatorGroup::new_with_id(start_point, in_handle, Some(handle_start), start_point_id), Some(handle_end)),
};
in_handle = next_in_handle;
manipulator_groups.push(manipulator_group);
}
if let Some(first) = manipulator_groups.first_mut() {
first.in_handle = in_handle;
}
Some((id, manipulator_groups))
})
}
@@ -888,19 +892,19 @@ impl VectorData {
}
StrokePathIter {
vector_data: self,
vector: self,
points,
skip: 0,
done_one: false,
}
}
/// Construct a [`bezier_rs::Bezier`] curve for stroke.
pub fn stroke_bezier_paths(&self) -> impl Iterator<Item = bezier_rs::Subpath<PointId>> {
self.build_stroke_path_iter().map(|(group, closed)| bezier_rs::Subpath::new(group, closed))
/// Construct a [`Bezier`] curve for stroke.
pub fn stroke_bezier_paths(&self) -> impl Iterator<Item = Subpath<PointId>> {
self.build_stroke_path_iter().map(|(manipulators_list, closed)| Subpath::new(manipulators_list, closed))
}
/// Construct and return an iterator of Vec of `(bezier_rs::ManipulatorGroup<PointId>], bool)` for stroke.
/// Construct and return an iterator of Vec of `(ManipulatorGroup<PointId>], bool)` for stroke.
/// The boolean in the tuple indicates if the path is closed.
pub fn stroke_manipulator_groups(&self) -> impl Iterator<Item = (Vec<ManipulatorGroup<PointId>>, bool)> {
self.build_stroke_path_iter()
@@ -908,15 +912,15 @@ impl VectorData {
/// Construct a [`kurbo::BezPath`] curve for stroke.
pub fn stroke_bezpath_iter(&self) -> impl Iterator<Item = kurbo::BezPath> {
self.build_stroke_path_iter().map(|(group, closed)| {
self.build_stroke_path_iter().map(|(manipulators_list, closed)| {
let mut bezpath = kurbo::BezPath::new();
let mut out_handle;
let Some(first) = group.first() else { return bezpath };
let Some(first) = manipulators_list.first() else { return bezpath };
bezpath.move_to(dvec2_to_point(first.anchor));
out_handle = first.out_handle;
for manipulator in group.iter().skip(1) {
for manipulator in manipulators_list.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)),
@@ -944,13 +948,11 @@ impl VectorData {
self.stroke_bezier_paths().flat_map(|mut path| std::mem::take(path.manipulator_groups_mut()))
}
/// Get manipulator by id
pub fn manipulator_group_id(&self, id: impl Into<PointId>) -> Option<ManipulatorGroup<PointId>> {
let id = id.into();
self.manipulator_groups().find(|group| group.id == id)
self.manipulator_groups().find(|manipulators| manipulators.id == id)
}
/// Transforms this vector data
pub fn transform(&mut self, transform: DAffine2) {
self.point_domain.transform(transform);
self.segment_domain.transform(transform);
@@ -1018,7 +1020,7 @@ impl StrokePathIterPointMetadata {
#[derive(Clone)]
pub struct StrokePathIter<'a> {
vector_data: &'a VectorData,
vector: &'a Vector,
points: Vec<StrokePathIterPointMetadata>,
skip: usize,
done_one: bool,
@@ -1041,36 +1043,36 @@ impl Iterator for StrokePathIter<'_> {
// There will always be one (seeing as we checked above)
let mut point_index = current_start;
let mut groups = Vec::new();
let mut manipulators_list = Vec::new();
let mut in_handle = None;
let mut closed = false;
loop {
let Some(val) = self.points[point_index].take_first() else {
// Dead end
groups.push(ManipulatorGroup {
anchor: self.vector_data.point_domain.positions()[point_index],
manipulators_list.push(ManipulatorGroup {
anchor: self.vector.point_domain.positions()[point_index],
in_handle,
out_handle: None,
id: self.vector_data.point_domain.ids()[point_index],
id: self.vector.point_domain.ids()[point_index],
});
break;
};
let mut handles = self.vector_data.segment_domain.handles()[val.segment_index];
let mut handles = self.vector.segment_domain.handles()[val.segment_index];
if val.start_from_end {
handles = handles.reversed();
}
let next_point_index = if val.start_from_end {
self.vector_data.segment_domain.start_point()[val.segment_index]
self.vector.segment_domain.start_point()[val.segment_index]
} else {
self.vector_data.segment_domain.end_point()[val.segment_index]
self.vector.segment_domain.end_point()[val.segment_index]
};
groups.push(ManipulatorGroup {
anchor: self.vector_data.point_domain.positions()[point_index],
manipulators_list.push(ManipulatorGroup {
anchor: self.vector.point_domain.positions()[point_index],
in_handle,
out_handle: handles.start(),
id: self.vector_data.point_domain.ids()[point_index],
id: self.vector.point_domain.ids()[point_index],
});
in_handle = handles.end();
@@ -1079,22 +1081,22 @@ impl Iterator for StrokePathIter<'_> {
self.points[next_point_index].take_eq(val.flipped());
if next_point_index == current_start {
closed = true;
groups[0].in_handle = in_handle;
manipulators_list[0].in_handle = in_handle;
break;
}
}
Some((groups, closed))
Some((manipulators_list, closed))
}
}
impl bezier_rs::Identifier for PointId {
impl Identifier for PointId {
fn new() -> Self {
Self::generate()
}
}
/// Represents the conversion of ids used when concatenating vector data with conflicting ids.
/// Represents the conversion of IDs used when concatenating vector paths with conflicting IDs.
pub struct IdMap {
pub point_offset: usize,
pub point_map: HashMap<PointId, PointId>,

View File

@@ -1,90 +0,0 @@
use super::{PointId, SegmentId, VectorData};
use glam::DVec2;
use petgraph::graph::{EdgeIndex, NodeIndex, UnGraph};
use rustc_hash::FxHashMap;
/// All the fixed fields of a point from the point domain.
pub struct Point {
pub id: PointId,
pub position: DVec2,
}
/// Useful indexes to speed up various operations on `VectorData`.
///
/// Important: It is the user's responsibility to ensure the indexes remain valid after mutations to the data.
pub struct VectorDataIndex {
/// 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 VectorDataIndex {
/// Construct a [`VectorDataIndex`] by building indexes from the given [`VectorData`]. Takes `O(n)` time.
pub fn build_from(data: &VectorData) -> 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(&self, id: PointId, data: &VectorData) -> DVec2 {
let offset = self.point_to_offset[&id];
data.point_domain.positions()[offset]
}
}

View File

@@ -1,14 +1,16 @@
use super::*;
use crate::Ctx;
use crate::instances::Instance;
use crate::subpath::BezierHandles;
use crate::table::{Table, TableRow};
use crate::uuid::{NodeId, generate_uuid};
use bezier_rs::BezierHandles;
use crate::vector::misc::{HandleId, HandleType, point_to_dvec2};
use dyn_any::DynAny;
use glam::{DAffine2, DVec2};
use kurbo::{BezPath, PathEl, Point};
use std::collections::{HashMap, HashSet};
use std::hash::BuildHasher;
/// Represents a procedural change to the [`PointDomain`] in [`VectorData`].
/// Represents a procedural change to the [`PointDomain`] in [`Vector`].
#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct PointModification {
add: Vec<PointId>,
@@ -58,12 +60,12 @@ impl PointModification {
}
}
/// Create a new modification that will convert an empty [`VectorData`] into the target [`VectorData`].
pub fn create_from_vector(vector_data: &VectorData) -> Self {
/// Create a new modification that will convert an empty [`Vector`] into the target [`Vector`].
pub fn create_from_vector(vector: &Vector) -> Self {
Self {
add: vector_data.point_domain.ids().to_vec(),
add: vector.point_domain.ids().to_vec(),
remove: HashSet::new(),
delta: vector_data.point_domain.ids().iter().copied().zip(vector_data.point_domain.positions().iter().cloned()).collect(),
delta: vector.point_domain.ids().iter().copied().zip(vector.point_domain.positions().iter().cloned()).collect(),
}
}
@@ -79,7 +81,7 @@ impl PointModification {
}
}
/// Represents a procedural change to the [`SegmentDomain`] in [`VectorData`].
/// Represents a procedural change to the [`SegmentDomain`] in [`Vector`].
#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct SegmentModification {
add: Vec<SegmentId>,
@@ -177,11 +179,11 @@ impl SegmentModification {
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);
warn!("invalid start id: {start:#?}");
continue;
};
let Some(end_index) = point_domain.resolve_id(end) else {
warn!("invalid end id: {:#?}", end);
warn!("invalid end id: {end:#?}");
continue;
};
@@ -206,27 +208,25 @@ impl SegmentModification {
assert!(
segment_domain.start_point().iter().all(|&index| index < point_domain.ids().len()),
"index should be in range {:#?}",
segment_domain
"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
"index should be in range {segment_domain:#?}"
);
}
/// Create a new modification that will convert an empty [`VectorData`] into the target [`VectorData`].
pub fn create_from_vector(vector_data: &VectorData) -> Self {
let point_id = |(&segment, &index)| (segment, vector_data.point_domain.ids()[index]);
/// Create a new modification that will convert an empty [`Vector`] into the target [`Vector`].
pub fn create_from_vector(vector: &Vector) -> Self {
let point_id = |(&segment, &index)| (segment, vector.point_domain.ids()[index]);
Self {
add: vector_data.segment_domain.ids().to_vec(),
add: vector.segment_domain.ids().to_vec(),
remove: HashSet::new(),
start_point: vector_data.segment_domain.ids().iter().zip(vector_data.segment_domain.start_point()).map(point_id).collect(),
end_point: vector_data.segment_domain.ids().iter().zip(vector_data.segment_domain.end_point()).map(point_id).collect(),
handle_primary: vector_data.segment_bezier_iter().map(|(id, b, _, _)| (id, b.handle_start().map(|handle| handle - b.start))).collect(),
handle_end: vector_data.segment_bezier_iter().map(|(id, b, _, _)| (id, b.handle_end().map(|handle| handle - b.end))).collect(),
stroke: vector_data.segment_domain.ids().iter().copied().zip(vector_data.segment_domain.stroke().iter().cloned()).collect(),
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(),
}
}
@@ -251,7 +251,7 @@ impl SegmentModification {
}
}
/// Represents a procedural change to the [`RegionDomain`] in [`VectorData`].
/// Represents a procedural change to the [`RegionDomain`] in [`Vector`].
#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct RegionModification {
add: Vec<RegionId>,
@@ -284,18 +284,18 @@ impl RegionModification {
}
}
/// Create a new modification that will convert an empty [`VectorData`] into the target [`VectorData`].
pub fn create_from_vector(vector_data: &VectorData) -> Self {
/// Create a new modification that will convert an empty [`Vector`] into the target [`Vector`].
pub fn create_from_vector(vector: &Vector) -> Self {
Self {
add: vector_data.region_domain.ids().to_vec(),
add: vector.region_domain.ids().to_vec(),
remove: HashSet::new(),
segment_range: vector_data.region_domain.ids().iter().copied().zip(vector_data.region_domain.segment_range().iter().cloned()).collect(),
fill: vector_data.region_domain.ids().iter().copied().zip(vector_data.region_domain.fill().iter().cloned()).collect(),
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 [`VectorData`].
/// Represents a procedural change to the [`Vector`].
#[derive(Clone, Debug, Default, PartialEq, DynAny, serde::Serialize, serde::Deserialize)]
pub struct VectorModification {
points: PointModification,
@@ -327,27 +327,27 @@ pub enum VectorModificationType {
}
impl VectorModification {
/// Apply this modification to the specified [`VectorData`].
pub fn apply(&self, vector_data: &mut VectorData) {
self.points.apply(&mut vector_data.point_domain, &mut vector_data.segment_domain);
self.segments.apply(&mut vector_data.segment_domain, &vector_data.point_domain);
self.regions.apply(&mut vector_data.region_domain);
/// Apply this modification to the specified [`Vector`].
pub fn apply(&self, vector: &mut Vector) {
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_data.segment_domain.ids().contains(&val[0].segment) && vector_data.segment_domain.ids().contains(&val[1].segment);
vector_data
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_data.colinear_manipulators.iter().any(|test| test == handles || test == &[handles[1], handles[0]]) && valid(handles) {
vector_data.colinear_manipulators.push(*handles);
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_data_modification: &VectorModificationType) {
match vector_data_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),
@@ -400,13 +400,13 @@ impl VectorModification {
}
}
/// Create a new modification that will convert an empty [`VectorData`] into the target [`VectorData`].
pub fn create_from_vector(vector_data: &VectorData) -> Self {
/// Create a new modification that will convert an empty [`Vector`] into the target [`Vector`].
pub fn create_from_vector(vector: &Vector) -> Self {
Self {
points: PointModification::create_from_vector(vector_data),
segments: SegmentModification::create_from_vector(vector_data),
regions: RegionModification::create_from_vector(vector_data),
add_g1_continuous: vector_data.colinear_manipulators.iter().copied().collect(),
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(),
}
}
@@ -420,38 +420,38 @@ impl Hash for VectorModification {
/// Applies a diff modification to a vector path.
#[node_macro::node(category(""))]
async fn path_modify(_ctx: impl Ctx, mut vector_data: VectorDataTable, modification: Box<VectorModification>, node_path: Vec<NodeId>) -> VectorDataTable {
if vector_data.is_empty() {
vector_data.push(Instance::default());
async fn path_modify(_ctx: impl Ctx, mut vector: Table<Vector>, modification: Box<VectorModification>, node_path: Vec<NodeId>) -> Table<Vector> {
if vector.is_empty() {
vector.push(TableRow::default());
}
let vector_data_instance = vector_data.get_mut(0).expect("push should give one item");
modification.apply(vector_data_instance.instance);
let row = vector.get_mut(0).expect("push should give one item");
modification.apply(row.element);
// Update the source node id
let this_node_path = node_path.iter().rev().nth(1).copied();
*vector_data_instance.source_node_id = vector_data_instance.source_node_id.or(this_node_path);
*row.source_node_id = row.source_node_id.or(this_node_path);
if vector_data.len() > 1 {
warn!("The path modify ran on {} instances of vector data. Only the first can be modified.", vector_data.len());
if vector.len() > 1 {
warn!("The path modify ran on {} vector rows. Only the first can be modified.", vector.len());
}
vector_data
vector
}
/// Applies the vector path's local transformation to its geometry and resets it to the identity.
#[node_macro::node(category("Vector"))]
async fn apply_transform(_ctx: impl Ctx, mut vector_data: VectorDataTable) -> VectorDataTable {
for vector_data_instance in vector_data.instance_mut_iter() {
let vector_data = vector_data_instance.instance;
let transform = *vector_data_instance.transform;
async fn apply_transform(_ctx: impl Ctx, mut vector: Table<Vector>) -> Table<Vector> {
for row in vector.iter_mut() {
let vector = row.element;
let transform = *row.transform;
for (_, point) in vector_data.point_domain.positions_mut() {
for (_, point) in vector.point_domain.positions_mut() {
*point = transform.transform_point2(*point);
}
*vector_data_instance.transform = DAffine2::IDENTITY;
*row.transform = DAffine2::IDENTITY;
}
vector_data
vector
}
// Do we want to enforce that all serialized/deserialized hashmaps are a vec of tuples?
@@ -524,11 +524,11 @@ pub struct AppendBezpath<'a> {
last_segment_id: Option<SegmentId>,
point_id: PointId,
segment_id: SegmentId,
vector_data: &'a mut VectorData,
vector: &'a mut Vector,
}
impl<'a> AppendBezpath<'a> {
fn new(vector_data: &'a mut VectorData) -> Self {
fn new(vector: &'a mut Vector) -> Self {
Self {
first_point: None,
last_point: None,
@@ -536,9 +536,9 @@ impl<'a> AppendBezpath<'a> {
last_point_index: None,
first_segment_id: None,
last_segment_id: None,
point_id: vector_data.point_domain.next_id(),
segment_id: vector_data.segment_domain.next_id(),
vector_data,
point_id: vector.point_domain.next_id(),
segment_id: vector.segment_domain.next_id(),
vector,
}
}
@@ -555,28 +555,28 @@ impl<'a> AppendBezpath<'a> {
// Create a new segment.
let next_segment_id = self.segment_id.next_id();
self.vector_data
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_data.region_domain.next_id();
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_data.region_domain.push(next_region_id, first_segment_id..=last_segment_id, FillId::ZERO);
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_data.point_domain.ids().len();
let next_point_index = self.vector.point_domain.ids().len();
let next_point_id = self.point_id.next_id();
self.vector_data.point_domain.push(next_point_id, point_to_dvec2(end_point));
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_data
self.vector
.segment_domain
.push(next_segment_id, self.last_point_index.unwrap(), next_point_index, handle, StrokeId::ZERO);
@@ -593,8 +593,8 @@ impl<'a> AppendBezpath<'a> {
self.last_point = Some(point);
// Append the first point.
let next_point_index = self.vector_data.point_domain.ids().len();
self.vector_data.point_domain.push(self.point_id.next_id(), point_to_dvec2(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);
@@ -610,8 +610,8 @@ impl<'a> AppendBezpath<'a> {
self.last_segment_id = None;
}
pub fn append_bezpath(vector_data: &'a mut VectorData, bezpath: BezPath) {
let mut this = Self::new(vector_data);
pub fn append_bezpath(vector: &'a mut Vector, bezpath: BezPath) {
let mut this = Self::new(vector);
let mut elements = bezpath.elements().iter().peekable();
while let Some(element) = elements.next() {
@@ -656,12 +656,11 @@ impl<'a> AppendBezpath<'a> {
}
}
pub trait VectorDataExt {
/// Appends a Kurbo BezPath to the vector data.
pub trait VectorExt {
fn append_bezpath(&mut self, bezpath: BezPath);
}
impl VectorDataExt for VectorData {
impl VectorExt for Vector {
fn append_bezpath(&mut self, bezpath: BezPath) {
AppendBezpath::append_bezpath(self, bezpath);
}
@@ -685,62 +684,62 @@ impl HandleExt for HandleId {
#[cfg(test)]
mod tests {
use kurbo::{PathSeg, QuadBez};
use super::*;
use crate::subpath::{Bezier, Subpath};
#[test]
fn modify_new() {
let vector_data = VectorData::from_subpaths(
[bezier_rs::Subpath::new_ellipse(DVec2::ZERO, DVec2::ONE), bezier_rs::Subpath::new_rect(DVec2::NEG_ONE, DVec2::ZERO)],
false,
);
let 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_data);
let modify = VectorModification::create_from_vector(&vector);
let mut new = VectorData::default();
let mut new = Vector::default();
modify.apply(&mut new);
assert_eq!(vector_data, new);
assert_eq!(vector, new);
}
#[test]
fn modify_existing() {
use bezier_rs::{Bezier, Subpath};
let subpaths = [
Subpath::new_ellipse(DVec2::ZERO, DVec2::ONE),
Subpath::new_rect(DVec2::NEG_ONE, DVec2::ZERO),
Subpath::from_beziers(
&[
Bezier::from_quadratic_dvec2(DVec2::new(0., 0.), DVec2::new(5., 10.), DVec2::new(10., 0.)),
Bezier::from_quadratic_dvec2(DVec2::new(10., 0.), DVec2::new(15., 10.), DVec2::new(20., 0.)),
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_data = VectorData::from_subpaths(subpaths, false);
let mut vector = Vector::from_subpaths(subpaths, false);
let mut modify_new = VectorModification::create_from_vector(&vector_data);
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_data.point_domain.ids()[0];
let point = vector.point_domain.ids()[0];
modification.modify(&VectorModificationType::ApplyPointDelta { point, delta: DVec2::X * 0.5 });
let point = vector_data.point_domain.ids()[9];
let point = vector.point_domain.ids()[9];
modification.modify(&VectorModificationType::ApplyPointDelta { point, delta: DVec2::X });
}
let mut new = VectorData::default();
let mut new = Vector::default();
modify_new.apply(&mut new);
modify_original.apply(&mut vector_data);
modify_original.apply(&mut vector);
assert_eq!(vector_data, new);
assert_eq!(vector_data.point_domain.positions()[0], DVec2::X);
assert_eq!(vector_data.point_domain.positions()[9], DVec2::new(11., 0.));
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_data.segment_bezier_iter().nth(8).unwrap().1,
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_data.segment_bezier_iter().nth(9).unwrap().1,
vector.segment_bezier_iter().nth(9).unwrap().1,
Bezier::from_quadratic_dvec2(DVec2::new(11., 0.), DVec2::new(16., 10.), DVec2::new(20., 0.))
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,85 +1,24 @@
mod attributes;
mod indexed;
mod modification;
use super::misc::{dvec2_to_point, point_to_dvec2};
use super::misc::dvec2_to_point;
use super::style::{PathStyle, Stroke};
use crate::bounds::BoundingBox;
use crate::instances::Instances;
pub use super::vector_attributes::*;
pub use super::vector_modification::*;
use crate::bounds::{BoundingBox, RenderBoundingBox};
use crate::math::quad::Quad;
use crate::subpath::{BezierHandles, ManipulatorGroup, Subpath};
use crate::table::Table;
use crate::transform::Transform;
use crate::vector::click_target::{ClickTargetType, FreePoint};
use crate::{AlphaBlending, Color, GraphicGroupTable};
pub use attributes::*;
use bezier_rs::{BezierHandles, ManipulatorGroup};
use crate::vector::misc::{HandleId, ManipulatorPointId};
use crate::{AlphaBlending, Color, Graphic};
use core::borrow::Borrow;
use core::hash::Hash;
use dyn_any::DynAny;
use glam::{DAffine2, DVec2};
pub use indexed::VectorDataIndex;
use kurbo::{Affine, Rect, Shape};
pub use modification::*;
use kurbo::{Affine, BezPath, Rect, Shape};
use std::collections::HashMap;
// TODO: Eventually remove this migration document upgrade code
pub fn migrate_vector_data<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<VectorDataTable, 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,
/// 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 graphic group during destructive Boolean Operations (and other nodes with a similar effect) so that click targets can be preserved.
pub upstream_graphic_group: Option<GraphicGroupTable>,
}
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(untagged)]
#[allow(clippy::large_enum_variant)]
enum EitherFormat {
VectorData(VectorData),
OldVectorData(OldVectorData),
VectorDataTable(VectorDataTable),
}
Ok(match EitherFormat::deserialize(deserializer)? {
EitherFormat::VectorData(vector_data) => VectorDataTable::new(vector_data),
EitherFormat::OldVectorData(old) => {
let mut vector_data_table = VectorDataTable::new(VectorData {
style: old.style,
colinear_manipulators: old.colinear_manipulators,
point_domain: old.point_domain,
segment_domain: old.segment_domain,
region_domain: old.region_domain,
upstream_graphic_group: old.upstream_graphic_group,
});
*vector_data_table.instance_mut_iter().next().unwrap().transform = old.transform;
*vector_data_table.instance_mut_iter().next().unwrap().alpha_blending = old.alpha_blending;
vector_data_table
}
EitherFormat::VectorDataTable(vector_data_table) => vector_data_table,
})
}
pub type VectorDataTable = Instances<VectorData>;
/// [VectorData] is passed between nodes.
/// It contains a list of subpaths (that may be open or closed), a transform, and some style information.
///
/// Segments are connected if they share endpoints.
/// Represents vector graphics data, composed of Bézier curves in a path or mesh arrangement.
#[derive(Clone, Debug, PartialEq, DynAny, serde::Serialize, serde::Deserialize)]
pub struct VectorData {
pub struct Vector {
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).
@@ -90,11 +29,13 @@ pub struct VectorData {
pub segment_domain: SegmentDomain,
pub region_domain: RegionDomain,
// Used to store the upstream graphic group during destructive Boolean Operations (and other nodes with a similar effect) so that click targets can be preserved.
pub upstream_graphic_group: Option<GraphicGroupTable>,
/// 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_nested_layers: Option<Table<Graphic>>,
}
impl Default for VectorData {
impl Default for Vector {
fn default() -> Self {
Self {
style: PathStyle::new(Some(Stroke::new(Some(Color::BLACK), 0.)), super::style::Fill::None),
@@ -102,12 +43,12 @@ impl Default for VectorData {
point_domain: PointDomain::new(),
segment_domain: SegmentDomain::new(),
region_domain: RegionDomain::new(),
upstream_graphic_group: None,
upstream_nested_layers: None,
}
}
}
impl std::hash::Hash for VectorData {
impl std::hash::Hash for Vector {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.point_domain.hash(state);
self.segment_domain.hash(state);
@@ -117,17 +58,17 @@ impl std::hash::Hash for VectorData {
}
}
impl VectorData {
/// Push a subpath to the vector data
pub fn append_subpath(&mut self, subpath: impl Borrow<bezier_rs::Subpath<PointId>>, preserve_id: bool) {
let subpath: &bezier_rs::Subpath<PointId> = subpath.borrow();
impl Vector {
/// 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) => bezier_rs::BezierHandles::Linear,
(Some(handle), None) | (None, Some(handle)) => bezier_rs::BezierHandles::Quadratic { handle },
(Some(handle_start), Some(handle_end)) => bezier_rs::BezierHandles::Cubic { handle_start, handle_end },
(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();
@@ -190,33 +131,40 @@ impl VectorData {
self.point_domain.push(id, point.position);
}
/// Construct some new vector data from a single subpath with an identity transform and black fill.
pub fn from_subpath(subpath: impl Borrow<bezier_rs::Subpath<PointId>>) -> Self {
/// 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 {
Self::from_subpaths([subpath], false)
}
/// Construct some new vector data from subpaths with an identity transform and black fill.
pub fn from_subpaths(subpaths: impl IntoIterator<Item = impl Borrow<bezier_rs::Subpath<PointId>>>, preserve_id: bool) -> Self {
let mut vector_data = Self::default();
/// Construct some new vector path from a single [`BezPath`] with an identity transform and black fill.
pub fn from_bezpath(bezpath: BezPath) -> Self {
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 {
let mut vector = Self::default();
for subpath in subpaths.into_iter() {
vector_data.append_subpath(subpath, preserve_id);
vector.append_subpath(subpath, preserve_id);
}
vector_data
vector
}
pub fn from_target_types(target_types: impl IntoIterator<Item = impl Borrow<ClickTargetType>>, preserve_id: bool) -> Self {
let mut vector_data = Self::default();
let mut vector = Self::default();
for target_type in target_types.into_iter() {
match target_type.borrow() {
ClickTargetType::Subpath(subpath) => vector_data.append_subpath(subpath, preserve_id),
ClickTargetType::FreePoint(point) => vector_data.append_free_point(point, preserve_id),
ClickTargetType::Subpath(subpath) => vector.append_subpath(subpath, preserve_id),
ClickTargetType::FreePoint(point) => vector.append_free_point(point, preserve_id),
}
}
vector_data
vector
}
/// Compute the bounding boxes of the bezpaths without any transform
@@ -237,7 +185,7 @@ impl VectorData {
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, bezier_rs::BezierHandles::Linear, StrokeId::ZERO);
self.segment_domain.push(segment_id, start, end, BezierHandles::Linear, StrokeId::ZERO);
}
}
@@ -296,14 +244,19 @@ impl VectorData {
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: bezier_rs::BezierHandles, stroke: StrokeId) {
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 bezier_rs::BezierHandles, PointId, PointId)> {
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]))
@@ -369,12 +322,12 @@ impl VectorData {
self.point_domain.resolve_id(point).map_or(0, |point| self.segment_domain.connected_count(point))
}
pub fn check_point_inside_shape(&self, vector_data_transform: DAffine2, point: DVec2) -> bool {
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(vector_data_transform.to_cols_array()));
bezpath.apply_affine(Affine::new(transform.to_cols_array()));
bezpath.close_path();
let bbox = bezpath.bounding_box();
(bezpath, bbox)
@@ -488,241 +441,142 @@ impl VectorData {
}
}
impl BoundingBox for VectorDataTable {
fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> Option<[DVec2; 2]> {
self.instance_ref_iter()
.flat_map(|instance| {
impl BoundingBox for Table<Vector> {
fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox {
let bounds = self
.iter()
.flat_map(|row| {
if !include_stroke {
return instance.instance.bounding_box_with_transform(transform * *instance.transform);
return row.element.bounding_box_with_transform(transform * *row.transform);
}
let stroke_width = instance.instance.style.stroke().map(|s| s.weight()).unwrap_or_default();
let stroke_width = row.element.style.stroke().map(|s| s.weight()).unwrap_or_default();
let miter_limit = instance.instance.style.stroke().map(|s| s.join_miter_limit).unwrap_or(1.);
let miter_limit = row.element.style.stroke().map(|s| s.join_miter_limit).unwrap_or(1.);
let scale = transform.decompose_scale();
// We use the full line width here to account for different styles of stroke caps
let offset = DVec2::splat(stroke_width * scale.x.max(scale.y) * miter_limit);
instance.instance.bounding_box_with_transform(transform * *instance.transform).map(|[a, b]| [a - offset, b + offset])
row.element.bounding_box_with_transform(transform * *row.transform).map(|[a, b]| [a - offset, b + offset])
})
.reduce(Quad::combine_bounds)
}
}
.reduce(Quad::combine_bounds);
/// A selectable part of a curve, either an anchor (start or end of a bézier) or a handle (doesn't necessarily go through the bézier but influences curvature).
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, DynAny, 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(&self, vector_data: &VectorData) -> Option<DVec2> {
match self {
ManipulatorPointId::Anchor(id) => vector_data.point_domain.position_from_id(*id),
ManipulatorPointId::PrimaryHandle(id) => vector_data.segment_from_id(*id).and_then(|bezier| bezier.handle_start()),
ManipulatorPointId::EndHandle(id) => vector_data.segment_from_id(*id).and_then(|bezier| bezier.handle_end()),
}
}
pub fn get_anchor_position(&self, vector_data: &VectorData) -> Option<DVec2> {
match self {
ManipulatorPointId::EndHandle(_) | ManipulatorPointId::PrimaryHandle(_) => self.get_anchor(vector_data).and_then(|id| vector_data.point_domain.position_from_id(id)),
_ => self.get_position(vector_data),
}
}
/// 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(self, vector_data: &VectorData) -> Option<[HandleId; 2]> {
match self {
ManipulatorPointId::Anchor(point) => vector_data.all_connected(point).take(2).collect::<Vec<_>>().try_into().ok(),
ManipulatorPointId::PrimaryHandle(segment) => {
let point = vector_data.segment_domain.segment_start_from_id(segment)?;
let current = HandleId::primary(segment);
let other = vector_data.segment_domain.all_connected(point).find(|&value| value != current);
other.map(|other| [current, other])
}
ManipulatorPointId::EndHandle(segment) => {
let point = vector_data.segment_domain.segment_end_from_id(segment)?;
let current = HandleId::end(segment);
let other = vector_data.segment_domain.all_connected(point).find(|&value| value != current);
other.map(|other| [current, other])
}
}
}
/// Attempt to find the closest anchor. If self is already an anchor then it is just self. If it is a start or end handle, then the start or end point is chosen.
#[must_use]
pub fn get_anchor(self, vector_data: &VectorData) -> Option<PointId> {
match self {
ManipulatorPointId::Anchor(point) => Some(point),
ManipulatorPointId::PrimaryHandle(segment) => vector_data.segment_start_from_id(segment),
ManipulatorPointId::EndHandle(segment) => vector_data.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,
match bounds {
Some(bounds) => RenderBoundingBox::Rectangle(bounds),
None => RenderBoundingBox::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,
}
// 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;
/// 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,
}
#[derive(Clone, Debug, PartialEq, DynAny, serde::Serialize, serde::Deserialize)]
pub struct OldVectorData {
pub transform: DAffine2,
pub alpha_blending: AlphaBlending,
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()),
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(serde::Serialize, serde::Deserialize)]
#[serde(untagged)]
#[allow(clippy::large_enum_variant)]
enum EitherFormat {
Vector(Vector),
OldVectorData(OldVectorData),
VectorTable(Table<Vector>),
}
Ok(match EitherFormat::deserialize(deserializer)? {
EitherFormat::Vector(vector) => Table::new_from_element(vector),
EitherFormat::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_nested_layers: 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
}
}
}
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(self, vector_data: &VectorData) -> f64 {
let Some(anchor_position) = self.to_manipulator_point().get_anchor_position(vector_data) 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_data);
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),
}
}
}
#[cfg(test)]
fn assert_subpath_eq(generated: &[bezier_rs::Subpath<PointId>], expected: &[bezier_rs::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);
}
}
EitherFormat::VectorTable(vector_table) => vector_table,
})
}
#[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 = bezier_rs::Subpath::new_ellipse(DVec2::NEG_ONE, DVec2::ONE);
let vector_data = VectorData::from_subpath(&circle);
assert_eq!(vector_data.point_domain.ids().len(), 4);
let bezier_paths = vector_data.segment_bezier_iter().map(|(_, bezier, _, _)| bezier).collect::<Vec<_>>();
let circle = Subpath::new_ellipse(DVec2::NEG_ONE, DVec2::ONE);
let 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_data.stroke_bezier_paths().collect::<Vec<_>>();
let generated = vector.stroke_bezier_paths().collect::<Vec<_>>();
assert_subpath_eq(&generated, &[circle]);
}
#[test]
fn construct_open_subpath() {
let bezier = bezier_rs::Bezier::from_cubic_dvec2(DVec2::ZERO, DVec2::NEG_ONE, DVec2::ONE, DVec2::X);
let subpath = bezier_rs::Subpath::from_bezier(&bezier);
let vector_data = VectorData::from_subpath(&subpath);
assert_eq!(vector_data.point_domain.ids().len(), 2);
let bezier_paths = vector_data.segment_bezier_iter().map(|(_, bezier, _, _)| bezier).collect::<Vec<_>>();
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::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_data.stroke_bezier_paths().collect::<Vec<_>>();
let generated = vector.stroke_bezier_paths().collect::<Vec<_>>();
assert_subpath_eq(&generated, &[subpath]);
}
#[test]
fn construct_many_subpath() {
let curve = bezier_rs::Bezier::from_cubic_dvec2(DVec2::ZERO, DVec2::NEG_ONE, DVec2::ONE, DVec2::X);
let curve = bezier_rs::Subpath::from_bezier(&curve);
let circle = bezier_rs::Subpath::new_ellipse(DVec2::NEG_ONE, DVec2::ONE);
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_data = VectorData::from_subpaths([&curve, &circle], false);
assert_eq!(vector_data.point_domain.ids().len(), 6);
let vector = Vector::from_subpaths([&curve, &circle], false);
assert_eq!(vector.point_domain.ids().len(), 6);
let bezier_paths = vector_data.segment_bezier_iter().map(|(_, bezier, _, _)| bezier).collect::<Vec<_>>();
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_data.stroke_bezier_paths().collect::<Vec<_>>();
let generated = vector.stroke_bezier_paths().collect::<Vec<_>>();
assert_subpath_eq(&generated, &[curve, circle]);
}
}

View File

@@ -1,6 +1,7 @@
use glam::{DAffine2, DVec2};
use graphene_core::gradient::GradientStops;
use graphene_core::registry::types::{Fraction, Percentage, PixelSize, TextArea};
use graphene_core::table::Table;
use graphene_core::transform::Footprint;
use graphene_core::{Color, Ctx, num_traits};
use log::warn;
@@ -286,7 +287,7 @@ fn cosine_inverse<U: num_traits::float::Float>(
/// The inverse tangent trigonometric function (atan or atan2, depending on input type) calculates:
/// atan: the angle whose tangent is the specified scalar number.
/// atan2: the angle of a ray from the origin to the specified coordinate.
/// atan2: the angle of a ray from the origin to the specified vec2.
///
/// The resulting angle is always in the range [0°, 180°] or, in radians, [-π/2, π/2].
#[node_macro::node(category("Math: Trig"))]
@@ -348,21 +349,21 @@ fn random<U: num_traits::float::Float>(
}
/// Convert a number to an integer of the type u32, which may be the required type for certain node inputs. This will be removed in the future when automatic type conversion is implemented.
#[node_macro::node(name("To u32"), category("Math: Numeric"))]
#[node_macro::node(name("To u32"), category("Type Conversion"))]
fn to_u32<U: num_traits::float::Float>(_: impl Ctx, #[implementations(f64, f32)] value: U) -> u32 {
let value = U::clamp(value, U::from(0.).unwrap(), U::from(u32::MAX as f64).unwrap());
value.to_u32().unwrap()
}
/// Convert a number to an integer of the type u64, which may be the required type for certain node inputs. This will be removed in the future when automatic type conversion is implemented.
#[node_macro::node(name("To u64"), category("Math: Numeric"))]
#[node_macro::node(name("To u64"), category("Type Conversion"))]
fn to_u64<U: num_traits::float::Float>(_: impl Ctx, #[implementations(f64, f32)] value: U) -> u64 {
let value = U::clamp(value, U::from(0.).unwrap(), U::from(u64::MAX as f64).unwrap());
value.to_u64().unwrap()
}
/// Convert an integer to a decimal number of the type f64, which may be the required type for certain node inputs. This will be removed in the future when automatic type conversion is implemented.
#[node_macro::node(name("To f64"), category("Math: Numeric"))]
#[node_macro::node(name("To f64"), category("Type Conversion"))]
fn to_f64<U: num_traits::int::PrimInt>(_: impl Ctx, #[implementations(u32, u64)] value: U) -> f64 {
value.to_f64().unwrap()
}
@@ -651,31 +652,38 @@ fn percentage_value(_: impl Ctx, _primary: (), percentage: Percentage) -> f64 {
percentage
}
/// Constructs a two-dimensional vector value which may be set to any XY coordinate.
#[node_macro::node(category("Value"))]
fn coordinate_value(_: impl Ctx, _primary: (), x: f64, y: f64) -> DVec2 {
/// Constructs a two-dimensional vector value which may be set to any XY pair.
#[node_macro::node(category("Value"), name("Vec2 Value"))]
fn vec2_value(_: impl Ctx, _primary: (), x: f64, y: f64) -> DVec2 {
DVec2::new(x, y)
}
/// Constructs a color value which may be set to any color, or no color.
#[node_macro::node(category("Value"))]
fn color_value(_: impl Ctx, _primary: (), #[default(Color::BLACK)] color: Option<Color>) -> Option<Color> {
fn color_value(_: impl Ctx, _primary: (), #[default(Color::RED)] color: Table<Color>) -> Table<Color> {
color
}
/// Gets the color at the specified position along the gradient, given a position from 0 (left) to 1 (right).
#[node_macro::node(category("Color"))]
fn sample_gradient(_: impl Ctx, _primary: (), gradient: GradientStops, position: Fraction) -> Color {
let position = position.clamp(0., 1.);
gradient.evaluate(position)
}
/// Constructs a gradient value which may be set to any sequence of color stops to represent the transition between colors.
#[node_macro::node(category("Value"))]
fn gradient_value(_: impl Ctx, _primary: (), gradient: GradientStops) -> GradientStops {
gradient
}
/// Constructs a gradient value which may be set to any sequence of color stops to represent the transition between colors.
#[node_macro::node(category("Value"))]
fn gradient_table_value(_: impl Ctx, _primary: (), gradient: GradientStops) -> Table<GradientStops> {
Table::new_from_element(gradient)
}
/// Gets the color at the specified position along the gradient, given a position from 0 (left) to 1 (right).
#[node_macro::node(category("Color"))]
fn sample_gradient(_: impl Ctx, _primary: (), gradient: GradientStops, position: Fraction) -> Table<Color> {
let position = position.clamp(0., 1.);
let color = gradient.evaluate(position);
Table::new_from_element(color)
}
/// Constructs a string value which may be set to any plain text.
#[node_macro::node(category("Value"))]
fn string_value(_: impl Ctx, _primary: (), string: TextArea) -> String {

View File

@@ -9,7 +9,6 @@ license = "MIT OR Apache-2.0"
[dependencies]
# Local dependencies
dyn-any = { workspace = true }
bezier-rs = { workspace = true }
graphene-core = { workspace = true }
node-macro = { workspace = true }
glam = { workspace = true }

View File

@@ -1,11 +1,11 @@
use bezier_rs::{ManipulatorGroup, Subpath};
use dyn_any::DynAny;
use glam::{DAffine2, DVec2};
use graphene_core::instances::{Instance, InstanceRef};
use graphene_core::subpath::{ManipulatorGroup, PathSegPoints, Subpath, pathseg_points};
use graphene_core::table::{Table, TableRow, TableRowRef};
use graphene_core::vector::algorithms::merge_by_distance::MergeByDistanceExt;
use graphene_core::vector::style::Fill;
use graphene_core::vector::{PointId, VectorData, VectorDataTable};
use graphene_core::{Color, Ctx, GraphicElement, GraphicGroupTable};
use graphene_core::vector::{PointId, Vector};
use graphene_core::{Color, Ctx, Graphic};
pub use path_bool as path_bool_lib;
use path_bool::{FillRule, PathBooleanOperation};
use std::ops::Mul;
@@ -32,11 +32,11 @@ pub enum BooleanOperation {
/// Combines the geometric forms of one or more closed paths into a new vector path that results from cutting or joining the paths by the chosen method.
#[node_macro::node(category(""))]
async fn boolean_operation<I: Into<GraphicGroupTable> + 'n + Send + Clone>(
async fn boolean_operation<I: Into<Table<Graphic>> + 'n + Send + Clone>(
_: impl Ctx,
/// The group of paths to perform the boolean operation on. Nested groups are automatically flattened.
#[implementations(GraphicGroupTable, VectorDataTable)]
group_of_paths: I,
/// The table of vector paths to perform the boolean operation on. Nested tables are automatically flattened.
#[implementations(Table<Graphic>, Table<Vector>)]
content: I,
/// Which boolean operation to perform on the paths.
///
/// Union combines all paths while cutting out overlapping areas (even the interiors of a single path).
@@ -44,55 +44,55 @@ async fn boolean_operation<I: Into<GraphicGroupTable> + 'n + Send + Clone>(
/// Intersection cuts away all but the overlapping areas shared by every path.
/// Difference cuts away the overlapping areas shared by every path, leaving only the non-overlapping areas.
operation: BooleanOperation,
) -> VectorDataTable {
let group_of_paths = group_of_paths.into();
) -> Table<Vector> {
let content = content.into();
// The first index is the bottom of the stack
let mut result_vector_data_table = boolean_operation_on_vector_data_table(flatten_vector_data(&group_of_paths).instance_ref_iter(), operation);
let mut result_vector_table = boolean_operation_on_vector_table(flatten_vector(&content).iter(), operation);
// Replace the transformation matrix with a mutation of the vector points themselves
if let Some(result_vector_data) = result_vector_data_table.instance_mut_iter().next() {
let transform = *result_vector_data.transform;
*result_vector_data.transform = DAffine2::IDENTITY;
if let Some(result_vector) = result_vector_table.iter_mut().next() {
let transform = *result_vector.transform;
*result_vector.transform = DAffine2::IDENTITY;
VectorData::transform(result_vector_data.instance, transform);
result_vector_data.instance.style.set_stroke_transform(DAffine2::IDENTITY);
result_vector_data.instance.upstream_graphic_group = Some(group_of_paths.clone());
Vector::transform(result_vector.element, transform);
result_vector.element.style.set_stroke_transform(DAffine2::IDENTITY);
result_vector.element.upstream_nested_layers = Some(content.clone());
// Clean up the boolean operation result by merging duplicated points
result_vector_data.instance.merge_by_distance_spatial(*result_vector_data.transform, 0.0001);
result_vector.element.merge_by_distance_spatial(*result_vector.transform, 0.0001);
}
result_vector_data_table
result_vector_table
}
fn boolean_operation_on_vector_data_table<'a>(vector_data: impl DoubleEndedIterator<Item = InstanceRef<'a, VectorData>> + Clone, boolean_operation: BooleanOperation) -> VectorDataTable {
fn boolean_operation_on_vector_table<'a>(vector: impl DoubleEndedIterator<Item = TableRowRef<'a, Vector>> + Clone, boolean_operation: BooleanOperation) -> Table<Vector> {
match boolean_operation {
BooleanOperation::Union => union(vector_data),
BooleanOperation::SubtractFront => subtract(vector_data),
BooleanOperation::SubtractBack => subtract(vector_data.rev()),
BooleanOperation::Intersect => intersect(vector_data),
BooleanOperation::Difference => difference(vector_data),
BooleanOperation::Union => union(vector),
BooleanOperation::SubtractFront => subtract(vector),
BooleanOperation::SubtractBack => subtract(vector.rev()),
BooleanOperation::Intersect => intersect(vector),
BooleanOperation::Difference => difference(vector),
}
}
fn union<'a>(vector_data: impl DoubleEndedIterator<Item = InstanceRef<'a, VectorData>>) -> VectorDataTable {
// Reverse vector data so that the result style is the style of the first vector data
let mut vector_data_reversed = vector_data.rev();
fn union<'a>(vector: impl DoubleEndedIterator<Item = TableRowRef<'a, Vector>>) -> Table<Vector> {
// Reverse the vector table rows so that the result style is the style of the first vector row
let mut vector_reversed = vector.rev();
let mut result_vector_data_table = VectorDataTable::new_instance(vector_data_reversed.next().map(|x| x.to_instance_cloned()).unwrap_or_default());
let mut first_instance = result_vector_data_table.instance_mut_iter().next().expect("Expected the one instance we just pushed");
let mut result_vector_table = Table::new_from_row(vector_reversed.next().map(|x| x.into_cloned()).unwrap_or_default());
let mut first_row = result_vector_table.iter_mut().next().expect("Expected the one row we just pushed");
// Loop over all vector data and union it with the result
let default = Instance::default();
let mut second_vector_data = Some(vector_data_reversed.next().unwrap_or(default.to_instance_ref()));
while let Some(lower_vector_data) = second_vector_data {
let transform_of_lower_into_space_of_upper = first_instance.transform.inverse() * *lower_vector_data.transform;
// Loop over all vector table rows and union it with the result
let default = TableRow::default();
let mut second_vector = Some(vector_reversed.next().unwrap_or(default.as_ref()));
while let Some(lower_vector) = second_vector {
let transform_of_lower_into_space_of_upper = first_row.transform.inverse() * *lower_vector.transform;
let result = &mut first_instance.instance;
let result = &mut first_row.element;
let upper_path_string = to_path(result, DAffine2::IDENTITY);
let lower_path_string = to_path(lower_vector_data.instance, transform_of_lower_into_space_of_upper);
let lower_path_string = to_path(lower_vector.element, transform_of_lower_into_space_of_upper);
#[allow(unused_unsafe)]
let boolean_operation_string = unsafe { boolean_union(upper_path_string, lower_path_string) };
@@ -103,27 +103,32 @@ fn union<'a>(vector_data: impl DoubleEndedIterator<Item = InstanceRef<'a, Vector
result.segment_domain = boolean_operation_result.segment_domain;
result.region_domain = boolean_operation_result.region_domain;
second_vector_data = vector_data_reversed.next();
second_vector = vector_reversed.next();
}
result_vector_data_table
result_vector_table
}
fn subtract<'a>(vector_data: impl Iterator<Item = InstanceRef<'a, VectorData>>) -> VectorDataTable {
let mut vector_data = vector_data.into_iter();
fn subtract<'a>(vector: impl Iterator<Item = TableRowRef<'a, Vector>>) -> Table<Vector> {
let mut vector = vector.into_iter();
let mut result_vector_data_table = VectorDataTable::new_instance(vector_data.next().map(|x| x.to_instance_cloned()).unwrap_or_default());
let mut first_instance = result_vector_data_table.instance_mut_iter().next().expect("Expected the one instance we just pushed");
let mut result_vector_table = Table::new_from_row(vector.next().map(|x| x.into_cloned()).unwrap_or_default());
let mut first_row = result_vector_table.iter_mut().next().expect("Expected the one row we just pushed");
let first_row_transform = if first_row.transform.matrix2.determinant() != 0. {
first_row.transform.inverse()
} else {
DAffine2::IDENTITY
};
let mut next_vector_data = vector_data.next();
let mut next_vector = vector.next();
while let Some(lower_vector_data) = next_vector_data {
let transform_of_lower_into_space_of_upper = first_instance.transform.inverse() * *lower_vector_data.transform;
while let Some(lower_vector) = next_vector {
let transform_of_lower_into_space_of_upper = first_row_transform * *lower_vector.transform;
let result = &mut first_instance.instance;
let result = &mut first_row.element;
let upper_path_string = to_path(result, DAffine2::IDENTITY);
let lower_path_string = to_path(lower_vector_data.instance, transform_of_lower_into_space_of_upper);
let lower_path_string = to_path(lower_vector.element, transform_of_lower_into_space_of_upper);
#[allow(unused_unsafe)]
let boolean_operation_string = unsafe { boolean_subtract(upper_path_string, lower_path_string) };
@@ -134,29 +139,29 @@ fn subtract<'a>(vector_data: impl Iterator<Item = InstanceRef<'a, VectorData>>)
result.segment_domain = boolean_operation_result.segment_domain;
result.region_domain = boolean_operation_result.region_domain;
next_vector_data = vector_data.next();
next_vector = vector.next();
}
result_vector_data_table
result_vector_table
}
fn intersect<'a>(vector_data: impl DoubleEndedIterator<Item = InstanceRef<'a, VectorData>>) -> VectorDataTable {
let mut vector_data = vector_data.rev();
fn intersect<'a>(vector: impl DoubleEndedIterator<Item = TableRowRef<'a, Vector>>) -> Table<Vector> {
let mut vector = vector.rev();
let mut result_vector_data_table = VectorDataTable::new_instance(vector_data.next().map(|x| x.to_instance_cloned()).unwrap_or_default());
let mut first_instance = result_vector_data_table.instance_mut_iter().next().expect("Expected the one instance we just pushed");
let mut result_vector_table = Table::new_from_row(vector.next().map(|x| x.into_cloned()).unwrap_or_default());
let mut first_row = result_vector_table.iter_mut().next().expect("Expected the one row we just pushed");
let default = Instance::default();
let mut second_vector_data = Some(vector_data.next().unwrap_or(default.to_instance_ref()));
let default = TableRow::default();
let mut second_vector = Some(vector.next().unwrap_or(default.as_ref()));
// For each vector data, set the result to the intersection of that data and the result
while let Some(lower_vector_data) = second_vector_data {
let transform_of_lower_into_space_of_upper = first_instance.transform.inverse() * *lower_vector_data.transform;
// For each vector table row, set the result to the intersection of that path and the current result
while let Some(lower_vector) = second_vector {
let transform_of_lower_into_space_of_upper = first_row.transform.inverse() * *lower_vector.transform;
let result = &mut first_instance.instance;
let result = &mut first_row.element;
let upper_path_string = to_path(result, DAffine2::IDENTITY);
let lower_path_string = to_path(lower_vector_data.instance, transform_of_lower_into_space_of_upper);
let lower_path_string = to_path(lower_vector.element, transform_of_lower_into_space_of_upper);
#[allow(unused_unsafe)]
let boolean_operation_string = unsafe { boolean_intersect(upper_path_string, lower_path_string) };
@@ -166,127 +171,162 @@ fn intersect<'a>(vector_data: impl DoubleEndedIterator<Item = InstanceRef<'a, Ve
result.point_domain = boolean_operation_result.point_domain;
result.segment_domain = boolean_operation_result.segment_domain;
result.region_domain = boolean_operation_result.region_domain;
second_vector_data = vector_data.next();
second_vector = vector.next();
}
result_vector_data_table
result_vector_table
}
fn difference<'a>(vector_data: impl DoubleEndedIterator<Item = InstanceRef<'a, VectorData>> + Clone) -> VectorDataTable {
let mut vector_data_iter = vector_data.clone().rev();
let mut any_intersection = Instance::default();
let default = Instance::default();
let mut second_vector_data = Some(vector_data_iter.next().unwrap_or(default.to_instance_ref()));
fn difference<'a>(vector: impl DoubleEndedIterator<Item = TableRowRef<'a, Vector>> + Clone) -> Table<Vector> {
let mut vector_iter = vector.clone().rev();
let mut any_intersection = TableRow::default();
let default = TableRow::default();
let mut second_vector = Some(vector_iter.next().unwrap_or(default.as_ref()));
// Find where all vector data intersect at least once
while let Some(lower_vector_data) = second_vector_data {
let filtered_vector_data = vector_data.clone().filter(|v| *v != lower_vector_data).collect::<Vec<_>>().into_iter();
let unioned = boolean_operation_on_vector_data_table(filtered_vector_data, BooleanOperation::Union);
let first_instance = unioned.instance_ref_iter().next().expect("Expected at least one instance after the boolean union");
// Find where all vector table row paths intersect at least once
while let Some(lower_vector) = second_vector {
let filtered_vector = vector.clone().filter(|v| *v != lower_vector).collect::<Vec<_>>().into_iter();
let unioned = boolean_operation_on_vector_table(filtered_vector, BooleanOperation::Union);
let first_row = unioned.iter().next().expect("Expected at least one row after the boolean union");
let transform_of_lower_into_space_of_upper = first_instance.transform.inverse() * *lower_vector_data.transform;
let transform_of_lower_into_space_of_upper = first_row.transform.inverse() * *lower_vector.transform;
let upper_path_string = to_path(first_instance.instance, DAffine2::IDENTITY);
let lower_path_string = to_path(lower_vector_data.instance, transform_of_lower_into_space_of_upper);
let upper_path_string = to_path(first_row.element, DAffine2::IDENTITY);
let lower_path_string = to_path(lower_vector.element, transform_of_lower_into_space_of_upper);
#[allow(unused_unsafe)]
let boolean_intersection_string = unsafe { boolean_intersect(upper_path_string, lower_path_string) };
let mut instance = from_path(&boolean_intersection_string);
instance.style = first_instance.instance.style.clone();
let boolean_intersection_result = Instance {
instance,
mask: None,
transform: *first_instance.transform,
alpha_blending: *first_instance.alpha_blending,
source_node_id: *first_instance.source_node_id,
let mut element = from_path(&boolean_intersection_string);
element.style = first_row.element.style.clone();
let boolean_intersection_result = TableRow {
element,
mask: first_row.mask.clone(),
transform: *first_row.transform,
alpha_blending: *first_row.alpha_blending,
source_node_id: *first_row.source_node_id,
};
let transform_of_lower_into_space_of_upper = boolean_intersection_result.transform.inverse() * any_intersection.transform;
let upper_path_string = to_path(&boolean_intersection_result.instance, DAffine2::IDENTITY);
let lower_path_string = to_path(&any_intersection.instance, transform_of_lower_into_space_of_upper);
let upper_path_string = to_path(&boolean_intersection_result.element, DAffine2::IDENTITY);
let lower_path_string = to_path(&any_intersection.element, transform_of_lower_into_space_of_upper);
#[allow(unused_unsafe)]
let union_result = from_path(&unsafe { boolean_union(upper_path_string, lower_path_string) });
any_intersection.instance = union_result;
any_intersection.element = union_result;
any_intersection.transform = boolean_intersection_result.transform;
any_intersection.instance.style = boolean_intersection_result.instance.style.clone();
any_intersection.element.style = boolean_intersection_result.element.style.clone();
any_intersection.alpha_blending = boolean_intersection_result.alpha_blending;
second_vector_data = vector_data_iter.next();
second_vector = vector_iter.next();
}
// Subtract the area where they intersect at least once from the union of all vector data
let union = boolean_operation_on_vector_data_table(vector_data, BooleanOperation::Union);
boolean_operation_on_vector_data_table(union.instance_ref_iter().chain(std::iter::once(any_intersection.to_instance_ref())), BooleanOperation::SubtractFront)
// Subtract the area where they intersect at least once from the union of all vector paths
let union = boolean_operation_on_vector_table(vector, BooleanOperation::Union);
boolean_operation_on_vector_table(union.iter().chain(std::iter::once(any_intersection.as_ref())), BooleanOperation::SubtractFront)
}
fn flatten_vector_data(graphic_group_table: &GraphicGroupTable) -> VectorDataTable {
graphic_group_table
.instance_ref_iter()
fn flatten_vector(graphic_table: &Table<Graphic>) -> Table<Vector> {
graphic_table
.iter()
.flat_map(|element| {
match element.instance.clone() {
GraphicElement::VectorData(vector_data) => {
// Apply the parent group's transform to each element of vector data
vector_data
.instance_iter()
.map(|mut sub_vector_data| {
sub_vector_data.transform = *element.transform * sub_vector_data.transform;
match element.element.clone() {
Graphic::Vector(vector) => {
// Apply the parent graphic's transform to each element of the vector table
vector
.into_iter()
.map(|mut sub_vector| {
sub_vector.transform = *element.transform * sub_vector.transform;
sub_vector_data
sub_vector
})
.collect::<Vec<_>>()
}
GraphicElement::RasterDataCPU(image) => {
let make_instance = |transform| {
Graphic::RasterCPU(image) => {
let make_row = |transform| {
// Convert the image frame into a rectangular subpath with the image's transform
let mut subpath = Subpath::new_rect(DVec2::ZERO, DVec2::ONE);
subpath.apply_transform(transform);
// Create a vector data table row from the rectangular subpath, with a default black fill
let mut instance = VectorData::from_subpath(subpath);
instance.style.set_fill(Fill::Solid(Color::BLACK));
// Create a vector table row from the rectangular subpath, with a default black fill
let mut element = Vector::from_subpath(subpath);
element.style.set_fill(Fill::Solid(Color::BLACK));
Instance { instance, ..Default::default() }
TableRow { element, ..Default::default() }
};
// Apply the parent group's transform to each element of raster data
image.instance_ref_iter().map(|instance| make_instance(*element.transform * *instance.transform)).collect::<Vec<_>>()
// Apply the parent graphic's transform to each raster element
image.iter().map(|row| make_row(*element.transform * *row.transform)).collect::<Vec<_>>()
}
GraphicElement::RasterDataGPU(image) => {
let make_instance = |transform| {
Graphic::RasterGPU(image) => {
let make_row = |transform| {
// Convert the image frame into a rectangular subpath with the image's transform
let mut subpath = Subpath::new_rect(DVec2::ZERO, DVec2::ONE);
subpath.apply_transform(transform);
// Create a vector data table row from the rectangular subpath, with a default black fill
let mut instance = VectorData::from_subpath(subpath);
instance.style.set_fill(Fill::Solid(Color::BLACK));
// Create a vector table row from the rectangular subpath, with a default black fill
let mut element = Vector::from_subpath(subpath);
element.style.set_fill(Fill::Solid(Color::BLACK));
Instance { instance, ..Default::default() }
TableRow { element, ..Default::default() }
};
// Apply the parent group's transform to each element of raster data
image.instance_ref_iter().map(|instance| make_instance(*element.transform * *instance.transform)).collect::<Vec<_>>()
// Apply the parent graphic's transform to each raster element
image.iter().map(|row| make_row(*element.transform * *row.transform)).collect::<Vec<_>>()
}
GraphicElement::GraphicGroup(mut graphic_group) => {
// Apply the parent group's transform to each element of inner group
for sub_element in graphic_group.instance_mut_iter() {
Graphic::Graphic(mut graphic) => {
// Apply the parent graphic's transform to each element of inner table
for sub_element in graphic.iter_mut() {
*sub_element.transform = *element.transform * *sub_element.transform;
}
// Recursively flatten the inner group into vector data
let unioned = boolean_operation_on_vector_data_table(flatten_vector_data(&graphic_group).instance_ref_iter(), BooleanOperation::Union);
// Recursively flatten the inner table into the output vector table
let unioned = boolean_operation_on_vector_table(flatten_vector(&graphic).iter(), BooleanOperation::Union);
unioned.instance_iter().collect::<Vec<_>>()
unioned.into_iter().collect::<Vec<_>>()
}
Graphic::Color(color) => color
.into_iter()
.map(|row| {
let mut element = Vector::default();
element.style.set_fill(Fill::Solid(row.element));
element.style.set_stroke_transform(DAffine2::IDENTITY);
TableRow {
element,
mask: row.mask.clone(),
transform: row.transform,
alpha_blending: row.alpha_blending,
source_node_id: row.source_node_id,
}
})
.collect::<Vec<_>>(),
Graphic::Gradient(gradient) => gradient
.into_iter()
.map(|row| {
let mut element = Vector::default();
element.style.set_fill(Fill::Gradient(graphene_core::gradient::Gradient {
stops: row.element,
..Default::default()
}));
element.style.set_stroke_transform(DAffine2::IDENTITY);
TableRow {
element,
mask: row.mask.clone(),
transform: row.transform,
alpha_blending: row.alpha_blending,
source_node_id: row.source_node_id,
}
})
.collect::<Vec<_>>(),
}
})
.collect()
}
fn to_path(vector: &VectorData, transform: DAffine2) -> Vec<path_bool::PathSegment> {
fn to_path(vector: &Vector, transform: DAffine2) -> Vec<path_bool::PathSegment> {
let mut path = Vec::new();
for subpath in vector.stroke_bezier_paths() {
to_path_segments(&mut path, &subpath, transform);
@@ -298,20 +338,29 @@ fn to_path_segments(path: &mut Vec<path_bool::PathSegment>, subpath: &Subpath<Po
use path_bool::PathSegment;
let mut global_start = None;
let mut global_end = DVec2::ZERO;
for bezier in subpath.iter() {
const EPS: f64 = 1e-8;
let transformed = bezier.apply_transformation(|pos| transform.transform_point2(pos).mul(EPS.recip()).round().mul(EPS));
let start = transformed.start;
let end = transformed.end;
let transform_point = |pos: DVec2| transform.transform_point2(pos).mul(EPS.recip()).round().mul(EPS);
let PathSegPoints { p0, p1, p2, p3 } = pathseg_points(bezier);
let p0 = transform_point(p0);
let p1 = p1.map(transform_point);
let p2 = p2.map(transform_point);
let p3 = transform_point(p3);
if global_start.is_none() {
global_start = Some(start);
global_start = Some(p0);
}
global_end = end;
let segment = match transformed.handles {
bezier_rs::BezierHandles::Linear => PathSegment::Line(start, end),
bezier_rs::BezierHandles::Quadratic { handle } => PathSegment::Quadratic(start, handle, end),
bezier_rs::BezierHandles::Cubic { handle_start, handle_end } => PathSegment::Cubic(start, handle_start, handle_end, end),
global_end = p3;
let segment = match (p1, p2) {
(None, None) => PathSegment::Line(p0, p3),
(None, Some(p2)) | (Some(p2), None) => PathSegment::Quadratic(p0, p2, p3),
(Some(p1), Some(p2)) => PathSegment::Cubic(p0, p1, p2, p3),
};
path.push(segment);
}
if let Some(start) = global_start {
@@ -319,7 +368,7 @@ fn to_path_segments(path: &mut Vec<path_bool::PathSegment>, subpath: &Subpath<Po
}
}
fn from_path(path_data: &[Path]) -> VectorData {
fn from_path(path_data: &[Path]) -> Vector {
const EPSILON: f64 = 1e-5;
fn is_close(a: DVec2, b: DVec2) -> bool {
@@ -330,7 +379,7 @@ fn from_path(path_data: &[Path]) -> VectorData {
for path in path_data.iter().filter(|path| !path.is_empty()) {
let cubics: Vec<[DVec2; 4]> = path.iter().map(|segment| segment.to_cubic()).collect();
let mut groups = Vec::new();
let mut manipulators_list = Vec::new();
let mut current_start = None;
for (index, cubic) in cubics.iter().enumerate() {
@@ -338,32 +387,32 @@ fn from_path(path_data: &[Path]) -> VectorData {
if current_start.is_none() || !is_close(start, current_start.unwrap()) {
// Start a new subpath
if !groups.is_empty() {
all_subpaths.push(Subpath::new(std::mem::take(&mut groups), true));
if !manipulators_list.is_empty() {
all_subpaths.push(Subpath::new(std::mem::take(&mut manipulators_list), true));
}
// Use the correct in-handle (None) and out-handle for the start point
groups.push(ManipulatorGroup::new(start, None, Some(handle1)));
manipulators_list.push(ManipulatorGroup::new(start, None, Some(handle1)));
} else {
// Update the out-handle of the previous point
if let Some(last) = groups.last_mut() {
if let Some(last) = manipulators_list.last_mut() {
last.out_handle = Some(handle1);
}
}
// Add the end point with the correct in-handle and out-handle (None)
groups.push(ManipulatorGroup::new(end, Some(handle2), None));
manipulators_list.push(ManipulatorGroup::new(end, Some(handle2), None));
current_start = Some(end);
// Check if this is the last segment
if index == cubics.len() - 1 {
all_subpaths.push(Subpath::new(groups, true));
groups = Vec::new(); // Reset groups for the next path
all_subpaths.push(Subpath::new(manipulators_list, true));
manipulators_list = Vec::new(); // Reset manipulators for the next path
}
}
}
VectorData::from_subpaths(all_subpaths, false)
Vector::from_subpaths(all_subpaths, false)
}
type Path = Vec<path_bool::PathSegment>;

View File

@@ -25,7 +25,6 @@ graphene-raster-nodes = { workspace = true }
# Workspace dependencies
log = { workspace = true }
glam = { workspace = true }
bezier-rs = { workspace = true }
specta = { workspace = true }
rustc-hash = { workspace = true }
url = { workspace = true }
@@ -38,15 +37,12 @@ tokio = { workspace = true, optional = true }
serde_json = { workspace = true, optional = true }
# Workspace dependencies
[target.'cfg(target_arch = "wasm32")'.dependencies]
web-sys = { workspace = true, features = [
"Navigator",
"Gpu",
] }
[target.'cfg(target_family = "wasm")'.dependencies]
web-sys = { workspace = true, features = ["Navigator", "Gpu"] }
js-sys = { workspace = true }
wasm-bindgen = { workspace = true }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
[target.'cfg(not(target_family = "wasm"))'.dependencies]
winit = { workspace = true }
[dev-dependencies]

View File

@@ -1,13 +1,13 @@
pub mod value;
use crate::document::value::TaggedValue;
use crate::proto::{ConstructionArgs, ProtoNetwork, ProtoNode, ProtoNodeInput};
use crate::proto::{ConstructionArgs, ProtoNetwork, ProtoNode};
use dyn_any::DynAny;
use glam::IVec2;
use graphene_core::memo::MemoHashGuard;
pub use graphene_core::uuid::NodeId;
pub use graphene_core::uuid::generate_uuid;
use graphene_core::{Cow, MemoHash, ProtoNodeIdentifier, Type};
use graphene_core::{Context, Cow, MemoHash, ProtoNodeIdentifier, Type};
use log::Metadata;
use rustc_hash::FxHashMap;
use std::collections::HashMap;
@@ -42,103 +42,11 @@ pub struct DocumentNode {
/// In the root network, it is resolved when evaluating the borrow tree.
/// Ensure the click target in the encapsulating network is updated when the inputs cause the node shape to change (currently only when exposing/hiding an input)
/// by using network.update_click_target(node_id).
#[cfg_attr(target_arch = "wasm32", serde(alias = "outputs"))]
#[cfg_attr(target_family = "wasm", serde(alias = "outputs"))]
pub inputs: Vec<NodeInput>,
/// Manual composition is the methodology by which most nodes are implemented, involving a call argument and upstream inputs.
/// By contrast, automatic composition is an alternative way to handle the composition of nodes as they execute in the graph.
/// Normally, the program (the compiled graph) builds up its call stack, with each node calling its upstream predecessor to acquire its input data.
/// When the document graph becomes the proto graph, that conceptual model changes into a model that's unique to the proto graph.
/// Automatic composition allows a document node to be translated into its place in the proto graph differently, such that
/// the node doesn't participate in that process of being called with a call argument and calling its upstream predecessor.
/// Instead, it is called directly with its input data from the upstream node, skipping the call stack building process.
/// The abstraction is provided by the compiler for nodes which opt for automatic composition. It works by inserting a `ComposeNode`
/// into the proto graph, which does the job of calling the upstream node and feeding its output into the downstream node's first input.
/// That first input is typically used by manual composition nodes as the call argument, but for automatic composition nodes,
/// that first input becomes the input data from the upstream node passed in by the `ComposeNode`.
///
/// Through automatic composition, the upstream node providing the first input for a proto node is evaluated before the proto node itself is run.
/// (That first input is usually the call argument when manual composition is used.)
/// - Abstract example: upstream node `G` is evaluated and its data feeds into the first input of downstream node `F`,
/// just like function composition where function `G` is evaluated and its result is fed into function `F`.
/// - Concrete example: a node that takes an image as its first input will get that image data from an upstream node that produces image output data and is evaluated first before being fed downstream.
///
/// This is achieved by automatically inserting `ComposeNode`s, which run the first node with the overall input and then feed the resulting output into the second node.
/// The `ComposeNode` is basically a function composition operator: the parentheses in `F(G(x))` or circle math operator in `(F ∘ G)(x)`.
/// For flexibility, instead of being a language construct, Graphene splits out composition itself as its own low-level node so that behavior can be overridden.
/// The `ComposeNode`s are then inserted during the graph rewriting step for nodes that don't opt out with `manual_composition`.
/// Instead of node `G` feeding into node `F` feeding as the result back to the caller,
/// the graph is rewritten so nodes `G` and `F` both feed as lambdas into the inputs of a `ComposeNode` which calls `F(G(input))` and returns the result to the caller.
///
/// A node's manual composition input represents an input that is not resolved through graph rewriting with a `ComposeNode`,
/// and is instead just passed in when evaluating this node within the borrow tree.
/// This is similar to having the first input be a `NodeInput::Network` after the graph flattening.
///
/// ## Example Use Case: CacheNode
///
/// The `CacheNode` is a pass-through node on cache miss, but on cache hit it needs to avoid evaluating the upstream node and instead just return the cached value.
///
/// First, let's consider what that would look like using the default composition flow if the `CacheNode` instead just always acted as a pass-through (akin to a cache that always misses):
///
/// ```text
/// ┌───────────────┐ ┌───────────────┐ ┌───────────────┐
/// │ │◄───┤ │◄───┤ │◄─── EVAL (START)
/// │ G │ │PassThroughNode│ │ F │
/// │ ├───►│ ├───►│ │───► RESULT (END)
/// └───────────────┘ └───────────────┘ └───────────────┘
/// ```
///
/// This acts like the function call `F(PassThroughNode(G(input)))` when evaluating `F` with some `input`: `F.eval(input)`.
/// - The diagram's upper track of arrows represents the flow of building up the call stack:
/// since `F` is the output it is encountered first but deferred to its upstream caller `PassThroughNode` and that is once again deferred to its upstream caller `G`.
/// - The diagram's lower track of arrows represents the flow of evaluating the call stack:
/// `G` is evaluated first, then `PassThroughNode` is evaluated with the result of `G`, and finally `F` is evaluated with the result of `PassThroughNode`.
///
/// With the default composition flow (no manual composition), `ComposeNode`s would be automatically inserted during the graph rewriting step like this:
///
/// ```text
/// ┌───────────────┐
/// │ │◄─── EVAL (START)
/// │ ComposeNode │
/// ┌───────────────┐ │ ├───► RESULT (END)
/// │ │◄─┐ ├───────────────┤
/// │ G │ └─┤ │
/// │ ├─┐ │ First │
/// └───────────────┘ └─►│ │
/// ┌───────────────┐ ├───────────────┤
/// │ │◄───┤ │
/// │ ComposeNode │ │ Second │
/// ┌───────────────┐ │ ├───►│ │
/// │ │◄─┐ ├───────────────┤ └───────────────┘
/// │PassThroughNode│ └─┤ │
/// │ ├─┐ │ First │
/// └───────────────┘ └─►│ │
/// ┌───────────────┐ ├───────────────┤
/// | │◄───┤ │
/// │ F │ │ Second │
/// │ ├───►│ │
/// └───────────────┘ └───────────────┘
/// ```
///
/// Now let's swap back from the `PassThroughNode` to the `CacheNode` to make caching actually work.
/// It needs to override the default composition flow so that `G` is not automatically evaluated when the cache is hit.
/// We need to give the `CacheNode` more manual control over the order of execution.
/// So the `CacheNode` opts into manual composition and, instead of deferring to its upstream caller, it consumes the input directly:
///
/// ```text
/// ┌───────────────┐ ┌───────────────┐
/// │ │◄───┤ │◄─── EVAL (START)
/// │ CacheNode │ │ F │
/// │ ├───►│ │───► RESULT (END)
/// ┌───────────────┐ ├───────────────┤ └───────────────┘
/// │ │◄───┤ │
/// │ G │ │ Cached Data │
/// │ ├───►│ │
/// └───────────────┘ └───────────────┘
/// ```
///
/// Now, the call from `F` directly reaches the `CacheNode` and the `CacheNode` can decide whether to call `G.eval(input_from_f)`
/// in the event of a cache miss or just return the cached data in the event of a cache hit.
pub manual_composition: Option<Type>,
/// Type of the argument which this node can be evaluated with.
#[serde(alias = "manual_composition", default)]
pub call_argument: Type,
// A nested document network or a proto-node identifier.
pub implementation: DocumentNodeImplementation,
/// Represents the eye icon for hiding/showing the node in the graph UI. When hidden, a node gets replaced with an identity node during the graph flattening step.
@@ -173,15 +81,13 @@ pub struct OriginalLocation {
pub dependants: Vec<Vec<NodeId>>,
/// A list of flags indicating whether the input is exposed in the UI
pub inputs_exposed: Vec<bool>,
/// Skipping inputs is useful for the manual composition thing - whereby a hidden `Footprint` input is added as the first input.
pub skip_inputs: usize,
}
impl Default for DocumentNode {
fn default() -> Self {
Self {
inputs: Default::default(),
manual_composition: Default::default(),
call_argument: concrete!(Context),
implementation: Default::default(),
visible: true,
skip_deduplication: Default::default(),
@@ -195,14 +101,13 @@ impl Hash for OriginalLocation {
self.path.hash(state);
self.inputs_source.iter().for_each(|val| val.hash(state));
self.inputs_exposed.hash(state);
self.skip_inputs.hash(state);
}
}
impl OriginalLocation {
pub fn inputs(&self, index: usize) -> impl Iterator<Item = Source> + '_ {
[(index >= self.skip_inputs).then(|| Source {
[(index >= 1).then(|| Source {
node: self.path.clone().unwrap_or_default(),
index: self.inputs_exposed.iter().take(index - self.skip_inputs).filter(|&&exposed| exposed).count(),
index: self.inputs_exposed.iter().take(index - 1).filter(|&&exposed| exposed).count(),
})]
.into_iter()
.flatten()
@@ -211,7 +116,7 @@ impl OriginalLocation {
}
impl DocumentNode {
/// Locate the input that is a [`NodeInput::Network`] at index `offset` and replace it with a [`NodeInput::Node`].
pub fn populate_first_network_input(&mut self, node_id: NodeId, output_index: usize, offset: usize, lambda: bool, source: impl Iterator<Item = Source>, skip: usize) {
pub fn populate_first_network_input(&mut self, node_id: NodeId, output_index: usize, offset: usize, source: impl Iterator<Item = Source>, skip: usize) {
let (index, _) = self
.inputs
.iter()
@@ -219,60 +124,38 @@ impl DocumentNode {
.nth(offset)
.unwrap_or_else(|| panic!("no network input found for {self:#?} and offset: {offset}"));
self.inputs[index] = NodeInput::Node { node_id, output_index, lambda };
self.inputs[index] = NodeInput::Node { node_id, output_index };
let input_source = &mut self.original_location.inputs_source;
for source in source {
input_source.insert(source, (index + self.original_location.skip_inputs).saturating_sub(skip));
input_source.insert(source, (index + 1).saturating_sub(skip));
}
}
fn resolve_proto_node(mut self) -> ProtoNode {
assert!(!self.inputs.is_empty() || self.manual_composition.is_some(), "Resolving document node {self:#?} with no inputs");
fn resolve_proto_node(self) -> ProtoNode {
let DocumentNodeImplementation::ProtoNode(identifier) = self.implementation else {
unreachable!("tried to resolve not flattened node on resolved node {self:?}");
};
let (input, mut args) = if let Some(ty) = self.manual_composition {
(ProtoNodeInput::ManualComposition(ty), ConstructionArgs::Nodes(vec![]))
} else {
let first = self.inputs.remove(0);
match first {
NodeInput::Value { tagged_value, .. } => {
assert_eq!(self.inputs.len(), 0, "A value node cannot have any inputs. Current inputs: {:?}", self.inputs);
(ProtoNodeInput::ManualComposition(concrete!(graphene_core::Context<'static>)), ConstructionArgs::Value(tagged_value))
}
NodeInput::Node { node_id, output_index, lambda } => {
assert_eq!(output_index, 0, "Outputs should be flattened before converting to proto node");
let node = if lambda { ProtoNodeInput::NodeLambda(node_id) } else { ProtoNodeInput::Node(node_id) };
(node, ConstructionArgs::Nodes(vec![]))
}
NodeInput::Network { import_type, .. } => (ProtoNodeInput::ManualComposition(import_type), ConstructionArgs::Nodes(vec![])),
NodeInput::Inline(inline) => (ProtoNodeInput::None, ConstructionArgs::Inline(inline)),
NodeInput::Scope(_) => unreachable!("Scope input was not resolved"),
NodeInput::Reflection(_) => unreachable!("Reflection input was not resolved"),
}
};
let (input, mut args) = (self.call_argument, ConstructionArgs::Nodes(vec![]));
assert!(!self.inputs.iter().any(|input| matches!(input, NodeInput::Network { .. })), "received non-resolved input");
assert!(
!self.inputs.iter().any(|input| matches!(input, NodeInput::Value { .. })),
"received value as input. inputs: {:#?}, construction_args: {:#?}",
self.inputs,
args
);
// If we have one input of the type inline, set it as the construction args
if let &[NodeInput::Inline(ref inline)] = self.inputs.as_slice() {
args = ConstructionArgs::Inline(inline.clone());
}
// If we have one input of the type inline, set it as the construction args
if let &[NodeInput::Value { ref tagged_value, .. }] = self.inputs.as_slice() {
args = ConstructionArgs::Value(tagged_value.clone());
}
if let ConstructionArgs::Nodes(nodes) = &mut args {
nodes.extend(self.inputs.iter().map(|input| match input {
NodeInput::Node { node_id, lambda, .. } => (*node_id, *lambda),
NodeInput::Node { node_id, .. } => *node_id,
_ => unreachable!(),
}));
}
ProtoNode {
identifier,
input,
call_argument: input,
construction_args: args,
original_location: self.original_location,
skip_deduplication: self.skip_deduplication,
@@ -284,7 +167,7 @@ impl DocumentNode {
#[derive(Debug, Clone, PartialEq, Hash, DynAny, serde::Serialize, serde::Deserialize)]
pub enum NodeInput {
/// A reference to another node in the same network from which this node can receive its input.
Node { node_id: NodeId, output_index: usize, lambda: bool },
Node { node_id: NodeId, output_index: usize },
/// A hardcoded value that can't change after the graph is compiled. Gets converted into a value node during graph compilation.
Value { tagged_value: MemoHash<TaggedValue>, exposed: bool },
@@ -323,11 +206,7 @@ pub enum DocumentNodeMetadata {
impl NodeInput {
pub const fn node(node_id: NodeId, output_index: usize) -> Self {
Self::Node { node_id, output_index, lambda: false }
}
pub const fn lambda(node_id: NodeId, output_index: usize) -> Self {
Self::Node { node_id, output_index, lambda: true }
Self::Node { node_id, output_index }
}
pub fn value(tagged_value: TaggedValue, exposed: bool) -> Self {
@@ -344,12 +223,8 @@ impl NodeInput {
}
fn map_ids(&mut self, f: impl Fn(NodeId) -> NodeId) {
if let &mut NodeInput::Node { node_id, output_index, lambda } = self {
*self = NodeInput::Node {
node_id: f(node_id),
output_index,
lambda,
}
if let &mut NodeInput::Node { node_id, output_index } = self {
*self = NodeInput::Node { node_id: f(node_id), output_index }
}
}
@@ -390,37 +265,13 @@ impl NodeInput {
}
}
// TODO: Eventually remove this document upgrade code
#[derive(Clone, Debug, DynAny, serde::Serialize, serde::Deserialize)]
/// Represents the implementation of a node, which can be a nested [`NodeNetwork`], a proto [`ProtoNodeIdentifier`], or `Extract`.
pub enum OldDocumentNodeImplementation {
/// This describes a (document) node built out of a subgraph of other (document) nodes.
///
/// A nested [`NodeNetwork`] that is flattened by the [`NodeNetwork::flatten`] function.
Network(OldNodeNetwork),
/// This describes a (document) node implemented as a proto node.
///
/// A proto node identifier which can be found in `node_registry.rs`.
#[serde(alias = "Unresolved")] // TODO: Eventually remove this alias document upgrade code
#[serde(alias = "Unresolved")]
ProtoNode(ProtoNodeIdentifier),
/// The Extract variant is a tag which tells the compilation process to do something special. It invokes language-level functionality built for use by the ExtractNode to enable metaprogramming.
/// When the ExtractNode is compiled, it gets replaced by a value node containing a representation of the source code for the function/lambda of the document node that's fed into the ExtractNode
/// (but only that one document node, not upstream nodes).
///
/// This is explained in more detail here: <https://www.youtube.com/watch?v=72KJa3jQClo>
///
/// Currently we use it for GPU execution, where a node has to get "extracted" to its source code representation and stored as a value that can be given to the GpuCompiler node at runtime
/// (to become a compute shader). Future use could involve the addition of an InjectNode to convert the source code form back into an executable node, enabling metaprogramming in the node graph.
/// We would use an assortment of nodes that operate on Graphene source code (just data, no different from any other data flowing through the graph) to make graph transformations.
///
/// We use this for dealing with macros in a syntactic way of modifying the node graph from within the graph itself. Just like we often deal with lambdas to represent a whole group of
/// operations/code/logic, this allows us to basically deal with a lambda at a meta/source-code level, because we need to pass the GPU SPIR-V compiler the source code for a lambda,
/// not the executable logic of a lambda.
///
/// This is analogous to how Rust macros operate at the level of source code, not executable code. When we speak of source code, that represents Graphene's source code in the form of a
/// DocumentNode network, not the text form of Rust's source code. (Analogous to the token stream/AST of a Rust macro.)
///
/// `DocumentNode`s with a `DocumentNodeImplementation::Extract` are converted into a `ClonedNode` that returns the `DocumentNode` specified by the single `NodeInput::Node`. The referenced node
/// (specified by the single `NodeInput::Node`) is removed from the network, and any `NodeInput::Node`s used by the referenced node are replaced with a generically typed network input.
Extract,
}
@@ -552,7 +403,7 @@ pub struct OldDocumentNode {
///
/// In the root network, it is resolved when evaluating the borrow tree.
/// Ensure the click target in the encapsulating network is updated when the inputs cause the node shape to change (currently only when exposing/hiding an input) by using network.update_click_target(node_id).
#[cfg_attr(target_arch = "wasm32", serde(alias = "outputs"))]
#[cfg_attr(target_family = "wasm", serde(alias = "outputs"))]
pub inputs: Vec<NodeInput>,
pub manual_composition: Option<Type>,
// TODO: Remove once this references its definition instead (see above TODO).
@@ -657,7 +508,7 @@ pub struct NodeNetwork {
/// The list of data outputs that are exported from this network to the parent network.
/// Each export is a reference to a node within this network, paired with its output index, that is the source of the network's exported data.
// TODO: Eventually remove this alias document upgrade code
#[cfg_attr(target_arch = "wasm32", serde(alias = "outputs", deserialize_with = "deserialize_exports"))]
#[cfg_attr(target_family = "wasm", serde(alias = "outputs", deserialize_with = "deserialize_exports"))]
pub exports: Vec<NodeInput>,
// TODO: Instead of storing import types in each NodeInput::Network connection, the types are stored here. This is similar to how types need to be defined for parameters when creating a function in Rust.
// pub import_types: Vec<Type>,
@@ -796,7 +647,6 @@ impl NodeNetwork {
node.original_location = OriginalLocation {
path: Some(new_path),
inputs_exposed: node.inputs.iter().map(|input| input.is_exposed()).collect(),
skip_inputs: if node.manual_composition.is_some() { 1 } else { 0 },
dependants: (0..node.implementation.output_count()).map(|_| Vec::new()).collect(),
..Default::default()
};
@@ -915,14 +765,15 @@ impl NodeNetwork {
warn!("The node which was supposed to be flattened does not exist in the network, id {node_id} network {self:#?}");
return;
};
// If the node is hidden, replace it with an identity node
let identity_node = DocumentNodeImplementation::ProtoNode("graphene_core::ops::IdentityNode".into());
if !node.visible && node.implementation != identity_node {
node.implementation = identity_node;
// Connect layer node to the graphic group below
// Connect layer node to the group below
node.inputs.drain(1..);
node.manual_composition = None;
node.call_argument = concrete!(());
self.nodes.insert(id, node);
return;
}
@@ -972,12 +823,11 @@ impl NodeNetwork {
for (nested_node_id, mut nested_node) in inner_network.nodes.into_iter() {
for (nested_input_index, nested_input) in nested_node.clone().inputs.iter().enumerate() {
if let NodeInput::Network { import_index, .. } = nested_input {
let parent_input = node.inputs.get(*import_index).unwrap_or_else(|| panic!("Import index {} should always exist", import_index));
let parent_input = node.inputs.get(*import_index).unwrap_or_else(|| panic!("Import index {import_index} should always exist"));
match *parent_input {
// If the input to self is a node, connect the corresponding output of the inner network to it
NodeInput::Node { node_id, output_index, lambda } => {
let skip = node.original_location.skip_inputs;
nested_node.populate_first_network_input(node_id, output_index, nested_input_index, lambda, node.original_location.inputs(*import_index), skip);
NodeInput::Node { node_id, output_index } => {
nested_node.populate_first_network_input(node_id, output_index, nested_input_index, node.original_location.inputs(*import_index), 1);
let input_node = self.nodes.get_mut(&node_id).unwrap_or_else(|| panic!("unable find input node {node_id:?}"));
input_node.original_location.dependants[output_index].push(nested_node_id);
}
@@ -1075,20 +925,10 @@ impl NodeNetwork {
*export = NodeInput::Node {
node_id: merged_node_id,
output_index: 0,
lambda: false,
};
}
}
// /// Locate the export that is a [`NodeInput::Network`] at index `offset` and replace it with a [`NodeInput::Node`].
// fn populate_first_network_export(&mut self, node: &mut DocumentNode, node_id: NodeId, output_index: usize, lambda: bool, export_index: usize, source: impl Iterator<Item = Source>, skip: usize) {
// self.exports[export_index] = NodeInput::Node { node_id, output_index, lambda };
// let input_source = &mut node.original_location.inputs_source;
// for source in source {
// input_source.insert(source, output_index + node.original_location.skip_inputs - skip);
// }
// }
fn remove_id_node(&mut self, id: NodeId) -> Result<(), String> {
let node = self.nodes.get(&id).ok_or_else(|| format!("Node with id {id} does not exist"))?.clone();
if let DocumentNodeImplementation::ProtoNode(ident) = &node.implementation {
@@ -1118,7 +958,7 @@ impl NodeNetwork {
let input_source = &mut output.original_location.inputs_source;
for source in node.original_location.inputs(index) {
input_source.insert(source, index + output.original_location.skip_inputs - node.original_location.skip_inputs);
input_source.insert(source, index);
}
}
}
@@ -1263,7 +1103,7 @@ impl<'a> Iterator for RecursiveNodeIter<'a> {
#[cfg(test)]
mod test {
use super::*;
use crate::proto::{ConstructionArgs, ProtoNetwork, ProtoNode, ProtoNodeInput};
use crate::proto::{ConstructionArgs, ProtoNetwork, ProtoNode};
use std::sync::atomic::AtomicU64;
fn gen_node_id() -> NodeId {
@@ -1342,7 +1182,7 @@ mod test {
nodes: [
id_node.clone(),
DocumentNode {
inputs: vec![NodeInput::lambda(NodeId(0), 0)],
inputs: vec![NodeInput::node(NodeId(0), 0)],
implementation: DocumentNodeImplementation::Extract,
..Default::default()
},
@@ -1388,7 +1228,8 @@ mod test {
#[test]
fn resolve_proto_node_add() {
let document_node = DocumentNode {
inputs: vec![NodeInput::network(concrete!(u32), 0), NodeInput::node(NodeId(0), 0)],
inputs: vec![NodeInput::node(NodeId(0), 0)],
call_argument: concrete!(u32),
implementation: DocumentNodeImplementation::ProtoNode("graphene_core::structural::ConsNode".into()),
..Default::default()
};
@@ -1396,8 +1237,8 @@ mod test {
let proto_node = document_node.resolve_proto_node();
let reference = ProtoNode {
identifier: "graphene_core::structural::ConsNode".into(),
input: ProtoNodeInput::ManualComposition(concrete!(u32)),
construction_args: ConstructionArgs::Nodes(vec![(NodeId(0), false)]),
call_argument: concrete!(u32),
construction_args: ConstructionArgs::Nodes(vec![NodeId(0)]),
..Default::default()
};
assert_eq!(proto_node, reference);
@@ -1413,13 +1254,12 @@ mod test {
NodeId(10),
ProtoNode {
identifier: "graphene_core::structural::ConsNode".into(),
input: ProtoNodeInput::ManualComposition(concrete!(u32)),
construction_args: ConstructionArgs::Nodes(vec![(NodeId(14), false)]),
call_argument: concrete!(u32),
construction_args: ConstructionArgs::Nodes(vec![NodeId(14)]),
original_location: OriginalLocation {
path: Some(vec![NodeId(1), NodeId(0)]),
inputs_source: [(Source { node: vec![NodeId(1)], index: 1 }, 1)].into(),
inputs_exposed: vec![true, true],
skip_inputs: 0,
..Default::default()
},
@@ -1430,13 +1270,12 @@ mod test {
NodeId(11),
ProtoNode {
identifier: "graphene_core::ops::AddPairNode".into(),
input: ProtoNodeInput::Node(NodeId(10)),
construction_args: ConstructionArgs::Nodes(vec![]),
call_argument: concrete!(Context),
construction_args: ConstructionArgs::Nodes(vec![NodeId(10)]),
original_location: OriginalLocation {
path: Some(vec![NodeId(1), NodeId(1)]),
inputs_source: HashMap::new(),
inputs_exposed: vec![true],
skip_inputs: 0,
..Default::default()
},
..Default::default()
@@ -1446,13 +1285,12 @@ mod test {
NodeId(14),
ProtoNode {
identifier: "graphene_core::value::ClonedNode".into(),
input: ProtoNodeInput::ManualComposition(concrete!(graphene_core::Context)),
call_argument: concrete!(graphene_core::Context),
construction_args: ConstructionArgs::Value(TaggedValue::U32(2).into()),
original_location: OriginalLocation {
path: Some(vec![NodeId(1), NodeId(4)]),
inputs_source: HashMap::new(),
inputs_exposed: vec![true, false],
skip_inputs: 0,
..Default::default()
},
..Default::default()
@@ -1478,13 +1316,13 @@ mod test {
(
NodeId(10),
DocumentNode {
inputs: vec![NodeInput::network(concrete!(u32), 0), NodeInput::node(NodeId(14), 0)],
inputs: vec![NodeInput::node(NodeId(14), 0)],
call_argument: concrete!(u32),
implementation: DocumentNodeImplementation::ProtoNode("graphene_core::structural::ConsNode".into()),
original_location: OriginalLocation {
path: Some(vec![NodeId(1), NodeId(0)]),
inputs_source: [(Source { node: vec![NodeId(1)], index: 1 }, 1)].into(),
inputs_exposed: vec![true, true],
skip_inputs: 0,
..Default::default()
},
..Default::default()
@@ -1499,7 +1337,6 @@ mod test {
path: Some(vec![NodeId(1), NodeId(4)]),
inputs_source: HashMap::new(),
inputs_exposed: vec![true, false],
skip_inputs: 0,
..Default::default()
},
..Default::default()
@@ -1514,7 +1351,6 @@ mod test {
path: Some(vec![NodeId(1), NodeId(1)]),
inputs_source: HashMap::new(),
inputs_exposed: vec![true],
skip_inputs: 0,
..Default::default()
},
..Default::default()
@@ -1599,49 +1435,4 @@ mod test {
}
// TODO: Write more tests
// #[test]
// fn out_of_order_duplicate() {
// let result = output_duplicate(vec![NodeInput::node(NodeId(10), 1), NodeInput::node(NodeId(10), 0)], NodeInput::node(NodeId(10), 0);
// assert_eq!(
// result.outputs[0],
// NodeInput::node(NodeId(101), 0),
// "The first network output should be from a duplicated nested network"
// );
// assert_eq!(
// result.outputs[1],
// NodeInput::node(NodeId(10), 0),
// "The second network output should be from the original nested network"
// );
// assert!(
// result.nodes.contains_key(&NodeId(10)) && result.nodes.contains_key(&NodeId(101)) && result.nodes.len() == 2,
// "Network should contain two duplicated nodes"
// );
// for (node_id, input_value, inner_id) in [(10, 1., 1), (101, 2., 2)] {
// let nested_network_node = result.nodes.get(&NodeId(node_id)).unwrap();
// assert_eq!(nested_network_node.name, "Nested network".to_string(), "Name should not change");
// assert_eq!(nested_network_node.inputs, vec![NodeInput::value(TaggedValue::F32(input_value), false)], "Input should be stable");
// let inner_network = nested_network_node.implementation.get_network().expect("Implementation should be network");
// assert_eq!(inner_network.inputs, vec![inner_id], "The input should be sent to the second node");
// assert_eq!(inner_network.outputs, vec![NodeInput::node(NodeId(inner_id), 0)], "The output should be node id");
// assert_eq!(inner_network.nodes.get(&NodeId(inner_id)).unwrap().name, format!("Identity {inner_id}"), "The node should be identity");
// }
// }
// #[test]
// fn using_other_node_duplicate() {
// let result = output_duplicate(vec![NodeInput::node(NodeId(11), 0)], NodeInput::node(NodeId(10), 1);
// assert_eq!(result.outputs, vec![NodeInput::node(NodeId(11), 0)], "The network output should be the result node");
// assert!(
// result.nodes.contains_key(&NodeId(11)) && result.nodes.contains_key(&NodeId(101)) && result.nodes.len() == 2,
// "Network should contain a duplicated node and a result node"
// );
// let result_node = result.nodes.get(&NodeId(11)).unwrap();
// assert_eq!(result_node.inputs, vec![NodeInput::node(NodeId(101), 0)], "Result node should refer to duplicate node as input");
// let nested_network_node = result.nodes.get(&NodeId(101)).unwrap();
// assert_eq!(nested_network_node.name, "Nested network".to_string(), "Name should not change");
// assert_eq!(nested_network_node.inputs, vec![NodeInput::value(TaggedValue::F32(2.), false)], "Input should be 2");
// let inner_network = nested_network_node.implementation.get_network().expect("Implementation should be network");
// assert_eq!(inner_network.inputs, vec![2], "The input should be sent to the second node");
// assert_eq!(inner_network.outputs, vec![NodeInput::node(NodeId(2), 0)], "The output should be node id 2");
// assert_eq!(inner_network.nodes.get(&NodeId(2)).unwrap().name, "Identity 2", "The node should be identity 2");
// }
}

View File

@@ -3,15 +3,20 @@ use crate::proto::{Any as DAny, FutureAny};
use crate::wasm_application_io::WasmEditorApi;
use dyn_any::DynAny;
pub use dyn_any::StaticType;
use glam::{Affine2, Vec2};
pub use glam::{DAffine2, DVec2, IVec2, UVec2};
use graphene_application_io::SurfaceFrame;
use graphene_application_io::{ImageTexture, SurfaceFrame};
use graphene_brush::brush_cache::BrushCache;
use graphene_brush::brush_stroke::BrushStroke;
use graphene_core::raster_types::CPU;
use graphene_core::raster::Image;
use graphene_core::raster_types::{CPU, Raster};
use graphene_core::table::Table;
use graphene_core::transform::ReferencePoint;
use graphene_core::uuid::NodeId;
use graphene_core::vector::Vector;
use graphene_core::vector::style::Fill;
use graphene_core::{Color, MemoHash, Node, Type};
use graphene_core::vector::style::GradientStops;
use graphene_core::{Artboard, Color, Graphic, MemoHash, Node, Type};
use graphene_svg_renderer::RenderMetadata;
use std::fmt::Display;
use std::hash::Hash;
@@ -159,54 +164,55 @@ tagged_value! {
// ===============
// PRIMITIVE TYPES
// ===============
#[serde(alias = "F32")] // TODO: Eventually remove this alias document upgrade code
F32(f32),
F64(f64),
U32(u32),
U64(u64),
Bool(bool),
String(String),
#[serde(alias = "IVec2", alias = "UVec2")]
DVec2(DVec2),
DAffine2(DAffine2),
OptionalF64(Option<f64>),
OptionalDVec2(Option<DVec2>),
// ==========================
// PRIMITIVE COLLECTION TYPES
// ==========================
ColorNotInTable(Color),
OptionalColorNotInTable(Option<Color>),
// ========================
// LISTS OF PRIMITIVE TYPES
// ========================
#[serde(alias = "VecF32")] // TODO: Eventually remove this alias document upgrade code
VecF64(Vec<f64>),
VecU64(Vec<u64>),
VecDVec2(Vec<DVec2>),
F64Array4([f64; 4]),
NodePath(Vec<NodeId>),
#[serde(alias = "ManipulatorGroupIds")] // TODO: Eventually remove this alias document upgrade code
PointIds(Vec<graphene_core::vector::PointId>),
// ====================
// GRAPHICAL DATA TYPES
// ====================
GraphicElement(graphene_core::GraphicElement),
#[cfg_attr(target_arch = "wasm32", serde(deserialize_with = "graphene_core::vector::migrate_vector_data"))] // TODO: Eventually remove this migration document upgrade code
VectorData(graphene_core::vector::VectorDataTable),
#[cfg_attr(target_arch = "wasm32", serde(alias = "ImageFrame", deserialize_with = "graphene_core::raster::image::migrate_image_frame"))] // TODO: Eventually remove this migration document upgrade code
RasterData(graphene_core::raster_types::RasterDataTable<CPU>),
#[cfg_attr(target_arch = "wasm32", serde(deserialize_with = "graphene_core::graphic_element::migrate_graphic_group"))] // TODO: Eventually remove this migration document upgrade code
GraphicGroup(graphene_core::GraphicGroupTable),
#[cfg_attr(target_arch = "wasm32", serde(deserialize_with = "graphene_core::graphic_element::migrate_artboard_group"))] // TODO: Eventually remove this migration document upgrade code
ArtboardGroup(graphene_core::ArtboardGroupTable),
// ===========
// TABLE TYPES
// ===========
GraphicUnused(Graphic), // TODO: This is unused but removing it causes `cargo test` to infinitely recurse its type solving; figure out why and then remove this
#[cfg_attr(target_family = "wasm", serde(deserialize_with = "graphene_core::vector::migrate_vector"))] // TODO: Eventually remove this migration document upgrade code
#[serde(alias = "VectorData")]
Vector(Table<Vector>),
#[cfg_attr(target_family = "wasm", serde(deserialize_with = "graphene_core::raster::image::migrate_image_frame"))] // TODO: Eventually remove this migration document upgrade code
#[serde(alias = "ImageFrame", alias = "RasterData")]
Raster(Table<Raster<CPU>>),
#[cfg_attr(target_family = "wasm", serde(deserialize_with = "graphene_core::graphic::migrate_graphic"))] // TODO: Eventually remove this migration document upgrade code
#[serde(alias = "GraphicGroup", alias = "Group")]
Graphic(Table<Graphic>),
#[cfg_attr(target_family = "wasm", serde(deserialize_with = "graphene_core::artboard::migrate_artboard"))] // TODO: Eventually remove this migration document upgrade code
#[serde(alias = "ArtboardGroup")]
Artboard(Table<Artboard>),
#[cfg_attr(target_family = "wasm", serde(deserialize_with = "graphene_core::misc::migrate_color"))] // TODO: Eventually remove this migration document upgrade code
#[serde(alias = "ColorTable", alias = "OptionalColor")]
Color(Table<Color>),
GradientTable(Table<GradientStops>),
// ============
// STRUCT TYPES
// ============
Artboard(graphene_core::Artboard),
Image(graphene_core::raster::Image<Color>),
Color(graphene_core::raster::color::Color),
OptionalColor(Option<graphene_core::raster::color::Color>),
Palette(Vec<Color>),
Subpaths(Vec<bezier_rs::Subpath<graphene_core::vector::PointId>>),
Fill(graphene_core::vector::style::Fill),
FVec2(Vec2),
FAffine2(Affine2),
#[serde(alias = "IVec2", alias = "UVec2")]
DVec2(DVec2),
DAffine2(DAffine2),
Stroke(graphene_core::vector::style::Stroke),
Gradient(graphene_core::vector::style::Gradient),
#[serde(alias = "GradientPositions")] // TODO: Eventually remove this alias document upgrade code
GradientStops(graphene_core::vector::style::GradientStops),
GradientStops(GradientStops),
Font(graphene_core::text::Font),
BrushStrokes(Vec<BrushStroke>),
BrushCache(BrushCache),
@@ -214,10 +220,10 @@ tagged_value! {
Curve(graphene_raster_nodes::curve::Curve),
Footprint(graphene_core::transform::Footprint),
VectorModification(Box<graphene_core::vector::VectorModification>),
FontCache(Arc<graphene_core::text::FontCache>),
// ==========
// ENUM TYPES
// ==========
Fill(graphene_core::vector::style::Fill),
BlendMode(graphene_core::blending::BlendMode),
LuminanceCalculation(graphene_raster_nodes::adjustments::LuminanceCalculation),
XY(graphene_core::extract_xy::XY),
@@ -242,11 +248,11 @@ tagged_value! {
StrokeAlign(graphene_core::vector::style::StrokeAlign),
PaintOrder(graphene_core::vector::style::PaintOrder),
FillType(graphene_core::vector::style::FillType),
FillChoice(graphene_core::vector::style::FillChoice),
GradientType(graphene_core::vector::style::GradientType),
ReferencePoint(graphene_core::transform::ReferencePoint),
CentroidType(graphene_core::vector::misc::CentroidType),
BooleanOperation(graphene_path_bool::BooleanOperation),
TextAlign(graphene_core::text::TextAlign),
}
impl TaggedValue {
@@ -256,10 +262,10 @@ impl TaggedValue {
TaggedValue::String(x) => format!("\"{x}\""),
TaggedValue::U32(x) => x.to_string() + "_u32",
TaggedValue::U64(x) => x.to_string() + "_u64",
TaggedValue::F32(x) => x.to_string() + "_f32",
TaggedValue::F64(x) => x.to_string() + "_f64",
TaggedValue::Bool(x) => x.to_string(),
TaggedValue::BlendMode(x) => "BlendMode::".to_string() + &x.to_string(),
TaggedValue::Color(x) => format!("Color {x:?}"),
_ => panic!("Cannot convert to primitive string"),
}
}
@@ -280,7 +286,7 @@ impl TaggedValue {
6 => return Color::from_rgb_str(color),
8 => return Color::from_rgba_str(color),
_ => {
log::error!("Invalid default value color string: {}", input);
log::error!("Invalid default value color string: {input}");
return None;
}
}
@@ -301,13 +307,13 @@ impl TaggedValue {
"MAGENTA" => Color::MAGENTA,
"TRANSPARENT" => Color::TRANSPARENT,
_ => {
log::error!("Invalid default value color constant: {}", input);
log::error!("Invalid default value color constant: {input}");
return None;
}
});
}
log::error!("Invalid default value color: {}", input);
log::error!("Invalid default value color: {input}");
None
}
@@ -327,13 +333,13 @@ impl TaggedValue {
"BottomCenter" => ReferencePoint::BottomCenter,
"BottomRight" => ReferencePoint::BottomRight,
_ => {
log::error!("Invalid ReferencePoint default type variant: {}", input);
log::error!("Invalid ReferencePoint default type variant: {input}");
return None;
}
});
}
log::error!("Invalid ReferencePoint default type: {}", input);
log::error!("Invalid ReferencePoint default type: {input}");
None
}
@@ -348,12 +354,14 @@ impl TaggedValue {
x if x == TypeId::of::<()>() => TaggedValue::None,
x if x == TypeId::of::<String>() => TaggedValue::String(string.into()),
x if x == TypeId::of::<f64>() => FromStr::from_str(string).map(TaggedValue::F64).ok()?,
x if x == TypeId::of::<f32>() => FromStr::from_str(string).map(TaggedValue::F32).ok()?,
x if x == TypeId::of::<u64>() => FromStr::from_str(string).map(TaggedValue::U64).ok()?,
x if x == TypeId::of::<u32>() => FromStr::from_str(string).map(TaggedValue::U32).ok()?,
x if x == TypeId::of::<DVec2>() => to_dvec2(string).map(TaggedValue::DVec2)?,
x if x == TypeId::of::<bool>() => FromStr::from_str(string).map(TaggedValue::Bool).ok()?,
x if x == TypeId::of::<Color>() => to_color(string).map(TaggedValue::Color)?,
x if x == TypeId::of::<Option<Color>>() => to_color(string).map(|color| TaggedValue::OptionalColor(Some(color)))?,
x if x == TypeId::of::<Table<Color>>() => to_color(string).map(|color| TaggedValue::Color(Table::new_from_element(color)))?,
x if x == TypeId::of::<Color>() => to_color(string).map(|color| TaggedValue::ColorNotInTable(color))?,
x if x == TypeId::of::<Option<Color>>() => TaggedValue::ColorNotInTable(to_color(string)?),
x if x == TypeId::of::<Fill>() => to_color(string).map(|color| TaggedValue::Fill(Fill::solid(color)))?,
x if x == TypeId::of::<ReferencePoint>() => to_reference_point(string).map(TaggedValue::ReferencePoint)?,
_ => return None,
@@ -379,6 +387,7 @@ impl Display for TaggedValue {
TaggedValue::String(x) => f.write_str(x),
TaggedValue::U32(x) => f.write_fmt(format_args!("{x}")),
TaggedValue::U64(x) => f.write_fmt(format_args!("{x}")),
TaggedValue::F32(x) => f.write_fmt(format_args!("{x}")),
TaggedValue::F64(x) => f.write_fmt(format_args!("{x}")),
TaggedValue::Bool(x) => f.write_fmt(format_args!("{x}")),
_ => panic!("Cannot convert to string"),
@@ -393,7 +402,8 @@ impl<'input> Node<'input, DAny<'input>> for UpcastNode {
type Output = FutureAny<'input>;
fn eval(&'input self, _: DAny<'input>) -> Self::Output {
Box::pin(async move { self.value.clone().into_inner().to_dynany() })
let memo_clone = MemoHash::clone(&self.value);
Box::pin(async move { memo_clone.into_inner().as_ref().clone().to_dynany() })
}
}
impl UpcastNode {
@@ -424,10 +434,15 @@ pub struct RenderOutput {
pub metadata: RenderMetadata,
}
#[derive(Debug, Clone, PartialEq, dyn_any::DynAny, Hash, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Clone, Hash, PartialEq, dyn_any::DynAny, serde::Serialize, serde::Deserialize)]
pub enum RenderOutputType {
CanvasFrame(SurfaceFrame),
Svg(String),
#[serde(skip)]
Texture(ImageTexture),
Svg {
svg: String,
image_data: Vec<(u64, Image<Color>)>,
},
Image(Vec<u8>),
}
@@ -448,17 +463,32 @@ mod fake_hash {
self.to_bits().hash(state)
}
}
impl FakeHash for f32 {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.to_bits().hash(state)
}
}
impl FakeHash for DVec2 {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.to_array().iter().for_each(|x| x.to_bits().hash(state))
}
}
impl FakeHash for Vec2 {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.to_array().iter().for_each(|x| x.to_bits().hash(state))
}
}
impl FakeHash for DAffine2 {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.to_cols_array().iter().for_each(|x| x.to_bits().hash(state))
}
}
impl<X: FakeHash> FakeHash for Option<X> {
impl FakeHash for Affine2 {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.to_cols_array().iter().for_each(|x| x.to_bits().hash(state))
}
}
impl<T: FakeHash> FakeHash for Option<T> {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
if let Some(x) = self {
1.hash(state);
@@ -468,7 +498,7 @@ mod fake_hash {
}
}
}
impl<X: FakeHash> FakeHash for Vec<X> {
impl<T: FakeHash> FakeHash for Vec<T> {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.len().hash(state);
self.iter().for_each(|x| x.hash(state))
@@ -486,3 +516,8 @@ mod fake_hash {
}
}
}
#[test]
fn can_construct_color() {
assert_eq!(TaggedValue::from_type(&concrete!(Color)).unwrap(), TaggedValue::ColorNotInTable(Color::default()));
}

View File

@@ -37,12 +37,7 @@ impl core::fmt::Display for ProtoNetwork {
f.write_str(&"\t".repeat(indent + 1))?;
f.write_str("Input: ")?;
match &node.input {
ProtoNodeInput::None => f.write_str("None")?,
ProtoNodeInput::ManualComposition(ty) => f.write_fmt(format_args!("Manual Composition (type = {ty:?})"))?,
ProtoNodeInput::Node(_) => f.write_str("Node")?,
ProtoNodeInput::NodeLambda(_) => f.write_str("Lambda Node")?,
}
f.write_fmt(format_args!("Call Argument (type = {:?})", node.call_argument))?;
f.write_str("\n")?;
match &node.construction_args {
@@ -52,7 +47,7 @@ impl core::fmt::Display for ProtoNetwork {
}
ConstructionArgs::Nodes(nodes) => {
for id in nodes {
write_node(f, network, id.0, indent + 1)?;
write_node(f, network, *id, indent + 1)?;
}
}
ConstructionArgs::Inline(inline) => {
@@ -78,7 +73,7 @@ pub enum ConstructionArgs {
/// A list of nodes used as inputs to the constructor function in `node_registry.rs`.
/// The bool indicates whether to treat the node as lambda node.
// TODO: use a struct for clearer naming.
Nodes(Vec<(NodeId, bool)>),
Nodes(Vec<NodeId>),
/// Used for GPU computation to work around the limitations of rust-gpu.
Inline(InlineRust),
}
@@ -119,10 +114,9 @@ impl Hash for ConstructionArgs {
}
impl ConstructionArgs {
// TODO: what? Used in the gpu_compiler crate for something.
pub fn new_function_args(&self) -> Vec<String> {
match self {
ConstructionArgs::Nodes(nodes) => nodes.iter().map(|(n, _)| format!("n{:0x}", n.0)).collect(),
ConstructionArgs::Nodes(nodes) => nodes.iter().map(|n| format!("n{:0x}", n.0)).collect(),
ConstructionArgs::Value(value) => vec![value.to_primitive_string()],
ConstructionArgs::Inline(inline) => vec![inline.expr.clone()],
}
@@ -134,7 +128,7 @@ impl ConstructionArgs {
/// At different stages in the compilation process, this struct will be transformed into a reduced (more restricted) form acting as a subset of its original form, but that restricted form is still valid in the earlier stage in the compilation process before it was transformed.
pub struct ProtoNode {
pub construction_args: ConstructionArgs,
pub input: ProtoNodeInput,
pub call_argument: Type,
pub identifier: ProtoNodeIdentifier,
pub original_location: OriginalLocation,
pub skip_deduplication: bool,
@@ -145,45 +139,13 @@ impl Default for ProtoNode {
Self {
identifier: ProtoNodeIdentifier::new("graphene_core::ops::IdentityNode"),
construction_args: ConstructionArgs::Value(value::TaggedValue::U32(0).into()),
input: ProtoNodeInput::None,
call_argument: concrete!(()),
original_location: OriginalLocation::default(),
skip_deduplication: false,
}
}
}
/// Similar to the document node's [`crate::document::NodeInput`].
#[derive(Debug, PartialEq, Eq, Clone, Hash, serde::Serialize, serde::Deserialize)]
pub enum ProtoNodeInput {
/// This input will be converted to `()` as the call argument.
None,
/// A ManualComposition input represents an input that opts out of being resolved through the `ComposeNode`, which first runs the previous (upstream) node, then passes that evaluated
/// result to this node. Instead, ManualComposition lets this node actually consume the provided input instead of passing it to its predecessor.
///
/// Say we have the network `a -> b -> c` where `c` is the output node and `a` is the input node.
/// We would expect `a` to get input from the network, `b` to get input from `a`, and `c` to get input from `b`.
/// This could be represented as `f(x) = c(b(a(x)))`. `a` is run with input `x` from the network. `b` is run with input from `a`. `c` is run with input from `b`.
///
/// However if `b`'s input is using manual composition, this means it would instead be `f(x) = c(b(x))`. This means that `b` actually gets input from the network, and `a` is not automatically
/// executed as it would be using the default ComposeNode flow. Now `b` can use its own logic to decide when or if it wants to run `a` and how to use its output. For example, the CacheNode can
/// look up `x` in its cache and return the result, or otherwise call `a`, cache the result, and return it.
ManualComposition(Type),
/// The previous node where automatic (not manual) composition occurs when compiled. The entire network, of which the node is the output, is fed as input.
///
/// Grayscale example:
///
/// We're interested in receiving an input of the desaturated image data which has been fed through a grayscale filter.
/// (If we were interested in the grayscale filter itself, we would use the `NodeLambda` variant.)
Node(NodeId),
/// Unlike the `Node` variant, with `NodeLambda` we treat the connected node singularly as a lambda node while ignoring all nodes which feed into it from upstream.
///
/// Grayscale example:
///
/// We're interested in receiving an input of a particular image filter, such as a grayscale filter in the form of a grayscale node lambda.
/// (If we were interested in some image data that had been fed through a grayscale filter, we would use the `Node` variant.)
NodeLambda(NodeId),
}
impl ProtoNode {
/// A stable node ID is a hash of a node that should stay constant. This is used in order to remove duplicates from the graph.
/// In the case of `skip_deduplication`, the `document_node_path` is also hashed in order to avoid duplicate monitor nodes from being removed (which would make it impossible to load thumbnails).
@@ -197,15 +159,8 @@ impl ProtoNode {
self.original_location.path.hash(&mut hasher);
}
std::mem::discriminant(&self.input).hash(&mut hasher);
match self.input {
ProtoNodeInput::None => (),
ProtoNodeInput::ManualComposition(ref ty) => {
ty.hash(&mut hasher);
}
ProtoNodeInput::Node(id) => (id, false).hash(&mut hasher),
ProtoNodeInput::NodeLambda(id) => (id, true).hash(&mut hasher),
};
std::mem::discriminant(&self.call_argument).hash(&mut hasher);
self.call_argument.hash(&mut hasher);
Some(NodeId(hasher.finish()))
}
@@ -219,7 +174,7 @@ impl ProtoNode {
Self {
identifier: ProtoNodeIdentifier::new("graphene_core::value::ClonedNode"),
construction_args: value,
input: ProtoNodeInput::ManualComposition(concrete!(Context)),
call_argument: concrete!(Context),
original_location: OriginalLocation {
path: Some(path),
inputs_exposed: vec![false; inputs_exposed],
@@ -231,23 +186,13 @@ impl ProtoNode {
/// Converts all references to other node IDs into new IDs by running the specified function on them.
/// This can be used when changing the IDs of the nodes, for example in the case of generating stable IDs.
pub fn map_ids(&mut self, f: impl Fn(NodeId) -> NodeId, skip_lambdas: bool) {
match self.input {
ProtoNodeInput::Node(id) => self.input = ProtoNodeInput::Node(f(id)),
ProtoNodeInput::NodeLambda(id) => {
if !skip_lambdas {
self.input = ProtoNodeInput::NodeLambda(f(id))
}
}
_ => (),
}
pub fn map_ids(&mut self, f: impl Fn(NodeId) -> NodeId) {
if let ConstructionArgs::Nodes(ids) = &mut self.construction_args {
ids.iter_mut().filter(|(_, lambda)| !(skip_lambdas && *lambda)).for_each(|(id, _)| *id = f(*id));
ids.iter_mut().for_each(|id| *id = f(*id));
}
}
pub fn unwrap_construction_nodes(&self) -> Vec<(NodeId, bool)> {
pub fn unwrap_construction_nodes(&self) -> Vec<NodeId> {
match &self.construction_args {
ConstructionArgs::Nodes(nodes) => nodes.clone(),
_ => panic!("tried to unwrap nodes from non node construction args \n node: {self:#?}"),
@@ -286,16 +231,8 @@ impl ProtoNetwork {
pub fn collect_outwards_edges(&self) -> HashMap<NodeId, Vec<NodeId>> {
let mut edges: HashMap<NodeId, Vec<NodeId>> = HashMap::new();
for (id, node) in &self.nodes {
match &node.input {
ProtoNodeInput::Node(ref_id) | ProtoNodeInput::NodeLambda(ref_id) => {
self.check_ref(ref_id, id);
edges.entry(*ref_id).or_default().push(*id)
}
_ => (),
}
if let ConstructionArgs::Nodes(ref_nodes) = &node.construction_args {
for (ref_id, _) in ref_nodes {
for ref_id in ref_nodes {
self.check_ref(ref_id, id);
edges.entry(*ref_id).or_default().push(*id)
}
@@ -314,7 +251,7 @@ impl ProtoNetwork {
let Some(sni) = self.nodes[index].1.stable_node_id() else {
panic!("failed to generate stable node id for node {:#?}", self.nodes[index].1);
};
self.replace_node_id(&outwards_edges, NodeId(index as u64), sni, false);
self.replace_node_id(&outwards_edges, NodeId(index as u64), sni);
self.nodes[index].0 = sni;
}
}
@@ -324,16 +261,8 @@ impl ProtoNetwork {
pub fn collect_inwards_edges(&self) -> HashMap<NodeId, Vec<NodeId>> {
let mut edges: HashMap<NodeId, Vec<NodeId>> = HashMap::new();
for (id, node) in &self.nodes {
match &node.input {
ProtoNodeInput::Node(ref_id) | ProtoNodeInput::NodeLambda(ref_id) => {
self.check_ref(ref_id, id);
edges.entry(*id).or_default().push(*ref_id)
}
_ => (),
}
if let ConstructionArgs::Nodes(ref_nodes) = &node.construction_args {
for (ref_id, _) in ref_nodes {
for ref_id in ref_nodes {
self.check_ref(ref_id, id);
edges.entry(*id).or_default().push(*ref_id)
}
@@ -349,16 +278,9 @@ impl ProtoNetwork {
let mut inwards_edges = vec![Vec::new(); self.nodes.len()];
for (node_id, node) in &self.nodes {
let node_index = id_map[node_id];
match &node.input {
ProtoNodeInput::Node(ref_id) | ProtoNodeInput::NodeLambda(ref_id) => {
self.check_ref(ref_id, &NodeId(node_index as u64));
inwards_edges[node_index].push(id_map[ref_id]);
}
_ => {}
}
if let ConstructionArgs::Nodes(ref_nodes) = &node.construction_args {
for (ref_id, _) in ref_nodes {
for ref_id in ref_nodes {
self.check_ref(ref_id, &NodeId(node_index as u64));
inwards_edges[node_index].push(id_map[ref_id]);
}
@@ -368,70 +290,31 @@ impl ProtoNetwork {
(inwards_edges, id_map)
}
/// Inserts a [`structural::ComposeNode`] for each node that has a [`ProtoNodeInput::Node`]. The compose node evaluates the first node, and then sends the result into the second node.
/// Performs topological sort and reorders ids.
pub fn resolve_inputs(&mut self) -> Result<(), String> {
// Perform topological sort once
self.reorder_ids()?;
let max_id = self.nodes.len() as u64 - 1;
// Collect outward edges once
let outwards_edges = self.collect_outwards_edges();
// Iterate over nodes in topological order
for node_id in 0..=max_id {
let node_id = NodeId(node_id);
let (_, node) = &mut self.nodes[node_id.0 as usize];
if let ProtoNodeInput::Node(input_node_id) = node.input {
// Create a new node that composes the current node and its input node
let compose_node_id = NodeId(self.nodes.len() as u64);
let (_, input_node_id_proto) = &self.nodes[input_node_id.0 as usize];
let input = input_node_id_proto.input.clone();
let mut path = input_node_id_proto.original_location.path.clone();
if let Some(path) = &mut path {
path.push(node_id);
}
self.nodes.push((
compose_node_id,
ProtoNode {
identifier: ProtoNodeIdentifier::new("graphene_core::structural::ComposeNode"),
construction_args: ConstructionArgs::Nodes(vec![(input_node_id, false), (node_id, true)]),
input,
original_location: OriginalLocation { path, ..Default::default() },
skip_deduplication: false,
},
));
self.replace_node_id(&outwards_edges, node_id, compose_node_id, true);
}
}
self.reorder_ids()?;
Ok(())
}
/// Update all of the references to a node ID in the graph with a new ID named `compose_node_id`.
fn replace_node_id(&mut self, outwards_edges: &HashMap<NodeId, Vec<NodeId>>, node_id: NodeId, compose_node_id: NodeId, skip_lambdas: bool) {
// Update references in other nodes to use the new compose node
/// Update all of the references to a node ID in the graph with a new ID named `replacement_node_id`.
fn replace_node_id(&mut self, outwards_edges: &HashMap<NodeId, Vec<NodeId>>, node_id: NodeId, replacement_node_id: NodeId) {
// Update references in other nodes to use the new node
if let Some(referring_nodes) = outwards_edges.get(&node_id) {
for &referring_node_id in referring_nodes {
let (_, referring_node) = &mut self.nodes[referring_node_id.0 as usize];
referring_node.map_ids(|id| if id == node_id { compose_node_id } else { id }, skip_lambdas)
referring_node.map_ids(|id| if id == node_id { replacement_node_id } else { id })
}
}
if self.output == node_id {
self.output = compose_node_id;
self.output = replacement_node_id;
}
self.inputs.iter_mut().for_each(|id| {
if *id == node_id {
*id = compose_node_id;
*id = replacement_node_id;
}
});
}
@@ -509,7 +392,7 @@ impl ProtoNetwork {
for (index, &id) in order.iter().enumerate() {
let mut node = std::mem::take(&mut self.nodes[id.0 as usize].1);
// Update node references to reflect the new order
node.map_ids(|id| NodeId(*new_positions.get(&id).expect("node not found in lookup table") as u64), false);
node.map_ids(|id| NodeId(*new_positions.get(&id).expect("node not found in lookup table") as u64));
new_nodes.push((NodeId(index as u64), node));
}
@@ -662,7 +545,6 @@ impl TypingContext {
let inputs = match node.construction_args {
// If the node has a value input we can infer the return type from it
ConstructionArgs::Value(ref v) => {
assert!(matches!(node.input, ProtoNodeInput::None) || matches!(node.input, ProtoNodeInput::ManualComposition(ref x) if x == &concrete!(Context)));
// TODO: This should return a reference to the value
let types = NodeIOTypes::new(concrete!(Context), Type::Future(Box::new(v.ty())), vec![]);
self.inferred.insert(node_id, types.clone());
@@ -671,7 +553,7 @@ impl TypingContext {
// If the node has nodes as inputs we can infer the types from the node outputs
ConstructionArgs::Nodes(ref nodes) => nodes
.iter()
.map(|(id, _)| {
.map(|id| {
self.inferred
.get(id)
.ok_or_else(|| vec![GraphError::new(node, GraphErrorType::NodeNotFound(*id))])
@@ -682,16 +564,7 @@ impl TypingContext {
};
// Get the node input type from the proto node declaration
// TODO: When removing automatic composition, rename this to just `call_argument`
let primary_input_or_call_argument = match node.input {
ProtoNodeInput::None => concrete!(()),
ProtoNodeInput::ManualComposition(ref ty) => ty.clone(),
ProtoNodeInput::Node(id) | ProtoNodeInput::NodeLambda(id) => {
let input = self.inferred.get(&id).ok_or_else(|| vec![GraphError::new(node, GraphErrorType::InputNodeNotFound(id))])?;
input.return_value.clone()
}
};
let using_manual_composition = matches!(node.input, ProtoNodeInput::ManualComposition(_) | ProtoNodeInput::None);
let call_argument = &node.call_argument;
let impls = self.lookup.get(&node.identifier).ok_or_else(|| vec![GraphError::new(node, GraphErrorType::NoImplementations)])?;
if let Some(index) = inputs.iter().position(|p| {
@@ -733,7 +606,7 @@ impl TypingContext {
// List of all implementations that match the input types
let valid_output_types = impls
.keys()
.filter(|node_io| valid_type(&node_io.call_argument, &primary_input_or_call_argument) && inputs.iter().zip(node_io.inputs.iter()).all(|(p1, p2)| valid_type(p1, p2)))
.filter(|node_io| valid_type(&node_io.call_argument, call_argument) && inputs.iter().zip(node_io.inputs.iter()).all(|(p1, p2)| valid_type(p1, p2)))
.collect::<Vec<_>>();
// Attempt to substitute generic types with concrete types and save the list of results
@@ -742,7 +615,7 @@ impl TypingContext {
.map(|node_io| {
let generics_lookup: Result<HashMap<_, _>, _> = collect_generics(node_io)
.iter()
.map(|generic| check_generic(node_io, &primary_input_or_call_argument, &inputs, generic).map(|x| (generic.to_string(), x)))
.map(|generic| check_generic(node_io, call_argument, &inputs, generic).map(|x| (generic.to_string(), x)))
.collect();
generics_lookup.map(|generics_lookup| {
@@ -762,7 +635,7 @@ impl TypingContext {
let mut best_errors = usize::MAX;
let mut error_inputs = Vec::new();
for node_io in impls.keys() {
let current_errors = [&primary_input_or_call_argument]
let current_errors = [call_argument]
.into_iter()
.chain(&inputs)
.cloned()
@@ -771,7 +644,6 @@ impl TypingContext {
.filter(|(_, (p1, p2))| !valid_type(p1, p2))
.map(|(index, ty)| {
let i = node.original_location.inputs(index).min_by_key(|s| s.node.len()).map(|s| s.index).unwrap_or(index);
let i = if using_manual_composition { i } else { i + 1 };
(i, ty)
})
.collect::<Vec<_>>();
@@ -783,15 +655,11 @@ impl TypingContext {
error_inputs.push(current_errors);
}
}
let inputs = [&primary_input_or_call_argument]
let inputs = [call_argument]
.into_iter()
.chain(&inputs)
.enumerate()
// TODO: Make the following line's if statement conditional on being a call argument or primary input
.filter_map(|(i, t)| {
let i = if using_manual_composition { i } else { i + 1 };
if i == 0 { None } else { Some(format!("• Input {i}: {t}")) }
})
.filter_map(|(i, t)| if i == 0 { None } else { Some(format!("• Input {i}: {t}")) })
.collect::<Vec<_>>()
.join("\n");
Err(vec![GraphError::new(node, GraphErrorType::InvalidImplementations { inputs, error_inputs })])
@@ -818,13 +686,13 @@ impl TypingContext {
return Ok(node_io.clone());
}
}
let inputs = [&primary_input_or_call_argument].into_iter().chain(&inputs).map(|t| t.to_string()).collect::<Vec<_>>().join(", ");
let inputs = [call_argument].into_iter().chain(&inputs).map(|t| t.to_string()).collect::<Vec<_>>().join(", ");
let valid = valid_output_types.into_iter().cloned().collect();
Err(vec![GraphError::new(node, GraphErrorType::MultipleImplementations { inputs, valid })])
}
_ => {
let inputs = [&primary_input_or_call_argument].into_iter().chain(&inputs).map(|t| t.to_string()).collect::<Vec<_>>().join(", ");
let inputs = [call_argument].into_iter().chain(&inputs).map(|t| t.to_string()).collect::<Vec<_>>().join(", ");
let valid = valid_output_types.into_iter().cloned().collect();
Err(vec![GraphError::new(node, GraphErrorType::MultipleImplementations { inputs, valid })])
}
@@ -883,7 +751,7 @@ fn replace_generics(types: &mut NodeIOTypes, lookup: &HashMap<String, Type>) {
#[cfg(test)]
mod test {
use super::*;
use crate::proto::{ConstructionArgs, ProtoNetwork, ProtoNode, ProtoNodeInput};
use crate::proto::{ConstructionArgs, ProtoNetwork, ProtoNode};
#[test]
fn topological_sort() {
@@ -930,16 +798,6 @@ mod test {
assert_eq!(ids, vec![NodeId(0), NodeId(1), NodeId(2), NodeId(3)]);
}
#[test]
fn input_resolution() {
let mut construction_network = test_network();
construction_network.resolve_inputs().expect("Error when calling 'resolve_inputs' on 'construction_network.");
println!("{construction_network:#?}");
assert_eq!(construction_network.nodes[0].1.identifier.name.as_ref(), "value");
assert_eq!(construction_network.nodes.len(), 6);
assert_eq!(construction_network.nodes[5].1.construction_args, ConstructionArgs::Nodes(vec![(NodeId(3), false), (NodeId(4), true)]));
}
#[test]
fn stable_node_id_generation() {
let mut construction_network = test_network();
@@ -947,16 +805,11 @@ mod test {
construction_network.generate_stable_node_ids();
assert_eq!(construction_network.nodes[0].1.identifier.name.as_ref(), "value");
let ids: Vec<_> = construction_network.nodes.iter().map(|(id, _)| *id).collect();
// If this assert fails: These NodeIds seem to be changing when you modify TaggedValue, just update them.
assert_eq!(
ids,
vec![
NodeId(16997244687192517417),
NodeId(12226224850522777131),
NodeId(9162113827627229771),
NodeId(12793582657066318419),
NodeId(16945623684036608820),
NodeId(2640415155091892458)
]
vec![NodeId(2791689253855410677), NodeId(11246167042277902310), NodeId(1014827049498980779), NodeId(4864562752646903491)]
);
}
@@ -969,8 +822,8 @@ mod test {
NodeId(7),
ProtoNode {
identifier: "id".into(),
input: ProtoNodeInput::Node(NodeId(11)),
construction_args: ConstructionArgs::Nodes(vec![]),
call_argument: concrete!(()),
construction_args: ConstructionArgs::Nodes(vec![NodeId(11)]),
..Default::default()
},
),
@@ -978,8 +831,8 @@ mod test {
NodeId(1),
ProtoNode {
identifier: "id".into(),
input: ProtoNodeInput::Node(NodeId(11)),
construction_args: ConstructionArgs::Nodes(vec![]),
call_argument: concrete!(()),
construction_args: ConstructionArgs::Nodes(vec![NodeId(11)]),
..Default::default()
},
),
@@ -987,8 +840,8 @@ mod test {
NodeId(10),
ProtoNode {
identifier: "cons".into(),
input: ProtoNodeInput::ManualComposition(concrete!(u32)),
construction_args: ConstructionArgs::Nodes(vec![(NodeId(14), false)]),
call_argument: concrete!(u32),
construction_args: ConstructionArgs::Nodes(vec![NodeId(14)]),
..Default::default()
},
),
@@ -996,8 +849,8 @@ mod test {
NodeId(11),
ProtoNode {
identifier: "add".into(),
input: ProtoNodeInput::Node(NodeId(10)),
construction_args: ConstructionArgs::Nodes(vec![]),
call_argument: concrete!(()),
construction_args: ConstructionArgs::Nodes(vec![NodeId(10)]),
..Default::default()
},
),
@@ -1005,7 +858,7 @@ mod test {
NodeId(14),
ProtoNode {
identifier: "value".into(),
input: ProtoNodeInput::None,
call_argument: concrete!(()),
construction_args: ConstructionArgs::Value(value::TaggedValue::U32(2).into()),
..Default::default()
},
@@ -1025,8 +878,8 @@ mod test {
NodeId(1),
ProtoNode {
identifier: "id".into(),
input: ProtoNodeInput::Node(NodeId(2)),
construction_args: ConstructionArgs::Nodes(vec![]),
call_argument: concrete!(()),
construction_args: ConstructionArgs::Nodes(vec![NodeId(2)]),
..Default::default()
},
),
@@ -1034,8 +887,8 @@ mod test {
NodeId(2),
ProtoNode {
identifier: "id".into(),
input: ProtoNodeInput::Node(NodeId(1)),
construction_args: ConstructionArgs::Nodes(vec![]),
call_argument: concrete!(()),
construction_args: ConstructionArgs::Nodes(vec![NodeId(1)]),
..Default::default()
},
),

View File

@@ -1,34 +1,35 @@
use dyn_any::StaticType;
use graphene_application_io::{ApplicationError, ApplicationIo, ResourceFuture, SurfaceHandle, SurfaceId};
#[cfg(target_arch = "wasm32")]
#[cfg(target_family = "wasm")]
use js_sys::{Object, Reflect};
use std::collections::HashMap;
use std::hash::Hash;
use std::sync::Arc;
#[cfg(target_arch = "wasm32")]
#[cfg(target_family = "wasm")]
use std::sync::atomic::AtomicU64;
use std::sync::atomic::Ordering;
#[cfg(feature = "tokio")]
use tokio::io::AsyncReadExt;
#[cfg(target_arch = "wasm32")]
#[cfg(target_family = "wasm")]
use wasm_bindgen::JsCast;
#[cfg(target_arch = "wasm32")]
#[cfg(target_family = "wasm")]
use wasm_bindgen::JsValue;
#[cfg(target_arch = "wasm32")]
#[cfg(target_family = "wasm")]
use web_sys::HtmlCanvasElement;
#[cfg(target_arch = "wasm32")]
#[cfg(target_family = "wasm")]
use web_sys::window;
#[cfg(feature = "wgpu")]
use wgpu_executor::WgpuExecutor;
#[derive(Debug)]
struct WindowWrapper {
#[cfg(target_arch = "wasm32")]
#[cfg(target_family = "wasm")]
window: SurfaceHandle<HtmlCanvasElement>,
#[cfg(not(target_arch = "wasm32"))]
#[cfg(not(target_family = "wasm"))]
window: SurfaceHandle<Arc<winit::window::Window>>,
}
#[cfg(target_arch = "wasm32")]
#[cfg(target_family = "wasm")]
impl Drop for WindowWrapper {
fn drop(&mut self) {
let window = window().expect("should have a window in this context");
@@ -39,7 +40,7 @@ impl Drop for WindowWrapper {
let wrapper = || {
if let Ok(canvases) = Reflect::get(&window, &image_canvases_key) {
// Convert key and value to JsValue
let js_key = JsValue::from_str(format!("canvas{}", self.window.window_id).as_str());
let js_key = JsValue::from_str(self.window.window_id.to_string().as_str());
// Use Reflect API to set property
Reflect::delete_property(&canvases.into(), &js_key)?;
@@ -51,14 +52,14 @@ impl Drop for WindowWrapper {
}
}
#[cfg(target_arch = "wasm32")]
#[cfg(target_family = "wasm")]
unsafe impl Sync for WindowWrapper {}
#[cfg(target_arch = "wasm32")]
#[cfg(target_family = "wasm")]
unsafe impl Send for WindowWrapper {}
#[derive(Debug, Default)]
pub struct WasmApplicationIo {
#[cfg(target_arch = "wasm32")]
#[cfg(target_family = "wasm")]
ids: AtomicU64,
#[cfg(feature = "wgpu")]
pub(crate) gpu_executor: Option<WgpuExecutor>,
@@ -69,14 +70,6 @@ pub struct WasmApplicationIo {
static WGPU_AVAILABLE: std::sync::atomic::AtomicI8 = std::sync::atomic::AtomicI8::new(-1);
pub fn wgpu_available() -> Option<bool> {
// Always enable wgpu when running with Tauri
#[cfg(target_arch = "wasm32")]
if let Some(window) = web_sys::window() {
if js_sys::Reflect::get(&window, &wasm_bindgen::JsValue::from_str("__TAURI__")).is_ok() {
return Some(true);
}
}
match WGPU_AVAILABLE.load(Ordering::SeqCst) {
-1 => None,
0 => Some(false),
@@ -86,7 +79,7 @@ pub fn wgpu_available() -> Option<bool> {
impl WasmApplicationIo {
pub async fn new() -> Self {
#[cfg(all(feature = "wgpu", target_arch = "wasm32"))]
#[cfg(all(feature = "wgpu", target_family = "wasm"))]
let executor = if let Some(gpu) = web_sys::window().map(|w| w.navigator().gpu()) {
let request_adapter = || {
let request_adapter = js_sys::Reflect::get(&gpu, &wasm_bindgen::JsValue::from_str("requestAdapter")).ok()?;
@@ -102,7 +95,7 @@ impl WasmApplicationIo {
None
};
#[cfg(all(feature = "wgpu", not(target_arch = "wasm32")))]
#[cfg(all(feature = "wgpu", not(target_family = "wasm")))]
let executor = WgpuExecutor::new().await;
#[cfg(not(feature = "wgpu"))]
@@ -112,7 +105,7 @@ impl WasmApplicationIo {
WGPU_AVAILABLE.store(wgpu_available as i8, Ordering::SeqCst);
let mut io = Self {
#[cfg(target_arch = "wasm32")]
#[cfg(target_family = "wasm")]
ids: AtomicU64::new(0),
#[cfg(feature = "wgpu")]
gpu_executor: executor,
@@ -136,9 +129,8 @@ impl WasmApplicationIo {
let wgpu_available = executor.is_some();
WGPU_AVAILABLE.store(wgpu_available as i8, Ordering::SeqCst);
// Always enable wgpu when running with Tauri
let mut io = Self {
#[cfg(target_arch = "wasm32")]
#[cfg(target_family = "wasm")]
ids: AtomicU64::new(0),
#[cfg(feature = "wgpu")]
gpu_executor: executor,
@@ -148,6 +140,27 @@ impl WasmApplicationIo {
io.resources.insert("null".to_string(), Arc::from(include_bytes!("null.png").to_vec()));
io
}
#[cfg(all(not(target_family = "wasm"), feature = "wgpu"))]
pub fn new_with_context(context: wgpu_executor::Context) -> Self {
#[cfg(feature = "wgpu")]
let executor = WgpuExecutor::with_context(context);
#[cfg(not(feature = "wgpu"))]
let wgpu_available = false;
#[cfg(feature = "wgpu")]
let wgpu_available = executor.is_some();
WGPU_AVAILABLE.store(wgpu_available as i8, Ordering::SeqCst);
let mut io = Self {
gpu_executor: executor,
windows: Vec::new(),
resources: HashMap::new(),
};
io.resources.insert("null".to_string(), Arc::from(include_bytes!("null.png").to_vec()));
io
}
}
@@ -171,16 +184,16 @@ impl<'a> From<&'a WasmApplicationIo> for &'a WgpuExecutor {
pub type WasmEditorApi = graphene_application_io::EditorApi<WasmApplicationIo>;
impl ApplicationIo for WasmApplicationIo {
#[cfg(target_arch = "wasm32")]
#[cfg(target_family = "wasm")]
type Surface = HtmlCanvasElement;
#[cfg(not(target_arch = "wasm32"))]
#[cfg(not(target_family = "wasm"))]
type Surface = Arc<winit::window::Window>;
#[cfg(feature = "wgpu")]
type Executor = WgpuExecutor;
#[cfg(not(feature = "wgpu"))]
type Executor = ();
#[cfg(target_arch = "wasm32")]
#[cfg(target_family = "wasm")]
fn create_window(&self) -> SurfaceHandle<Self::Surface> {
let wrapper = || {
let document = window().expect("should have a window in this context").document().expect("window should have a document");
@@ -200,7 +213,7 @@ impl ApplicationIo for WasmApplicationIo {
}
// Convert key and value to JsValue
let js_key = JsValue::from_str(format!("canvas{}", id).as_str());
let js_key = JsValue::from_str(id.to_string().as_str());
let js_value = JsValue::from(canvas.clone());
let canvases = Object::from(canvases.unwrap());
@@ -215,31 +228,35 @@ impl ApplicationIo for WasmApplicationIo {
wrapper().expect("should be able to set canvas in global scope")
}
#[cfg(not(target_arch = "wasm32"))]
#[cfg(not(target_family = "wasm"))]
fn create_window(&self) -> SurfaceHandle<Self::Surface> {
log::trace!("Spawning window");
todo!("winit api changed, calling create_window on EventLoop is deprecated");
#[cfg(all(not(test), target_os = "linux", feature = "wayland"))]
use winit::platform::wayland::EventLoopBuilderExtWayland;
// log::trace!("Spawning window");
#[cfg(all(not(test), target_os = "linux", feature = "wayland"))]
let event_loop = winit::event_loop::EventLoopBuilder::new().with_any_thread(true).build().unwrap();
#[cfg(not(all(not(test), target_os = "linux", feature = "wayland")))]
let event_loop = winit::event_loop::EventLoop::new().unwrap();
// #[cfg(all(not(test), target_os = "linux", feature = "wayland"))]
// use winit::platform::wayland::EventLoopBuilderExtWayland;
let window = winit::window::WindowBuilder::new()
.with_title("Graphite")
.with_inner_size(winit::dpi::PhysicalSize::new(800, 600))
.build(&event_loop)
.unwrap();
// #[cfg(all(not(test), target_os = "linux", feature = "wayland"))]
// let event_loop = winit::event_loop::EventLoopBuilder::new().with_any_thread(true).build().unwrap();
// #[cfg(not(all(not(test), target_os = "linux", feature = "wayland")))]
// let event_loop = winit::event_loop::EventLoop::new().unwrap();
SurfaceHandle {
window_id: SurfaceId(window.id().into()),
surface: Arc::new(window),
}
// let window = event_loop
// .create_window(
// winit::window::WindowAttributes::default()
// .with_title("Graphite")
// .with_inner_size(winit::dpi::PhysicalSize::new(800, 600)),
// )
// .unwrap();
// SurfaceHandle {
// window_id: SurfaceId(window.id().into()),
// surface: Arc::new(window),
// }
}
#[cfg(target_arch = "wasm32")]
#[cfg(target_family = "wasm")]
fn destroy_window(&self, surface_id: SurfaceId) {
let window = window().expect("should have a window in this context");
let window = Object::from(window);
@@ -249,7 +266,7 @@ impl ApplicationIo for WasmApplicationIo {
let wrapper = || {
if let Ok(canvases) = Reflect::get(&window, &image_canvases_key) {
// Convert key and value to JsValue
let js_key = JsValue::from_str(format!("canvas{}", surface_id.0).as_str());
let js_key = JsValue::from_str(surface_id.0.to_string().as_str());
// Use Reflect API to set property
Reflect::delete_property(&canvases.into(), &js_key)?;
@@ -260,7 +277,7 @@ impl ApplicationIo for WasmApplicationIo {
wrapper().expect("should be able to set canvas in global scope")
}
#[cfg(not(target_arch = "wasm32"))]
#[cfg(not(target_family = "wasm"))]
fn destroy_window(&self, _surface_id: SurfaceId) {}
#[cfg(feature = "wgpu")]
@@ -329,9 +346,9 @@ impl graphene_application_io::GetEditorPreferences for EditorPreferences {
impl Default for EditorPreferences {
fn default() -> Self {
Self {
#[cfg(target_arch = "wasm32")]
#[cfg(target_family = "wasm")]
use_vello: false,
#[cfg(not(target_arch = "wasm32"))]
#[cfg(not(target_family = "wasm"))]
use_vello: true,
}
}

View File

@@ -10,8 +10,6 @@ license = "MIT OR Apache-2.0"
default = ["wgpu"]
wgpu = ["wgpu-executor", "gpu", "graphene-std/wgpu"]
wayland = ["graphene-std/wayland"]
profiling = ["wgpu-executor/profiling"]
passthrough = ["wgpu-executor/passthrough"]
gpu = ["interpreted-executor/gpu", "graphene-std/gpu", "wgpu-executor"]
[dependencies]
@@ -29,9 +27,7 @@ fern = { workspace = true }
chrono = { workspace = true }
wgpu = { workspace = true }
tokio = { workspace = true, features = ["rt-multi-thread"] }
# Required dependencies
clap = { version = "4.5.31", features = ["cargo", "derive"] }
clap = { workspace = true, features = ["cargo", "derive"] }
# Optional local dependencies
wgpu-executor = { path = "../wgpu-executor", optional = true }

View File

@@ -88,10 +88,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
}
let device = application_io.gpu_executor().unwrap().context.device.clone();
let preferences = EditorPreferences {
use_vello: true,
..Default::default()
};
let preferences = EditorPreferences { use_vello: true };
let editor_api = Arc::new(WasmEditorApi {
font_cache: FontCache::default(),
application_io: Some(application_io.into()),
@@ -104,7 +101,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
match app.command {
Command::Compile { print_proto, .. } => {
if print_proto {
println!("{}", proto_graph);
println!("{proto_graph}");
}
}
Command::Run { run_loop, .. } => {
@@ -120,7 +117,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
loop {
let result = (&executor).execute(render_config).await?;
if !run_loop {
println!("{:?}", result);
println!("{result:?}");
break;
}
tokio::time::sleep(std::time::Duration::from_millis(16)).await;

View File

@@ -7,28 +7,45 @@ authors = ["Graphite Authors <contact@graphite.rs>"]
license = "MIT OR Apache-2.0"
[features]
default = ["serde"]
serde = ["dep:serde"]
default = ["std"]
std = [
"dep:graphene-core",
"dep:dyn-any",
"dep:image",
"dep:ndarray",
"dep:rand",
"dep:rand_chacha",
"dep:fastnoise-lite",
"dep:serde",
"dep:specta",
"dep:kurbo",
"glam/debug-glam-assert",
"glam/serde",
]
[dependencies]
# Local dependencies
dyn-any = { workspace = true }
graphene-core = { workspace = true }
graphene-core-shaders = { workspace = true }
node-macro = { workspace = true }
# Workspace dependencies
glam = { workspace = true }
specta = { workspace = true }
image = { workspace = true }
bytemuck = { workspace = true }
ndarray = { workspace = true }
bezier-rs = { workspace = true }
rand = { workspace = true }
rand_chacha = { workspace = true }
fastnoise-lite = { workspace = true }
# Local std dependencies
dyn-any = { workspace = true, optional = true }
graphene-core = { workspace = true, optional = true }
# Optional workspace dependencies
serde = { workspace = true, optional = true, features = ["derive"] }
# Workspace dependencies
bytemuck = { workspace = true }
glam = { workspace = true }
num-traits = { workspace = true }
# Workspace std dependencies
specta = { workspace = true, optional = true }
image = { workspace = true, optional = true }
ndarray = { workspace = true, optional = true }
rand = { workspace = true, optional = true }
rand_chacha = { workspace = true, optional = true }
fastnoise-lite = { workspace = true, optional = true }
serde = { workspace = true, optional = true }
kurbo = { workspace = true, optional = true }
[dev-dependencies]
tokio = { workspace = true }

View File

@@ -0,0 +1,49 @@
use graphene_core_shaders::color::Color;
pub trait Adjust<P> {
fn adjust(&mut self, map_fn: impl Fn(&P) -> P);
}
impl Adjust<Color> for Color {
fn adjust(&mut self, map_fn: impl Fn(&Color) -> Color) {
*self = map_fn(self);
}
}
#[cfg(feature = "std")]
mod adjust_std {
use super::*;
use graphene_core::gradient::GradientStops;
use graphene_core::raster_types::{CPU, Raster};
use graphene_core::table::Table;
impl Adjust<Color> for Table<Raster<CPU>> {
fn adjust(&mut self, map_fn: impl Fn(&Color) -> Color) {
for row in self.iter_mut() {
for color in row.element.data_mut().data.iter_mut() {
*color = map_fn(color);
}
}
}
}
impl Adjust<Color> for Table<Color> {
fn adjust(&mut self, map_fn: impl Fn(&Color) -> Color) {
for row in self.iter_mut() {
*row.element = map_fn(row.element);
}
}
}
impl Adjust<Color> for Table<GradientStops> {
fn adjust(&mut self, map_fn: impl Fn(&Color) -> Color) {
for row in self.iter_mut() {
row.element.adjust(&map_fn);
}
}
}
impl Adjust<Color> for GradientStops {
fn adjust(&mut self, map_fn: impl Fn(&Color) -> Color) {
for (_, color) in self.iter_mut() {
*color = map_fn(color);
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,212 @@
use crate::adjust::Adjust;
#[cfg(feature = "std")]
use graphene_core::gradient::GradientStops;
#[cfg(feature = "std")]
use graphene_core::raster_types::{CPU, Raster};
#[cfg(feature = "std")]
use graphene_core::table::Table;
use graphene_core_shaders::Ctx;
use graphene_core_shaders::blending::BlendMode;
use graphene_core_shaders::color::{Color, Pixel};
use graphene_core_shaders::registry::types::PercentageF32;
pub trait Blend<P: Pixel> {
fn blend(&self, under: &Self, blend_fn: impl Fn(P, P) -> P) -> Self;
}
impl Blend<Color> for Color {
fn blend(&self, under: &Self, blend_fn: impl Fn(Color, Color) -> Color) -> Self {
blend_fn(*self, *under)
}
}
#[cfg(feature = "std")]
mod blend_std {
use super::*;
use core::cmp::Ordering;
use graphene_core::raster::Image;
use graphene_core::raster_types::Raster;
use graphene_core::table::Table;
impl Blend<Color> for Table<Raster<CPU>> {
fn blend(&self, under: &Self, blend_fn: impl Fn(Color, Color) -> Color) -> Self {
let mut result_table = self.clone();
for (over, under) in result_table.iter_mut().zip(under.iter()) {
let data = over.element.data.iter().zip(under.element.data.iter()).map(|(a, b)| blend_fn(*a, *b)).collect();
*over.element = Raster::new_cpu(Image {
data,
width: over.element.width,
height: over.element.height,
base64_string: None,
});
}
result_table
}
}
impl Blend<Color> for Table<Color> {
fn blend(&self, under: &Self, blend_fn: impl Fn(Color, Color) -> Color) -> Self {
let mut result_table = self.clone();
for (over, under) in result_table.iter_mut().zip(under.iter()) {
*over.element = blend_fn(*over.element, *under.element);
}
result_table
}
}
impl Blend<Color> for Table<GradientStops> {
fn blend(&self, under: &Self, blend_fn: impl Fn(Color, Color) -> Color) -> Self {
let mut result_table = self.clone();
for (over, under) in result_table.iter_mut().zip(under.iter()) {
*over.element = over.element.blend(under.element, &blend_fn);
}
result_table
}
}
impl Blend<Color> for GradientStops {
fn blend(&self, under: &Self, blend_fn: impl Fn(Color, Color) -> Color) -> Self {
let mut combined_stops = self.iter().map(|(position, _)| position).chain(under.iter().map(|(position, _)| position)).collect::<Vec<_>>();
combined_stops.dedup_by(|&mut a, &mut b| (a - b).abs() < 1e-6);
combined_stops.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal));
let stops = combined_stops
.into_iter()
.map(|&position| {
let over_color = self.evaluate(position);
let under_color = under.evaluate(position);
let color = blend_fn(over_color, under_color);
(position, color)
})
.collect::<Vec<_>>();
GradientStops::new(stops)
}
}
}
#[inline(always)]
pub fn blend_colors(foreground: Color, background: Color, blend_mode: BlendMode, opacity: f32) -> Color {
let target_color = match blend_mode {
// Other utility blend modes (hidden from the normal list) - do not have alpha blend
BlendMode::Erase => return background.alpha_subtract(foreground),
BlendMode::Restore => return background.alpha_add(foreground),
BlendMode::MultiplyAlpha => return background.alpha_multiply(foreground),
blend_mode => apply_blend_mode(foreground, background, blend_mode),
};
background.alpha_blend(target_color.to_associated_alpha(opacity as f32))
}
pub fn apply_blend_mode(foreground: Color, background: Color, blend_mode: BlendMode) -> Color {
match blend_mode {
// Normal group
BlendMode::Normal => background.blend_rgb(foreground, Color::blend_normal),
// Darken group
BlendMode::Darken => background.blend_rgb(foreground, Color::blend_darken),
BlendMode::Multiply => background.blend_rgb(foreground, Color::blend_multiply),
BlendMode::ColorBurn => background.blend_rgb(foreground, Color::blend_color_burn),
BlendMode::LinearBurn => background.blend_rgb(foreground, Color::blend_linear_burn),
BlendMode::DarkerColor => background.blend_darker_color(foreground),
// Lighten group
BlendMode::Lighten => background.blend_rgb(foreground, Color::blend_lighten),
BlendMode::Screen => background.blend_rgb(foreground, Color::blend_screen),
BlendMode::ColorDodge => background.blend_rgb(foreground, Color::blend_color_dodge),
BlendMode::LinearDodge => background.blend_rgb(foreground, Color::blend_linear_dodge),
BlendMode::LighterColor => background.blend_lighter_color(foreground),
// Contrast group
BlendMode::Overlay => foreground.blend_rgb(background, Color::blend_hardlight),
BlendMode::SoftLight => background.blend_rgb(foreground, Color::blend_softlight),
BlendMode::HardLight => background.blend_rgb(foreground, Color::blend_hardlight),
BlendMode::VividLight => background.blend_rgb(foreground, Color::blend_vivid_light),
BlendMode::LinearLight => background.blend_rgb(foreground, Color::blend_linear_light),
BlendMode::PinLight => background.blend_rgb(foreground, Color::blend_pin_light),
BlendMode::HardMix => background.blend_rgb(foreground, Color::blend_hard_mix),
// Inversion group
BlendMode::Difference => background.blend_rgb(foreground, Color::blend_difference),
BlendMode::Exclusion => background.blend_rgb(foreground, Color::blend_exclusion),
BlendMode::Subtract => background.blend_rgb(foreground, Color::blend_subtract),
BlendMode::Divide => background.blend_rgb(foreground, Color::blend_divide),
// Component group
BlendMode::Hue => background.blend_hue(foreground),
BlendMode::Saturation => background.blend_saturation(foreground),
BlendMode::Color => background.blend_color(foreground),
BlendMode::Luminosity => background.blend_luminosity(foreground),
// Other utility blend modes (hidden from the normal list) - do not have alpha blend
_ => panic!("Used blend mode without alpha blend"),
}
}
#[node_macro::node(category("Raster"), shader_node(PerPixelAdjust))]
fn blend<T: Blend<Color> + Send>(
_: impl Ctx,
#[implementations(
Table<Raster<CPU>>,
Table<Color>,
Table<GradientStops>,
GradientStops,
)]
over: T,
#[expose]
#[implementations(
Table<Raster<CPU>>,
Table<Color>,
Table<GradientStops>,
GradientStops,
)]
under: T,
blend_mode: BlendMode,
#[default(100.)] opacity: PercentageF32,
) -> T {
over.blend(&under, |a, b| blend_colors(a, b, blend_mode, opacity / 100.))
}
#[node_macro::node(category("Raster: Adjustment"), shader_node(PerPixelAdjust))]
fn color_overlay<T: Adjust<Color>>(
_: impl Ctx,
#[implementations(
Table<Raster<CPU>>,
Table<Color>,
Table<GradientStops>,
GradientStops,
)]
mut image: T,
#[default(Color::BLACK)] color: Color,
blend_mode: BlendMode,
#[default(100.)] opacity: PercentageF32,
) -> T {
let opacity = (opacity as f32 / 100.).clamp(0., 1.);
image.adjust(|pixel| {
let image = pixel.map_rgb(|channel| channel * (1. - opacity));
// The apply blend mode function divides rgb by the alpha channel for the background. This undoes that.
let associated_pixel = Color::from_rgbaf32_unchecked(pixel.r() * pixel.a(), pixel.g() * pixel.a(), pixel.b() * pixel.a(), pixel.a());
let overlay = apply_blend_mode(color, associated_pixel, blend_mode).map_rgb(|channel| channel * opacity);
Color::from_rgbaf32_unchecked(image.r() + overlay.r(), image.g() + overlay.g(), image.b() + overlay.b(), pixel.a())
});
image
}
#[cfg(all(feature = "std", test))]
mod test {
use graphene_core::blending::BlendMode;
use graphene_core::color::Color;
use graphene_core::raster::image::Image;
use graphene_core::raster_types::Raster;
use graphene_core::table::Table;
#[tokio::test]
async fn color_overlay_multiply() {
let image_color = Color::from_rgbaf32_unchecked(0.7, 0.6, 0.5, 0.4);
let image = Image::new(1, 1, image_color);
// Color { red: 0., green: 1., blue: 0., alpha: 1. }
let overlay_color = Color::GREEN;
// 100% of the output should come from the multiplied value
let opacity = 100.;
let result = super::color_overlay((), Table::new_from_element(Raster::new_cpu(image.clone())), overlay_color, BlendMode::Multiply, opacity);
let result = result.iter().next().unwrap().element;
// The output should just be the original green and alpha channels (as we multiply them by 1 and other channels by 0)
assert_eq!(result.data[0], Color::from_rgbaf32_unchecked(0., image_color.g(), 0., image_color.a()));
}
}

View File

@@ -0,0 +1,123 @@
#[derive(Debug)]
pub struct CubicSplines {
pub x: [f32; 4],
pub y: [f32; 4],
}
impl CubicSplines {
pub fn solve(&self) -> [f32; 4] {
let (x, y) = (&self.x, &self.y);
// Build an augmented matrix to solve the system of equations using Gaussian elimination
let mut augmented_matrix = [
[
2. / (x[1] - x[0]),
1. / (x[1] - x[0]),
0.,
0.,
// |
3. * (y[1] - y[0]) / ((x[1] - x[0]) * (x[1] - x[0])),
],
[
1. / (x[1] - x[0]),
2. * (1. / (x[1] - x[0]) + 1. / (x[2] - x[1])),
1. / (x[2] - x[1]),
0.,
// |
3. * ((y[1] - y[0]) / ((x[1] - x[0]) * (x[1] - x[0])) + (y[2] - y[1]) / ((x[2] - x[1]) * (x[2] - x[1]))),
],
[
0.,
1. / (x[2] - x[1]),
2. * (1. / (x[2] - x[1]) + 1. / (x[3] - x[2])),
1. / (x[3] - x[2]),
// |
3. * ((y[2] - y[1]) / ((x[2] - x[1]) * (x[2] - x[1])) + (y[3] - y[2]) / ((x[3] - x[2]) * (x[3] - x[2]))),
],
[
0.,
0.,
1. / (x[3] - x[2]),
2. / (x[3] - x[2]),
// |
3. * (y[3] - y[2]) / ((x[3] - x[2]) * (x[3] - x[2])),
],
];
// Gaussian elimination: forward elimination
for row in 0..4 {
let pivot_row_index = (row..4)
.max_by(|&a_row, &b_row| {
augmented_matrix[a_row][row]
.abs()
.partial_cmp(&augmented_matrix[b_row][row].abs())
.unwrap_or(core::cmp::Ordering::Equal)
})
.unwrap();
// Swap the current row with the row that has the largest pivot element
augmented_matrix.swap(row, pivot_row_index);
// Eliminate the current column in all rows below the current one
for row_below_current in row + 1..4 {
assert!(augmented_matrix[row][row].abs() > f32::EPSILON);
let scale_factor = augmented_matrix[row_below_current][row] / augmented_matrix[row][row];
for col in row..5 {
augmented_matrix[row_below_current][col] -= augmented_matrix[row][col] * scale_factor
}
}
}
// Gaussian elimination: back substitution
let mut solutions = [0.; 4];
for col in (0..4).rev() {
assert!(augmented_matrix[col][col].abs() > f32::EPSILON);
solutions[col] = augmented_matrix[col][4] / augmented_matrix[col][col];
for row in (0..col).rev() {
augmented_matrix[row][4] -= augmented_matrix[row][col] * solutions[col];
augmented_matrix[row][col] = 0.;
}
}
solutions
}
pub fn interpolate(&self, input: f32, solutions: &[f32]) -> f32 {
if input <= self.x[0] {
return self.y[0];
}
if input >= self.x[self.x.len() - 1] {
return self.y[self.x.len() - 1];
}
// Find the segment that the input falls between
let mut segment = 1;
while self.x[segment] < input {
segment += 1;
}
let segment_start = segment - 1;
let segment_end = segment;
// Calculate the output value using quadratic interpolation
let input_value = self.x[segment_start];
let input_value_prev = self.x[segment_end];
let output_value = self.y[segment_start];
let output_value_prev = self.y[segment_end];
let solutions_value = solutions[segment_start];
let solutions_value_prev = solutions[segment_end];
let output_delta = solutions_value_prev * (input_value - input_value_prev) - (output_value - output_value_prev);
let solution_delta = (output_value - output_value_prev) - solutions_value * (input_value - input_value_prev);
let input_ratio = (input - input_value_prev) / (input_value - input_value_prev);
let prev_output_ratio = (1. - input_ratio) * output_value_prev;
let output_ratio = input_ratio * output_value;
let quadratic_ratio = input_ratio * (1. - input_ratio) * (output_delta * (1. - input_ratio) + solution_delta * input_ratio);
let result = prev_output_ratio + output_ratio + quadratic_ratio;
result.clamp(0., 1.)
}
}

View File

@@ -45,125 +45,6 @@ impl Hash for CurveManipulatorGroup {
}
}
#[derive(Debug)]
pub struct CubicSplines {
pub x: [f32; 4],
pub y: [f32; 4],
}
impl CubicSplines {
pub fn solve(&self) -> [f32; 4] {
let (x, y) = (&self.x, &self.y);
// Build an augmented matrix to solve the system of equations using Gaussian elimination
let mut augmented_matrix = [
[
2. / (x[1] - x[0]),
1. / (x[1] - x[0]),
0.,
0.,
// |
3. * (y[1] - y[0]) / ((x[1] - x[0]) * (x[1] - x[0])),
],
[
1. / (x[1] - x[0]),
2. * (1. / (x[1] - x[0]) + 1. / (x[2] - x[1])),
1. / (x[2] - x[1]),
0.,
// |
3. * ((y[1] - y[0]) / ((x[1] - x[0]) * (x[1] - x[0])) + (y[2] - y[1]) / ((x[2] - x[1]) * (x[2] - x[1]))),
],
[
0.,
1. / (x[2] - x[1]),
2. * (1. / (x[2] - x[1]) + 1. / (x[3] - x[2])),
1. / (x[3] - x[2]),
// |
3. * ((y[2] - y[1]) / ((x[2] - x[1]) * (x[2] - x[1])) + (y[3] - y[2]) / ((x[3] - x[2]) * (x[3] - x[2]))),
],
[
0.,
0.,
1. / (x[3] - x[2]),
2. / (x[3] - x[2]),
// |
3. * (y[3] - y[2]) / ((x[3] - x[2]) * (x[3] - x[2])),
],
];
// Gaussian elimination: forward elimination
for row in 0..4 {
let pivot_row_index = (row..4)
.max_by(|&a_row, &b_row| augmented_matrix[a_row][row].abs().partial_cmp(&augmented_matrix[b_row][row].abs()).unwrap_or(std::cmp::Ordering::Equal))
.unwrap();
// Swap the current row with the row that has the largest pivot element
augmented_matrix.swap(row, pivot_row_index);
// Eliminate the current column in all rows below the current one
for row_below_current in row + 1..4 {
assert!(augmented_matrix[row][row].abs() > f32::EPSILON);
let scale_factor = augmented_matrix[row_below_current][row] / augmented_matrix[row][row];
for col in row..5 {
augmented_matrix[row_below_current][col] -= augmented_matrix[row][col] * scale_factor
}
}
}
// Gaussian elimination: back substitution
let mut solutions = [0.; 4];
for col in (0..4).rev() {
assert!(augmented_matrix[col][col].abs() > f32::EPSILON);
solutions[col] = augmented_matrix[col][4] / augmented_matrix[col][col];
for row in (0..col).rev() {
augmented_matrix[row][4] -= augmented_matrix[row][col] * solutions[col];
augmented_matrix[row][col] = 0.;
}
}
solutions
}
pub fn interpolate(&self, input: f32, solutions: &[f32]) -> f32 {
if input <= self.x[0] {
return self.y[0];
}
if input >= self.x[self.x.len() - 1] {
return self.y[self.x.len() - 1];
}
// Find the segment that the input falls between
let mut segment = 1;
while self.x[segment] < input {
segment += 1;
}
let segment_start = segment - 1;
let segment_end = segment;
// Calculate the output value using quadratic interpolation
let input_value = self.x[segment_start];
let input_value_prev = self.x[segment_end];
let output_value = self.y[segment_start];
let output_value_prev = self.y[segment_end];
let solutions_value = solutions[segment_start];
let solutions_value_prev = solutions[segment_end];
let output_delta = solutions_value_prev * (input_value - input_value_prev) - (output_value - output_value_prev);
let solution_delta = (output_value - output_value_prev) - solutions_value * (input_value - input_value_prev);
let input_ratio = (input - input_value_prev) / (input_value - input_value_prev);
let prev_output_ratio = (1. - input_ratio) * output_value_prev;
let output_ratio = input_ratio * output_value;
let quadratic_ratio = input_ratio * (1. - input_ratio) * (output_delta * (1. - input_ratio) + solution_delta * input_ratio);
let result = prev_output_ratio + output_ratio + quadratic_ratio;
result.clamp(0., 1.)
}
}
pub struct ValueMapperNode<C> {
lut: Vec<C>,
}

View File

@@ -1,17 +1,18 @@
use graphene_core::context::Ctx;
use graphene_core::raster::image::Image;
use graphene_core::raster_types::{CPU, Raster, RasterDataTable};
use graphene_core::raster_types::{CPU, Raster};
use graphene_core::registry::types::Percentage;
use graphene_core::table::Table;
use image::{DynamicImage, GenericImage, GenericImageView, GrayImage, ImageBuffer, Luma, Rgba, RgbaImage};
use ndarray::{Array2, ArrayBase, Dim, OwnedRepr};
use std::cmp::{max, min};
#[node_macro::node(category("Raster: Filter"))]
async fn dehaze(_: impl Ctx, image_frame: RasterDataTable<CPU>, strength: Percentage) -> RasterDataTable<CPU> {
async fn dehaze(_: impl Ctx, image_frame: Table<Raster<CPU>>, strength: Percentage) -> Table<Raster<CPU>> {
image_frame
.instance_iter()
.map(|mut image_frame_instance| {
let image = image_frame_instance.instance;
.into_iter()
.map(|mut row| {
let image = row.element;
// Prepare the image data for processing
let image_data = bytemuck::cast_vec(image.data.clone());
let image_buffer = image::Rgba32FImage::from_raw(image.width, image.height, image_data).expect("Failed to convert internal image format into image-rs data type.");
@@ -30,9 +31,8 @@ async fn dehaze(_: impl Ctx, image_frame: RasterDataTable<CPU>, strength: Percen
base64_string: None,
};
image_frame_instance.instance = Raster::new_cpu(dehazed_image);
image_frame_instance.source_node_id = None;
image_frame_instance
row.element = Raster::new_cpu(dehazed_image);
row
})
.collect()
}

View File

@@ -2,15 +2,16 @@ use graphene_core::color::Color;
use graphene_core::context::Ctx;
use graphene_core::raster::image::Image;
use graphene_core::raster::{Bitmap, BitmapMut};
use graphene_core::raster_types::{CPU, Raster, RasterDataTable};
use graphene_core::raster_types::{CPU, Raster};
use graphene_core::registry::types::PixelLength;
use graphene_core::table::Table;
/// Blurs the image with a Gaussian or blur kernel filter.
#[node_macro::node(category("Raster: Filter"))]
async fn blur(
_: impl Ctx,
/// The image to be blurred.
image_frame: RasterDataTable<CPU>,
image_frame: Table<Raster<CPU>>,
/// The radius of the blur kernel.
#[range((0., 100.))]
#[hard_min(0.)]
@@ -19,11 +20,11 @@ async fn blur(
box_blur: bool,
/// Opt to incorrectly apply the filter with color calculations in gamma space for compatibility with the results from other software.
gamma: bool,
) -> RasterDataTable<CPU> {
) -> Table<Raster<CPU>> {
image_frame
.instance_iter()
.map(|mut image_instance| {
let image = image_instance.instance.clone();
.into_iter()
.map(|mut row| {
let image = row.element.clone();
// Run blur algorithm
let blurred_image = if radius < 0.1 {
@@ -35,9 +36,8 @@ async fn blur(
Raster::new_cpu(gaussian_blur_algorithm(image.into_data(), radius, gamma))
};
image_instance.instance = blurred_image;
image_instance.source_node_id = None;
image_instance
row.element = blurred_image;
row
})
.collect()
}

View File

@@ -1,9 +1,8 @@
//! requires bezier-rs
use crate::curve::{Curve, CurveManipulatorGroup, ValueMapperNode};
use bezier_rs::{Bezier, TValue};
use graphene_core::color::{Channel, Linear};
use graphene_core::context::Ctx;
use graphene_core::vector::algorithms::bezpath_algorithms::pathseg_find_tvalues_for_x;
use kurbo::{CubicBez, ParamCurve, PathSeg, Point};
const WINDOW_SIZE: usize = 1024;
@@ -18,7 +17,7 @@ fn generate_curves<C: Channel + Linear>(_: impl Ctx, curve: Curve, #[implementat
for sample in curve.manipulator_groups.iter().chain(std::iter::once(&end)) {
let [x0, y0, x1, y1, x2, y2, x3, y3] = [pos[0], pos[1], param[0], param[1], sample.handles[0][0], sample.handles[0][1], sample.anchor[0], sample.anchor[1]].map(f64::from);
let bezier = Bezier::from_cubic_coordinates(x0, y0, x1, y1, x2, y2, x3, y3);
let segment = PathSeg::Cubic(CubicBez::new(Point::new(x0, y0), Point::new(x1, y1), Point::new(x2, y2), Point::new(x3, y3)));
let [left, right] = [pos[0], sample.anchor[0]].map(|c| c.clamp(0., 1.));
let lut_index_left: usize = (left * (lut.len() - 1) as f32).floor() as _;
@@ -30,10 +29,10 @@ fn generate_curves<C: Channel + Linear>(_: impl Ctx, curve: Curve, #[implementat
} else if x >= x3 {
y3
} else {
bezier.find_tvalues_for_x(x)
pathseg_find_tvalues_for_x(segment, x)
.next()
.map(|t| bezier.evaluate(TValue::Parametric(t.clamp(0., 1.))).y)
// Fall back to a very bad approximation if Bezier-rs fails
.map(|t| segment.eval(t.clamp(0., 1.)).y)
// Fall back to a very bad approximation if the above fails
.unwrap_or_else(|| (x - x0) / (x3 - x0) * (y3 - y0) + y0)
};
lut[index] = C::from_f64(y);

View File

@@ -0,0 +1,32 @@
//! Not immediately shader compatible due to needing [`GradientStops`] as a param, which needs [`Vec`]
use crate::adjust::Adjust;
use graphene_core::gradient::GradientStops;
use graphene_core::raster_types::{CPU, Raster};
use graphene_core::table::Table;
use graphene_core::{Color, Ctx};
// Aims for interoperable compatibility with:
// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=%27grdm%27%20%3D%20Gradient%20Map
// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=Gradient%20settings%20(Photoshop%206.0)
#[node_macro::node(category("Raster: Adjustment"))]
async fn gradient_map<T: Adjust<Color>>(
_: impl Ctx,
#[implementations(
Table<Raster<CPU>>,
Table<Color>,
Table<GradientStops>,
GradientStops,
)]
mut image: T,
gradient: GradientStops,
reverse: bool,
) -> T {
image.adjust(|color| {
let intensity = color.luminance_srgb();
let intensity = if reverse { 1. - intensity } else { intensity };
gradient.evaluate(intensity as f64).to_linear_srgb()
});
image
}

View File

@@ -1,24 +1,25 @@
use graphene_core::color::Color;
use graphene_core::context::Ctx;
use graphene_core::raster_types::{CPU, RasterDataTable};
use graphene_core::raster_types::{CPU, Raster};
use graphene_core::table::{Table, TableRow};
#[node_macro::node(category("Color"))]
async fn image_color_palette(
_: impl Ctx,
image: RasterDataTable<CPU>,
image: Table<Raster<CPU>>,
#[hard_min(1.)]
#[soft_max(28.)]
max_size: u32,
) -> Vec<Color> {
) -> Table<Color> {
const GRID: f32 = 3.;
let bins = GRID * GRID * GRID;
let mut histogram: Vec<usize> = vec![0; (bins + 1.) as usize];
let mut colors: Vec<Vec<Color>> = vec![vec![]; (bins + 1.) as usize];
let mut histogram = vec![0; (bins + 1.) as usize];
let mut color_bins = vec![Vec::new(); (bins + 1.) as usize];
for image_instance in image.instance_ref_iter() {
for pixel in image_instance.instance.data.iter() {
for row in image.iter() {
for pixel in row.element.data.iter() {
let r = pixel.r() * GRID;
let g = pixel.g() * GRID;
let b = pixel.b() * GRID;
@@ -26,53 +27,51 @@ async fn image_color_palette(
let bin = (r * GRID + g * GRID + b * GRID) as usize;
histogram[bin] += 1;
colors[bin].push(pixel.to_gamma_srgb());
color_bins[bin].push(pixel.to_gamma_srgb());
}
}
let shorted = histogram.iter().enumerate().filter(|&(_, &count)| count > 0).map(|(i, _)| i).collect::<Vec<usize>>();
let mut palette = vec![];
shorted
.iter()
.take(max_size as usize)
.flat_map(|&i| {
let list = &color_bins[i];
for i in shorted.iter().take(max_size as usize) {
let list = colors[*i].clone();
let mut r = 0.;
let mut g = 0.;
let mut b = 0.;
let mut a = 0.;
let mut r = 0.;
let mut g = 0.;
let mut b = 0.;
let mut a = 0.;
for color in list.iter() {
r += color.r();
g += color.g();
b += color.b();
a += color.a();
}
for color in list.iter() {
r += color.r();
g += color.g();
b += color.b();
a += color.a();
}
r /= list.len() as f32;
g /= list.len() as f32;
b /= list.len() as f32;
a /= list.len() as f32;
r /= list.len() as f32;
g /= list.len() as f32;
b /= list.len() as f32;
a /= list.len() as f32;
let color = Color::from_rgbaf32(r, g, b, a).unwrap();
palette.push(color);
}
palette
Color::from_rgbaf32(r, g, b, a).map(TableRow::new_from_element).into_iter()
})
.collect()
}
#[cfg(test)]
mod test {
use super::*;
use graphene_core::raster::image::Image;
use graphene_core::raster_types::{Raster, RasterDataTable};
use graphene_core::raster_types::Raster;
#[test]
fn test_image_color_palette() {
let result = image_color_palette(
(),
RasterDataTable::new(Raster::new_cpu(Image {
Table::new_from_element(Raster::new_cpu(Image {
width: 100,
height: 100,
data: vec![Color::from_rgbaf32(0., 0., 0., 1.).unwrap(); 10000],
@@ -80,6 +79,6 @@ mod test {
})),
1,
);
assert_eq!(futures::executor::block_on(result), [Color::from_rgbaf32(0., 0., 0., 1.).unwrap()]);
assert_eq!(futures::executor::block_on(result), Table::new_from_element(Color::from_rgbaf32(0., 0., 0., 1.).unwrap()));
}
}

View File

@@ -1,7 +1,21 @@
#![cfg_attr(not(feature = "std"), no_std)]
pub mod adjust;
pub mod adjustments;
pub mod blending_nodes;
pub mod cubic_spline;
#[cfg(feature = "std")]
pub mod curve;
#[cfg(feature = "std")]
pub mod dehaze;
#[cfg(feature = "std")]
pub mod filter;
#[cfg(feature = "std")]
pub mod generate_curves;
#[cfg(feature = "std")]
pub mod gradient_map;
#[cfg(feature = "std")]
pub mod image_color_palette;
#[cfg(feature = "std")]
pub mod std_nodes;

View File

@@ -4,17 +4,16 @@ use fastnoise_lite;
use glam::{DAffine2, DVec2, Vec2};
use graphene_core::blending::AlphaBlending;
use graphene_core::color::Color;
use graphene_core::color::{Alpha, AlphaMut, Channel, LinearChannel, Luminance, RGBMut};
use graphene_core::color::{AlphaMut, Channel, LinearChannel, Luminance, RGBMut};
use graphene_core::context::{Ctx, ExtractFootprint};
use graphene_core::instances::Instance;
use graphene_core::instances::Instances;
use graphene_core::math::bbox::Bbox;
use graphene_core::raster::image::Image;
use graphene_core::raster::{Bitmap, BitmapMut};
use graphene_core::raster_types::{CPU, Raster, RasterDataTable};
use graphene_core::raster_types::{CPU, Raster};
use graphene_core::table::{Table, TableRow};
use graphene_core::transform::Transform;
use graphene_core::vector::VectorDataTable;
use graphene_core::{GraphicElement, GraphicGroupTable};
use graphene_core::vector::Vector;
use graphene_core::Graphic;
use rand::prelude::*;
use rand_chacha::ChaCha8Rng;
use std::fmt::Debug;
@@ -33,12 +32,12 @@ impl From<std::io::Error> for Error {
}
#[node_macro::node(category("Debug: Raster"))]
pub fn sample_image(ctx: impl ExtractFootprint + Clone + Send, image_frame: RasterDataTable<CPU>) -> RasterDataTable<CPU> {
pub fn sample_image(ctx: impl ExtractFootprint + Clone + Send, image_frame: Table<Raster<CPU>>) -> Table<Raster<CPU>> {
image_frame
.instance_iter()
.filter_map(|mut image_frame_instance| {
let image_frame_transform = image_frame_instance.transform;
let image = image_frame_instance.instance;
.into_iter()
.filter_map(|mut row| {
let image_frame_transform = row.transform;
let image = row.element;
// Resize the image using the image crate
let data = bytemuck::cast_vec(image.data.clone());
@@ -90,10 +89,9 @@ pub fn sample_image(ctx: impl ExtractFootprint + Clone + Send, image_frame: Rast
let new_transform = image_frame_transform * DAffine2::from_translation(offset) * DAffine2::from_scale(size);
image_frame_instance.transform = new_transform;
image_frame_instance.source_node_id = None;
image_frame_instance.instance = Raster::new_cpu(image);
Some(image_frame_instance)
row.transform = new_transform;
row.element = Raster::new_cpu(image);
Some(row)
})
.collect()
}
@@ -102,38 +100,39 @@ pub fn sample_image(ctx: impl ExtractFootprint + Clone + Send, image_frame: Rast
pub fn combine_channels(
_: impl Ctx,
_primary: (),
#[expose] red: RasterDataTable<CPU>,
#[expose] green: RasterDataTable<CPU>,
#[expose] blue: RasterDataTable<CPU>,
#[expose] alpha: RasterDataTable<CPU>,
) -> RasterDataTable<CPU> {
#[expose] red: Table<Raster<CPU>>,
#[expose] green: Table<Raster<CPU>>,
#[expose] blue: Table<Raster<CPU>>,
#[expose] alpha: Table<Raster<CPU>>,
) -> Table<Raster<CPU>> {
let max_len = red.len().max(green.len()).max(blue.len()).max(alpha.len());
let red = red.instance_iter().map(Some).chain(std::iter::repeat(None)).take(max_len);
let green = green.instance_iter().map(Some).chain(std::iter::repeat(None)).take(max_len);
let blue = blue.instance_iter().map(Some).chain(std::iter::repeat(None)).take(max_len);
let alpha = alpha.instance_iter().map(Some).chain(std::iter::repeat(None)).take(max_len);
let red = red.into_iter().map(Some).chain(std::iter::repeat(None)).take(max_len);
let green = green.into_iter().map(Some).chain(std::iter::repeat(None)).take(max_len);
let blue = blue.into_iter().map(Some).chain(std::iter::repeat(None)).take(max_len);
let alpha = alpha.into_iter().map(Some).chain(std::iter::repeat(None)).take(max_len);
red.zip(green)
.zip(blue)
.zip(alpha)
.filter_map(|(((red, green), blue), alpha)| {
// Turn any default zero-sized image instances into None
let red = red.filter(|i| i.instance.width > 0 && i.instance.height > 0);
let green = green.filter(|i| i.instance.width > 0 && i.instance.height > 0);
let blue = blue.filter(|i| i.instance.width > 0 && i.instance.height > 0);
let alpha = alpha.filter(|i| i.instance.width > 0 && i.instance.height > 0);
// Turn any default zero-sized image rows into None
let red = red.filter(|i| i.element.width > 0 && i.element.height > 0);
let green = green.filter(|i| i.element.width > 0 && i.element.height > 0);
let blue = blue.filter(|i| i.element.width > 0 && i.element.height > 0);
let alpha = alpha.filter(|i| i.element.width > 0 && i.element.height > 0);
// Get this instance's transform and alpha blending mode from the first non-empty channel
let Some((transform, alpha_blending)) = [&red, &green, &blue, &alpha].iter().find_map(|i| i.as_ref()).map(|i| (i.transform, i.alpha_blending)) else {
return None;
};
// Get this row's transform and alpha blending mode from the first non-empty channel
let (transform, alpha_blending, source_node_id) = [&red, &green, &blue, &alpha]
.iter()
.find_map(|i| i.as_ref())
.map(|i| (i.transform, i.alpha_blending, i.source_node_id))?;
// Get the common width and height of the channels, which must have equal dimensions
let channel_dimensions = [
red.as_ref().map(|r| (r.instance.width, r.instance.height)),
green.as_ref().map(|g| (g.instance.width, g.instance.height)),
blue.as_ref().map(|b| (b.instance.width, b.instance.height)),
alpha.as_ref().map(|a| (a.instance.width, a.instance.height)),
red.as_ref().map(|r| (r.element.width, r.element.height)),
green.as_ref().map(|g| (g.element.width, g.element.height)),
blue.as_ref().map(|b| (b.element.width, b.element.height)),
alpha.as_ref().map(|a| (a.element.width, a.element.height)),
];
if channel_dimensions.iter().all(Option::is_none)
|| channel_dimensions
@@ -143,11 +142,9 @@ pub fn combine_channels(
{
return None;
}
let Some(&(width, height)) = channel_dimensions.iter().flatten().next() else {
return None;
};
let &(width, height) = channel_dimensions.iter().flatten().next()?;
// Create a new image for this instance output
// Create a new image for the output element
let mut image = Image::new(width, height, Color::TRANSPARENT);
// Iterate over all pixels in the image and set the color channels
@@ -155,22 +152,22 @@ pub fn combine_channels(
for x in 0..image.width() {
let image_pixel = image.get_pixel_mut(x, y).unwrap();
if let Some(r) = red.as_ref().and_then(|r| r.instance.get_pixel(x, y)) {
if let Some(r) = red.as_ref().and_then(|r| r.element.get_pixel(x, y)) {
image_pixel.set_red(r.l().cast_linear_channel());
} else {
image_pixel.set_red(Channel::from_linear(0.));
}
if let Some(g) = green.as_ref().and_then(|g| g.instance.get_pixel(x, y)) {
if let Some(g) = green.as_ref().and_then(|g| g.element.get_pixel(x, y)) {
image_pixel.set_green(g.l().cast_linear_channel());
} else {
image_pixel.set_green(Channel::from_linear(0.));
}
if let Some(b) = blue.as_ref().and_then(|b| b.instance.get_pixel(x, y)) {
if let Some(b) = blue.as_ref().and_then(|b| b.element.get_pixel(x, y)) {
image_pixel.set_blue(b.l().cast_linear_channel());
} else {
image_pixel.set_blue(Channel::from_linear(0.));
}
if let Some(a) = alpha.as_ref().and_then(|a| a.instance.get_pixel(x, y)) {
if let Some(a) = alpha.as_ref().and_then(|a| a.element.get_pixel(x, y)) {
image_pixel.set_alpha(a.l().cast_linear_channel());
} else {
image_pixel.set_alpha(Channel::from_linear(1.));
@@ -178,12 +175,12 @@ pub fn combine_channels(
}
}
Some(Instance {
instance: Raster::new_cpu(image),
Some(TableRow {
element: Raster::new_cpu(image),
mask: None,
transform,
alpha_blending,
source_node_id: None,
source_node_id,
})
})
.collect()
@@ -194,102 +191,62 @@ pub fn mask<T, E>(
_: impl Ctx,
/// The image to be masked.
#[implementations(
VectorDataTable,
VectorDataTable,
VectorDataTable,
RasterDataTable<CPU>,
RasterDataTable<CPU>,
RasterDataTable<CPU>,
GraphicGroupTable,
GraphicGroupTable,
GraphicGroupTable
Table<Vector>,
Table<Graphic>,
Table<Raster<CPU>>,
Table<Vector>,
Table<Graphic>,
Table<Raster<CPU>>,
Table<Vector>,
Table<Graphic>,
Table<Raster<CPU>>,
)]
mut image: Instances<T>,
/// The stencil to be used for masking.
mut image: Table<T>,
#[expose]
#[implementations(
VectorDataTable,
RasterDataTable<CPU>,
GraphicGroupTable,
VectorDataTable,
RasterDataTable<CPU>,
GraphicGroupTable,
VectorDataTable,
RasterDataTable<CPU>,
GraphicGroupTable
Table<Vector>,
Table<Vector>,
Table<Vector>,
Table<Graphic>,
Table<Graphic>,
Table<Graphic>,
Table<Raster<CPU>>,
Table<Raster<CPU>>,
Table<Raster<CPU>>,
)]
stencil: Instances<E>,
) -> Instances<T>
stencil: Table<E>,
) -> Table<T>
where
Instances<E>: Into<GraphicElement> + Clone,
Table<E>: Into<Graphic> + Clone,
{
for instance in image.instance_mut_iter() {
for instance in image.iter_mut() {
*instance.mask = Some(stencil.clone().into());
}
image
}
// TODO: Use as in-place raster modifier
fn _mask_lambda(image: RasterDataTable<CPU>, stencil: RasterDataTable<CPU>) -> RasterDataTable<CPU> {
// TODO: Support multiple stencil instances
let Some(stencil_instance) = stencil.instance_iter().next() else {
// No stencil provided so we return the original image
return image;
};
let stencil_size = DVec2::new(stencil_instance.instance.width as f64, stencil_instance.instance.height as f64);
image
.instance_iter()
.filter_map(|mut image_instance| {
let image_size = DVec2::new(image_instance.instance.width as f64, image_instance.instance.height as f64);
let mask_size = stencil_instance.transform.decompose_scale();
if mask_size == DVec2::ZERO {
return None;
}
// Transforms a point from the background image to the foreground image
let bg_to_fg = image_instance.transform * DAffine2::from_scale(1. / image_size);
let stencil_transform_inverse = stencil_instance.transform.inverse();
for y in 0..image_instance.instance.height {
for x in 0..image_instance.instance.width {
let image_point = DVec2::new(x as f64, y as f64);
let mask_point = bg_to_fg.transform_point2(image_point);
let local_mask_point = stencil_transform_inverse.transform_point2(mask_point);
let mask_point = stencil_instance.transform.transform_point2(local_mask_point.clamp(DVec2::ZERO, DVec2::ONE));
let mask_point = (DAffine2::from_scale(stencil_size) * stencil_instance.transform.inverse()).transform_point2(mask_point);
let image_pixel = image_instance.instance.data_mut().get_pixel_mut(x, y).unwrap();
let mask_pixel = stencil_instance.instance.sample(mask_point);
*image_pixel = image_pixel.multiplied_alpha(mask_pixel.l().cast_linear_channel());
}
}
Some(image_instance)
})
.collect()
}
#[node_macro::node(category(""))]
pub fn extend_image_to_bounds(_: impl Ctx, image: RasterDataTable<CPU>, bounds: DAffine2) -> RasterDataTable<CPU> {
pub fn extend_image_to_bounds(_: impl Ctx, image: Table<Raster<CPU>>, bounds: DAffine2) -> Table<Raster<CPU>> {
image
.instance_iter()
.map(|mut image_instance| {
let image_aabb = Bbox::unit().affine_transform(image_instance.transform).to_axis_aligned_bbox();
.into_iter()
.map(|mut row| {
let image_aabb = Bbox::unit().affine_transform(row.transform).to_axis_aligned_bbox();
let bounds_aabb = Bbox::unit().affine_transform(bounds.transform()).to_axis_aligned_bbox();
if image_aabb.contains(bounds_aabb.start) && image_aabb.contains(bounds_aabb.end) {
return image_instance;
return row;
}
let image_data = &image_instance.instance.data;
let (image_width, image_height) = (image_instance.instance.width, image_instance.instance.height);
let image_data = &row.element.data;
let (image_width, image_height) = (row.element.width, row.element.height);
if image_width == 0 || image_height == 0 {
return empty_image((), bounds, Color::TRANSPARENT).instance_iter().next().unwrap();
return empty_image((), bounds, Table::new_from_element(Color::TRANSPARENT)).into_iter().next().unwrap();
}
let orig_image_scale = DVec2::new(image_width as f64, image_height as f64);
let layer_to_image_space = DAffine2::from_scale(orig_image_scale) * image_instance.transform.inverse();
let layer_to_image_space = DAffine2::from_scale(orig_image_scale) * row.transform.inverse();
let bounds_in_image_space = Bbox::unit().affine_transform(layer_to_image_space * bounds).to_axis_aligned_bbox();
let new_start = bounds_in_image_space.start.floor().min(DVec2::ZERO);
@@ -309,27 +266,27 @@ pub fn extend_image_to_bounds(_: impl Ctx, image: RasterDataTable<CPU>, bounds:
// Compute new transform.
// let layer_to_new_texture_space = (DAffine2::from_scale(1. / new_scale) * DAffine2::from_translation(new_start) * layer_to_image_space).inverse();
let new_texture_to_layer_space = image_instance.transform * DAffine2::from_scale(1. / orig_image_scale) * DAffine2::from_translation(new_start) * DAffine2::from_scale(new_scale);
let new_texture_to_layer_space = row.transform * DAffine2::from_scale(1. / orig_image_scale) * DAffine2::from_translation(new_start) * DAffine2::from_scale(new_scale);
image_instance.instance = Raster::new_cpu(new_image);
image_instance.transform = new_texture_to_layer_space;
image_instance.source_node_id = None;
image_instance
row.element = Raster::new_cpu(new_image);
row.transform = new_texture_to_layer_space;
row
})
.collect()
}
#[node_macro::node(category("Debug: Raster"))]
pub fn empty_image(_: impl Ctx, transform: DAffine2, color: Color) -> RasterDataTable<CPU> {
pub fn empty_image(_: impl Ctx, transform: DAffine2, color: Table<Color>) -> Table<Raster<CPU>> {
let width = transform.transform_vector2(DVec2::new(1., 0.)).length() as u32;
let height = transform.transform_vector2(DVec2::new(0., 1.)).length() as u32;
let image = Image::new(width, height, color);
let color: Option<Color> = color.into();
let image = Image::new(width, height, color.unwrap_or(Color::WHITE));
let mut result_table = RasterDataTable::new(Raster::new_cpu(image));
let image_instance = result_table.get_mut(0).unwrap();
*image_instance.transform = transform;
*image_instance.alpha_blending = AlphaBlending::default();
let mut result_table = Table::new_from_element(Raster::new_cpu(image));
let row = result_table.get_mut(0).unwrap();
*row.transform = transform;
*row.alpha_blending = AlphaBlending::default();
// Callers of empty_image can safely unwrap on returned table
result_table
@@ -337,7 +294,7 @@ pub fn empty_image(_: impl Ctx, transform: DAffine2, color: Color) -> RasterData
/// Constructs a raster image.
#[node_macro::node(category(""))]
pub fn image_value(_: impl Ctx, _primary: (), image: RasterDataTable<CPU>) -> RasterDataTable<CPU> {
pub fn image_value(_: impl Ctx, _primary: (), image: Table<Raster<CPU>>) -> Table<Raster<CPU>> {
image
}
@@ -361,7 +318,7 @@ pub fn noise_pattern(
cellular_distance_function: CellularDistanceFunction,
cellular_return_type: CellularReturnType,
cellular_jitter: f64,
) -> RasterDataTable<CPU> {
) -> Table<Raster<CPU>> {
let footprint = ctx.footprint();
let viewport_bounds = footprint.viewport_bounds_in_local_space();
@@ -379,7 +336,7 @@ pub fn noise_pattern(
// If the image would not be visible, return an empty image
if size.x <= 0. || size.y <= 0. {
return RasterDataTable::default();
return Table::new();
}
let footprint_scale = footprint.scale();
@@ -423,8 +380,8 @@ pub fn noise_pattern(
}
}
return RasterDataTable::new_instance(Instance {
instance: Raster::new_cpu(image),
return Table::new_from_row(TableRow {
element: Raster::new_cpu(image),
transform: DAffine2::from_translation(offset) * DAffine2::from_scale(size),
..Default::default()
});
@@ -485,15 +442,15 @@ pub fn noise_pattern(
}
}
RasterDataTable::new_instance(Instance {
instance: Raster::new_cpu(image),
Table::new_from_row(TableRow {
element: Raster::new_cpu(image),
transform: DAffine2::from_translation(offset) * DAffine2::from_scale(size),
..Default::default()
})
}
#[node_macro::node(category("Raster: Pattern"))]
pub fn mandelbrot(ctx: impl ExtractFootprint + Send) -> RasterDataTable<CPU> {
pub fn mandelbrot(ctx: impl ExtractFootprint + Send) -> Table<Raster<CPU>> {
let footprint = ctx.footprint();
let viewport_bounds = footprint.viewport_bounds_in_local_space();
@@ -505,7 +462,7 @@ pub fn mandelbrot(ctx: impl ExtractFootprint + Send) -> RasterDataTable<CPU> {
// If the image would not be visible, return an empty image
if size.x <= 0. || size.y <= 0. {
return RasterDataTable::default();
return Table::new();
}
let scale = footprint.scale();
@@ -527,8 +484,8 @@ pub fn mandelbrot(ctx: impl ExtractFootprint + Send) -> RasterDataTable<CPU> {
}
}
RasterDataTable::new_instance(Instance {
instance: Raster::new_cpu(Image {
Table::new_from_row(TableRow {
element: Raster::new_cpu(Image {
width,
height,
data,

View File

@@ -36,15 +36,10 @@ graphene-raster-nodes = { workspace = true }
graphene-brush = { workspace = true }
# Workspace dependencies
fastnoise-lite = { workspace = true }
log = { workspace = true }
glam = { workspace = true }
node-macro = { workspace = true }
reqwest = { workspace = true }
futures = { workspace = true }
rand_chacha = { workspace = true }
rand = { workspace = true }
bytemuck = { workspace = true }
image = { workspace = true }
base64 = { workspace = true }
@@ -65,8 +60,5 @@ web-sys = { workspace = true, optional = true, features = [
"ImageBitmapRenderingContext",
] }
# Required dependencies
ndarray = "0.16.1"
[dev-dependencies]
tokio = { workspace = true }

View File

@@ -1,6 +1,6 @@
use dyn_any::StaticType;
pub use graph_craft::proto::{Any, NodeContainer, TypeErasedBox, TypeErasedNode};
use graph_craft::proto::{DynFuture, FutureAny, SharedNodeContainer};
use graph_craft::proto::{FutureAny, SharedNodeContainer};
use graphene_core::NodeIO;
use graphene_core::WasmNotSend;
pub use graphene_core::registry::{DowncastBothNode, DynAnyNode, FutureWrapperNode, PanicNode};
@@ -19,27 +19,6 @@ where
}
}
pub struct ComposeTypeErased {
first: SharedNodeContainer,
second: SharedNodeContainer,
}
impl<'i> Node<'i, Any<'i>> for ComposeTypeErased {
type Output = DynFuture<'i, Any<'i>>;
fn eval(&'i self, input: Any<'i>) -> Self::Output {
Box::pin(async move {
let arg = self.first.eval(input).await;
self.second.eval(arg).await
})
}
}
impl ComposeTypeErased {
pub const fn new(first: SharedNodeContainer, second: SharedNodeContainer) -> Self {
ComposeTypeErased { first, second }
}
}
pub fn input_node<O: StaticType>(n: SharedNodeContainer) -> DowncastBothNode<(), O> {
downcast_node(n)
}

View File

@@ -1,7 +1,6 @@
use crate::vector::VectorDataTable;
use graph_craft::wasm_application_io::WasmEditorApi;
use graphene_core::Ctx;
pub use graphene_core::text::*;
use graphene_core::{Ctx, table::Table, vector::Vector};
#[node_macro::node(category(""))]
fn text<'i: 'n>(
@@ -18,20 +17,17 @@ fn text<'i: 'n>(
#[unit(" px")]
#[default(0.)]
character_spacing: f64,
#[unit(" px")]
#[default(None)]
max_width: Option<f64>,
#[unit(" px")]
#[default(None)]
max_height: Option<f64>,
#[unit(" px")] max_width: Option<f64>,
#[unit(" px")] max_height: Option<f64>,
/// Faux italic.
#[unit("°")]
#[default(0.)]
tilt: f64,
/// Splits each text glyph into its own instance, i.e. row in the table of vector data.
align: TextAlign,
/// Splits each text glyph into its own row in the table of vector geometry.
#[default(false)]
per_glyph_instances: bool,
) -> VectorDataTable {
) -> Table<Vector> {
let typesetting = TypesettingConfig {
font_size,
line_height_ratio,
@@ -39,6 +35,7 @@ fn text<'i: 'n>(
max_width,
max_height,
tilt,
align,
};
let font_data = editor.font_cache.get(&font_name).map(|f| load_font(f));

View File

@@ -2,26 +2,26 @@ use graph_craft::document::value::RenderOutput;
pub use graph_craft::document::value::RenderOutputType;
pub use graph_craft::wasm_application_io::*;
use graphene_application_io::{ApplicationIo, ExportFormat, RenderConfig};
#[cfg(target_arch = "wasm32")]
use graphene_core::instances::Instances;
#[cfg(target_arch = "wasm32")]
use graphene_core::Artboard;
use graphene_core::gradient::GradientStops;
#[cfg(target_family = "wasm")]
use graphene_core::math::bbox::Bbox;
use graphene_core::raster::image::Image;
use graphene_core::raster_types::{CPU, Raster, RasterDataTable};
use graphene_core::raster_types::{CPU, Raster};
use graphene_core::table::Table;
#[cfg(target_family = "wasm")]
use graphene_core::transform::Footprint;
use graphene_core::vector::VectorDataTable;
use graphene_core::{Color, Context, Ctx, ExtractFootprint, GraphicGroupTable, OwnedContextImpl, WasmNotSend};
use graphene_core::vector::Vector;
use graphene_core::{Color, Context, Ctx, ExtractFootprint, Graphic, OwnedContextImpl, WasmNotSend};
use graphene_svg_renderer::RenderMetadata;
use graphene_svg_renderer::{GraphicElementRendered, RenderParams, RenderSvgSegmentList, SvgRender, format_transform_matrix};
use graphene_svg_renderer::{Render, RenderParams, RenderSvgSegmentList, SvgRender, format_transform_matrix};
#[cfg(target_arch = "wasm32")]
#[cfg(target_family = "wasm")]
use base64::Engine;
#[cfg(target_arch = "wasm32")]
use glam::DAffine2;
use std::sync::Arc;
#[cfg(target_arch = "wasm32")]
#[cfg(target_family = "wasm")]
use wasm_bindgen::JsCast;
#[cfg(target_arch = "wasm32")]
#[cfg(target_family = "wasm")]
use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement};
#[cfg(feature = "wgpu")]
@@ -30,38 +30,9 @@ async fn create_surface<'a: 'n>(_: impl Ctx, editor: &'a WasmEditorApi) -> Arc<W
Arc::new(editor.application_io.as_ref().unwrap().create_window())
}
// TODO: Fix and reenable in order to get the 'Draw Canvas' node working again.
// #[cfg(target_arch = "wasm32")]
// use wasm_bindgen::Clamped;
//
// #[node_macro::node(category("Debug: GPU"))]
// #[cfg(target_arch = "wasm32")]
// async fn draw_image_frame(
// _: impl Ctx,
// image: RasterDataTable<graphene_core::raster::SRGBA8>,
// surface_handle: Arc<WasmSurfaceHandle>,
// ) -> graphene_core::application_io::SurfaceHandleFrame<HtmlCanvasElement> {
// let image = image.instance_ref_iter().next().unwrap().instance;
// let image_data = image.image.data;
// let array: Clamped<&[u8]> = Clamped(bytemuck::cast_slice(image_data.as_slice()));
// if image.image.width > 0 && image.image.height > 0 {
// let canvas = &surface_handle.surface;
// canvas.set_width(image.image.width);
// canvas.set_height(image.image.height);
// // TODO: replace "2d" with "bitmaprenderer" once we switch to ImageBitmap (lives on gpu) from RasterData (lives on cpu)
// let context = canvas.get_context("2d").unwrap().unwrap().dyn_into::<CanvasRenderingContext2d>().unwrap();
// let image_data = web_sys::ImageData::new_with_u8_clamped_array_and_sh(array, image.image.width, image.image.height).expect("Failed to construct RasterData");
// context.put_image_data(&image_data, 0., 0.).unwrap();
// }
// graphene_core::application_io::SurfaceHandleFrame {
// surface_handle,
// transform: image.transform,
// }
// }
#[node_macro::node(category("Web Request"))]
async fn get_request(_: impl Ctx, _primary: (), #[name("URL")] url: String, discard_result: bool) -> String {
#[cfg(target_arch = "wasm32")]
#[cfg(target_family = "wasm")]
{
if discard_result {
wasm_bindgen_futures::spawn_local(async move {
@@ -70,7 +41,7 @@ async fn get_request(_: impl Ctx, _primary: (), #[name("URL")] url: String, disc
return String::new();
}
}
#[cfg(not(target_arch = "wasm32"))]
#[cfg(not(target_family = "wasm"))]
{
#[cfg(feature = "tokio")]
if discard_result {
@@ -91,7 +62,7 @@ async fn get_request(_: impl Ctx, _primary: (), #[name("URL")] url: String, disc
#[node_macro::node(category("Web Request"))]
async fn post_request(_: impl Ctx, _primary: (), #[name("URL")] url: String, body: Vec<u8>, discard_result: bool) -> String {
#[cfg(target_arch = "wasm32")]
#[cfg(target_family = "wasm")]
{
if discard_result {
wasm_bindgen_futures::spawn_local(async move {
@@ -100,7 +71,7 @@ async fn post_request(_: impl Ctx, _primary: (), #[name("URL")] url: String, bod
return String::new();
}
}
#[cfg(not(target_arch = "wasm32"))]
#[cfg(not(target_family = "wasm"))]
{
#[cfg(feature = "tokio")]
if discard_result {
@@ -129,9 +100,9 @@ fn string_to_bytes(_: impl Ctx, string: String) -> Vec<u8> {
}
#[node_macro::node(category("Web Request"), name("Image to Bytes"))]
fn image_to_bytes(_: impl Ctx, image: RasterDataTable<CPU>) -> Vec<u8> {
let Some(image) = image.instance_ref_iter().next() else { return vec![] };
image.instance.data.iter().flat_map(|color| color.to_rgb8_srgb().into_iter()).collect::<Vec<u8>>()
fn image_to_bytes(_: impl Ctx, image: Table<Raster<CPU>>) -> Vec<u8> {
let Some(image) = image.iter().next() else { return vec![] };
image.element.data.iter().flat_map(|color| color.to_rgb8_srgb().into_iter()).collect::<Vec<u8>>()
}
#[node_macro::node(category("Web Request"))]
@@ -150,9 +121,9 @@ async fn load_resource<'a: 'n>(_: impl Ctx, _primary: (), #[scope("editor-api")]
}
#[node_macro::node(category("Web Request"))]
fn decode_image(_: impl Ctx, data: Arc<[u8]>) -> RasterDataTable<CPU> {
fn decode_image(_: impl Ctx, data: Arc<[u8]>) -> Table<Raster<CPU>> {
let Some(image) = image::load_from_memory(data.as_ref()).ok() else {
return RasterDataTable::default();
return Table::new();
};
let image = image.to_rgba32f();
let image = Image {
@@ -165,10 +136,12 @@ fn decode_image(_: impl Ctx, data: Arc<[u8]>) -> RasterDataTable<CPU> {
..Default::default()
};
RasterDataTable::new(Raster::new_cpu(image))
Table::new_from_element(Raster::new_cpu(image))
}
fn render_svg(data: impl GraphicElementRendered, mut render: SvgRender, render_params: RenderParams, footprint: Footprint) -> RenderOutputType {
fn render_svg(data: impl Render, mut render: SvgRender, render_params: RenderParams) -> RenderOutputType {
let footprint = render_params.footprint;
if !data.contains_artboard() && !render_params.hide_artboards {
render.leaf_tag("rect", |attributes| {
attributes.push("x", "0");
@@ -187,21 +160,19 @@ fn render_svg(data: impl GraphicElementRendered, mut render: SvgRender, render_p
render.wrap_with_transform(footprint.transform, Some(footprint.resolution.as_dvec2()));
RenderOutputType::Svg(render.svg.to_svg_string())
RenderOutputType::Svg {
svg: render.svg.to_svg_string(),
image_data: render.image_data,
}
}
#[cfg(feature = "vello")]
#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
async fn render_canvas(
render_config: RenderConfig,
data: impl GraphicElementRendered,
editor: &WasmEditorApi,
surface_handle: wgpu_executor::WgpuSurface,
render_params: RenderParams,
) -> RenderOutputType {
use graphene_application_io::SurfaceFrame;
#[cfg_attr(not(target_family = "wasm"), allow(dead_code))]
async fn render_canvas(render_config: RenderConfig, data: impl Render, editor: &WasmEditorApi, surface_handle: Option<wgpu_executor::WgpuSurface>, render_params: RenderParams) -> RenderOutputType {
use graphene_application_io::{ImageTexture, SurfaceFrame};
let footprint = render_config.viewport;
let mut footprint = render_config.viewport;
footprint.resolution = footprint.resolution.max(glam::UVec2::splat(1));
let Some(exec) = editor.application_io.as_ref().unwrap().gpu_executor() else {
unreachable!("Attempted to render with Vello when no GPU executor is available");
};
@@ -220,40 +191,51 @@ async fn render_canvas(
if !data.contains_artboard() && !render_config.hide_artboards {
background = Color::WHITE;
}
exec.render_vello_scene(&scene, &surface_handle, footprint.resolution, &context, background)
.await
.expect("Failed to render Vello scene");
if let Some(surface_handle) = surface_handle {
exec.render_vello_scene(&scene, &surface_handle, footprint.resolution, &context, background)
.await
.expect("Failed to render Vello scene");
let frame = SurfaceFrame {
surface_id: surface_handle.window_id,
resolution: render_config.viewport.resolution,
transform: glam::DAffine2::IDENTITY,
};
let frame = SurfaceFrame {
surface_id: surface_handle.window_id,
resolution: render_config.viewport.resolution,
transform: glam::DAffine2::IDENTITY,
};
RenderOutputType::CanvasFrame(frame)
RenderOutputType::CanvasFrame(frame)
} else {
let texture = exec
.render_vello_scene_to_texture(&scene, footprint.resolution, &context, background)
.await
.expect("Failed to render Vello scene");
RenderOutputType::Texture(ImageTexture { texture })
}
}
#[cfg(target_arch = "wasm32")]
#[cfg(target_family = "wasm")]
#[node_macro::node(category(""))]
async fn rasterize<T: WasmNotSend + 'n>(
_: impl Ctx,
#[implementations(
VectorDataTable,
RasterDataTable<CPU>,
GraphicGroupTable,
Table<Vector>,
Table<Raster<CPU>>,
Table<Graphic>,
Table<Color>,
Table<GradientStops>,
)]
mut data: Instances<T>,
mut data: Table<T>,
footprint: Footprint,
surface_handle: Arc<graphene_application_io::SurfaceHandle<HtmlCanvasElement>>,
) -> RasterDataTable<CPU>
) -> Table<Raster<CPU>>
where
Instances<T>: GraphicElementRendered,
Table<T>: Render,
{
use graphene_core::instances::Instance;
use graphene_core::table::TableRow;
if footprint.transform.matrix2.determinant() == 0. {
log::trace!("Invalid footprint received for rasterization");
return RasterDataTable::default();
return Table::new();
}
let mut render = SvgRender::new();
@@ -261,12 +243,13 @@ where
let size = aabb.size();
let resolution = footprint.resolution;
let render_params = RenderParams {
culling_bounds: None,
footprint,
for_export: true,
..Default::default()
};
for instance in data.instance_mut_iter() {
*instance.transform = DAffine2::from_translation(-aabb.start) * *instance.transform;
for row in data.iter_mut() {
*row.transform = glam::DAffine2::from_translation(-aabb.start) * *row.transform;
}
data.render_svg(&mut render, &render_params);
render.format_svg(glam::DVec2::ZERO, size);
@@ -293,29 +276,24 @@ where
let rasterized = context.get_image_data(0., 0., resolution.x as f64, resolution.y as f64).unwrap();
let image = Image::from_image_data(&rasterized.data().0, resolution.x as u32, resolution.y as u32);
RasterDataTable::new_instance(Instance {
instance: Raster::new_cpu(image),
Table::new_from_row(TableRow {
element: Raster::new_cpu(image),
transform: footprint.transform,
..Default::default()
})
}
#[node_macro::node(category(""))]
async fn render<'a: 'n, T: 'n + GraphicElementRendered + WasmNotSend>(
async fn render<'a: 'n, T: 'n + Render + WasmNotSend>(
render_config: RenderConfig,
editor_api: impl Node<Context<'static>, Output = &'a WasmEditorApi>,
#[implementations(
Context -> VectorDataTable,
Context -> RasterDataTable<CPU>,
Context -> GraphicGroupTable,
Context -> graphene_core::Artboard,
Context -> graphene_core::ArtboardGroupTable,
Context -> Option<Color>,
Context -> Vec<Color>,
Context -> bool,
Context -> f32,
Context -> f64,
Context -> String,
Context -> Table<Artboard>,
Context -> Table<Graphic>,
Context -> Table<Vector>,
Context -> Table<Raster<CPU>>,
Context -> Table<Color>,
Context -> Table<GradientStops>,
)]
data: impl Node<Context<'static>, Output = T>,
_surface_handle: impl Node<Context<'static>, Output = Option<wgpu_executor::WgpuSurface>>,
@@ -328,44 +306,43 @@ async fn render<'a: 'n, T: 'n + GraphicElementRendered + WasmNotSend>(
.into_context();
ctx.footprint();
let RenderConfig { hide_artboards, for_export, .. } = render_config;
let render_params = RenderParams {
view_mode: render_config.view_mode,
culling_bounds: None,
thumbnail: false,
hide_artboards,
for_export,
for_mask: false,
alignment_parent_transform: None,
hide_artboards: render_config.hide_artboards,
for_export: render_config.for_export,
footprint,
..Default::default()
};
let data = data.eval(ctx.clone()).await;
let editor_api = editor_api.eval(None).await;
#[cfg(all(feature = "vello", not(test)))]
let surface_handle = _surface_handle.eval(None).await;
#[cfg(all(feature = "vello", not(test), target_family = "wasm"))]
let _surface_handle = _surface_handle.eval(None).await;
#[cfg(not(target_family = "wasm"))]
let _surface_handle: Option<wgpu_executor::WgpuSurface> = None;
let use_vello = editor_api.editor_preferences.use_vello();
#[cfg(all(feature = "vello", not(test)))]
let use_vello = use_vello && surface_handle.is_some();
#[cfg(all(feature = "vello", not(test), target_family = "wasm"))]
let use_vello = use_vello && _surface_handle.is_some();
let mut metadata = RenderMetadata::default();
data.collect_metadata(&mut metadata, footprint, None);
let output_format = render_config.export_format;
let data = match output_format {
ExportFormat::Svg => render_svg(data, SvgRender::new(), render_params, footprint),
ExportFormat::Svg => render_svg(data, SvgRender::new(), render_params),
ExportFormat::Canvas => {
if use_vello && editor_api.application_io.as_ref().unwrap().gpu_executor().is_some() {
#[cfg(all(feature = "vello", not(test)))]
return RenderOutput {
data: render_canvas(render_config, data, editor_api, surface_handle.unwrap(), render_params).await,
data: render_canvas(render_config, data, editor_api, _surface_handle, render_params).await,
metadata,
};
#[cfg(any(not(feature = "vello"), test))]
render_svg(data, SvgRender::new(), render_params, footprint)
render_svg(data, SvgRender::new(), render_params)
} else {
render_svg(data, SvgRender::new(), render_params, footprint)
render_svg(data, SvgRender::new(), render_params)
}
}
_ => todo!("Non-SVG render output for {output_format:?}"),

View File

@@ -6,14 +6,10 @@ description = "graphene svg renderer"
authors = ["Graphite Authors <contact@graphite.rs>"]
license = "MIT OR Apache-2.0"
[features]
vello = ["dep:vello", "bezier-rs/kurbo"]
[dependencies]
# Local dependencies
dyn-any = { workspace = true }
graphene-core = { workspace = true }
bezier-rs = { workspace = true }
# Workspace dependencies
glam = { workspace = true }
@@ -22,6 +18,7 @@ base64 = { workspace = true }
log = { workspace = true }
num-traits = { workspace = true }
usvg = { workspace = true }
kurbo = { workspace = true }
# Optional workspace dependencies
vello = { workspace = true, optional = true }

View File

@@ -1,10 +1,10 @@
use bezier_rs::{ManipulatorGroup, Subpath};
use glam::DVec2;
use graphene_core::subpath::{ManipulatorGroup, Subpath};
use graphene_core::vector::PointId;
pub fn convert_usvg_path(path: &usvg::Path) -> Vec<Subpath<PointId>> {
let mut subpaths = Vec::new();
let mut groups = 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);
@@ -12,36 +12,36 @@ pub fn convert_usvg_path(path: &usvg::Path) -> Vec<Subpath<PointId>> {
for verb in path.data().verbs() {
match verb {
usvg::tiny_skia_path::PathVerb::Move => {
subpaths.push(Subpath::new(std::mem::take(&mut groups), false));
subpaths.push(Subpath::new(std::mem::take(&mut manipulators_list), false));
let Some(start) = points.next().map(to_vec) else { continue };
groups.push(ManipulatorGroup::new(start, Some(start), Some(start)));
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 };
groups.push(ManipulatorGroup::new(end, Some(end), Some(end)));
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) = groups.last_mut() {
if let Some(last) = manipulators_list.last_mut() {
last.out_handle = Some(last.anchor + (2. / 3.) * (handle - last.anchor));
}
groups.push(ManipulatorGroup::new(end, Some(end + (2. / 3.) * (handle - end)), Some(end)));
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) = groups.last_mut() {
if let Some(last) = manipulators_list.last_mut() {
last.out_handle = Some(first_handle);
}
groups.push(ManipulatorGroup::new(end, Some(second_handle), Some(end)));
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 groups), true));
subpaths.push(Subpath::new(std::mem::take(&mut manipulators_list), true));
}
}
}
subpaths.push(Subpath::new(groups, false));
subpaths.push(Subpath::new(manipulators_list, false));
subpaths
}

Some files were not shown because too many files have changed in this diff Show More