Skip to content

Commit 4360359

Browse files
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>
1 parent 7077e87 commit 4360359

File tree

23 files changed

+959
-337
lines changed

23 files changed

+959
-337
lines changed

editor/src/consts.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,3 +186,6 @@ pub const DOUBLE_CLICK_MILLISECONDS: u64 = 500;
186186
pub const UI_SCALE_DEFAULT: f64 = 1.;
187187
pub const UI_SCALE_MIN: f64 = 0.5;
188188
pub const UI_SCALE_MAX: f64 = 3.;
189+
190+
// ACTIONS
191+
pub const BLEND_COUNT_PER_LAYER: usize = 10;

editor/src/messages/input_mapper/input_mappings.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,8 @@ pub fn input_mappings(zoom_with_scroll: bool) -> Mapping {
357357
entry!(KeyDown(KeyS); modifiers=[Accel, Shift], action_dispatch=DocumentMessage::SaveDocumentAs),
358358
entry!(KeyDown(KeyD); modifiers=[Accel], canonical, action_dispatch=DocumentMessage::DuplicateSelectedLayers),
359359
entry!(KeyDown(KeyJ); modifiers=[Accel], action_dispatch=DocumentMessage::DuplicateSelectedLayers),
360+
entry!(KeyDown(KeyB); modifiers=[Accel, Alt], action_dispatch=DocumentMessage::BlendSelectedLayers),
361+
entry!(KeyDown(KeyM); modifiers=[Accel, Alt], action_dispatch=DocumentMessage::MorphSelectedLayers), // Might get eaten by the GeForce Experience overlay for some Windows users
360362
entry!(KeyDown(KeyG); modifiers=[Accel], action_dispatch=DocumentMessage::GroupSelectedLayers { group_folder_type: GroupFolderType::Layer }),
361363
entry!(KeyDown(KeyG); modifiers=[Accel, Shift], action_dispatch=DocumentMessage::UngroupSelectedLayers),
362364
entry!(KeyDown(KeyN); modifiers=[Accel, Shift], action_dispatch=DocumentMessage::CreateEmptyFolder),

editor/src/messages/menu_bar/menu_bar_message_handler.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,18 @@ impl LayoutHolder for MenuBarMessageHandler {
495495
})
496496
.disabled(no_active_document || !has_selected_layers),
497497
]]),
498+
MenuListEntry::new("Blend")
499+
.label("Blend")
500+
.icon("InterpolationBlend")
501+
.tooltip_shortcut(action_shortcut!(DocumentMessageDiscriminant::BlendSelectedLayers))
502+
.on_commit(|_| DocumentMessage::BlendSelectedLayers.into())
503+
.disabled(no_active_document || !has_selected_layers),
504+
MenuListEntry::new("Morph")
505+
.label("Morph")
506+
.icon("InterpolationMorph")
507+
.tooltip_shortcut(action_shortcut!(DocumentMessageDiscriminant::MorphSelectedLayers))
508+
.on_commit(|_| DocumentMessage::MorphSelectedLayers.into())
509+
.disabled(no_active_document || !has_selected_layers),
498510
],
499511
vec![
500512
MenuListEntry::new("Make Path Editable")

editor/src/messages/portfolio/document/document_message.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ pub enum DocumentMessage {
8484
GridVisibility {
8585
visible: bool,
8686
},
87+
BlendSelectedLayers,
88+
MorphSelectedLayers,
8789
GroupSelectedLayers {
8890
group_folder_type: GroupFolderType,
8991
},

editor/src/messages/portfolio/document/document_message_handler.rs

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ use super::utility_types::network_interface::{self, NodeNetworkInterface, Transa
55
use super::utility_types::nodes::{CollapsedLayers, LayerStructureEntry, SelectedNodes};
66
use crate::application::{GRAPHITE_GIT_COMMIT_HASH, generate_uuid};
77
use crate::consts::{
8-
ASYMPTOTIC_EFFECT, COLOR_OVERLAY_GRAY, DEFAULT_DOCUMENT_NAME, FILE_EXTENSION, LAYER_INDENT_OFFSET, NODE_CHAIN_WIDTH, SCALE_EFFECT, SCROLLBAR_SPACING, VIEWPORT_ROTATE_SNAP_INTERVAL,
8+
ASYMPTOTIC_EFFECT, BLEND_COUNT_PER_LAYER, COLOR_OVERLAY_GRAY, DEFAULT_DOCUMENT_NAME, FILE_EXTENSION, LAYER_INDENT_OFFSET, NODE_CHAIN_WIDTH, SCALE_EFFECT, SCROLLBAR_SPACING,
9+
VIEWPORT_ROTATE_SNAP_INTERVAL,
910
};
1011
use crate::messages::input_mapper::utility_types::macros::action_shortcut;
1112
use crate::messages::layout::utility_types::widget_prelude::*;
@@ -625,6 +626,12 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
625626
self.snapping_state.grid_snapping = visible;
626627
responses.add(OverlaysMessage::Draw);
627628
}
629+
DocumentMessage::BlendSelectedLayers => {
630+
self.handle_group_selected_layers(GroupFolderType::Blend, responses);
631+
}
632+
DocumentMessage::MorphSelectedLayers => {
633+
self.handle_group_selected_layers(GroupFolderType::Morph, responses);
634+
}
628635
DocumentMessage::GroupSelectedLayers { group_folder_type } => {
629636
self.handle_group_selected_layers(group_folder_type, responses);
630637
}
@@ -1485,6 +1492,8 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
14851492
DeleteSelectedLayers,
14861493
DuplicateSelectedLayers,
14871494
GroupSelectedLayers,
1495+
BlendSelectedLayers,
1496+
MorphSelectedLayers,
14881497
SelectedLayersLower,
14891498
SelectedLayersLowerToBack,
14901499
SelectedLayersRaise,
@@ -2160,6 +2169,57 @@ impl DocumentMessageHandler {
21602169
});
21612170
}
21622171
}
2172+
GroupFolderType::Blend | GroupFolderType::Morph => {
2173+
let control_path_id = NodeId(generate_uuid());
2174+
let all_layers_to_group = network_interface.shallowest_unique_layers_sorted(&[]);
2175+
let blend_count = matches!(group_folder_type, GroupFolderType::Blend).then(|| all_layers_to_group.len() * BLEND_COUNT_PER_LAYER);
2176+
2177+
responses.add(GraphOperationMessage::NewInterpolationLayer {
2178+
id: folder_id,
2179+
control_path_id,
2180+
parent,
2181+
insert_index,
2182+
blend_count,
2183+
});
2184+
2185+
let new_group_folder = LayerNodeIdentifier::new_unchecked(folder_id);
2186+
2187+
// Move selected layers into the group as children
2188+
for layer_to_group in all_layers_to_group.into_iter().rev() {
2189+
responses.add(NodeGraphMessage::MoveLayerToStack {
2190+
layer: layer_to_group,
2191+
parent: new_group_folder,
2192+
insert_index: 0,
2193+
});
2194+
}
2195+
2196+
// Connect the child stack to the control path layer as a co-parent
2197+
responses.add(GraphOperationMessage::ConnectInterpolationControlPathToChildren {
2198+
interpolation_layer_id: folder_id,
2199+
control_path_id,
2200+
});
2201+
2202+
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![folder_id] });
2203+
responses.add(NodeGraphMessage::RunDocumentGraph);
2204+
responses.add(DocumentMessage::DocumentStructureChanged);
2205+
responses.add(NodeGraphMessage::SendGraph);
2206+
2207+
// The control path layer (Blend Path / Morph Path) should start collapsed.
2208+
let instance_path = {
2209+
// Build instance path from root down to the control path layer, which is a sibling of the main layer under `parent`.
2210+
let mut instance_path: Vec<NodeId> = parent
2211+
.ancestors(network_interface.document_metadata())
2212+
.take_while(|&ancestor| ancestor != LayerNodeIdentifier::ROOT_PARENT)
2213+
.map(LayerNodeIdentifier::to_node)
2214+
.collect();
2215+
instance_path.reverse();
2216+
instance_path.push(control_path_id);
2217+
instance_path
2218+
};
2219+
responses.add(DocumentMessage::ToggleLayerExpansion { instance_path, recursive: false });
2220+
2221+
return folder_id;
2222+
}
21632223
}
21642224

