Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
58e8f47
Fix Morph node transform interpolation and preservation in the table
Keavon Mar 24, 2026
92c7c5e
Fix click target positions for Morph's nested layers by pre-compensat…
Keavon Mar 24, 2026
858fce6
Redesign Morph node (v3) with control path input and uniformly spaced…
Keavon Mar 26, 2026
bace1ce
Add migration from Morph node v2 to v3
Keavon Mar 26, 2026
f5ae79a
Redesign the 'Blend Shapes' node behavior and subgraph definition
Keavon Mar 26, 2026
d54358c
Add the Layer > Blend menu entry to easily set up a blend
Keavon Mar 26, 2026
ee94dc6
Optimize the Morph node
Keavon Mar 27, 2026
a866d8b
Refactor the Morph node to remove the roundtrip through BezPath
Keavon Mar 27, 2026
0360aac
Fine-tune Morph node Bezier order promotion and handle interpolation
Keavon Mar 27, 2026
9f82364
Add the Layer > Morph menu bar entry
Keavon Mar 27, 2026
86d8c12
Fix NaN and guard against other potential NaN bugs breaking the editor
Keavon Mar 28, 2026
d7ca58e
Add InterpolationDistribution parameter to Morph with weighted progre…
Keavon Mar 27, 2026
61b7008
Add the Reverse parameter to the Morph node
Keavon Mar 28, 2026
3106c0d
Update the order of the inputs to Blend Shapes for consistency with M…
Keavon Mar 28, 2026
02f15ab
Make Layer > Morph create the Morph Path control layer
Keavon Mar 28, 2026
172298f
Fix migrations
Keavon Mar 29, 2026
d3dee3e
Move 10 to a constant
Keavon Mar 29, 2026
f75ec64
Avoid division by 0 in the Blend Shapes node internals
Keavon Mar 29, 2026
5f38106
Rename nodes 'Blend' -> 'Mix' and 'Blend Shapes' to 'Blend'
Keavon Mar 29, 2026
d15c038
Fix a crash encountered while testing
Keavon Mar 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .branding
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
https://github.com/Keavon/graphite-branded-assets/archive/8ae15dc9c51a3855475d8cab1d0f29d9d9bc622c.tar.gz
c19abe4ac848f3c835e43dc065c59e20e60233ae023ea0a064c5fed442be2d3d
https://github.com/Keavon/graphite-branded-assets/archive/fc02baf37d7428e86aa0e5f772c1f6e42173d405.tar.gz
061a880c753472ea0a84fbc09e54d3596bea6e299067169d34347c717de13ded
3 changes: 3 additions & 0 deletions editor/src/consts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,3 +186,6 @@ pub const DOUBLE_CLICK_MILLISECONDS: u64 = 500;
pub const UI_SCALE_DEFAULT: f64 = 1.;
pub const UI_SCALE_MIN: f64 = 0.5;
pub const UI_SCALE_MAX: f64 = 3.;

