mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-08-04 13:30:48 +00:00
Add nondestructive vector editing (#1676)
* Initial vector modify node * Initial extraction of data from monitor nodes * Migrate to point id * Start converting to modify node * Non destructive spline tool (tout le reste est cassé) * Fix unconnected modify node * Fix freehand tool * Pen tool * Migrate demo art * Select points * Fix the demo artwork * Fix the X and Y inputs for path tool * G1 continous toggle * Delete points * Fix test * Insert point * Improve robustness of handles * Fix GRS shortcuts on path * Dragging points * Fix build * Preserve opposing handle lengths * Update demo art and snapping * Fix polygon tool * Double click end anchor * Improve dragging * Fix text shifting * Select only connected verts * Colinear alt * Cleanup * Fix imports * Improve pen tool avoiding handle placement * Improve disolve * Remove pivot widget from Transform node properties * Fix demo art * Fix bugs * Re-save demo artwork * Code review * Serialize hashmap as tuple vec to enable deserialize_inputs * Fix migrate * Add document upgrade function to editor_api.rs * Finalize document upgrading * Rename to the Path node * Remove smoothing from Freehand tool * Upgrade demo artwork * Propertly disable raw-rs tests --------- Co-authored-by: Keavon Chambers <keavon@keavon.com> Co-authored-by: Adam <adamgerhant@gmail.com> Co-authored-by: Dennis Kobert <dennis@kobert.dev>
This commit is contained in:
parent
fd3613018a
commit
1652c713a6
96 changed files with 3343 additions and 2622 deletions
|
@ -65,3 +65,7 @@ rand = { workspace = true, default-features = false, features = ["std_rng"] }
|
|||
|
||||
[dev-dependencies]
|
||||
tokio = { workspace = true, features = ["rt", "macros"] }
|
||||
|
||||
[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"))'] }
|
||||
|
|
|
@ -334,8 +334,14 @@ impl GraphicElementRendered for VectorData {
|
|||
|
||||
fn add_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
|
||||
let stroke_width = self.style.stroke().as_ref().map_or(0., crate::vector::style::Stroke::weight);
|
||||
click_targets.extend(self.region_bezier_paths().map(|(_, subpath)| ClickTarget { stroke_width, subpath }));
|
||||
click_targets.extend(self.stroke_bezier_paths().map(|subpath| ClickTarget { stroke_width, subpath }));
|
||||
let filled = self.style.fill() != &crate::vector::style::Fill::None;
|
||||
let fill = |mut subpath: bezier_rs::Subpath<_>| {
|
||||
if filled {
|
||||
subpath.set_closed(true);
|
||||
}
|
||||
subpath
|
||||
};
|
||||
click_targets.extend(self.stroke_bezier_paths().map(fill).map(|subpath| ClickTarget { stroke_width, subpath }));
|
||||
}
|
||||
|
||||
fn to_usvg_node(&self) -> usvg::Node {
|
||||
|
@ -475,7 +481,7 @@ impl GraphicElementRendered for crate::ArtboardGroup {
|
|||
}
|
||||
|
||||
fn contains_artboard(&self) -> bool {
|
||||
self.artboards.len() > 0
|
||||
!self.artboards.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -581,8 +581,9 @@ fn vibrance_node(color: Color, vibrance: f64) -> Color {
|
|||
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(feature = "std", derive(specta::Type))]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, DynAny)]
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny)]
|
||||
pub enum RedGreenBlue {
|
||||
#[default]
|
||||
Red,
|
||||
Green,
|
||||
Blue,
|
||||
|
@ -600,8 +601,9 @@ impl core::fmt::Display for RedGreenBlue {
|
|||
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(feature = "std", derive(specta::Type))]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, DynAny)]
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny)]
|
||||
pub enum RedGreenBlueAlpha {
|
||||
#[default]
|
||||
Red,
|
||||
Green,
|
||||
Blue,
|
||||
|
@ -621,8 +623,9 @@ impl core::fmt::Display for RedGreenBlueAlpha {
|
|||
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(feature = "std", derive(specta::Type))]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, DynAny)]
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny)]
|
||||
pub enum NoiseType {
|
||||
#[default]
|
||||
Perlin,
|
||||
OpenSimplex2,
|
||||
OpenSimplex2S,
|
||||
|
@ -662,8 +665,9 @@ impl NoiseType {
|
|||
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(feature = "std", derive(specta::Type))]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, DynAny)]
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny)]
|
||||
pub enum FractalType {
|
||||
#[default]
|
||||
None,
|
||||
FBm,
|
||||
Ridged,
|
||||
|
@ -700,8 +704,9 @@ impl FractalType {
|
|||
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(feature = "std", derive(specta::Type))]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, DynAny)]
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny)]
|
||||
pub enum CellularDistanceFunction {
|
||||
#[default]
|
||||
Euclidean,
|
||||
EuclideanSq,
|
||||
Manhattan,
|
||||
|
@ -732,9 +737,10 @@ impl CellularDistanceFunction {
|
|||
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(feature = "std", derive(specta::Type))]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, DynAny)]
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny)]
|
||||
pub enum CellularReturnType {
|
||||
CellValue,
|
||||
#[default]
|
||||
Nearest,
|
||||
NextNearest,
|
||||
Average,
|
||||
|
@ -773,8 +779,9 @@ impl CellularReturnType {
|
|||
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(feature = "std", derive(specta::Type))]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, DynAny)]
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny)]
|
||||
pub enum DomainWarpType {
|
||||
#[default]
|
||||
None,
|
||||
OpenSimplex2,
|
||||
OpenSimplex2Reduced,
|
||||
|
@ -867,8 +874,9 @@ fn channel_mixer_node(
|
|||
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(feature = "std", derive(specta::Type))]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, DynAny)]
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny)]
|
||||
pub enum RelativeAbsolute {
|
||||
#[default]
|
||||
Relative,
|
||||
Absolute,
|
||||
}
|
||||
|
@ -885,8 +893,9 @@ impl core::fmt::Display for RelativeAbsolute {
|
|||
#[repr(C)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(feature = "std", derive(specta::Type))]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, DynAny)]
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny)]
|
||||
pub enum SelectiveColorChoice {
|
||||
#[default]
|
||||
Reds,
|
||||
Yellows,
|
||||
Greens,
|
||||
|
|
|
@ -774,7 +774,7 @@ impl Color {
|
|||
/// ```
|
||||
#[inline(always)]
|
||||
pub fn to_rgba8_srgb(&self) -> [u8; 4] {
|
||||
let gamma = self.to_gamma_srgb();
|
||||
let gamma = self.to_gamma_srgb().to_gamma_srgb();
|
||||
[(gamma.red * 255.) as u8, (gamma.green * 255.) as u8, (gamma.blue * 255.) as u8, (gamma.alpha * 255.) as u8]
|
||||
}
|
||||
|
||||
|
|
|
@ -17,5 +17,5 @@ pub struct TextGeneratorNode<Text, FontName, Size> {
|
|||
#[node_fn(TextGeneratorNode)]
|
||||
fn generate_text<'a: 'input, T>(editor: EditorApi<'a, T>, text: String, font_name: Font, font_size: f64) -> crate::vector::VectorData {
|
||||
let buzz_face = editor.font_cache.get(&font_name).map(|data| load_face(data));
|
||||
crate::vector::VectorData::from_subpaths(to_path(&text, buzz_face, font_size, None))
|
||||
crate::vector::VectorData::from_subpaths(to_path(&text, buzz_face, font_size, None), false)
|
||||
}
|
||||
|
|
|
@ -15,7 +15,11 @@ impl Font {
|
|||
Self { font_family, font_style }
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Font {
|
||||
fn default() -> Self {
|
||||
Self::new(crate::consts::DEFAULT_FONT_FAMILY.into(), crate::consts::DEFAULT_FONT_STYLE.into())
|
||||
}
|
||||
}
|
||||
/// A cache of all loaded font data and preview urls along with the default font (send from `init_app` in `editor_api.rs`)
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default, PartialEq)]
|
||||
pub struct FontCache {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use crate::uuid::ManipulatorGroupId;
|
||||
use crate::vector::PointId;
|
||||
|
||||
use bezier_rs::{ManipulatorGroup, Subpath};
|
||||
|
||||
|
@ -7,13 +7,13 @@ use rustybuzz::ttf_parser::{GlyphId, OutlineBuilder};
|
|||
use rustybuzz::{GlyphBuffer, UnicodeBuffer};
|
||||
|
||||
struct Builder {
|
||||
current_subpath: Subpath<ManipulatorGroupId>,
|
||||
other_subpaths: Vec<Subpath<ManipulatorGroupId>>,
|
||||
current_subpath: Subpath<PointId>,
|
||||
other_subpaths: Vec<Subpath<PointId>>,
|
||||
pos: DVec2,
|
||||
offset: DVec2,
|
||||
ascender: f64,
|
||||
scale: f64,
|
||||
id: ManipulatorGroupId,
|
||||
id: PointId,
|
||||
}
|
||||
|
||||
impl Builder {
|
||||
|
@ -37,7 +37,7 @@ impl OutlineBuilder for Builder {
|
|||
fn quad_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32) {
|
||||
let [handle, anchor] = [self.point(x1, y1), self.point(x2, y2)];
|
||||
self.current_subpath.last_manipulator_group_mut().unwrap().out_handle = Some(handle);
|
||||
self.current_subpath.push_manipulator_group(ManipulatorGroup::new_anchor_with_id(anchor, self.id.next_id()));
|
||||
self.current_subpath.push_manipulator_group(ManipulatorGroup::new_with_id(anchor, None, None, self.id.next_id()));
|
||||
}
|
||||
|
||||
fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x3: f32, y3: f32) {
|
||||
|
@ -80,7 +80,7 @@ fn wrap_word(line_width: Option<f64>, glyph_buffer: &GlyphBuffer, scale: f64, x_
|
|||
false
|
||||
}
|
||||
|
||||
pub fn to_path(str: &str, buzz_face: Option<rustybuzz::Face>, font_size: f64, line_width: Option<f64>) -> Vec<Subpath<ManipulatorGroupId>> {
|
||||
pub fn to_path(str: &str, buzz_face: Option<rustybuzz::Face>, font_size: f64, line_width: Option<f64>) -> Vec<Subpath<PointId>> {
|
||||
let buzz_face = match buzz_face {
|
||||
Some(face) => face,
|
||||
// Show blank layer if font has not loaded
|
||||
|
@ -96,7 +96,7 @@ pub fn to_path(str: &str, buzz_face: Option<rustybuzz::Face>, font_size: f64, li
|
|||
offset: DVec2::ZERO,
|
||||
ascender: (buzz_face.ascender() as f64 / buzz_face.height() as f64) * font_size / scale,
|
||||
scale,
|
||||
id: ManipulatorGroupId::ZERO,
|
||||
id: PointId::ZERO,
|
||||
};
|
||||
|
||||
for line in str.split('\n') {
|
||||
|
|
|
@ -152,7 +152,7 @@ pub struct TransformNode<TransformTarget, Translation, Rotation, Scale, Shear, P
|
|||
pub(crate) rotate: Rotation,
|
||||
pub(crate) scale: Scale,
|
||||
pub(crate) shear: Shear,
|
||||
pub(crate) pivot: Pivot,
|
||||
pub(crate) _pivot: Pivot,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, dyn_any::DynAny, PartialEq)]
|
||||
|
@ -165,7 +165,7 @@ pub enum RenderQuality {
|
|||
Scale(f32),
|
||||
/// Flip a coin to decide if the render should be available with the current quality or done at full quality
|
||||
/// This should be used to gradually update the render quality of a cached node
|
||||
Probabilty(f32),
|
||||
Probability(f32),
|
||||
/// Render at full quality
|
||||
Full,
|
||||
}
|
||||
|
@ -249,23 +249,18 @@ pub(crate) async fn transform_vector_data<Fut: Future>(
|
|||
rotate: f64,
|
||||
scale: DVec2,
|
||||
shear: DVec2,
|
||||
pivot: DVec2,
|
||||
_pivot: DVec2,
|
||||
) -> Fut::Output
|
||||
where
|
||||
Fut::Output: TransformMut,
|
||||
{
|
||||
// TODO: This is hack and might break for Vector data because the pivot may be incorrect
|
||||
let transform = DAffine2::from_scale_angle_translation(scale, rotate, translate) * DAffine2::from_cols_array(&[1., shear.y, shear.x, 1., 0., 0.]);
|
||||
let modification = DAffine2::from_scale_angle_translation(scale, rotate, translate) * DAffine2::from_cols_array(&[1., shear.y, shear.x, 1., 0., 0.]);
|
||||
if !footprint.ignore_modifications {
|
||||
let pivot_transform = DAffine2::from_translation(pivot);
|
||||
let modification = pivot_transform * transform * pivot_transform.inverse();
|
||||
*footprint.transform_mut() = footprint.transform() * modification;
|
||||
}
|
||||
|
||||
let mut data = self.transform_target.eval(footprint).await;
|
||||
let pivot_transform = DAffine2::from_translation(data.local_pivot(pivot));
|
||||
|
||||
let modification = pivot_transform * transform * pivot_transform.inverse();
|
||||
let data_transform = data.transform_mut();
|
||||
*data_transform = modification * (*data_transform);
|
||||
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
use dyn_any::{DynAny, StaticType};
|
||||
|
||||
#[derive(Clone, Copy, serde::Serialize, serde::Deserialize, specta::Type)]
|
||||
pub struct Uuid(
|
||||
#[serde(with = "u64_string")]
|
||||
|
@ -69,33 +67,3 @@ mod uuid_generation {
|
|||
}
|
||||
|
||||
pub use uuid_generation::*;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, DynAny)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct ManipulatorGroupId(u64);
|
||||
|
||||
impl bezier_rs::Identifier for ManipulatorGroupId {
|
||||
fn new() -> Self {
|
||||
Self(generate_uuid())
|
||||
}
|
||||
}
|
||||
|
||||
impl ManipulatorGroupId {
|
||||
pub const ZERO: ManipulatorGroupId = ManipulatorGroupId(0);
|
||||
|
||||
pub fn next_id(&mut self) -> Self {
|
||||
let old = self.0;
|
||||
self.0 += 1;
|
||||
Self(old)
|
||||
}
|
||||
|
||||
pub(crate) fn inner(self) -> u64 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::vector::PointId> for ManipulatorGroupId {
|
||||
fn from(value: crate::vector::PointId) -> Self {
|
||||
Self(value.inner())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use crate::uuid::ManipulatorGroupId;
|
||||
use crate::vector::VectorData;
|
||||
use super::HandleId;
|
||||
use crate::vector::{PointId, VectorData};
|
||||
use crate::Node;
|
||||
|
||||
use bezier_rs::Subpath;
|
||||
|
@ -28,7 +28,14 @@ fn ellipse_generator(_input: (), radius_x: f64, radius_y: f64) -> VectorData {
|
|||
let radius = DVec2::new(radius_x, radius_y);
|
||||
let corner1 = -radius;
|
||||
let corner2 = radius;
|
||||
super::VectorData::from_subpath(Subpath::new_ellipse(corner1, corner2))
|
||||
let mut ellipse = super::VectorData::from_subpath(Subpath::new_ellipse(corner1, corner2));
|
||||
let len = ellipse.segment_domain.ids().len();
|
||||
for i in 0..len {
|
||||
ellipse
|
||||
.colinear_manipulators
|
||||
.push([HandleId::end(ellipse.segment_domain.ids()[i]), HandleId::primary(ellipse.segment_domain.ids()[(i + 1) % len])]);
|
||||
}
|
||||
ellipse
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
|
@ -46,7 +53,7 @@ trait CornerRadius {
|
|||
impl CornerRadius for f64 {
|
||||
fn generate(self, size: DVec2, clamped: bool) -> super::VectorData {
|
||||
let clamped_radius = if clamped { self.clamp(0., size.x.min(size.y).max(0.) / 2.) } else { self };
|
||||
super::VectorData::from_subpaths(vec![Subpath::new_rounded_rect(size / -2., size / 2., [clamped_radius; 4])])
|
||||
super::VectorData::from_subpath(Subpath::new_rounded_rect(size / -2., size / 2., [clamped_radius; 4]))
|
||||
}
|
||||
}
|
||||
impl CornerRadius for [f64; 4] {
|
||||
|
@ -66,7 +73,7 @@ impl CornerRadius for [f64; 4] {
|
|||
} else {
|
||||
self
|
||||
};
|
||||
super::VectorData::from_subpaths(vec![Subpath::new_rounded_rect(size / -2., size / 2., clamped_radius)])
|
||||
super::VectorData::from_subpath(Subpath::new_rounded_rect(size / -2., size / 2., clamped_radius))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -122,7 +129,11 @@ pub struct SplineGenerator<Positions> {
|
|||
|
||||
#[node_macro::node_fn(SplineGenerator)]
|
||||
fn spline_generator(_input: (), positions: Vec<DVec2>) -> VectorData {
|
||||
super::VectorData::from_subpath(Subpath::new_cubic_spline(positions))
|
||||
let mut spline = super::VectorData::from_subpath(Subpath::new_cubic_spline(positions));
|
||||
for pair in spline.segment_domain.ids().windows(2) {
|
||||
spline.colinear_manipulators.push([HandleId::end(pair[0]), HandleId::primary(pair[1])]);
|
||||
}
|
||||
spline
|
||||
}
|
||||
|
||||
// TODO(TrueDoctor): I removed the Arc requirement we should think about when it makes sense to use it vs making a generic value node
|
||||
|
@ -132,9 +143,12 @@ pub struct PathGenerator<ColinearManipulators> {
|
|||
}
|
||||
|
||||
#[node_macro::node_fn(PathGenerator)]
|
||||
fn generate_path(path_data: Vec<Subpath<ManipulatorGroupId>>, colinear_manipulators: Vec<ManipulatorGroupId>) -> super::VectorData {
|
||||
let mut vector_data = super::VectorData::from_subpaths(path_data);
|
||||
vector_data.colinear_manipulators = colinear_manipulators;
|
||||
fn generate_path(path_data: Vec<Subpath<PointId>>, colinear_manipulators: Vec<PointId>) -> super::VectorData {
|
||||
let mut vector_data = super::VectorData::from_subpaths(path_data, false);
|
||||
vector_data.colinear_manipulators = colinear_manipulators
|
||||
.iter()
|
||||
.filter_map(|&point| super::ManipulatorPointId::Anchor(point).get_handle_pair(&vector_data))
|
||||
.collect();
|
||||
vector_data
|
||||
}
|
||||
|
||||
|
|
|
@ -371,16 +371,18 @@ impl From<Fill> for FillChoice {
|
|||
|
||||
/// Enum describing the type of [Fill].
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize, DynAny, Hash, specta::Type)]
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, serde::Serialize, serde::Deserialize, DynAny, Hash, specta::Type)]
|
||||
pub enum FillType {
|
||||
#[default]
|
||||
Solid,
|
||||
Gradient,
|
||||
}
|
||||
|
||||
/// The stroke (outline) style of an SVG element.
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type)]
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type)]
|
||||
pub enum LineCap {
|
||||
#[default]
|
||||
Butt,
|
||||
Round,
|
||||
Square,
|
||||
|
@ -397,8 +399,9 @@ impl Display for LineCap {
|
|||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type)]
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type)]
|
||||
pub enum LineJoin {
|
||||
#[default]
|
||||
Miter,
|
||||
Bevel,
|
||||
Round,
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
mod attributes;
|
||||
mod modification;
|
||||
pub use attributes::*;
|
||||
pub use modification::*;
|
||||
|
||||
use super::style::{PathStyle, Stroke};
|
||||
use crate::Color;
|
||||
use crate::{uuid::ManipulatorGroupId, AlphaBlending};
|
||||
pub use attributes::*;
|
||||
use crate::{AlphaBlending, Color};
|
||||
|
||||
use bezier_rs::ManipulatorGroup;
|
||||
use dyn_any::{DynAny, StaticType};
|
||||
|
||||
use core::borrow::Borrow;
|
||||
use glam::{DAffine2, DVec2};
|
||||
|
||||
/// [VectorData] is passed between nodes.
|
||||
|
@ -20,7 +22,7 @@ pub struct VectorData {
|
|||
pub alpha_blending: AlphaBlending,
|
||||
/// 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<ManipulatorGroupId>,
|
||||
pub colinear_manipulators: Vec<[HandleId; 2]>,
|
||||
|
||||
pub point_domain: PointDomain,
|
||||
pub segment_domain: SegmentDomain,
|
||||
|
@ -54,15 +56,15 @@ impl VectorData {
|
|||
}
|
||||
|
||||
/// Construct some new vector data from a single subpath with an identity transform and black fill.
|
||||
pub fn from_subpath(subpath: bezier_rs::Subpath<ManipulatorGroupId>) -> Self {
|
||||
Self::from_subpaths([subpath])
|
||||
pub fn from_subpath(subpath: impl Borrow<bezier_rs::Subpath<PointId>>) -> Self {
|
||||
Self::from_subpaths([subpath], false)
|
||||
}
|
||||
|
||||
/// Push a subpath to the vector data
|
||||
pub fn append_subpath<Id: bezier_rs::Identifier + Into<PointId> + Copy>(&mut self, subpath: bezier_rs::Subpath<Id>) {
|
||||
for point in subpath.manipulator_groups() {
|
||||
self.point_domain.push(point.id.into(), point.anchor);
|
||||
}
|
||||
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,
|
||||
|
@ -70,33 +72,57 @@ impl VectorData {
|
|||
(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;
|
||||
for pair in subpath.manipulator_groups().windows(2) {
|
||||
let id = SegmentId::generate();
|
||||
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);
|
||||
id
|
||||
});
|
||||
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()
|
||||
};
|
||||
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, pair[0].id.into(), pair[1].id.into(), handles(&pair[0], &pair[1]), StrokeId::generate());
|
||||
self.segment_domain.push(id, start, end, handles(&pair[0], &pair[1]), stroke_id);
|
||||
|
||||
last_point = Some(end);
|
||||
}
|
||||
|
||||
let fill_id = FillId::ZERO;
|
||||
|
||||
if subpath.closed() {
|
||||
if let (Some(last), Some(first)) = (subpath.manipulator_groups().last(), subpath.manipulator_groups().first()) {
|
||||
let id = SegmentId::generate();
|
||||
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.into(), first.id.into(), handles(last, first), StrokeId::generate());
|
||||
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(RegionId::generate(), first_seg..=last_seg, FillId::generate());
|
||||
self.region_domain.push(self.region_domain.next_id(), first_seg..=last_seg, fill_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Construct some new vector data from subpaths with an identity transform and black fill.
|
||||
pub fn from_subpaths(subpaths: impl IntoIterator<Item = bezier_rs::Subpath<ManipulatorGroupId>>) -> Self {
|
||||
pub fn from_subpaths(subpaths: impl IntoIterator<Item = impl Borrow<bezier_rs::Subpath<PointId>>>, preserve_id: bool) -> Self {
|
||||
let mut vector_data = Self::empty();
|
||||
|
||||
for subpath in subpaths.into_iter() {
|
||||
vector_data.append_subpath(subpath);
|
||||
vector_data.append_subpath(subpath, preserve_id);
|
||||
}
|
||||
|
||||
vector_data
|
||||
|
@ -142,6 +168,33 @@ impl VectorData {
|
|||
pub fn local_pivot(&self, normalized_pivot: DVec2) -> DVec2 {
|
||||
self.transform.transform_point2(self.layerspace_pivot(normalized_pivot))
|
||||
}
|
||||
|
||||
/// Points connected to a single segment
|
||||
pub fn single_connected_points(&self) -> impl Iterator<Item = PointId> + '_ {
|
||||
self.point_domain.ids().iter().copied().filter(|&point| self.segment_domain.connected_count(point) == 1)
|
||||
}
|
||||
|
||||
/// 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.segment_domain.start_connected(id).all(|segment| has_handle(HandleId::primary(segment))) && self.segment_domain.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.iter().any(|&val| val == 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for VectorData {
|
||||
|
@ -150,64 +203,192 @@ impl Default for VectorData {
|
|||
}
|
||||
}
|
||||
|
||||
/// 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 curviture).
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, DynAny)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct ManipulatorPointId {
|
||||
pub group: ManipulatorGroupId,
|
||||
pub manipulator_type: SelectedType,
|
||||
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 {
|
||||
pub fn new(group: ManipulatorGroupId, manipulator_type: SelectedType) -> Self {
|
||||
Self { group, manipulator_type }
|
||||
/// Attempt to retrieve the manipulator position in layer space (no transformation applied).
|
||||
#[must_use]
|
||||
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()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempt to get a pair of handles. For an anchor this is the first to 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.segment_domain.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_domain.segment_start_from_id(segment),
|
||||
ManipulatorPointId::EndHandle(segment) => vector_data.segment_domain.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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The type of handle found on a bézier curve.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, DynAny)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum SelectedType {
|
||||
Anchor = 1 << 0,
|
||||
InHandle = 1 << 1,
|
||||
OutHandle = 1 << 2,
|
||||
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,
|
||||
}
|
||||
impl SelectedType {
|
||||
/// Get the location of the [SelectedType] in the [ManipulatorGroup]
|
||||
pub fn get_position(&self, manipulator_group: &ManipulatorGroup<ManipulatorGroupId>) -> Option<DVec2> {
|
||||
match self {
|
||||
Self::Anchor => Some(manipulator_group.anchor),
|
||||
Self::InHandle => manipulator_group.in_handle,
|
||||
Self::OutHandle => manipulator_group.out_handle,
|
||||
|
||||
/// Represents a primary or end handle found in a particular segment.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, DynAny)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct HandleId {
|
||||
pub ty: HandleType,
|
||||
pub segment: SegmentId,
|
||||
}
|
||||
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the closest [SelectedType] in the [ManipulatorGroup].
|
||||
pub fn closest_widget(manipulator_group: &ManipulatorGroup<ManipulatorGroupId>, transform_space: DAffine2, target: DVec2, hide_handle_distance: f64) -> (Self, f64) {
|
||||
let anchor = transform_space.transform_point2(manipulator_group.anchor);
|
||||
// Skip handles under the anchor
|
||||
let not_under_anchor = |&(selected_type, position): &(SelectedType, DVec2)| selected_type == Self::Anchor || position.distance_squared(anchor) > hide_handle_distance.powi(2);
|
||||
let compute_distance = |selected_type: Self| {
|
||||
selected_type.get_position(manipulator_group).and_then(|position| {
|
||||
Some((selected_type, transform_space.transform_point2(position)))
|
||||
.filter(not_under_anchor)
|
||||
.map(|(selected_type, pos)| (selected_type, pos.distance_squared(target)))
|
||||
})
|
||||
};
|
||||
[Self::Anchor, Self::InHandle, Self::OutHandle]
|
||||
.into_iter()
|
||||
.filter_map(compute_distance)
|
||||
.min_by(|a, b| a.1.total_cmp(&b.1))
|
||||
.unwrap_or((Self::Anchor, manipulator_group.anchor.distance_squared(target)))
|
||||
}
|
||||
|
||||
/// Opposite handle
|
||||
pub fn opposite(&self) -> Self {
|
||||
match self {
|
||||
SelectedType::Anchor => SelectedType::Anchor,
|
||||
SelectedType::InHandle => SelectedType::OutHandle,
|
||||
SelectedType::OutHandle => SelectedType::InHandle,
|
||||
/// Set the handle's position relative to the anchor which is the start anchor for the primary handle and end anchor for the end handle.
|
||||
#[must_use]
|
||||
pub fn set_relative_position(self, relative_position: DVec2) -> VectorModificationType {
|
||||
let Self { ty, segment } = self;
|
||||
match ty {
|
||||
HandleType::Primary => VectorModificationType::SetPrimaryHandle { segment, relative_position },
|
||||
HandleType::End => VectorModificationType::SetEndHandle { segment, relative_position },
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if handle
|
||||
pub fn is_handle(self) -> bool {
|
||||
self != SelectedType::Anchor
|
||||
/// 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: &Vec<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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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 bézier_paths = vector_data.segment_bezier_iter().map(|(_, bézier, _, _)| bézier).collect::<Vec<_>>();
|
||||
assert_eq!(bézier_paths.len(), 4);
|
||||
assert!(bézier_paths.iter().all(|&bézier| circle.iter().any(|original_bézier| original_bézier == bézier)));
|
||||
|
||||
let generated = vector_data.stroke_bezier_paths().collect::<Vec<_>>();
|
||||
assert_subpath_eq(&generated, &[circle]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn construct_open_subpath() {
|
||||
let bézier = bezier_rs::Bezier::from_cubic_dvec2(DVec2::ZERO, DVec2::NEG_ONE, DVec2::ONE, DVec2::X);
|
||||
let subpath = bezier_rs::Subpath::from_bezier(&bézier);
|
||||
let vector_data = VectorData::from_subpath(&subpath);
|
||||
assert_eq!(vector_data.point_domain.ids().len(), 2);
|
||||
let bézier_paths = vector_data.segment_bezier_iter().map(|(_, bézier, _, _)| bézier).collect::<Vec<_>>();
|
||||
assert_eq!(bézier_paths, vec![bézier]);
|
||||
|
||||
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 bézier_paths = vector_data.segment_bezier_iter().map(|(_, bézier, _, _)| bézier).collect::<Vec<_>>();
|
||||
assert_eq!(bézier_paths.len(), 5);
|
||||
assert!(bézier_paths.iter().all(|&bézier| circle.iter().chain(curve.iter()).any(|original_bézier| original_bézier == bézier)));
|
||||
|
||||
let generated = vector_data.stroke_bezier_paths().collect::<Vec<_>>();
|
||||
assert_subpath_eq(&generated, &[curve, circle]);
|
||||
}
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
use super::HandleId;
|
||||
|
||||
use dyn_any::{DynAny, StaticType};
|
||||
|
||||
use glam::{DAffine2, DVec2};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// A simple macro for creating strongly typed ids (to avoid confusion when passing around ids).
|
||||
macro_rules! create_ids {
|
||||
($($id:ident),*) => {
|
||||
$(
|
||||
|
@ -12,14 +15,23 @@ macro_rules! create_ids {
|
|||
pub struct $id(u64);
|
||||
|
||||
impl $id {
|
||||
pub const ZERO: $id = $id(0);
|
||||
|
||||
/// Generate a new random id
|
||||
pub fn generate() -> Self {
|
||||
Self(crate::uuid::generate_uuid())
|
||||
}
|
||||
|
||||
/// Gets the inner raw value.
|
||||
pub fn inner(self) -> u64 {
|
||||
self.0
|
||||
}
|
||||
|
||||
/// Adds one to the current value and returns the old value. Note that the ids are not going to be unique unless you use the largest id.
|
||||
pub fn next_id(&mut self) -> Self {
|
||||
self.0 += 1;
|
||||
*self
|
||||
}
|
||||
}
|
||||
)*
|
||||
};
|
||||
|
@ -55,7 +67,17 @@ impl PointDomain {
|
|||
self.positions.clear();
|
||||
}
|
||||
|
||||
pub fn retain(&mut self, f: impl Fn(&PointId) -> bool) {
|
||||
let mut keep = self.id.iter().map(&f);
|
||||
self.positions.retain(|_| keep.next().unwrap_or_default());
|
||||
self.id.retain(f);
|
||||
}
|
||||
|
||||
pub fn push(&mut self, id: PointId, position: DVec2) {
|
||||
if self.id.contains(&id) {
|
||||
warn!("Duplicate point");
|
||||
return;
|
||||
}
|
||||
self.id.push(id);
|
||||
self.positions.push(position);
|
||||
}
|
||||
|
@ -64,11 +86,19 @@ impl PointDomain {
|
|||
&self.positions
|
||||
}
|
||||
|
||||
pub fn positions_mut(&mut self) -> impl Iterator<Item = (PointId, &mut DVec2)> {
|
||||
self.id.iter().copied().zip(self.positions.iter_mut())
|
||||
}
|
||||
|
||||
pub fn ids(&self) -> &[PointId] {
|
||||
&self.id
|
||||
}
|
||||
|
||||
pub fn pos_from_id(&self, id: PointId) -> Option<DVec2> {
|
||||
pub fn next_id(&self) -> PointId {
|
||||
self.ids().iter().copied().max_by(|a, b| a.0.cmp(&b.0)).map(|mut id| id.next_id()).unwrap_or(PointId::ZERO)
|
||||
}
|
||||
|
||||
pub fn position_from_id(&self, id: PointId) -> Option<DVec2> {
|
||||
let pos = self.resolve_id(id).map(|index| self.positions[index]);
|
||||
if pos.is_none() {
|
||||
warn!("Resolving pos of invalid id");
|
||||
|
@ -99,7 +129,6 @@ pub struct SegmentDomain {
|
|||
ids: Vec<SegmentId>,
|
||||
start_point: Vec<PointId>,
|
||||
end_point: Vec<PointId>,
|
||||
// TODO: Also store handle points as `PointId`s rather than Bezier-rs's internal `DVec2`s
|
||||
handles: Vec<bezier_rs::BezierHandles>,
|
||||
stroke: Vec<StrokeId>,
|
||||
}
|
||||
|
@ -123,21 +152,116 @@ impl SegmentDomain {
|
|||
self.stroke.clear();
|
||||
}
|
||||
|
||||
pub fn push(&mut self, id: SegmentId, start: PointId, end: PointId, handles: bezier_rs::BezierHandles, stroke: StrokeId) {
|
||||
self.ids.push(id);
|
||||
self.start_point.push(start);
|
||||
self.end_point.push(end);
|
||||
self.handles.push(handles);
|
||||
self.stroke.push(stroke);
|
||||
pub fn retain(&mut self, f: impl Fn(&SegmentId) -> bool) {
|
||||
let mut keep = self.ids.iter().map(&f);
|
||||
self.start_point.retain(|_| keep.next().unwrap_or_default());
|
||||
let mut keep = self.ids.iter().map(&f);
|
||||
self.end_point.retain(|_| keep.next().unwrap_or_default());
|
||||
let mut keep = self.ids.iter().map(&f);
|
||||
self.handles.retain(|_| keep.next().unwrap_or_default());
|
||||
let mut keep = self.ids.iter().map(&f);
|
||||
self.stroke.retain(|_| keep.next().unwrap_or_default());
|
||||
self.ids.retain(f);
|
||||
}
|
||||
|
||||
fn resolve_id(&self, id: SegmentId) -> Option<usize> {
|
||||
pub fn ids(&self) -> &[SegmentId] {
|
||||
&self.ids
|
||||
}
|
||||
|
||||
pub fn next_id(&self) -> SegmentId {
|
||||
self.ids().iter().copied().max_by(|a, b| a.0.cmp(&b.0)).map(|mut id| id.next_id()).unwrap_or(SegmentId::ZERO)
|
||||
}
|
||||
|
||||
pub fn start_point(&self) -> &[PointId] {
|
||||
&self.start_point
|
||||
}
|
||||
|
||||
pub fn end_point(&self) -> &[PointId] {
|
||||
&self.end_point
|
||||
}
|
||||
|
||||
pub fn handles(&self) -> &[bezier_rs::BezierHandles] {
|
||||
&self.handles
|
||||
}
|
||||
|
||||
pub fn stroke(&self) -> &[StrokeId] {
|
||||
&self.stroke
|
||||
}
|
||||
|
||||
pub fn push(&mut self, id: SegmentId, start: PointId, end: PointId, handles: bezier_rs::BezierHandles, stroke: StrokeId) {
|
||||
if self.ids.contains(&id) {
|
||||
warn!("Duplicate segment");
|
||||
return;
|
||||
}
|
||||
// Attempt to keep line joins?
|
||||
let after = self.end_point.iter().copied().position(|other_end| other_end == start || other_end == end);
|
||||
let before = self.start_point.iter().copied().position(|other_start| other_start == start || other_start == end);
|
||||
let (index, flip) = match (before, after) {
|
||||
(_, Some(after)) => (after + 1, self.end_point[after] == end),
|
||||
(Some(before), _) => (before, self.start_point[before] == start),
|
||||
(None, None) => (self.ids.len(), false),
|
||||
};
|
||||
self.ids.insert(index, id);
|
||||
self.start_point.insert(index, if flip { end } else { start });
|
||||
self.end_point.insert(index, if flip { start } else { end });
|
||||
self.handles.insert(index, if flip { handles.flipped() } else { handles });
|
||||
self.stroke.insert(index, stroke);
|
||||
}
|
||||
|
||||
pub fn start_point_mut(&mut self) -> impl Iterator<Item = (SegmentId, &mut PointId)> {
|
||||
self.ids.iter().copied().zip(self.start_point.iter_mut())
|
||||
}
|
||||
|
||||
pub fn end_point_mut(&mut self) -> impl Iterator<Item = (SegmentId, &mut PointId)> {
|
||||
self.ids.iter().copied().zip(self.end_point.iter_mut())
|
||||
}
|
||||
|
||||
pub fn handles_mut(&mut self) -> impl Iterator<Item = (SegmentId, &mut bezier_rs::BezierHandles, PointId, PointId)> {
|
||||
let nested = self.ids.iter().zip(&mut self.handles).zip(&self.start_point).zip(&self.end_point);
|
||||
nested.map(|(((&a, b), &c), &d)| (a, b, c, d))
|
||||
}
|
||||
|
||||
pub fn stroke_mut(&mut self) -> impl Iterator<Item = (SegmentId, &mut StrokeId)> {
|
||||
self.ids.iter().copied().zip(self.stroke.iter_mut())
|
||||
}
|
||||
|
||||
pub fn segment_start_from_id(&self, segment: SegmentId) -> Option<PointId> {
|
||||
self.id_to_index(segment).and_then(|index| self.start_point.get(index)).copied()
|
||||
}
|
||||
|
||||
pub fn segment_end_from_id(&self, segment: SegmentId) -> Option<PointId> {
|
||||
self.id_to_index(segment).and_then(|index| self.end_point.get(index)).copied()
|
||||
}
|
||||
|
||||
/// 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_start_from_id(segment).and_then(|start| self.segment_end_from_id(segment).map(|end| [start, end]))
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
self.points_from_id(segment).and_then(|points| points.into_iter().find(|&point| point != current))
|
||||
}
|
||||
|
||||
/// 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> + '_ {
|
||||
self.start_point.iter().zip(&self.end_point).filter_map(move |(&a, &b)| match (a == current, b == current) {
|
||||
(true, false) => Some(b),
|
||||
(false, true) => Some(a),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
fn id_to_index(&self, id: SegmentId) -> Option<usize> {
|
||||
debug_assert_eq!(self.ids.len(), self.handles.len());
|
||||
debug_assert_eq!(self.ids.len(), self.start_point.len());
|
||||
debug_assert_eq!(self.ids.len(), self.end_point.len());
|
||||
self.ids.iter().position(|&check_id| check_id == id)
|
||||
}
|
||||
|
||||
fn resolve_range(&self, range: &core::ops::RangeInclusive<SegmentId>) -> Option<core::ops::RangeInclusive<usize>> {
|
||||
match (self.resolve_id(*range.start()), self.resolve_id(*range.end())) {
|
||||
(Some(start), Some(end)) => Some(start..=end),
|
||||
match (self.id_to_index(*range.start()), self.id_to_index(*range.end())) {
|
||||
(Some(start), Some(end)) if start.max(end) < self.handles.len().min(self.ids.len()).min(self.start_point.len()).min(self.end_point.len()) => Some(start..=end),
|
||||
_ => {
|
||||
warn!("Resolving range with invalid id");
|
||||
None
|
||||
|
@ -158,6 +282,26 @@ impl SegmentDomain {
|
|||
*handles = handles.apply_transformation(|p| transform.transform_point2(p));
|
||||
}
|
||||
}
|
||||
|
||||
/// Enumerate all segments that start at the point.
|
||||
pub fn start_connected(&self, point: PointId) -> impl Iterator<Item = SegmentId> + '_ {
|
||||
self.start_point.iter().zip(&self.ids).filter(move |&(&found_point, _)| found_point == point).map(|(_, &seg)| seg)
|
||||
}
|
||||
|
||||
/// Enumerate all segments that end at the point.
|
||||
pub fn end_connected(&self, point: PointId) -> impl Iterator<Item = SegmentId> + '_ {
|
||||
self.end_point.iter().zip(&self.ids).filter(move |&(&found_point, _)| found_point == point).map(|(_, &seg)| seg)
|
||||
}
|
||||
|
||||
/// 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> + '_ {
|
||||
self.start_connected(point).map(HandleId::primary).chain(self.end_connected(point).map(HandleId::end))
|
||||
}
|
||||
|
||||
/// 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.all_connected(point).count()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Hash, DynAny)]
|
||||
|
@ -184,7 +328,19 @@ impl RegionDomain {
|
|||
self.fill.clear();
|
||||
}
|
||||
|
||||
pub fn retain(&mut self, f: impl Fn(&RegionId) -> bool) {
|
||||
let mut keep = self.ids.iter().map(&f);
|
||||
self.segment_range.retain(|_| keep.next().unwrap_or_default());
|
||||
let mut keep = self.ids.iter().map(&f);
|
||||
self.fill.retain(|_| keep.next().unwrap_or_default());
|
||||
self.ids.retain(&f);
|
||||
}
|
||||
|
||||
pub fn push(&mut self, id: RegionId, segment_range: core::ops::RangeInclusive<SegmentId>, fill: FillId) {
|
||||
if self.ids.contains(&id) {
|
||||
warn!("Duplicate region");
|
||||
return;
|
||||
}
|
||||
self.ids.push(id);
|
||||
self.segment_range.push(segment_range);
|
||||
self.fill.push(fill);
|
||||
|
@ -194,6 +350,30 @@ impl RegionDomain {
|
|||
self.ids.iter().position(|&check_id| check_id == id)
|
||||
}
|
||||
|
||||
pub fn next_id(&self) -> RegionId {
|
||||
self.ids.iter().copied().max_by(|a, b| a.0.cmp(&b.0)).map(|mut id| id.next_id()).unwrap_or(RegionId::ZERO)
|
||||
}
|
||||
|
||||
pub fn segment_range_mut(&mut self) -> impl Iterator<Item = (RegionId, &mut core::ops::RangeInclusive<SegmentId>)> {
|
||||
self.ids.iter().copied().zip(self.segment_range.iter_mut())
|
||||
}
|
||||
|
||||
pub fn fill_mut(&mut self) -> impl Iterator<Item = (RegionId, &mut FillId)> {
|
||||
self.ids.iter().copied().zip(self.fill.iter_mut())
|
||||
}
|
||||
|
||||
pub fn ids(&self) -> &[RegionId] {
|
||||
&self.ids
|
||||
}
|
||||
|
||||
pub fn segment_range(&self) -> &[core::ops::RangeInclusive<SegmentId>] {
|
||||
&self.segment_range
|
||||
}
|
||||
|
||||
pub fn fill(&self) -> &[FillId] {
|
||||
&self.fill
|
||||
}
|
||||
|
||||
fn concat(&mut self, other: &Self, _transform: DAffine2, id_map: &IdMap) {
|
||||
self.ids.extend(other.ids.iter().map(|id| *id_map.region_map.get(id).unwrap_or(id)));
|
||||
self.segment_range.extend(
|
||||
|
@ -209,15 +389,22 @@ impl RegionDomain {
|
|||
impl super::VectorData {
|
||||
/// Construct a [`bezier_rs::Bezier`] curve spanning from the resolved position of the start and end points with the specified handles. Returns [`None`] if either ID is invalid.
|
||||
fn segment_to_bezier(&self, start: PointId, end: PointId, handles: bezier_rs::BezierHandles) -> Option<bezier_rs::Bezier> {
|
||||
let start = self.point_domain.pos_from_id(start)?;
|
||||
let end = self.point_domain.pos_from_id(end)?;
|
||||
let start = self.point_domain.position_from_id(start)?;
|
||||
let end = self.point_domain.position_from_id(end)?;
|
||||
Some(bezier_rs::Bezier { start, end, handles })
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
let index = self.segment_domain.resolve_id(id)?;
|
||||
self.segment_to_bezier(self.segment_domain.start_point[index], self.segment_domain.end_point[index], self.segment_domain.handles[index])
|
||||
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)> {
|
||||
let index: usize = self.segment_domain.id_to_index(id)?;
|
||||
let start = self.segment_domain.start_point[index];
|
||||
let end = self.segment_domain.end_point[index];
|
||||
Some((start, end, self.segment_to_bezier(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.
|
||||
|
@ -237,17 +424,6 @@ impl super::VectorData {
|
|||
let mut first_point = None;
|
||||
let mut groups = Vec::new();
|
||||
let mut last: Option<(PointId, bezier_rs::BezierHandles)> = None;
|
||||
let end_point = |last: Option<(PointId, bezier_rs::BezierHandles)>, next: Option<PointId>, groups: &mut Vec<_>| {
|
||||
if let Some((disconnected_previous, previous_handle)) = last.filter(|(end, _)| !next.is_some_and(|next| next == *end)) {
|
||||
groups.push(bezier_rs::ManipulatorGroup {
|
||||
anchor: self.point_domain.pos_from_id(disconnected_previous)?,
|
||||
in_handle: previous_handle.end(),
|
||||
out_handle: None,
|
||||
id: disconnected_previous,
|
||||
});
|
||||
}
|
||||
Some(())
|
||||
};
|
||||
|
||||
for (handle, start, end) in segments {
|
||||
if last.is_some_and(|(previous_end, _)| previous_end != start) {
|
||||
|
@ -255,10 +431,9 @@ impl super::VectorData {
|
|||
return None;
|
||||
}
|
||||
first_point = Some(first_point.unwrap_or(start));
|
||||
end_point(last, Some(start), &mut groups)?;
|
||||
|
||||
groups.push(bezier_rs::ManipulatorGroup {
|
||||
anchor: self.point_domain.pos_from_id(start)?,
|
||||
anchor: self.point_domain.position_from_id(start)?,
|
||||
in_handle: last.and_then(|(_, handle)| handle.end()),
|
||||
out_handle: handle.start(),
|
||||
id: start,
|
||||
|
@ -266,8 +441,21 @@ impl super::VectorData {
|
|||
|
||||
last = Some((end, handle));
|
||||
}
|
||||
end_point(last, None, &mut groups)?;
|
||||
|
||||
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(bezier_rs::ManipulatorGroup {
|
||||
anchor: self.point_domain.position_from_id(end)?,
|
||||
in_handle: last_handle.end(),
|
||||
out_handle: None,
|
||||
id: end,
|
||||
});
|
||||
}
|
||||
}
|
||||
Some(bezier_rs::Subpath::new(groups, closed))
|
||||
}
|
||||
|
||||
|
@ -279,10 +467,13 @@ impl super::VectorData {
|
|||
.zip(&self.region_domain.segment_range)
|
||||
.filter_map(|(&id, segment_range)| self.segment_domain.resolve_range(segment_range).map(|range| (id, range)))
|
||||
.filter_map(|(id, range)| {
|
||||
let segments_iter = self.segment_domain.handles[range.clone()]
|
||||
let segments_iter = self
|
||||
.segment_domain
|
||||
.handles
|
||||
.get(range.clone())?
|
||||
.iter()
|
||||
.zip(&self.segment_domain.start_point[range.clone()])
|
||||
.zip(&self.segment_domain.end_point[range])
|
||||
.zip(self.segment_domain.start_point.get(range.clone())?)
|
||||
.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))
|
||||
|
@ -294,6 +485,17 @@ impl super::VectorData {
|
|||
StrokePathIter { vector_data: self, segment_index: 0 }
|
||||
}
|
||||
|
||||
/// Construct an iterator [`bezier_rs::ManipulatorGroup`] for stroke.
|
||||
pub fn manipulator_groups(&self) -> impl Iterator<Item = bezier_rs::ManipulatorGroup<PointId>> + '_ {
|
||||
self.stroke_bezier_paths().flat_map(|mut path| std::mem::take(path.manipulator_groups_mut()))
|
||||
}
|
||||
|
||||
/// Get manipulator by id
|
||||
pub fn manipulator_group_id(&self, id: impl Into<PointId>) -> Option<bezier_rs::ManipulatorGroup<PointId>> {
|
||||
let id = id.into();
|
||||
self.manipulator_groups().find(|group| group.id == id)
|
||||
}
|
||||
|
||||
/// Transforms this vector data
|
||||
pub fn transform(&mut self, transform: DAffine2) {
|
||||
self.point_domain.transform(transform);
|
||||
|
@ -301,6 +503,7 @@ impl super::VectorData {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct StrokePathIter<'a> {
|
||||
vector_data: &'a super::VectorData,
|
||||
segment_index: usize,
|
||||
|
@ -339,11 +542,6 @@ impl bezier_rs::Identifier for PointId {
|
|||
Self::generate()
|
||||
}
|
||||
}
|
||||
impl From<crate::uuid::ManipulatorGroupId> for PointId {
|
||||
fn from(value: crate::uuid::ManipulatorGroupId) -> Self {
|
||||
Self(value.inner())
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::vector::ConcatElement for super::VectorData {
|
||||
fn concat(&mut self, other: &Self, transform: glam::DAffine2) {
|
||||
|
@ -369,6 +567,7 @@ impl crate::vector::ConcatElement for super::VectorData {
|
|||
}
|
||||
}
|
||||
|
||||
/// Represents the conversion of ids used when concatenating vector data with conflicting ids.
|
||||
struct IdMap {
|
||||
point_map: HashMap<PointId, PointId>,
|
||||
segment_map: HashMap<SegmentId, SegmentId>,
|
||||
|
|
530
node-graph/gcore/src/vector/vector_data/modification.rs
Normal file
530
node-graph/gcore/src/vector/vector_data/modification.rs
Normal file
|
@ -0,0 +1,530 @@
|
|||
use super::*;
|
||||
use crate::Node;
|
||||
|
||||
use bezier_rs::BezierHandles;
|
||||
use dyn_any::{DynAny, StaticType};
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
/// Represents a procedural change to the [`PointDomain`] in [`VectorData`].
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct PointModification {
|
||||
add: Vec<PointId>,
|
||||
remove: HashSet<PointId>,
|
||||
#[serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap")]
|
||||
delta: HashMap<PointId, DVec2>,
|
||||
}
|
||||
|
||||
impl PointModification {
|
||||
/// Apply this modification to the specified [`PointDomain`].
|
||||
pub fn apply(&self, point_domain: &mut PointDomain, segment_domain: &mut SegmentDomain) {
|
||||
point_domain.retain(|id| !self.remove.contains(id));
|
||||
|
||||
for (id, position) in point_domain.positions_mut() {
|
||||
let Some(&delta) = self.delta.get(&id) else { continue };
|
||||
if !delta.is_finite() {
|
||||
warn!("Invalid delta when applying a point modification");
|
||||
continue;
|
||||
}
|
||||
|
||||
*position += delta;
|
||||
|
||||
for (_, handles, start, end) in segment_domain.handles_mut() {
|
||||
if start == id {
|
||||
handles.move_start(delta);
|
||||
}
|
||||
if end == id {
|
||||
handles.move_end(delta);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for &add_id in &self.add {
|
||||
let Some(&position) = self.delta.get(&add_id) else { continue };
|
||||
if !position.is_finite() {
|
||||
warn!("Invalid position when applying a point modification");
|
||||
continue;
|
||||
}
|
||||
|
||||
point_domain.push(add_id, position);
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new modification that will convert an empty [`VectorData`] into the target [`VectorData`].
|
||||
pub fn create_from_vector(vector_data: &VectorData) -> Self {
|
||||
Self {
|
||||
add: vector_data.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(),
|
||||
}
|
||||
}
|
||||
|
||||
fn push(&mut self, id: PointId, position: DVec2) {
|
||||
self.add.push(id);
|
||||
self.delta.insert(id, position);
|
||||
}
|
||||
|
||||
fn remove(&mut self, id: PointId) {
|
||||
self.remove.insert(id);
|
||||
self.add.retain(|&add| add != id);
|
||||
self.delta.remove(&id);
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a procedural change to the [`SegmentDomain`] in [`VectorData`].
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct SegmentModification {
|
||||
add: Vec<SegmentId>,
|
||||
remove: HashSet<SegmentId>,
|
||||
#[serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap")]
|
||||
start_point: HashMap<SegmentId, PointId>,
|
||||
#[serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap")]
|
||||
end_point: HashMap<SegmentId, PointId>,
|
||||
#[serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap")]
|
||||
handle_primary: HashMap<SegmentId, Option<DVec2>>,
|
||||
#[serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap")]
|
||||
handle_end: HashMap<SegmentId, Option<DVec2>>,
|
||||
#[serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap")]
|
||||
stroke: HashMap<SegmentId, StrokeId>,
|
||||
}
|
||||
|
||||
impl SegmentModification {
|
||||
/// Apply this modification to the specified [`SegmentDomain`].
|
||||
pub fn apply(&self, segment_domain: &mut SegmentDomain, point_domain: &PointDomain) {
|
||||
segment_domain.retain(|id| !self.remove.contains(id));
|
||||
|
||||
for (id, point) in segment_domain.start_point_mut() {
|
||||
let Some(&new) = self.start_point.get(&id) else { continue };
|
||||
if !point_domain.ids().contains(&new) {
|
||||
warn!("Invalid start ID when applying a segment modification");
|
||||
continue;
|
||||
}
|
||||
|
||||
*point = new;
|
||||
}
|
||||
|
||||
for (id, point) in segment_domain.end_point_mut() {
|
||||
let Some(&new) = self.end_point.get(&id) else { continue };
|
||||
if !point_domain.ids().contains(&new) {
|
||||
warn!("Invalid end ID when applying a segment modification");
|
||||
continue;
|
||||
}
|
||||
|
||||
*point = new;
|
||||
}
|
||||
|
||||
for (id, handles, start, end) in segment_domain.handles_mut() {
|
||||
let Some(start) = point_domain.position_from_id(start) else { continue };
|
||||
let Some(end) = point_domain.position_from_id(end) else { continue };
|
||||
|
||||
// Compute the actual start and end position based on the offset from the anchor
|
||||
let start = self.handle_primary.get(&id).copied().map(|handle| handle.map(|handle| handle + start));
|
||||
let end = self.handle_end.get(&id).copied().map(|handle| handle.map(|handle| handle + end));
|
||||
|
||||
if !start.unwrap_or_default().map_or(true, |start| start.is_finite()) || !end.unwrap_or_default().map_or(true, |end| end.is_finite()) {
|
||||
warn!("Invalid handles when applying a segment modification");
|
||||
continue;
|
||||
}
|
||||
|
||||
match (start, end) {
|
||||
// The new handles are fully specified by the modification
|
||||
(Some(Some(handle_start)), Some(Some(handle_end))) => *handles = BezierHandles::Cubic { handle_start, handle_end },
|
||||
(Some(Some(handle)), Some(None)) | (Some(None), Some(Some(handle))) => *handles = BezierHandles::Quadratic { handle },
|
||||
(Some(None), Some(None)) => *handles = BezierHandles::Linear,
|
||||
// Remove the end handle
|
||||
(None, Some(None)) => {
|
||||
if let BezierHandles::Cubic { handle_start, .. } = *handles {
|
||||
*handles = BezierHandles::Quadratic { handle: handle_start }
|
||||
}
|
||||
}
|
||||
// Change the end handle
|
||||
(None, Some(Some(handle_end))) => match *handles {
|
||||
BezierHandles::Linear => *handles = BezierHandles::Quadratic { handle: handle_end },
|
||||
BezierHandles::Quadratic { handle: handle_start } => *handles = BezierHandles::Cubic { handle_start, handle_end },
|
||||
BezierHandles::Cubic { handle_start, .. } => *handles = BezierHandles::Cubic { handle_start, handle_end },
|
||||
},
|
||||
// Remove the start handle
|
||||
(Some(None), None) => *handles = BezierHandles::Linear,
|
||||
// Change the start handle
|
||||
(Some(Some(handle_start)), None) => match *handles {
|
||||
BezierHandles::Linear => *handles = BezierHandles::Quadratic { handle: handle_start },
|
||||
BezierHandles::Quadratic { .. } => *handles = BezierHandles::Quadratic { handle: handle_start },
|
||||
BezierHandles::Cubic { handle_end, .. } => *handles = BezierHandles::Cubic { handle_start, handle_end },
|
||||
},
|
||||
// No change
|
||||
(None, None) => {}
|
||||
};
|
||||
}
|
||||
|
||||
for (id, stroke) in segment_domain.stroke_mut() {
|
||||
let Some(&new) = self.stroke.get(&id) else { continue };
|
||||
*stroke = new;
|
||||
}
|
||||
|
||||
for &add_id in &self.add {
|
||||
let Some(&start) = self.start_point.get(&add_id) else { continue };
|
||||
let Some(&end) = self.end_point.get(&add_id) else { continue };
|
||||
let Some(&handle_start) = self.handle_primary.get(&add_id) else { continue };
|
||||
let Some(&handle_end) = self.handle_end.get(&add_id) else { continue };
|
||||
let Some(&stroke) = self.stroke.get(&add_id) else { continue };
|
||||
|
||||
if !point_domain.ids().contains(&start) {
|
||||
warn!("invalid start id");
|
||||
continue;
|
||||
}
|
||||
if !point_domain.ids().contains(&end) {
|
||||
warn!("invalid end id");
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(start_position) = point_domain.position_from_id(start) else { continue };
|
||||
let Some(end_position) = point_domain.position_from_id(end) else { continue };
|
||||
let handles = match (handle_start, handle_end) {
|
||||
(Some(handle_start), Some(handle_end)) => BezierHandles::Cubic {
|
||||
handle_start: handle_start + start_position,
|
||||
handle_end: handle_end + end_position,
|
||||
},
|
||||
(Some(handle), None) | (None, Some(handle)) => BezierHandles::Quadratic { handle: handle + start_position },
|
||||
(None, None) => BezierHandles::Linear,
|
||||
};
|
||||
|
||||
if !handles.is_finite() {
|
||||
warn!("invalid handles");
|
||||
continue;
|
||||
}
|
||||
|
||||
segment_domain.push(add_id, start, end, handles, stroke);
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new modification that will convert an empty [`VectorData`] into the target [`VectorData`].
|
||||
pub fn create_from_vector(vector_data: &VectorData) -> Self {
|
||||
Self {
|
||||
add: vector_data.segment_domain.ids().to_vec(),
|
||||
remove: HashSet::new(),
|
||||
start_point: vector_data.segment_domain.ids().iter().copied().zip(vector_data.segment_domain.start_point().iter().cloned()).collect(),
|
||||
end_point: vector_data.segment_domain.ids().iter().copied().zip(vector_data.segment_domain.end_point().iter().cloned()).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(),
|
||||
}
|
||||
}
|
||||
|
||||
fn push(&mut self, id: SegmentId, points: [PointId; 2], handles: [Option<DVec2>; 2], stroke: StrokeId) {
|
||||
self.remove.remove(&id);
|
||||
self.add.push(id);
|
||||
self.start_point.insert(id, points[0]);
|
||||
self.end_point.insert(id, points[1]);
|
||||
self.handle_primary.insert(id, handles[0]);
|
||||
self.handle_end.insert(id, handles[1]);
|
||||
self.stroke.insert(id, stroke);
|
||||
}
|
||||
|
||||
fn remove(&mut self, id: SegmentId) {
|
||||
self.remove.insert(id);
|
||||
self.add.retain(|&add| add != id);
|
||||
self.start_point.remove(&id);
|
||||
self.end_point.remove(&id);
|
||||
self.handle_primary.remove(&id);
|
||||
self.handle_end.remove(&id);
|
||||
self.stroke.remove(&id);
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a procedural change to the [`RegionDomain`] in [`VectorData`].
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct RegionModification {
|
||||
add: Vec<RegionId>,
|
||||
remove: HashSet<RegionId>,
|
||||
#[serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap")]
|
||||
segment_range: HashMap<RegionId, core::ops::RangeInclusive<SegmentId>>,
|
||||
#[serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap")]
|
||||
fill: HashMap<RegionId, FillId>,
|
||||
}
|
||||
|
||||
impl RegionModification {
|
||||
/// Apply this modification to the specified [`RegionDomain`].
|
||||
pub fn apply(&self, region_domain: &mut RegionDomain) {
|
||||
region_domain.retain(|id| !self.remove.contains(id));
|
||||
|
||||
for (id, segment_range) in region_domain.segment_range_mut() {
|
||||
let Some(new) = self.segment_range.get(&id) else { continue };
|
||||
*segment_range = new.clone(); // Range inclusive is not copy
|
||||
}
|
||||
|
||||
for (id, fill) in region_domain.fill_mut() {
|
||||
let Some(&new) = self.fill.get(&id) else { continue };
|
||||
*fill = new;
|
||||
}
|
||||
|
||||
for &add_id in &self.add {
|
||||
let Some(segment_range) = self.segment_range.get(&add_id) else { continue };
|
||||
let Some(&fill) = self.fill.get(&add_id) else { continue };
|
||||
region_domain.push(add_id, segment_range.clone(), fill);
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new modification that will convert an empty [`VectorData`] into the target [`VectorData`].
|
||||
pub fn create_from_vector(vector_data: &VectorData) -> Self {
|
||||
Self {
|
||||
add: vector_data.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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a procedural change to the [`VectorData`].
|
||||
#[derive(Clone, Debug, Default, PartialEq, DynAny)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct VectorModification {
|
||||
points: PointModification,
|
||||
segments: SegmentModification,
|
||||
regions: RegionModification,
|
||||
add_g1_continuous: HashSet<[HandleId; 2]>,
|
||||
remove_g1_continuous: HashSet<[HandleId; 2]>,
|
||||
}
|
||||
|
||||
/// A modification type that can be added to a [`VectorModification`].
|
||||
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub enum VectorModificationType {
|
||||
InsertSegment { id: SegmentId, points: [PointId; 2], handles: [Option<DVec2>; 2] },
|
||||
InsertPoint { id: PointId, position: DVec2 },
|
||||
|
||||
RemoveSegment { id: SegmentId },
|
||||
RemovePoint { id: PointId },
|
||||
|
||||
SetG1Continuous { handles: [HandleId; 2], enabled: bool },
|
||||
SetHandles { segment: SegmentId, handles: [Option<DVec2>; 2] },
|
||||
SetPrimaryHandle { segment: SegmentId, relative_position: DVec2 },
|
||||
SetEndHandle { segment: SegmentId, relative_position: DVec2 },
|
||||
SetStartPoint { segment: SegmentId, id: PointId },
|
||||
SetEndPoint { segment: SegmentId, id: PointId },
|
||||
|
||||
ApplyPointDelta { point: PointId, delta: DVec2 },
|
||||
ApplyPrimaryDelta { segment: SegmentId, delta: DVec2 },
|
||||
ApplyEndDelta { segment: SegmentId, delta: DVec2 },
|
||||
}
|
||||
|
||||
impl VectorModification {
|
||||
/// Apply this modification to the specified [`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);
|
||||
|
||||
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
|
||||
.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a [`VectorModificationType`] to this modification.
|
||||
pub fn modify(&mut self, vector_data_modification: &VectorModificationType) {
|
||||
match vector_data_modification {
|
||||
VectorModificationType::InsertSegment { id, points, handles } => self.segments.push(*id, *points, *handles, StrokeId::ZERO),
|
||||
VectorModificationType::InsertPoint { id, position } => self.points.push(*id, *position),
|
||||
|
||||
VectorModificationType::RemoveSegment { id } => self.segments.remove(*id),
|
||||
VectorModificationType::RemovePoint { id } => self.points.remove(*id),
|
||||
|
||||
VectorModificationType::SetG1Continuous { handles, enabled } => {
|
||||
if *enabled {
|
||||
if !self.add_g1_continuous.contains(&[handles[1], handles[0]]) {
|
||||
self.add_g1_continuous.insert(*handles);
|
||||
}
|
||||
self.remove_g1_continuous.remove(handles);
|
||||
self.remove_g1_continuous.remove(&[handles[1], handles[0]]);
|
||||
} else {
|
||||
if !self.remove_g1_continuous.contains(&[handles[1], handles[0]]) {
|
||||
self.remove_g1_continuous.insert(*handles);
|
||||
}
|
||||
self.add_g1_continuous.remove(handles);
|
||||
self.add_g1_continuous.remove(&[handles[1], handles[0]]);
|
||||
}
|
||||
}
|
||||
VectorModificationType::SetHandles { segment, handles } => {
|
||||
self.segments.handle_primary.insert(*segment, handles[0]);
|
||||
self.segments.handle_end.insert(*segment, handles[1]);
|
||||
}
|
||||
VectorModificationType::SetPrimaryHandle { segment, relative_position } => {
|
||||
self.segments.handle_primary.insert(*segment, Some(*relative_position));
|
||||
}
|
||||
VectorModificationType::SetEndHandle { segment, relative_position } => {
|
||||
self.segments.handle_end.insert(*segment, Some(*relative_position));
|
||||
}
|
||||
VectorModificationType::SetStartPoint { segment, id } => {
|
||||
self.segments.start_point.insert(*segment, *id);
|
||||
}
|
||||
VectorModificationType::SetEndPoint { segment, id } => {
|
||||
self.segments.end_point.insert(*segment, *id);
|
||||
}
|
||||
|
||||
VectorModificationType::ApplyPointDelta { point, delta } => {
|
||||
*self.points.delta.entry(*point).or_default() += *delta;
|
||||
}
|
||||
VectorModificationType::ApplyPrimaryDelta { segment, delta } => {
|
||||
let position = self.segments.handle_primary.entry(*segment).or_default();
|
||||
*position = Some(position.unwrap_or_default() + *delta);
|
||||
}
|
||||
VectorModificationType::ApplyEndDelta { segment, delta } => {
|
||||
let position = self.segments.handle_end.entry(*segment).or_default();
|
||||
*position = Some(position.unwrap_or_default() + *delta);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new modification that will convert an empty [`VectorData`] into the target [`VectorData`].
|
||||
pub fn create_from_vector(vector_data: &VectorData) -> 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(),
|
||||
remove_g1_continuous: HashSet::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl core::hash::Hash for VectorModification {
|
||||
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
|
||||
// TODO: properly implement (hashing a hashset is difficult because ordering is unstable)
|
||||
PointId::generate().hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
/// A node that applies a procedural modification to some [`VectorData`].
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct PathModify<VectorModificationNode> {
|
||||
modification: VectorModificationNode,
|
||||
}
|
||||
|
||||
#[node_macro::node_fn(PathModify)]
|
||||
fn path_modify(mut vector_data: VectorData, modification: VectorModification) -> VectorData {
|
||||
modification.apply(&mut vector_data);
|
||||
vector_data
|
||||
}
|
||||
|
||||
#[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 modify = VectorModification::create_from_vector(&vector_data);
|
||||
|
||||
let mut new = VectorData::empty();
|
||||
modify.apply(&mut new);
|
||||
assert_eq!(vector_data, 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.)),
|
||||
],
|
||||
false,
|
||||
),
|
||||
];
|
||||
let mut vector_data = VectorData::from_subpaths(&subpaths, false);
|
||||
|
||||
let mut modify_new = VectorModification::create_from_vector(&vector_data);
|
||||
let mut modify_original = VectorModification::default();
|
||||
|
||||
for modification in [&mut modify_new, &mut modify_original] {
|
||||
let point = vector_data.point_domain.ids()[0];
|
||||
modification.modify(&VectorModificationType::ApplyPointDelta { point, delta: DVec2::X * 0.5 });
|
||||
let point = vector_data.point_domain.ids()[9];
|
||||
modification.modify(&VectorModificationType::ApplyPointDelta { point, delta: DVec2::X });
|
||||
}
|
||||
|
||||
let mut new = VectorData::empty();
|
||||
modify_new.apply(&mut new);
|
||||
|
||||
modify_original.apply(&mut vector_data);
|
||||
|
||||
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_data.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,
|
||||
Bezier::from_quadratic_dvec2(DVec2::new(11., 0.), DVec2::new(16., 10.), DVec2::new(20., 0.))
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: Eventually remove this (probably starting late 2024)
|
||||
use serde::de::{SeqAccess, Visitor};
|
||||
use serde::ser::SerializeSeq;
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use std::fmt;
|
||||
use std::hash::Hash;
|
||||
fn serialize_hashmap<K, V, S>(hashmap: &HashMap<K, V>, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
K: Serialize + Eq + Hash,
|
||||
V: Serialize,
|
||||
S: Serializer,
|
||||
{
|
||||
let mut seq = serializer.serialize_seq(Some(hashmap.len()))?;
|
||||
for (key, value) in hashmap {
|
||||
seq.serialize_element(&(key, value))?;
|
||||
}
|
||||
seq.end()
|
||||
}
|
||||
|
||||
fn deserialize_hashmap<'de, K, V, D>(deserializer: D) -> Result<HashMap<K, V>, D::Error>
|
||||
where
|
||||
K: Deserialize<'de> + Eq + Hash,
|
||||
V: Deserialize<'de>,
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct HashMapVisitor<K, V> {
|
||||
marker: std::marker::PhantomData<fn() -> HashMap<K, V>>,
|
||||
}
|
||||
|
||||
impl<'de, K, V> Visitor<'de> for HashMapVisitor<K, V>
|
||||
where
|
||||
K: Deserialize<'de> + Eq + Hash,
|
||||
V: Deserialize<'de>,
|
||||
{
|
||||
type Value = HashMap<K, V>;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("a sequence of tuples")
|
||||
}
|
||||
|
||||
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
|
||||
where
|
||||
A: SeqAccess<'de>,
|
||||
{
|
||||
let mut hashmap = HashMap::new();
|
||||
while let Some((key, value)) = seq.next_element()? {
|
||||
hashmap.insert(key, value);
|
||||
}
|
||||
Ok(hashmap)
|
||||
}
|
||||
}
|
||||
|
||||
let visitor = HashMapVisitor { marker: std::marker::PhantomData };
|
||||
deserializer.deserialize_seq(visitor)
|
||||
}
|
|
@ -170,11 +170,11 @@ fn solidify_stroke(vector_data: VectorData) -> VectorData {
|
|||
// This is where we determine whether we have a closed or open path. Ex: Oval vs line segment.
|
||||
if subpath_out.1.is_some() {
|
||||
// Two closed subpaths, closed shape. Add both subpaths.
|
||||
result.append_subpath(subpath_out.0);
|
||||
result.append_subpath(subpath_out.1.unwrap());
|
||||
result.append_subpath(subpath_out.0, false);
|
||||
result.append_subpath(subpath_out.1.unwrap(), false);
|
||||
} else {
|
||||
// One closed subpath, open path.
|
||||
result.append_subpath(subpath_out.0);
|
||||
result.append_subpath(subpath_out.0, false);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -363,7 +363,7 @@ pub struct PoissonDiskPoints<SeparationDiskDiameter> {
|
|||
fn poisson_disk_points(vector_data: VectorData, separation_disk_diameter: f64) -> VectorData {
|
||||
let mut rng = rand::rngs::StdRng::seed_from_u64(0);
|
||||
let mut result = VectorData::empty();
|
||||
for (_, mut subpath) in vector_data.region_bezier_paths() {
|
||||
for mut subpath in vector_data.stroke_bezier_paths() {
|
||||
if subpath.manipulator_groups().len() < 3 {
|
||||
continue;
|
||||
}
|
||||
|
@ -400,6 +400,8 @@ fn splines_from_points(mut vector_data: VectorData) -> VectorData {
|
|||
|
||||
let first_handles = bezier_rs::solve_spline_first_handle(points.positions());
|
||||
|
||||
let stroke_id = StrokeId::ZERO;
|
||||
|
||||
for (start_index, end_index) in (0..(points.positions().len())).zip(1..(points.positions().len())) {
|
||||
let handle_start = first_handles[start_index];
|
||||
let handle_end = points.positions()[end_index] * 2. - first_handles[end_index];
|
||||
|
@ -407,7 +409,7 @@ fn splines_from_points(mut vector_data: VectorData) -> VectorData {
|
|||
|
||||
vector_data
|
||||
.segment_domain
|
||||
.push(SegmentId::generate(), points.ids()[start_index], points.ids()[end_index], handles, StrokeId::generate())
|
||||
.push(SegmentId::generate(), points.ids()[start_index], points.ids()[end_index], handles, stroke_id)
|
||||
}
|
||||
|
||||
vector_data
|
||||
|
@ -484,7 +486,7 @@ async fn morph<SourceFuture: Future<Output = VectorData>, TargetFuture: Future<O
|
|||
manipulator.anchor = manipulator.anchor.lerp(target.anchor, time);
|
||||
}
|
||||
|
||||
result.append_subpath(source_path);
|
||||
result.append_subpath(source_path, true);
|
||||
}
|
||||
// Mismatched subpath count
|
||||
for mut source_path in source_paths {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue