mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-21 03:08:12 +08:00
WIP
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
[package]
|
||||
name = "graphene-vector"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
description = "graphene vector data format"
|
||||
authors = ["Graphite Authors <contact@graphite.rs>"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
[features]
|
||||
default = ["serde"]
|
||||
serde = [
|
||||
"dep:serde",
|
||||
"bezier-rs/serde",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
# Local dependencies
|
||||
dyn-any = { workspace = true }
|
||||
bezier-rs = { workspace = true }
|
||||
graphene-core = { workspace = true }
|
||||
node-macro = { workspace = true }
|
||||
|
||||
# Workspace dependencies
|
||||
kurbo = { workspace = true }
|
||||
glam = { workspace = true }
|
||||
specta = { workspace = true }
|
||||
log = { workspace = true }
|
||||
tinyvec = { workspace = true }
|
||||
rustc-hash = { workspace = true }
|
||||
petgraph = { workspace = true }
|
||||
|
||||
# Optional workspace dependencies
|
||||
serde = { workspace = true, optional = true, features = ["derive"] }
|
||||
@@ -0,0 +1,162 @@
|
||||
use crate::math_ext::QuadExt;
|
||||
use crate::vector_data::PointId;
|
||||
use bezier_rs::Subpath;
|
||||
use glam::{DAffine2, DMat2, DVec2};
|
||||
use graphene_core::math::quad::Quad;
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct FreePoint {
|
||||
pub id: PointId,
|
||||
pub position: DVec2,
|
||||
}
|
||||
|
||||
impl FreePoint {
|
||||
pub fn new(id: PointId, position: DVec2) -> Self {
|
||||
Self { id, position }
|
||||
}
|
||||
|
||||
pub fn apply_transform(&mut self, transform: DAffine2) {
|
||||
self.position = transform.transform_point2(self.position);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum ClickTargetType {
|
||||
Subpath(Subpath<PointId>),
|
||||
FreePoint(FreePoint),
|
||||
}
|
||||
|
||||
/// Represents a clickable target for the layer
|
||||
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ClickTarget {
|
||||
target_type: ClickTargetType,
|
||||
stroke_width: f64,
|
||||
bounding_box: Option<[DVec2; 2]>,
|
||||
}
|
||||
|
||||
impl ClickTarget {
|
||||
pub fn new_with_subpath(subpath: Subpath<PointId>, stroke_width: f64) -> Self {
|
||||
let bounding_box = subpath.loose_bounding_box();
|
||||
Self {
|
||||
target_type: ClickTargetType::Subpath(subpath),
|
||||
stroke_width,
|
||||
bounding_box,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_with_free_point(point: FreePoint) -> Self {
|
||||
const MAX_LENGTH_FOR_NO_WIDTH_OR_HEIGHT: f64 = 1e-4 / 2.;
|
||||
let stroke_width = 10.;
|
||||
let bounding_box = Some([
|
||||
point.position - DVec2::splat(MAX_LENGTH_FOR_NO_WIDTH_OR_HEIGHT),
|
||||
point.position + DVec2::splat(MAX_LENGTH_FOR_NO_WIDTH_OR_HEIGHT),
|
||||
]);
|
||||
|
||||
Self {
|
||||
target_type: ClickTargetType::FreePoint(point),
|
||||
stroke_width,
|
||||
bounding_box,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn target_type(&self) -> &ClickTargetType {
|
||||
&self.target_type
|
||||
}
|
||||
|
||||
pub fn bounding_box(&self) -> Option<[DVec2; 2]> {
|
||||
self.bounding_box
|
||||
}
|
||||
|
||||
pub fn bounding_box_with_transform(&self, transform: DAffine2) -> Option<[DVec2; 2]> {
|
||||
self.bounding_box.map(|[a, b]| [transform.transform_point2(a), transform.transform_point2(b)])
|
||||
}
|
||||
|
||||
pub fn apply_transform(&mut self, affine_transform: DAffine2) {
|
||||
match self.target_type {
|
||||
ClickTargetType::Subpath(ref mut subpath) => {
|
||||
subpath.apply_transform(affine_transform);
|
||||
}
|
||||
ClickTargetType::FreePoint(ref mut point) => {
|
||||
point.apply_transform(affine_transform);
|
||||
}
|
||||
}
|
||||
self.update_bbox();
|
||||
}
|
||||
|
||||
fn update_bbox(&mut self) {
|
||||
match self.target_type {
|
||||
ClickTargetType::Subpath(ref subpath) => {
|
||||
self.bounding_box = subpath.bounding_box();
|
||||
}
|
||||
ClickTargetType::FreePoint(ref point) => {
|
||||
self.bounding_box = Some([point.position - DVec2::splat(self.stroke_width / 2.), point.position + DVec2::splat(self.stroke_width / 2.)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Does the click target intersect the path
|
||||
pub fn intersect_path<It: Iterator<Item = bezier_rs::Bezier>>(&self, mut bezier_iter: impl FnMut() -> It, layer_transform: DAffine2) -> bool {
|
||||
// Check if the matrix is not invertible
|
||||
let mut layer_transform = layer_transform;
|
||||
if layer_transform.matrix2.determinant().abs() <= f64::EPSILON {
|
||||
layer_transform.matrix2 += DMat2::IDENTITY * 1e-4; // TODO: Is this the cleanest way to handle this?
|
||||
}
|
||||
|
||||
let inverse = layer_transform.inverse();
|
||||
let mut bezier_iter = || bezier_iter().map(|bezier| bezier.apply_transformation(|point| inverse.transform_point2(point)));
|
||||
|
||||
match self.target_type() {
|
||||
ClickTargetType::Subpath(subpath) => {
|
||||
// Check if outlines intersect
|
||||
let outline_intersects = |path_segment: bezier_rs::Bezier| bezier_iter().any(|line| !path_segment.intersections(&line, None, None).is_empty());
|
||||
if subpath.iter().any(outline_intersects) {
|
||||
return true;
|
||||
}
|
||||
// Check if selection is entirely within the shape
|
||||
if subpath.closed() && bezier_iter().next().is_some_and(|bezier| subpath.contains_point(bezier.start)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if shape is entirely within selection
|
||||
let any_point_from_subpath = subpath.manipulator_groups().first().map(|group| group.anchor);
|
||||
any_point_from_subpath.is_some_and(|shape_point| bezier_iter().map(|bezier| bezier.winding(shape_point)).sum::<i32>() != 0)
|
||||
}
|
||||
ClickTargetType::FreePoint(point) => bezier_iter().map(|bezier: bezier_rs::Bezier| bezier.winding(point.position)).sum::<i32>() != 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Does the click target intersect the point (accounting for stroke size)
|
||||
pub fn intersect_point(&self, point: DVec2, layer_transform: DAffine2) -> bool {
|
||||
let target_bounds = [point - DVec2::splat(self.stroke_width / 2.), point + DVec2::splat(self.stroke_width / 2.)];
|
||||
let intersects = |a: [DVec2; 2], b: [DVec2; 2]| a[0].x <= b[1].x && a[1].x >= b[0].x && a[0].y <= b[1].y && a[1].y >= b[0].y;
|
||||
// This bounding box is not very accurate as it is the axis aligned version of the transformed bounding box. However it is fast.
|
||||
if !self
|
||||
.bounding_box
|
||||
.is_some_and(|loose| (loose[0] - loose[1]).abs().cmpgt(DVec2::splat(1e-4)).any() && intersects((layer_transform * Quad::from_box(loose)).bounding_box(), target_bounds))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Allows for selecting lines
|
||||
// TODO: actual intersection of stroke
|
||||
let inflated_quad = Quad::from_box(target_bounds);
|
||||
self.intersect_path(|| inflated_quad.bezier_lines(), layer_transform)
|
||||
}
|
||||
|
||||
/// Does the click target intersect the point (not accounting for stroke size)
|
||||
pub fn intersect_point_no_stroke(&self, point: DVec2) -> bool {
|
||||
// Check if the point is within the bounding box
|
||||
if self
|
||||
.bounding_box
|
||||
.is_some_and(|bbox| bbox[0].x <= point.x && point.x <= bbox[1].x && bbox[0].y <= point.y && point.y <= bbox[1].y)
|
||||
{
|
||||
// Check if the point is within the shape
|
||||
match self.target_type() {
|
||||
ClickTargetType::Subpath(subpath) => subpath.closed() && subpath.contains_point(point),
|
||||
ClickTargetType::FreePoint(free_point) => free_point.position == point,
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
pub mod click_target;
|
||||
pub mod math_ext;
|
||||
pub mod reference_point;
|
||||
pub mod style;
|
||||
mod vector_data;
|
||||
|
||||
pub use bezier_rs;
|
||||
|
||||
pub use vector_data::*;
|
||||
|
||||
pub fn point_to_dvec2(point: kurbo::Point) -> glam::DVec2 {
|
||||
glam::DVec2 { x: point.x, y: point.y }
|
||||
}
|
||||
|
||||
pub fn dvec2_to_point(value: glam::DVec2) -> kurbo::Point {
|
||||
kurbo::Point { x: value.x, y: value.y }
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
use bezier_rs::Bezier;
|
||||
use graphene_core::math::quad::Quad;
|
||||
use graphene_core::math::rect::Rect;
|
||||
|
||||
pub trait QuadExt {
|
||||
/// Get all the edges in the rect as linear bezier curves
|
||||
fn bezier_lines(&self) -> impl Iterator<Item = Bezier> + '_;
|
||||
}
|
||||
|
||||
impl QuadExt for Quad {
|
||||
fn bezier_lines(&self) -> impl Iterator<Item = Bezier> + '_ {
|
||||
self.all_edges().into_iter().map(|[start, end]| Bezier::from_linear_dvec2(start, end))
|
||||
}
|
||||
}
|
||||
|
||||
pub trait RectExt {
|
||||
/// Get all the edges in the quad as linear bezier curves
|
||||
fn bezier_lines(&self) -> impl Iterator<Item = Bezier> + '_;
|
||||
}
|
||||
|
||||
impl RectExt for Rect {
|
||||
fn bezier_lines(&self) -> impl Iterator<Item = Bezier> + '_ {
|
||||
self.edges().into_iter().map(|[start, end]| Bezier::from_linear_dvec2(start, end))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
use glam::DVec2;
|
||||
use graphene_core::math::bbox::AxisAlignedBbox;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Hash, Eq, PartialEq, dyn_any::DynAny, serde::Serialize, serde::Deserialize, specta::Type)]
|
||||
pub enum ReferencePoint {
|
||||
#[default]
|
||||
None,
|
||||
TopLeft,
|
||||
TopCenter,
|
||||
TopRight,
|
||||
CenterLeft,
|
||||
Center,
|
||||
CenterRight,
|
||||
BottomLeft,
|
||||
BottomCenter,
|
||||
BottomRight,
|
||||
}
|
||||
|
||||
impl ReferencePoint {
|
||||
pub fn point_in_bounding_box(&self, bounding_box: AxisAlignedBbox) -> Option<DVec2> {
|
||||
let size = bounding_box.size();
|
||||
let offset = match self {
|
||||
ReferencePoint::None => return None,
|
||||
ReferencePoint::TopLeft => DVec2::ZERO,
|
||||
ReferencePoint::TopCenter => DVec2::new(size.x / 2., 0.),
|
||||
ReferencePoint::TopRight => DVec2::new(size.x, 0.),
|
||||
ReferencePoint::CenterLeft => DVec2::new(0., size.y / 2.),
|
||||
ReferencePoint::Center => DVec2::new(size.x / 2., size.y / 2.),
|
||||
ReferencePoint::CenterRight => DVec2::new(size.x, size.y / 2.),
|
||||
ReferencePoint::BottomLeft => DVec2::new(0., size.y),
|
||||
ReferencePoint::BottomCenter => DVec2::new(size.x / 2., size.y),
|
||||
ReferencePoint::BottomRight => DVec2::new(size.x, size.y),
|
||||
};
|
||||
Some(bounding_box.start + offset)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for ReferencePoint {
|
||||
fn from(input: &str) -> Self {
|
||||
match input {
|
||||
"None" => ReferencePoint::None,
|
||||
"TopLeft" => ReferencePoint::TopLeft,
|
||||
"TopCenter" => ReferencePoint::TopCenter,
|
||||
"TopRight" => ReferencePoint::TopRight,
|
||||
"CenterLeft" => ReferencePoint::CenterLeft,
|
||||
"Center" => ReferencePoint::Center,
|
||||
"CenterRight" => ReferencePoint::CenterRight,
|
||||
"BottomLeft" => ReferencePoint::BottomLeft,
|
||||
"BottomCenter" => ReferencePoint::BottomCenter,
|
||||
"BottomRight" => ReferencePoint::BottomRight,
|
||||
_ => panic!("Failed parsing unrecognized ReferencePosition enum value '{input}'"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ReferencePoint> for Option<DVec2> {
|
||||
fn from(input: ReferencePoint) -> Self {
|
||||
match input {
|
||||
ReferencePoint::None => None,
|
||||
ReferencePoint::TopLeft => Some(DVec2::new(0., 0.)),
|
||||
ReferencePoint::TopCenter => Some(DVec2::new(0.5, 0.)),
|
||||
ReferencePoint::TopRight => Some(DVec2::new(1., 0.)),
|
||||
ReferencePoint::CenterLeft => Some(DVec2::new(0., 0.5)),
|
||||
ReferencePoint::Center => Some(DVec2::new(0.5, 0.5)),
|
||||
ReferencePoint::CenterRight => Some(DVec2::new(1., 0.5)),
|
||||
ReferencePoint::BottomLeft => Some(DVec2::new(0., 1.)),
|
||||
ReferencePoint::BottomCenter => Some(DVec2::new(0.5, 1.)),
|
||||
ReferencePoint::BottomRight => Some(DVec2::new(1., 1.)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DVec2> for ReferencePoint {
|
||||
fn from(input: DVec2) -> Self {
|
||||
const TOLERANCE: f64 = 1e-5_f64;
|
||||
if input.y.abs() < TOLERANCE {
|
||||
if input.x.abs() < TOLERANCE {
|
||||
return ReferencePoint::TopLeft;
|
||||
} else if (input.x - 0.5).abs() < TOLERANCE {
|
||||
return ReferencePoint::TopCenter;
|
||||
} else if (input.x - 1.).abs() < TOLERANCE {
|
||||
return ReferencePoint::TopRight;
|
||||
}
|
||||
} else if (input.y - 0.5).abs() < TOLERANCE {
|
||||
if input.x.abs() < TOLERANCE {
|
||||
return ReferencePoint::CenterLeft;
|
||||
} else if (input.x - 0.5).abs() < TOLERANCE {
|
||||
return ReferencePoint::Center;
|
||||
} else if (input.x - 1.).abs() < TOLERANCE {
|
||||
return ReferencePoint::CenterRight;
|
||||
}
|
||||
} else if (input.y - 1.).abs() < TOLERANCE {
|
||||
if input.x.abs() < TOLERANCE {
|
||||
return ReferencePoint::BottomLeft;
|
||||
} else if (input.x - 0.5).abs() < TOLERANCE {
|
||||
return ReferencePoint::BottomCenter;
|
||||
} else if (input.x - 1.).abs() < TOLERANCE {
|
||||
return ReferencePoint::BottomRight;
|
||||
}
|
||||
}
|
||||
ReferencePoint::None
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,636 @@
|
||||
//! Contains stylistic options for SVG elements.
|
||||
|
||||
use dyn_any::DynAny;
|
||||
use glam::DAffine2;
|
||||
use graphene_core::color::Color;
|
||||
use graphene_core::gradient::{Gradient, GradientStops};
|
||||
|
||||
/// Describes the fill of a layer.
|
||||
///
|
||||
/// Can be None, a solid [Color], or a linear/radial [Gradient].
|
||||
///
|
||||
/// In the future we'll probably also add a pattern fill. This will probably be named "Paint" in the future.
|
||||
#[repr(C)]
|
||||
#[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, DynAny, Hash, specta::Type)]
|
||||
pub enum Fill {
|
||||
#[default]
|
||||
None,
|
||||
Solid(Color),
|
||||
Gradient(Gradient),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Fill {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::None => write!(f, "None"),
|
||||
Self::Solid(color) => write!(f, "#{} (Alpha: {}%)", color.to_rgb_hex_srgb(), color.a() * 100.),
|
||||
Self::Gradient(gradient) => write!(f, "{}", gradient),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Fill {
|
||||
/// Construct a new [Fill::Solid] from a [Color].
|
||||
pub fn solid(color: Color) -> Self {
|
||||
Self::Solid(color)
|
||||
}
|
||||
|
||||
/// Construct a new [Fill::Solid] or [Fill::None] from an optional [Color].
|
||||
pub fn solid_or_none(color: Option<Color>) -> Self {
|
||||
match color {
|
||||
Some(color) => Self::Solid(color),
|
||||
None => Self::None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Evaluate the color at some point on the fill. Doesn't currently work for Gradient.
|
||||
pub fn color(&self) -> Color {
|
||||
match self {
|
||||
Self::None => Color::BLACK,
|
||||
Self::Solid(color) => *color,
|
||||
// TODO: Should correctly sample the gradient the equation here: https://svgwg.org/svg2-draft/pservers.html#Gradients
|
||||
Self::Gradient(Gradient { stops, .. }) => stops.0[0].1,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn lerp(&self, other: &Self, time: f64) -> Self {
|
||||
let transparent = Self::solid(Color::TRANSPARENT);
|
||||
let a = if *self == Self::None { &transparent } else { self };
|
||||
let b = if *other == Self::None { &transparent } else { other };
|
||||
|
||||
match (a, b) {
|
||||
(Self::Solid(a), Self::Solid(b)) => Self::Solid(a.lerp(b, time as f32)),
|
||||
(Self::Solid(a), Self::Gradient(b)) => {
|
||||
let mut solid_to_gradient = b.clone();
|
||||
solid_to_gradient.stops.0.iter_mut().for_each(|(_, color)| *color = *a);
|
||||
let a = &solid_to_gradient;
|
||||
Self::Gradient(a.lerp(b, time))
|
||||
}
|
||||
(Self::Gradient(a), Self::Solid(b)) => {
|
||||
let mut gradient_to_solid = a.clone();
|
||||
gradient_to_solid.stops.0.iter_mut().for_each(|(_, color)| *color = *b);
|
||||
let b = &gradient_to_solid;
|
||||
Self::Gradient(a.lerp(b, time))
|
||||
}
|
||||
(Self::Gradient(a), Self::Gradient(b)) => Self::Gradient(a.lerp(b, time)),
|
||||
_ => Self::None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract a gradient from the fill
|
||||
pub fn as_gradient(&self) -> Option<&Gradient> {
|
||||
match self {
|
||||
Self::Gradient(gradient) => Some(gradient),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract a solid color from the fill
|
||||
pub fn as_solid(&self) -> Option<Color> {
|
||||
match self {
|
||||
Self::Solid(color) => Some(*color),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Find if fill can be represented with only opaque colors
|
||||
pub fn is_opaque(&self) -> bool {
|
||||
match self {
|
||||
Fill::Solid(color) => color.is_opaque(),
|
||||
Fill::Gradient(gradient) => gradient.stops.iter().all(|(_, color)| color.is_opaque()),
|
||||
Fill::None => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns if fill is none
|
||||
pub fn is_none(&self) -> bool {
|
||||
*self == Self::None
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Color> for Fill {
|
||||
fn from(color: Color) -> Fill {
|
||||
Fill::Solid(color)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Option<Color>> for Fill {
|
||||
fn from(color: Option<Color>) -> Fill {
|
||||
Fill::solid_or_none(color)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Gradient> for Fill {
|
||||
fn from(gradient: Gradient) -> Fill {
|
||||
Fill::Gradient(gradient)
|
||||
}
|
||||
}
|
||||
|
||||
/// Describes the fill of a layer, but unlike [`Fill`], this doesn't store a [`Gradient`] directly but just its [`GradientStops`].
|
||||
///
|
||||
/// Can be None, a solid [Color], or a linear/radial [Gradient].
|
||||
///
|
||||
/// In the future we'll probably also add a pattern fill.
|
||||
#[repr(C)]
|
||||
#[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, DynAny, Hash, specta::Type)]
|
||||
pub enum FillChoice {
|
||||
#[default]
|
||||
None,
|
||||
/// WARNING: Color is gamma, not linear!
|
||||
Solid(Color),
|
||||
/// WARNING: Color stops are gamma, not linear!
|
||||
Gradient(GradientStops),
|
||||
}
|
||||
|
||||
impl FillChoice {
|
||||
pub fn as_solid(&self) -> Option<Color> {
|
||||
let Self::Solid(color) = self else { return None };
|
||||
Some(*color)
|
||||
}
|
||||
|
||||
pub fn as_gradient(&self) -> Option<&GradientStops> {
|
||||
let Self::Gradient(gradient) = self else { return None };
|
||||
Some(gradient)
|
||||
}
|
||||
|
||||
/// Convert this [`FillChoice`] to a [`Fill`] using the provided [`Gradient`] as a base for the positional information of the gradient.
|
||||
/// If a gradient isn't provided, default gradient positional information is used in cases where the [`FillChoice`] is a [`Gradient`].
|
||||
pub fn to_fill(&self, existing_gradient: Option<&Gradient>) -> Fill {
|
||||
match self {
|
||||
Self::None => Fill::None,
|
||||
Self::Solid(color) => Fill::Solid(*color),
|
||||
Self::Gradient(stops) => {
|
||||
let mut fill = existing_gradient.cloned().unwrap_or_default();
|
||||
fill.stops = stops.clone();
|
||||
Fill::Gradient(fill)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Fill> for FillChoice {
|
||||
fn from(fill: Fill) -> Self {
|
||||
match fill {
|
||||
Fill::None => FillChoice::None,
|
||||
Fill::Solid(color) => FillChoice::Solid(color),
|
||||
Fill::Gradient(gradient) => FillChoice::Gradient(gradient.stops),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Enum describing the type of [Fill].
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, serde::Serialize, serde::Deserialize, DynAny, Hash, specta::Type, node_macro::ChoiceType)]
|
||||
#[widget(Radio)]
|
||||
pub enum FillType {
|
||||
#[default]
|
||||
Solid,
|
||||
Gradient,
|
||||
}
|
||||
|
||||
/// The stroke (outline) style of an SVG element.
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)]
|
||||
#[widget(Radio)]
|
||||
pub enum StrokeCap {
|
||||
#[default]
|
||||
Butt,
|
||||
Round,
|
||||
Square,
|
||||
}
|
||||
|
||||
impl StrokeCap {
|
||||
pub fn svg_name(&self) -> &'static str {
|
||||
match self {
|
||||
StrokeCap::Butt => "butt",
|
||||
StrokeCap::Round => "round",
|
||||
StrokeCap::Square => "square",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)]
|
||||
#[widget(Radio)]
|
||||
pub enum StrokeJoin {
|
||||
#[default]
|
||||
Miter,
|
||||
Bevel,
|
||||
Round,
|
||||
}
|
||||
|
||||
impl StrokeJoin {
|
||||
pub fn svg_name(&self) -> &'static str {
|
||||
match self {
|
||||
StrokeJoin::Bevel => "bevel",
|
||||
StrokeJoin::Miter => "miter",
|
||||
StrokeJoin::Round => "round",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)]
|
||||
#[widget(Radio)]
|
||||
pub enum StrokeAlign {
|
||||
#[default]
|
||||
Center,
|
||||
Inside,
|
||||
Outside,
|
||||
}
|
||||
|
||||
impl StrokeAlign {
|
||||
pub fn is_not_centered(self) -> bool {
|
||||
self != Self::Center
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)]
|
||||
#[widget(Radio)]
|
||||
pub enum PaintOrder {
|
||||
#[default]
|
||||
StrokeAbove,
|
||||
StrokeBelow,
|
||||
}
|
||||
|
||||
impl PaintOrder {
|
||||
pub fn is_default(self) -> bool {
|
||||
self == Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn daffine2_identity() -> DAffine2 {
|
||||
DAffine2::IDENTITY
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, DynAny, specta::Type)]
|
||||
#[serde(default)]
|
||||
pub struct Stroke {
|
||||
/// Stroke color
|
||||
pub color: Option<Color>,
|
||||
/// Line thickness
|
||||
pub weight: f64,
|
||||
pub dash_lengths: Vec<f64>,
|
||||
pub dash_offset: f64,
|
||||
#[serde(alias = "line_cap")]
|
||||
pub cap: StrokeCap,
|
||||
#[serde(alias = "line_join")]
|
||||
pub join: StrokeJoin,
|
||||
#[serde(alias = "line_join_miter_limit")]
|
||||
pub join_miter_limit: f64,
|
||||
#[serde(default)]
|
||||
pub align: StrokeAlign,
|
||||
#[serde(default = "daffine2_identity")]
|
||||
pub transform: DAffine2,
|
||||
#[serde(default)]
|
||||
pub non_scaling: bool,
|
||||
#[serde(default)]
|
||||
pub paint_order: PaintOrder,
|
||||
}
|
||||
|
||||
impl std::hash::Hash for Stroke {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.color.hash(state);
|
||||
self.weight.to_bits().hash(state);
|
||||
{
|
||||
self.dash_lengths.len().hash(state);
|
||||
self.dash_lengths.iter().for_each(|length| length.to_bits().hash(state));
|
||||
}
|
||||
self.dash_offset.to_bits().hash(state);
|
||||
self.cap.hash(state);
|
||||
self.join.hash(state);
|
||||
self.join_miter_limit.to_bits().hash(state);
|
||||
self.align.hash(state);
|
||||
self.transform.to_cols_array().iter().for_each(|x| x.to_bits().hash(state));
|
||||
self.non_scaling.hash(state);
|
||||
self.paint_order.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl 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 {
|
||||
color,
|
||||
weight,
|
||||
dash_lengths: Vec::new(),
|
||||
dash_offset: 0.,
|
||||
cap: StrokeCap::Butt,
|
||||
join: StrokeJoin::Miter,
|
||||
join_miter_limit: 4.,
|
||||
align: StrokeAlign::Center,
|
||||
transform: DAffine2::IDENTITY,
|
||||
non_scaling: false,
|
||||
paint_order: PaintOrder::StrokeAbove,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn lerp(&self, other: &Self, time: f64) -> Self {
|
||||
Self {
|
||||
color: self.color.map(|color| color.lerp(&other.color.unwrap_or(color), time as f32)),
|
||||
weight: self.weight + (other.weight - self.weight) * time,
|
||||
dash_lengths: self.dash_lengths.iter().zip(other.dash_lengths.iter()).map(|(a, b)| a + (b - a) * time).collect(),
|
||||
dash_offset: self.dash_offset + (other.dash_offset - self.dash_offset) * time,
|
||||
cap: if time < 0.5 { self.cap } else { other.cap },
|
||||
join: if time < 0.5 { self.join } else { other.join },
|
||||
join_miter_limit: self.join_miter_limit + (other.join_miter_limit - self.join_miter_limit) * time,
|
||||
align: if time < 0.5 { self.align } else { other.align },
|
||||
transform: DAffine2::from_mat2_translation(
|
||||
time * self.transform.matrix2 + (1. - time) * other.transform.matrix2,
|
||||
self.transform.translation * time + other.transform.translation * (1. - time),
|
||||
),
|
||||
non_scaling: if time < 0.5 { self.non_scaling } else { other.non_scaling },
|
||||
paint_order: if time < 0.5 { self.paint_order } else { other.paint_order },
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the current stroke color.
|
||||
pub fn color(&self) -> Option<Color> {
|
||||
self.color
|
||||
}
|
||||
|
||||
/// Get the current stroke weight.
|
||||
pub fn weight(&self) -> f64 {
|
||||
self.weight
|
||||
}
|
||||
|
||||
pub fn dash_lengths(&self) -> String {
|
||||
if self.dash_lengths.is_empty() {
|
||||
"none".to_string()
|
||||
} else {
|
||||
self.dash_lengths.iter().map(|v| v.to_string()).collect::<Vec<_>>().join(", ")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dash_offset(&self) -> f64 {
|
||||
self.dash_offset
|
||||
}
|
||||
|
||||
pub fn cap_index(&self) -> u32 {
|
||||
self.cap as u32
|
||||
}
|
||||
|
||||
pub fn join_index(&self) -> u32 {
|
||||
self.join as u32
|
||||
}
|
||||
|
||||
pub fn join_miter_limit(&self) -> f32 {
|
||||
self.join_miter_limit as f32
|
||||
}
|
||||
|
||||
pub fn with_color(mut self, color: &Option<Color>) -> Option<Self> {
|
||||
self.color = *color;
|
||||
|
||||
Some(self)
|
||||
}
|
||||
|
||||
pub fn with_weight(mut self, weight: f64) -> Self {
|
||||
self.weight = weight;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_dash_lengths(mut self, dash_lengths: &str) -> Option<Self> {
|
||||
dash_lengths
|
||||
.split(&[',', ' '])
|
||||
.filter(|x| !x.is_empty())
|
||||
.map(str::parse::<f64>)
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.ok()
|
||||
.map(|lengths| {
|
||||
self.dash_lengths = lengths;
|
||||
self
|
||||
})
|
||||
}
|
||||
|
||||
pub fn with_dash_offset(mut self, dash_offset: f64) -> Self {
|
||||
self.dash_offset = dash_offset;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_stroke_cap(mut self, stroke_cap: StrokeCap) -> Self {
|
||||
self.cap = stroke_cap;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_stroke_join(mut self, stroke_join: StrokeJoin) -> Self {
|
||||
self.join = stroke_join;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_stroke_join_miter_limit(mut self, limit: f64) -> Self {
|
||||
self.join_miter_limit = limit;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_stroke_align(mut self, stroke_align: StrokeAlign) -> Self {
|
||||
self.align = stroke_align;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_non_scaling(mut self, non_scaling: bool) -> Self {
|
||||
self.non_scaling = non_scaling;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn has_renderable_stroke(&self) -> bool {
|
||||
self.weight > 0. && self.color.is_some_and(|color| color.a() != 0.)
|
||||
}
|
||||
}
|
||||
|
||||
// Having an alpha of 1 to start with leads to a better experience with the properties panel
|
||||
impl Default for Stroke {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
weight: 0.,
|
||||
color: Some(Color::from_rgba8_srgb(0, 0, 0, 255)),
|
||||
dash_lengths: Vec::new(),
|
||||
dash_offset: 0.,
|
||||
cap: StrokeCap::Butt,
|
||||
join: StrokeJoin::Miter,
|
||||
join_miter_limit: 4.,
|
||||
align: StrokeAlign::Center,
|
||||
transform: DAffine2::IDENTITY,
|
||||
non_scaling: false,
|
||||
paint_order: PaintOrder::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize, DynAny, specta::Type)]
|
||||
pub struct PathStyle {
|
||||
pub stroke: Option<Stroke>,
|
||||
pub fill: Fill,
|
||||
}
|
||||
|
||||
impl std::hash::Hash for PathStyle {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.stroke.hash(state);
|
||||
self.fill.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for PathStyle {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let fill = &self.fill;
|
||||
|
||||
let stroke = match &self.stroke {
|
||||
Some(stroke) => format!("#{} (Weight: {} px)", stroke.color.map_or("None".to_string(), |c| c.to_rgba_hex_srgb()), stroke.weight),
|
||||
None => "None".to_string(),
|
||||
};
|
||||
|
||||
write!(f, "Fill: {fill}\nStroke: {stroke}")
|
||||
}
|
||||
}
|
||||
|
||||
impl PathStyle {
|
||||
pub const fn new(stroke: Option<Stroke>, fill: Fill) -> Self {
|
||||
Self { stroke, fill }
|
||||
}
|
||||
|
||||
pub fn lerp(&self, other: &Self, time: f64) -> Self {
|
||||
Self {
|
||||
fill: self.fill.lerp(&other.fill, time),
|
||||
stroke: match (self.stroke.as_ref(), other.stroke.as_ref()) {
|
||||
(Some(a), Some(b)) => Some(a.lerp(b, time)),
|
||||
(Some(a), None) => {
|
||||
if time < 0.5 {
|
||||
Some(a.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
(None, Some(b)) => {
|
||||
if time < 0.5 {
|
||||
Some(b.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
(None, None) => None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the current path's [Fill].
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use graphene_vector::style::{Fill, PathStyle};
|
||||
/// # use graphene_core::color::Color;
|
||||
/// let fill = Fill::solid(Color::RED);
|
||||
/// let style = PathStyle::new(None, fill.clone());
|
||||
///
|
||||
/// assert_eq!(*style.fill(), fill);
|
||||
/// ```
|
||||
pub fn fill(&self) -> &Fill {
|
||||
&self.fill
|
||||
}
|
||||
|
||||
/// Get the current path's [Stroke].
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use graphene_vector::style::{Fill, Stroke, PathStyle};
|
||||
/// # use graphene_core::color::Color;
|
||||
/// let stroke = Stroke::new(Some(Color::GREEN), 42.);
|
||||
/// let style = PathStyle::new(Some(stroke.clone()), Fill::None);
|
||||
///
|
||||
/// assert_eq!(style.stroke(), Some(stroke));
|
||||
/// ```
|
||||
pub fn stroke(&self) -> Option<Stroke> {
|
||||
self.stroke.clone()
|
||||
}
|
||||
|
||||
/// Replace the path's [Fill] with a provided one.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use graphene_vector::style::{Fill, PathStyle};
|
||||
/// # use graphene_core::color::Color;
|
||||
/// let mut style = PathStyle::default();
|
||||
///
|
||||
/// assert_eq!(*style.fill(), Fill::None);
|
||||
///
|
||||
/// let fill = Fill::solid(Color::RED);
|
||||
/// style.set_fill(fill.clone());
|
||||
///
|
||||
/// assert_eq!(*style.fill(), fill);
|
||||
/// ```
|
||||
pub fn set_fill(&mut self, fill: Fill) {
|
||||
self.fill = fill;
|
||||
}
|
||||
|
||||
pub fn set_stroke_transform(&mut self, transform: DAffine2) {
|
||||
if let Some(stroke) = &mut self.stroke {
|
||||
stroke.transform = transform;
|
||||
}
|
||||
}
|
||||
|
||||
/// Replace the path's [Stroke] with a provided one.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use graphene_vector::style::{Stroke, PathStyle};
|
||||
/// # use graphene_core::color::Color;
|
||||
/// let mut style = PathStyle::default();
|
||||
///
|
||||
/// assert_eq!(style.stroke(), None);
|
||||
///
|
||||
/// let stroke = Stroke::new(Some(Color::GREEN), 42.);
|
||||
/// style.set_stroke(stroke.clone());
|
||||
///
|
||||
/// assert_eq!(style.stroke(), Some(stroke));
|
||||
/// ```
|
||||
pub fn set_stroke(&mut self, stroke: Stroke) {
|
||||
self.stroke = Some(stroke);
|
||||
}
|
||||
|
||||
/// Set the path's fill to None.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use graphene_vector::style::{Fill, PathStyle};
|
||||
/// # use graphene_core::color::Color;
|
||||
/// let mut style = PathStyle::new(None, Fill::Solid(Color::RED));
|
||||
///
|
||||
/// assert_ne!(*style.fill(), Fill::None);
|
||||
///
|
||||
/// style.clear_fill();
|
||||
///
|
||||
/// assert_eq!(*style.fill(), Fill::None);
|
||||
/// ```
|
||||
pub fn clear_fill(&mut self) {
|
||||
self.fill = Fill::None;
|
||||
}
|
||||
|
||||
/// Set the path's stroke to None.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use graphene_vector::style::{Fill, Stroke, PathStyle};
|
||||
/// # use graphene_core::color::Color;
|
||||
/// let mut style = PathStyle::new(Some(Stroke::new(Some(Color::GREEN), 42.)), Fill::None);
|
||||
///
|
||||
/// assert!(style.stroke().is_some());
|
||||
///
|
||||
/// style.clear_stroke();
|
||||
///
|
||||
/// assert!(!style.stroke().is_some());
|
||||
/// ```
|
||||
pub fn clear_stroke(&mut self) {
|
||||
self.stroke = None;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,744 @@
|
||||
mod attributes;
|
||||
mod indexed;
|
||||
|
||||
use crate::click_target::{ClickTargetType, FreePoint};
|
||||
use crate::dvec2_to_point;
|
||||
use crate::style::{PathStyle, Stroke};
|
||||
pub use attributes::*;
|
||||
use bezier_rs::{BezierHandles, ManipulatorGroup};
|
||||
use core::borrow::Borrow;
|
||||
use core::hash::Hash;
|
||||
use dyn_any::DynAny;
|
||||
use glam::{DAffine2, DVec2};
|
||||
use graphene_core::blending::AlphaBlending;
|
||||
use graphene_core::bounds::BoundingBox;
|
||||
use graphene_core::color::Color;
|
||||
use graphene_core::instances::Instances;
|
||||
use graphene_core::math::quad::Quad;
|
||||
use graphene_core::transform::Transform;
|
||||
pub use indexed::VectorDataIndex;
|
||||
use kurbo::{Affine, Rect, Shape};
|
||||
use std::any::Any;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub trait AnyUpstreamGraphicGroup: Any + serde::Serialize + for<'a> serde::Deserialize<'a> {}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DynAny, serde::Serialize, serde::Deserialize)]
|
||||
pub struct UpstreamGraphicGroup(Arc<dyn AnyUpstreamGraphicGroup>);
|
||||
|
||||
// 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<UpstreamGraphicGroup>,
|
||||
}
|
||||
|
||||
#[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.
|
||||
#[derive(Clone, Debug, PartialEq, DynAny, serde::Serialize, serde::Deserialize)]
|
||||
pub struct VectorData {
|
||||
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<UpstreamGraphicGroup>,
|
||||
}
|
||||
|
||||
impl Default for VectorData {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
style: PathStyle::new(Some(Stroke::new(Some(Color::BLACK), 0.)), super::style::Fill::None),
|
||||
colinear_manipulators: Vec::new(),
|
||||
point_domain: PointDomain::new(),
|
||||
segment_domain: SegmentDomain::new(),
|
||||
region_domain: RegionDomain::new(),
|
||||
upstream_graphic_group: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::hash::Hash for VectorData {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.point_domain.hash(state);
|
||||
self.segment_domain.hash(state);
|
||||
self.region_domain.hash(state);
|
||||
self.style.hash(state);
|
||||
self.colinear_manipulators.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
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 },
|
||||
};
|
||||
let [mut first_seg, mut last_seg] = [None, None];
|
||||
let mut segment_id = self.segment_domain.next_id();
|
||||
let mut last_point = None;
|
||||
let mut first_point = None;
|
||||
|
||||
// Construct a bezier segment from the two manipulators on the subpath.
|
||||
for pair in subpath.manipulator_groups().windows(2) {
|
||||
let start = last_point.unwrap_or_else(|| {
|
||||
let id = if preserve_id && !self.point_domain.ids().contains(&pair[0].id) {
|
||||
pair[0].id
|
||||
} else {
|
||||
point_id.next_id()
|
||||
};
|
||||
self.point_domain.push(id, pair[0].anchor);
|
||||
self.point_domain.ids().len() - 1
|
||||
});
|
||||
first_point = Some(first_point.unwrap_or(start));
|
||||
let end = if preserve_id && !self.point_domain.ids().contains(&pair[1].id) {
|
||||
pair[1].id
|
||||
} else {
|
||||
point_id.next_id()
|
||||
};
|
||||
let end_index = self.point_domain.ids().len();
|
||||
self.point_domain.push(end, pair[1].anchor);
|
||||
|
||||
let id = segment_id.next_id();
|
||||
first_seg = Some(first_seg.unwrap_or(id));
|
||||
last_seg = Some(id);
|
||||
self.segment_domain.push(id, start, end_index, handles(&pair[0], &pair[1]), stroke_id);
|
||||
|
||||
last_point = Some(end_index);
|
||||
}
|
||||
|
||||
let fill_id = FillId::ZERO;
|
||||
|
||||
if subpath.closed() {
|
||||
if let (Some(last), Some(first), Some(first_id), Some(last_id)) = (subpath.manipulator_groups().last(), subpath.manipulator_groups().first(), first_point, last_point) {
|
||||
let id = segment_id.next_id();
|
||||
first_seg = Some(first_seg.unwrap_or(id));
|
||||
last_seg = Some(id);
|
||||
self.segment_domain.push(id, last_id, first_id, handles(last, first), stroke_id);
|
||||
}
|
||||
|
||||
if let [Some(first_seg), Some(last_seg)] = [first_seg, last_seg] {
|
||||
self.region_domain.push(self.region_domain.next_id(), first_seg..=last_seg, fill_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn append_free_point(&mut self, point: &FreePoint, preserve_id: bool) {
|
||||
let mut point_id = self.point_domain.next_id();
|
||||
|
||||
// Use the current point ID if it's not already in the domain, otherwise generate a new one
|
||||
let id = if preserve_id && !self.point_domain.ids().contains(&point.id) {
|
||||
point.id
|
||||
} else {
|
||||
point_id.next_id()
|
||||
};
|
||||
self.point_domain.push(id, point.position);
|
||||
}
|
||||
|
||||
/// Construct some new vector data from a single subpath with an identity transform and black fill.
|
||||
pub fn from_subpath(subpath: impl Borrow<bezier_rs::Subpath<PointId>>) -> Self {
|
||||
Self::from_subpaths([subpath], false)
|
||||
}
|
||||
|
||||
/// 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();
|
||||
|
||||
for subpath in subpaths.into_iter() {
|
||||
vector_data.append_subpath(subpath, preserve_id);
|
||||
}
|
||||
|
||||
vector_data
|
||||
}
|
||||
|
||||
pub fn from_target_types(target_types: impl IntoIterator<Item = impl Borrow<ClickTargetType>>, preserve_id: bool) -> Self {
|
||||
let mut vector_data = 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),
|
||||
}
|
||||
}
|
||||
|
||||
vector_data
|
||||
}
|
||||
|
||||
/// Compute the bounding boxes of the bezpaths without any transform
|
||||
pub fn bounding_box_rect(&self) -> Option<Rect> {
|
||||
self.bounding_box_with_transform_rect(DAffine2::IDENTITY)
|
||||
}
|
||||
|
||||
pub fn close_subpaths(&mut self) {
|
||||
let segments_to_add: Vec<_> = self
|
||||
.stroke_bezier_paths()
|
||||
.filter(|subpath| !subpath.closed)
|
||||
.filter_map(|subpath| {
|
||||
let (first, last) = subpath.manipulator_groups().first().zip(subpath.manipulator_groups().last())?;
|
||||
let (start, end) = self.point_domain.resolve_id(first.id).zip(self.point_domain.resolve_id(last.id))?;
|
||||
Some((start, end))
|
||||
})
|
||||
.collect();
|
||||
|
||||
for (start, end) in segments_to_add {
|
||||
let segment_id = self.segment_domain.next_id().next_id();
|
||||
self.segment_domain.push(segment_id, start, end, bezier_rs::BezierHandles::Linear, StrokeId::ZERO);
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute the bounding boxes of the subpaths without any transform
|
||||
pub fn bounding_box(&self) -> Option<[DVec2; 2]> {
|
||||
self.bounding_box_with_transform_rect(DAffine2::IDENTITY)
|
||||
.map(|rect| [DVec2::new(rect.x0, rect.y0), DVec2::new(rect.x1, rect.y1)])
|
||||
}
|
||||
|
||||
/// Compute the bounding boxes of the subpaths with the specified transform
|
||||
pub fn bounding_box_with_transform(&self, transform: DAffine2) -> Option<[DVec2; 2]> {
|
||||
self.bounding_box_with_transform_rect(transform)
|
||||
.map(|rect| [DVec2::new(rect.x0, rect.y0), DVec2::new(rect.x1, rect.y1)])
|
||||
}
|
||||
|
||||
/// Compute the bounding boxes of the bezpaths with the specified transform
|
||||
pub fn bounding_box_with_transform_rect(&self, transform: DAffine2) -> Option<Rect> {
|
||||
let combine = |r1: Rect, r2: Rect| r1.union(r2);
|
||||
self.stroke_bezpath_iter()
|
||||
.map(|mut bezpath| {
|
||||
bezpath.apply_affine(Affine::new(transform.to_cols_array()));
|
||||
bezpath.bounding_box()
|
||||
})
|
||||
.reduce(combine)
|
||||
}
|
||||
|
||||
/// Calculate the corners of the bounding box but with a nonzero size.
|
||||
///
|
||||
/// If the layer bounds are `0` in either axis then they are changed to be `1`.
|
||||
pub fn nonzero_bounding_box(&self) -> [DVec2; 2] {
|
||||
let [bounds_min, mut bounds_max] = self.bounding_box().unwrap_or_default();
|
||||
|
||||
let bounds_size = bounds_max - bounds_min;
|
||||
if bounds_size.x < 1e-10 {
|
||||
bounds_max.x = bounds_min.x + 1.;
|
||||
}
|
||||
if bounds_size.y < 1e-10 {
|
||||
bounds_max.y = bounds_min.y + 1.;
|
||||
}
|
||||
|
||||
[bounds_min, bounds_max]
|
||||
}
|
||||
|
||||
/// Compute the pivot of the layer in layerspace (the coordinates of the subpaths)
|
||||
pub fn layerspace_pivot(&self, normalized_pivot: DVec2) -> DVec2 {
|
||||
let [bounds_min, bounds_max] = self.nonzero_bounding_box();
|
||||
let bounds_size = bounds_max - bounds_min;
|
||||
bounds_min + bounds_size * normalized_pivot
|
||||
}
|
||||
|
||||
pub fn start_point(&self) -> impl Iterator<Item = PointId> + '_ {
|
||||
self.segment_domain.start_point().iter().map(|&index| self.point_domain.ids()[index])
|
||||
}
|
||||
|
||||
pub fn end_point(&self) -> impl Iterator<Item = PointId> + '_ {
|
||||
self.segment_domain.end_point().iter().map(|&index| self.point_domain.ids()[index])
|
||||
}
|
||||
|
||||
pub fn push(&mut self, id: SegmentId, start: PointId, end: PointId, handles: bezier_rs::BezierHandles, stroke: StrokeId) {
|
||||
let [Some(start), Some(end)] = [start, end].map(|id| self.point_domain.resolve_id(id)) else {
|
||||
return;
|
||||
};
|
||||
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)> {
|
||||
self.segment_domain
|
||||
.handles_mut()
|
||||
.map(|(id, handles, start, end)| (id, handles, self.point_domain.ids()[start], self.point_domain.ids()[end]))
|
||||
}
|
||||
|
||||
pub fn segment_start_from_id(&self, segment: SegmentId) -> Option<PointId> {
|
||||
self.segment_domain.segment_start_from_id(segment).map(|index| self.point_domain.ids()[index])
|
||||
}
|
||||
|
||||
pub fn segment_end_from_id(&self, segment: SegmentId) -> Option<PointId> {
|
||||
self.segment_domain.segment_end_from_id(segment).map(|index| self.point_domain.ids()[index])
|
||||
}
|
||||
|
||||
/// Returns an array for the start and end points of a segment.
|
||||
pub fn points_from_id(&self, segment: SegmentId) -> Option<[PointId; 2]> {
|
||||
self.segment_domain.points_from_id(segment).map(|val| val.map(|index| self.point_domain.ids()[index]))
|
||||
}
|
||||
|
||||
/// Attempts to find another point in the segment that is not the one passed in.
|
||||
pub fn other_point(&self, segment: SegmentId, current: PointId) -> Option<PointId> {
|
||||
let index = self.point_domain.resolve_id(current);
|
||||
index.and_then(|index| self.segment_domain.other_point(segment, index)).map(|index| self.point_domain.ids()[index])
|
||||
}
|
||||
|
||||
/// Gets all points connected to the current one but not including the current one.
|
||||
pub fn connected_points(&self, current: PointId) -> impl Iterator<Item = PointId> + '_ {
|
||||
let index = [self.point_domain.resolve_id(current)].into_iter().flatten();
|
||||
index.flat_map(|index| self.segment_domain.connected_points(index).map(|index| self.point_domain.ids()[index]))
|
||||
}
|
||||
|
||||
/// Returns the number of linear segments connected to the given point.
|
||||
pub fn connected_linear_segments(&self, point_id: PointId) -> usize {
|
||||
self.segment_bezier_iter()
|
||||
.filter(|(_, bez, start, end)| ((*start == point_id || *end == point_id) && matches!(bez.handles, BezierHandles::Linear)))
|
||||
.count()
|
||||
}
|
||||
|
||||
/// Get an array slice of all segment IDs.
|
||||
pub fn segment_ids(&self) -> &[SegmentId] {
|
||||
self.segment_domain.ids()
|
||||
}
|
||||
|
||||
/// Enumerate all segments that start at the point.
|
||||
pub fn start_connected(&self, point: PointId) -> impl Iterator<Item = SegmentId> + '_ {
|
||||
let index = [self.point_domain.resolve_id(point)].into_iter().flatten();
|
||||
index.flat_map(|index| self.segment_domain.start_connected(index))
|
||||
}
|
||||
|
||||
/// Enumerate all segments that end at the point.
|
||||
pub fn end_connected(&self, point: PointId) -> impl Iterator<Item = SegmentId> + '_ {
|
||||
let index = [self.point_domain.resolve_id(point)].into_iter().flatten();
|
||||
index.flat_map(|index| self.segment_domain.end_connected(index))
|
||||
}
|
||||
|
||||
/// Enumerate all segments that start or end at a point, converting them to [`HandleId`s]. Note that the handles may not exist e.g. for a linear segment.
|
||||
pub fn all_connected(&self, point: PointId) -> impl Iterator<Item = HandleId> + '_ {
|
||||
let index = [self.point_domain.resolve_id(point)].into_iter().flatten();
|
||||
index.flat_map(|index| self.segment_domain.all_connected(index))
|
||||
}
|
||||
|
||||
/// Enumerate the number of segments connected to a point. If a segment starts and ends at a point then it is counted twice.
|
||||
pub fn connected_count(&self, point: PointId) -> usize {
|
||||
self.point_domain.resolve_id(point).map_or(0, |point| self.segment_domain.connected_count(point))
|
||||
}
|
||||
|
||||
pub fn check_point_inside_shape(&self, vector_data_transform: DAffine2, point: DVec2) -> bool {
|
||||
let bez_paths: Vec<_> = 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.close_path();
|
||||
let bbox = bezpath.bounding_box();
|
||||
(bezpath, bbox)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Check against all paths the point is contained in to compute the correct winding number
|
||||
let mut number = 0;
|
||||
|
||||
for (shape, bbox) in bez_paths {
|
||||
if bbox.x0 > point.x || bbox.y0 > point.y || bbox.x1 < point.x || bbox.y1 < point.y {
|
||||
continue;
|
||||
}
|
||||
|
||||
let winding = shape.winding(dvec2_to_point(point));
|
||||
number += winding;
|
||||
}
|
||||
|
||||
// Non-zero fill rule
|
||||
number != 0
|
||||
}
|
||||
|
||||
/// Points that can be extended from.
|
||||
///
|
||||
/// This is usually only points with exactly one connection unless vector meshes are enabled.
|
||||
pub fn extendable_points(&self, vector_meshes: bool) -> impl Iterator<Item = PointId> + '_ {
|
||||
let point_ids = self.point_domain.ids().iter().enumerate();
|
||||
point_ids.filter(move |(index, _)| vector_meshes || self.segment_domain.connected_count(*index) == 1).map(|(_, &id)| id)
|
||||
}
|
||||
|
||||
/// Computes if all the connected handles are colinear for an anchor, or if that handle is colinear for a handle.
|
||||
pub fn colinear(&self, point: ManipulatorPointId) -> bool {
|
||||
let has_handle = |target| self.colinear_manipulators.iter().flatten().any(|&handle| handle == target);
|
||||
match point {
|
||||
ManipulatorPointId::Anchor(id) => {
|
||||
self.start_connected(id).all(|segment| has_handle(HandleId::primary(segment))) && self.end_connected(id).all(|segment| has_handle(HandleId::end(segment)))
|
||||
}
|
||||
ManipulatorPointId::PrimaryHandle(segment) => has_handle(HandleId::primary(segment)),
|
||||
ManipulatorPointId::EndHandle(segment) => has_handle(HandleId::end(segment)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn other_colinear_handle(&self, handle: HandleId) -> Option<HandleId> {
|
||||
let pair = self.colinear_manipulators.iter().find(|pair| pair.contains(&handle))?;
|
||||
let other = pair.iter().copied().find(|&val| val != handle)?;
|
||||
if handle.to_manipulator_point().get_anchor(self) == other.to_manipulator_point().get_anchor(self) {
|
||||
Some(other)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn adjacent_segment(&self, manipulator_id: &ManipulatorPointId) -> Option<(PointId, SegmentId)> {
|
||||
match manipulator_id {
|
||||
ManipulatorPointId::PrimaryHandle(segment_id) => {
|
||||
// For start handle, find segments ending at our start point
|
||||
let (start_point_id, _, _) = self.segment_points_from_id(*segment_id)?;
|
||||
let start_index = self.point_domain.resolve_id(start_point_id)?;
|
||||
|
||||
self.segment_domain.end_connected(start_index).find(|&id| id != *segment_id).map(|id| (start_point_id, id)).or(self
|
||||
.segment_domain
|
||||
.start_connected(start_index)
|
||||
.find(|&id| id != *segment_id)
|
||||
.map(|id| (start_point_id, id)))
|
||||
}
|
||||
ManipulatorPointId::EndHandle(segment_id) => {
|
||||
// For end handle, find segments starting at our end point
|
||||
let (_, end_point_id, _) = self.segment_points_from_id(*segment_id)?;
|
||||
let end_index = self.point_domain.resolve_id(end_point_id)?;
|
||||
|
||||
self.segment_domain.start_connected(end_index).find(|&id| id != *segment_id).map(|id| (end_point_id, id)).or(self
|
||||
.segment_domain
|
||||
.end_connected(end_index)
|
||||
.find(|&id| id != *segment_id)
|
||||
.map(|id| (end_point_id, id)))
|
||||
}
|
||||
ManipulatorPointId::Anchor(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn concat(&mut self, additional: &Self, transform_of_additional: DAffine2, collision_hash_seed: u64) {
|
||||
let point_map = additional
|
||||
.point_domain
|
||||
.ids()
|
||||
.iter()
|
||||
.filter(|id| self.point_domain.ids().contains(id))
|
||||
.map(|&old| (old, old.generate_from_hash(collision_hash_seed)))
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
let segment_map = additional
|
||||
.segment_domain
|
||||
.ids()
|
||||
.iter()
|
||||
.filter(|id| self.segment_domain.ids().contains(id))
|
||||
.map(|&old| (old, old.generate_from_hash(collision_hash_seed)))
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
let region_map = additional
|
||||
.region_domain
|
||||
.ids()
|
||||
.iter()
|
||||
.filter(|id| self.region_domain.ids().contains(id))
|
||||
.map(|&old| (old, old.generate_from_hash(collision_hash_seed)))
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
let id_map = IdMap {
|
||||
point_offset: self.point_domain.ids().len(),
|
||||
point_map,
|
||||
segment_map,
|
||||
region_map,
|
||||
};
|
||||
|
||||
self.point_domain.concat(&additional.point_domain, transform_of_additional, &id_map);
|
||||
self.segment_domain.concat(&additional.segment_domain, transform_of_additional, &id_map);
|
||||
self.region_domain.concat(&additional.region_domain, transform_of_additional, &id_map);
|
||||
|
||||
// TODO: properly deal with fills such as gradients
|
||||
self.style = additional.style.clone();
|
||||
|
||||
self.colinear_manipulators.extend(additional.colinear_manipulators.iter().copied());
|
||||
}
|
||||
}
|
||||
|
||||
impl BoundingBox for VectorDataTable {
|
||||
fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> Option<[DVec2; 2]> {
|
||||
self.instance_ref_iter()
|
||||
.flat_map(|instance| {
|
||||
if !include_stroke {
|
||||
return instance.instance.bounding_box_with_transform(transform * *instance.transform);
|
||||
}
|
||||
|
||||
let stroke_width = instance.instance.style.stroke().map(|s| s.weight()).unwrap_or_default();
|
||||
|
||||
let miter_limit = instance.instance.style.stroke().map(|s| s.join_miter_limit).unwrap_or(1.);
|
||||
|
||||
let scale = transform.decompose_scale();
|
||||
|
||||
// We use the full line width here to account for different styles of stroke caps
|
||||
let offset = DVec2::splat(stroke_width * scale.x.max(scale.y) * miter_limit);
|
||||
|
||||
instance.instance.bounding_box_with_transform(transform * *instance.transform).map(|[a, b]| [a - offset, b + offset])
|
||||
})
|
||||
.reduce(Quad::combine_bounds)
|
||||
}
|
||||
}
|
||||
|
||||
/// A selectable part of a curve, either an anchor (start or end of a bézier) or a handle (doesn't necessarily go through the bézier but influences curvature).
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, DynAny, 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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_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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[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<_>>();
|
||||
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<_>>();
|
||||
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<_>>();
|
||||
assert_eq!(bezier_paths, vec![bezier]);
|
||||
|
||||
let generated = vector_data.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 vector_data = VectorData::from_subpaths([&curve, &circle], false);
|
||||
assert_eq!(vector_data.point_domain.ids().len(), 6);
|
||||
|
||||
let bezier_paths = vector_data.segment_bezier_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<_>>();
|
||||
assert_subpath_eq(&generated, &[curve, circle]);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,90 @@
|
||||
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 point_graph: UnGraph<Point, ()>,
|
||||
pub segment_to_edge: FxHashMap<SegmentId, EdgeIndex>,
|
||||
/// Get the offset from the point ID.
|
||||
pub 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]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user