// ACTIONS
pub const BLEND_COUNT_PER_LAYER: usize = 10;
2 changes: 2 additions & 0 deletions editor/src/messages/input_mapper/input_mappings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,8 @@ pub fn input_mappings(zoom_with_scroll: bool) -> Mapping {
entry!(KeyDown(KeyS); modifiers=[Accel, Shift], action_dispatch=DocumentMessage::SaveDocumentAs),
entry!(KeyDown(KeyD); modifiers=[Accel], canonical, action_dispatch=DocumentMessage::DuplicateSelectedLayers),
entry!(KeyDown(KeyJ); modifiers=[Accel], action_dispatch=DocumentMessage::DuplicateSelectedLayers),
entry!(KeyDown(KeyB); modifiers=[Accel, Alt], action_dispatch=DocumentMessage::BlendSelectedLayers),
entry!(KeyDown(KeyM); modifiers=[Accel, Alt], action_dispatch=DocumentMessage::MorphSelectedLayers), // Might get eaten by the GeForce Experience overlay for some Windows users
entry!(KeyDown(KeyG); modifiers=[Accel], action_dispatch=DocumentMessage::GroupSelectedLayers { group_folder_type: GroupFolderType::Layer }),
entry!(KeyDown(KeyG); modifiers=[Accel, Shift], action_dispatch=DocumentMessage::UngroupSelectedLayers),
entry!(KeyDown(KeyN); modifiers=[Accel, Shift], action_dispatch=DocumentMessage::CreateEmptyFolder),
Expand Down
12 changes: 12 additions & 0 deletions editor/src/messages/menu_bar/menu_bar_message_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,18 @@ impl LayoutHolder for MenuBarMessageHandler {
})
.disabled(no_active_document || !has_selected_layers),
]]),
MenuListEntry::new("Blend")
.label("Blend")
.icon("InterpolationBlend")
.tooltip_shortcut(action_shortcut!(DocumentMessageDiscriminant::BlendSelectedLayers))
.on_commit(|_| DocumentMessage::BlendSelectedLayers.into())
.disabled(no_active_document || !has_selected_layers),
MenuListEntry::new("Morph")
.label("Morph")
.icon("InterpolationMorph")
.tooltip_shortcut(action_shortcut!(DocumentMessageDiscriminant::MorphSelectedLayers))
.on_commit(|_| DocumentMessage::MorphSelectedLayers.into())
.disabled(no_active_document || !has_selected_layers),
],
vec![
MenuListEntry::new("Make Path Editable")
Expand Down
2 changes: 2 additions & 0 deletions editor/src/messages/portfolio/document/document_message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ pub enum DocumentMessage {
GridVisibility {
visible: bool,
},
BlendSelectedLayers,
MorphSelectedLayers,
GroupSelectedLayers {
group_folder_type: GroupFolderType,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ use super::utility_types::network_interface::{self, NodeNetworkInterface, Transa
use super::utility_types::nodes::{CollapsedLayers, LayerStructureEntry, SelectedNodes};
use crate::application::{GRAPHITE_GIT_COMMIT_HASH, generate_uuid};
use crate::consts::{
ASYMPTOTIC_EFFECT, COLOR_OVERLAY_GRAY, DEFAULT_DOCUMENT_NAME, FILE_EXTENSION, LAYER_INDENT_OFFSET, NODE_CHAIN_WIDTH, SCALE_EFFECT, SCROLLBAR_SPACING, VIEWPORT_ROTATE_SNAP_INTERVAL,
ASYMPTOTIC_EFFECT, BLEND_COUNT_PER_LAYER, COLOR_OVERLAY_GRAY, DEFAULT_DOCUMENT_NAME, FILE_EXTENSION, LAYER_INDENT_OFFSET, NODE_CHAIN_WIDTH, SCALE_EFFECT, SCROLLBAR_SPACING,
VIEWPORT_ROTATE_SNAP_INTERVAL,
};
use crate::messages::input_mapper::utility_types::macros::action_shortcut;
use crate::messages::layout::utility_types::widget_prelude::*;
Expand Down Expand Up @@ -624,6 +625,12 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
self.snapping_state.grid_snapping = visible;
responses.add(OverlaysMessage::Draw);
}
DocumentMessage::BlendSelectedLayers => {
self.handle_group_selected_layers(GroupFolderType::Blend, responses);
}
DocumentMessage::MorphSelectedLayers => {
self.handle_group_selected_layers(GroupFolderType::Morph, responses);
}
DocumentMessage::GroupSelectedLayers { group_folder_type } => {
self.handle_group_selected_layers(group_folder_type, responses);
}
Expand Down Expand Up @@ -1482,6 +1489,8 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
DeleteSelectedLayers,
DuplicateSelectedLayers,
GroupSelectedLayers,
BlendSelectedLayers,
MorphSelectedLayers,
SelectedLayersLower,
SelectedLayersLowerToBack,
SelectedLayersRaise,
Expand Down Expand Up @@ -1961,6 +1970,43 @@ impl DocumentMessageHandler {
});
}
}
GroupFolderType::Blend | GroupFolderType::Morph => {
let control_path_id = NodeId(generate_uuid());
let all_layers_to_group = network_interface.shallowest_unique_layers_sorted(&[]);
let blend_count = matches!(group_folder_type, GroupFolderType::Blend).then(|| all_layers_to_group.len() * BLEND_COUNT_PER_LAYER);

responses.add(GraphOperationMessage::NewInterpolationLayer {
id: folder_id,
control_path_id,
parent,
insert_index,
blend_count,
});

let new_group_folder = LayerNodeIdentifier::new_unchecked(folder_id);

// Move selected layers into the group as children
for layer_to_group in all_layers_to_group.into_iter().rev() {
responses.add(NodeGraphMessage::MoveLayerToStack {
layer: layer_to_group,
parent: new_group_folder,
insert_index: 0,
});
}

// Connect the child stack to the control path layer as a co-parent
responses.add(GraphOperationMessage::ConnectInterpolationControlPathToChildren {
interpolation_layer_id: folder_id,
control_path_id,
});

responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![folder_id] });
responses.add(NodeGraphMessage::RunDocumentGraph);
responses.add(DocumentMessage::DocumentStructureChanged);
responses.add(NodeGraphMessage::SendGraph);