21652225
let new_group_folder = LayerNodeIdentifier::new_unchecked(folder_id);

editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,17 @@ pub enum GraphOperationMessage {
7474
parent: LayerNodeIdentifier,
7575
insert_index: usize,
7676
},
77+
NewInterpolationLayer {
78+
id: NodeId,
79+
control_path_id: NodeId,
80+
parent: LayerNodeIdentifier,
81+
insert_index: usize,
82+
blend_count: Option<usize>,
83+
},
84+
ConnectInterpolationControlPathToChildren {
85+
interpolation_layer_id: NodeId,
86+
control_path_id: NodeId,
87+
},
7788
NewBooleanOperationLayer {
7889
id: NodeId,
7990
operation: graphene_std::vector::misc::BooleanOperation,

editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,77 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
172172
network_interface.move_layer_to_stack(layer, parent, insert_index, &[]);
173173
responses.add(NodeGraphMessage::RunDocumentGraph);
174174
}
175+
GraphOperationMessage::NewInterpolationLayer {
176+
id,
177+
control_path_id,
178+
parent,
179+
insert_index,
180+
blend_count,
181+
} => {
182+
let mut modify_inputs = ModifyInputsContext::new(network_interface, responses);
183+
let layer = modify_inputs.create_layer(id);
184+
185+
// Insert the main chain node (Blend or Morph) depending on whether a blend count is provided
186+
let (chain_node_id, layer_alias, path_alias) = if let Some(count) = blend_count {
187+
(modify_inputs.insert_blend_data(layer, count as f64), "Blend", "Blend Path")
188+
} else {
189+
(modify_inputs.insert_morph_data(layer), "Morph", "Morph Path")
190+
};
191+
192+
// Create the control path layer (Path → Auto-Tangents → Origins to Polyline)
193+
let control_path_layer = modify_inputs.create_layer(control_path_id);
194+
let path_node_id = modify_inputs.insert_control_path_data(control_path_layer);
195+
196+
network_interface.move_layer_to_stack(control_path_layer, parent, insert_index, &[]);
197+
network_interface.move_layer_to_stack(layer, parent, insert_index + 1, &[]);
198+
199+
// Connect the Path node's output to the chain node's path parameter input (input 4 for both Morph and Blend).
200+
// Done after move_layer_to_stack so chain nodes have correct positions when converted to absolute.
201+
network_interface.set_input(&InputConnector::node(chain_node_id, 4), NodeInput::node(path_node_id, 0), &[]);
202+
203+
responses.add(NodeGraphMessage::SetDisplayNameImpl {
204+
node_id: id,
205+
alias: layer_alias.to_string(),
206+
});
207+
responses.add(NodeGraphMessage::SetDisplayNameImpl {
208+
node_id: control_path_id,
209+
alias: path_alias.to_string(),
210+
});
211+
}
212+
GraphOperationMessage::ConnectInterpolationControlPathToChildren {
213+
interpolation_layer_id,
214+
control_path_id,
215+
} => {
216+
// Find the chain node (Blend or Morph, first in chain of the layer)
217+
let Some(OutputConnector::Node { node_id: chain_node, .. }) = network_interface.upstream_output_connector(&InputConnector::node(interpolation_layer_id, 1), &[]) else {
218+
log::error!("Could not find chain node for layer {interpolation_layer_id}");
219+
return;
220+
};
221+
222+
// Get what feeds into the chain node's primary input (the children stack)
223+
let Some(OutputConnector::Node { node_id: children_id, output_index }) = network_interface.upstream_output_connector(&InputConnector::node(chain_node, 0), &[]) else {
224+
log::error!("Could not find children stack feeding chain node {chain_node}");
225+
return;
226+
};
227+
228+
// Find the deepest node in the control path layer's chain (Origins to Polyline)
229+
let mut deepest_chain_node = None;
230+
let mut current_connector = InputConnector::node(control_path_id, 1);
231+
while let Some(OutputConnector::Node { node_id, .. }) = network_interface.upstream_output_connector(&current_connector, &[]) {
232+
deepest_chain_node = Some(node_id);
233+
current_connector = InputConnector::node(node_id, 0);
234+
}
235+
236+
// Connect children to the deepest chain node's input 0 (or the layer's input 1 if no chain)
237+
let target_connector = match deepest_chain_node {
238+
Some(node_id) => InputConnector::node(node_id, 0),
239+
None => InputConnector::node(control_path_id, 1),
240+
};
241+
network_interface.set_input(&target_connector, NodeInput::node(children_id, output_index), &[]);
242+
243+
// Shift the child stack (topmost child only, the rest follow) down 3 and left 10
244+
network_interface.shift_node(&children_id, IVec2::new(-10, 3), &[]);
245+
}
175246
GraphOperationMessage::NewBooleanOperationLayer { id, operation, parent, insert_index } => {
176247
let mut modify_inputs = ModifyInputsContext::new(network_interface, responses);
177248
let layer = modify_inputs.create_layer(id);

editor/src/messages/portfolio/document/graph_operation/utility_types.rs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,61 @@ impl<'a> ModifyInputsContext<'a> {
156156
self.network_interface.move_node_to_chain_start(&boolean_id, layer, &[], self.import);
157157
}
158158

159+
pub fn insert_blend_data(&mut self, layer: LayerNodeIdentifier, count: f64) -> NodeId {
160+
let blend = resolve_network_node_type("Blend").expect("Blend node does not exist").node_template_input_override([
161+
Some(NodeInput::value(TaggedValue::Graphic(Default::default()), true)),
162+
Some(NodeInput::value(TaggedValue::F64(count), false)),
163+
]);
164+
165+
let blend_id = NodeId::new();
166+
self.network_interface.insert_node(blend_id, blend, &[]);
167+
self.network_interface.move_node_to_chain_start(&blend_id, layer, &[], self.import);
168+
169+
blend_id
170+
}
171+
172+
pub fn insert_morph_data(&mut self, layer: LayerNodeIdentifier) -> NodeId {
173+
let morph = resolve_proto_node_type(graphene_std::vector::morph::IDENTIFIER)
174+
.expect("Morph node does not exist")
175+
.node_template_input_override([
176+
Some(NodeInput::value(TaggedValue::Graphic(Default::default()), true)),
177+
Some(NodeInput::value(TaggedValue::F64(0.5), false)),
178+
]);
179+
180+
let morph_id = NodeId::new();
181+
self.network_interface.insert_node(morph_id, morph, &[]);
182+
self.network_interface.move_node_to_chain_start(&morph_id, layer, &[], self.import);
183+
184+
morph_id
185+
}
186+
187+
/// Returns the Path node ID (the node closest to the layer's merge node in the chain).
188+
pub fn insert_control_path_data(&mut self, layer: LayerNodeIdentifier) -> NodeId {
189+
// Add Origins to Polyline node first (will be pushed deepest in the chain)
190+
let origins_to_polyline = resolve_network_node_type("Origins to Polyline")
191+
.expect("Origins to Polyline node does not exist")
192+
.default_node_template();
193+
let origins_to_polyline_id = NodeId::new();
194+
self.network_interface.insert_node(origins_to_polyline_id, origins_to_polyline, &[]);
195+
self.network_interface.move_node_to_chain_start(&origins_to_polyline_id, layer, &[], self.import);
196+
197+
// Add Auto-Tangents node (between Origins to Polyline and Path), with spread=1 and preserve_existing=false
198+
let auto_tangents = resolve_proto_node_type(graphene_std::vector::auto_tangents::IDENTIFIER)
199+
.expect("Auto-Tangents node does not exist")
200+
.node_template_input_override([None, Some(NodeInput::value(TaggedValue::F64(1.), false)), Some(NodeInput::value(TaggedValue::Bool(false), false))]);
201+
let auto_tangents_id = NodeId::new();
202+
self.network_interface.insert_node(auto_tangents_id, auto_tangents, &[]);
203+
self.network_interface.move_node_to_chain_start(&auto_tangents_id, layer, &[], self.import);
204+
205+
// Add Path node to chain start (closest to the Merge node)
206+
let path = resolve_network_node_type("Path").expect("Path node does not exist").default_node_template();
207+
let path_id = NodeId::new();
208+
self.network_interface.insert_node(path_id, path, &[]);
209+
self.network_interface.move_node_to_chain_start(&path_id, layer, &[], self.import);
210+
211+
path_id
212+
}
213+
159214
pub fn insert_vector(&mut self, subpaths: Vec<Subpath<PointId>>, layer: LayerNodeIdentifier, include_transform: bool, include_fill: bool, include_stroke: bool) {
160215
let vector = Table::new_from_element(Vector::from_subpaths(subpaths, true));
161216

editor/src/messages/portfolio/document/navigation/navigation_message_handler.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -343,8 +343,8 @@ impl MessageHandler<NavigationMessage, NavigationMessageContext<'_>> for Navigat
343343
let (pos1, pos2) = (pos1.min(pos2), pos1.max(pos2));
344344
let diagonal = pos2 - pos1;
345345

346-
if diagonal.length() < f64::EPSILON * 1000. || viewport.size().into_dvec2() == DVec2::ZERO {
347-
warn!("Cannot center since the viewport size is 0");
346+
if !diagonal.is_finite() || diagonal.length() < f64::EPSILON * 1000. || viewport.size().into_dvec2() == DVec2::ZERO {
347+
warn!("Cannot center since the viewport size is 0 or the bounds are non-finite");
348348
return;
349349
}
350350

0 commit comments

Comments
 (0)