mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 22:28:10 +08:00
Finalize and unify the design of the 'Morph' and 'Blend' nodes (#3974)
* Fix Morph node transform interpolation and preservation in the table * Fix click target positions for Morph's nested layers by pre-compensating upstream_data transforms * Redesign Morph node (v3) with control path input and uniformly spaced progression, and fix Stroke::lerp interpolation weights * Add migration from Morph node v2 to v3 * Redesign the 'Blend Shapes' node behavior and subgraph definition * Add the Layer > Blend menu entry to easily set up a blend * Optimize the Morph node * Refactor the Morph node to remove the roundtrip through BezPath * Fine-tune Morph node Bezier order promotion and handle interpolation * Add the Layer > Morph menu bar entry * Fix NaN and guard against other potential NaN bugs breaking the editor * Add InterpolationDistribution parameter to Morph with weighted progression, swap parameter orders, and rename shear to skew * Add the Reverse parameter to the Morph node * Update the order of the inputs to Blend Shapes for consistency with Morph * Make Layer > Morph create the Morph Path control layer * Fix migrations * Move 10 to a constant * Avoid division by 0 in the Blend Shapes node internals * Rename nodes 'Blend' -> 'Mix' and 'Blend Shapes' to 'Blend' * Fix a crash encountered while testing * Final code review * Make domain push dupe checks debug-only and use push_unchecked in the Morph node * Pre-allocate for pushes to the vector domains * Add fast path at t=0 * Inline reserve() * Set up the control path layer above not below, and starting collapsed * Review fixes --------- Co-authored-by: Timon <me@timon.zip>
This commit is contained in:
@@ -554,3 +554,21 @@ pub enum SpiralType {
|
||||
Archimedean,
|
||||
Logarithmic,
|
||||
}
|
||||
|
||||
/// Controls how the morph/blend progression spends its time along the interpolation path, allowing for constant speed/spacing with respect to different parameters of change.
|
||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, node_macro::ChoiceType)]
|
||||
#[widget(Dropdown)]
|
||||
pub enum InterpolationDistribution {
|
||||
/// All objects occupy an equal portion of the progression range, regardless of their changing distances, angles, sizes, or slants.
|
||||
#[default]
|
||||
Objects,
|
||||
/// All distances along the interpolation path are covered at a constant rate, meaning more time is spent traversing further distances.
|
||||
Distances,
|
||||
/// All angles of rotation between objects are covered at a constant rate, meaning more time is spent turning through larger angles.
|
||||
Angles,
|
||||
/// All sizes of expansion/contraction between objects are covered at a constant rate, meaning more time is spent scaling through larger (logarithmic) changes in size.
|
||||
Sizes,
|
||||
/// All slants (changes in skew angle) between objects are covered at a constant rate, meaning more time is spent skewing through larger changes in slant.
|
||||
Slants,
|
||||
}
|
||||
|
||||
@@ -104,6 +104,12 @@ impl PointDomain {
|
||||
self.position.clear();
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn reserve(&mut self, additional: usize) {
|
||||
self.id.reserve(additional);
|
||||
self.position.reserve(additional);
|
||||
}
|
||||
|
||||
pub fn retain(&mut self, segment_domain: &mut SegmentDomain, f: impl Fn(&PointId) -> bool) {
|
||||
let mut keep = self.id.iter().map(&f);
|
||||
self.position.retain(|_| keep.next().unwrap_or_default());
|
||||
@@ -129,14 +135,16 @@ impl PointDomain {
|
||||
}
|
||||
|
||||
pub fn push(&mut self, id: PointId, position: DVec2) {
|
||||
#[cfg(debug_assertions)]
|
||||
if self.id.contains(&id) {
|
||||
warn!("Tried to push a duplicate point to a point domain");
|
||||
return;
|
||||
}
|
||||
|
||||
self.id.push(id);
|
||||
self.position.push(position);
|
||||
self.push_unchecked(id, position);
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn push_unchecked(&mut self, id: PointId, position: DVec2) {
|
||||
self.id.push(id);
|
||||
self.position.push(position);
|
||||
@@ -234,6 +242,15 @@ impl SegmentDomain {
|
||||
self.stroke.clear();
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn reserve(&mut self, additional: usize) {
|
||||
self.id.reserve(additional);
|
||||
self.start_point.reserve(additional);
|
||||
self.end_point.reserve(additional);
|
||||
self.handles.reserve(additional);
|
||||
self.stroke.reserve(additional);
|
||||
}
|
||||
|
||||
pub fn retain(&mut self, f: impl Fn(&SegmentId) -> bool, points_length: usize) {
|
||||
let additional_delete_ids = self
|
||||
.id
|
||||
@@ -316,9 +333,15 @@ impl SegmentDomain {
|
||||
pub fn push(&mut self, id: SegmentId, start: usize, end: usize, handles: BezierHandles, stroke: StrokeId) {
|
||||
#[cfg(debug_assertions)]
|
||||
if self.id.contains(&id) {
|
||||
warn!("Tried to push an existing point to a point domain");
|
||||
warn!("Tried to push a duplicate segment to a segment domain");
|
||||
return;
|
||||
}
|
||||
|
||||
self.push_unchecked(id, start, end, handles, stroke);
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn push_unchecked(&mut self, id: SegmentId, start: usize, end: usize, handles: BezierHandles, stroke: StrokeId) {
|
||||
self.id.push(id);
|
||||
self.start_point.push(start);
|
||||
self.end_point.push(end);
|
||||
@@ -596,6 +619,13 @@ impl RegionDomain {
|
||||
self.fill.clear();
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn reserve(&mut self, additional: usize) {
|
||||
self.id.reserve(additional);
|
||||
self.segment_range.reserve(additional);
|
||||
self.fill.reserve(additional);
|
||||
}
|
||||
|
||||
pub fn retain(&mut self, f: impl Fn(&RegionId) -> bool) {
|
||||
let mut keep = self.id.iter().map(&f);
|
||||
self.segment_range.retain(|_| keep.next().unwrap_or_default());
|
||||
@@ -618,10 +648,17 @@ impl RegionDomain {
|
||||
}
|
||||
|
||||
pub fn push(&mut self, id: RegionId, segment_range: std::ops::RangeInclusive<SegmentId>, fill: FillId) {
|
||||
#[cfg(debug_assertions)]
|
||||
if self.id.contains(&id) {
|
||||
warn!("Duplicate region");
|
||||
warn!("Tried to push a duplicate region to a region domain");
|
||||
return;
|
||||
}
|
||||
|
||||
self.push_unchecked(id, segment_range, fill);
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn push_unchecked(&mut self, id: RegionId, segment_range: std::ops::RangeInclusive<SegmentId>, fill: FillId) {
|
||||
self.id.push(id);
|
||||
self.segment_range.push(segment_range);
|
||||
self.fill.push(fill);
|
||||
|
||||
Reference in New Issue
Block a user