return folder_id;
}
}

let new_group_folder = LayerNodeIdentifier::new_unchecked(folder_id);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,17 @@ pub enum GraphOperationMessage {
parent: LayerNodeIdentifier,
insert_index: usize,
},
NewInterpolationLayer {
id: NodeId,
control_path_id: NodeId,
parent: LayerNodeIdentifier,
insert_index: usize,
blend_count: Option<usize>,
},
ConnectInterpolationControlPathToChildren {
interpolation_layer_id: NodeId,
control_path_id: NodeId,
},
NewBooleanOperationLayer {
id: NodeId,
operation: graphene_std::vector::misc::BooleanOperation,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,77 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
network_interface.move_layer_to_stack(layer, parent, insert_index, &[]);
responses.add(NodeGraphMessage::RunDocumentGraph);
}
GraphOperationMessage::NewInterpolationLayer {
id,
control_path_id,
parent,
insert_index,
blend_count,
} => {
let mut modify_inputs = ModifyInputsContext::new(network_interface, responses);
let layer = modify_inputs.create_layer(id);

// Insert the main chain node (Blend or Morph) depending on whether a blend count is provided
let (chain_node_id, layer_alias, path_alias) = if let Some(count) = blend_count {
(modify_inputs.insert_blend_data(layer, count as f64), "Blend", "Blend Path")
} else {
(modify_inputs.insert_morph_data(layer), "Morph", "Morph Path")
};

// Create the control path layer (Path → Auto-Tangents → Origins to Polyline)
let control_path_layer = modify_inputs.create_layer(control_path_id);
let path_node_id = modify_inputs.insert_control_path_data(control_path_layer);

network_interface.move_layer_to_stack(layer, parent, insert_index, &[]);
network_interface.move_layer_to_stack(control_path_layer, parent, insert_index + 1, &[]);

// Connect the Path node's output to the chain node's path parameter input (input 4 for both Morph and Blend).
// Done after move_layer_to_stack so chain nodes have correct positions when converted to absolute.
network_interface.set_input(&InputConnector::node(chain_node_id, 4), NodeInput::node(path_node_id, 0), &[]);

responses.add(NodeGraphMessage::SetDisplayNameImpl {
node_id: id,
alias: layer_alias.to_string(),
});
responses.add(NodeGraphMessage::SetDisplayNameImpl {
node_id: control_path_id,
alias: path_alias.to_string(),
});
}
GraphOperationMessage::ConnectInterpolationControlPathToChildren {
interpolation_layer_id,
control_path_id,
} => {
// Find the chain node (Blend or Morph, first in chain of the layer)
let Some(OutputConnector::Node { node_id: chain_node, .. }) = network_interface.upstream_output_connector(&InputConnector::node(interpolation_layer_id, 1), &[]) else {
log::error!("Could not find chain node for layer {interpolation_layer_id}");
return;
};

// Get what feeds into the chain node's primary input (the children stack)
let Some(OutputConnector::Node { node_id: children_id, output_index }) = network_interface.upstream_output_connector(&InputConnector::node(chain_node, 0), &[]) else {
log::error!("Could not find children stack feeding chain node {chain_node}");
return;
};

// Find the deepest node in the control path layer's chain (Origins to Polyline)
let mut deepest_chain_node = None;
let mut current_connector = InputConnector::node(control_path_id, 1);
while let Some(OutputConnector::Node { node_id, .. }) = network_interface.upstream_output_connector(&current_connector, &[]) {
deepest_chain_node = Some(node_id);
current_connector = InputConnector::node(node_id, 0);
}

// Connect children to the deepest chain node's input 0 (or the layer's input 1 if no chain)
let target_connector = match deepest_chain_node {
Some(node_id) => InputConnector::node(node_id, 0),
None => InputConnector::node(control_path_id, 1),
};
network_interface.set_input(&target_connector, NodeInput::node(children_id, output_index), &[]);

// Shift the child stack (topmost child only, the rest follow) down 3 and left 10
network_interface.shift_node(&children_id, IVec2::new(-10, 3), &[]);
}
GraphOperationMessage::NewBooleanOperationLayer { id, operation, parent, insert_index } => {
let mut modify_inputs = ModifyInputsContext::new(network_interface, responses);
let layer = modify_inputs.create_layer(id);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,64 @@ impl<'a> ModifyInputsContext<'a> {
self.network_interface.move_node_to_chain_start(&boolean_id, layer, &[], self.import);
}

pub fn insert_blend_data(&mut self, layer: LayerNodeIdentifier, count: f64) -> NodeId {
let blend = resolve_network_node_type("Blend").expect("Blend node does not exist").node_template_input_override([
Some(NodeInput::value(TaggedValue::Graphic(Default::default()), true)),
Some(NodeInput::value(TaggedValue::F64(count), false)),
]);

let blend_id = NodeId::new();
self.network_interface.insert_node(blend_id, blend, &[]);
self.network_interface.move_node_to_chain_start(&blend_id, layer, &[], self.import);

blend_id
}

pub fn insert_morph_data(&mut self, layer: LayerNodeIdentifier) -> NodeId {
let morph = resolve_proto_node_type(graphene_std::vector::morph::IDENTIFIER)
.expect("Morph node does not exist")
.node_template_input_override([
Some(NodeInput::value(TaggedValue::Graphic(Default::default()), true)),
Some(NodeInput::value(TaggedValue::F64(0.5), false)),
None,
None,
Some(NodeInput::value(TaggedValue::Vector(Default::default()), false)),
]);

let morph_id = NodeId::new();
self.network_interface.insert_node(morph_id, morph, &[]);
self.network_interface.move_node_to_chain_start(&morph_id, layer, &[], self.import);

morph_id
}

/// Returns the Path node ID (the node closest to the layer's merge node in the chain).
pub fn insert_control_path_data(&mut self, layer: LayerNodeIdentifier) -> NodeId {
// Add Origins to Polyline node first (will be pushed deepest in the chain)
let origins_to_polyline = resolve_network_node_type("Origins to Polyline")
.expect("Origins to Polyline node does not exist")
.default_node_template();
let origins_to_polyline_id = NodeId::new();
self.network_interface.insert_node(origins_to_polyline_id, origins_to_polyline, &[]);
self.network_interface.move_node_to_chain_start(&origins_to_polyline_id, layer, &[], self.import);

// Add Auto-Tangents node (between Origins to Polyline and Path), with spread=1 and preserve_existing=false
let auto_tangents = resolve_proto_node_type(graphene_std::vector::auto_tangents::IDENTIFIER)
.expect("Auto-Tangents node does not exist")
.node_template_input_override([None, Some(NodeInput::value(TaggedValue::F64(1.), false)), Some(NodeInput::value(TaggedValue::Bool(false), false))]);
let auto_tangents_id = NodeId::new();
self.network_interface.insert_node(auto_tangents_id, auto_tangents, &[]);
self.network_interface.move_node_to_chain_start(&auto_tangents_id, layer, &[], self.import);

// Add Path node to chain start (closest to the Merge node)
let path = resolve_network_node_type("Path").expect("Path node does not exist").default_node_template();
let path_id = NodeId::new();
self.network_interface.insert_node(path_id, path, &[]);
self.network_interface.move_node_to_chain_start(&path_id, layer, &[], self.import);

path_id
}

pub fn insert_vector(&mut self, subpaths: Vec<Subpath<PointId>>, layer: LayerNodeIdentifier, include_transform: bool, include_fill: bool, include_stroke: bool) {
let vector = Table::new_from_element(Vector::from_subpaths(subpaths, true));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -343,8 +343,8 @@ impl MessageHandler<NavigationMessage, NavigationMessageContext<'_>> for Navigat
let (pos1, pos2) = (pos1.min(pos2), pos1.max(pos2));
let diagonal = pos2 - pos1;

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

Expand Down
Loading
Loading