Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
f1138d5
Fix Morph node transform interpolation and preservation in the table
Keavon Mar 24, 2026
c149c6f
Fix click target positions for Morph's nested layers by pre-compensat…
Keavon Mar 24, 2026
21b8463
Redesign Morph node (v3) with control path input and uniformly spaced…
Keavon Mar 26, 2026
6e1f6b7
Add migration from Morph node v2 to v3
Keavon Mar 26, 2026
0ba482b
Redesign the 'Blend Shapes' node behavior and subgraph definition
Keavon Mar 26, 2026
376000f
Add the Layer > Blend menu entry to easily set up a blend
Keavon Mar 26, 2026
7321757
Optimize the Morph node
Keavon Mar 27, 2026
3d74c8b
Refactor the Morph node to remove the roundtrip through BezPath
Keavon Mar 27, 2026
f63a548
Fine-tune Morph node Bezier order promotion and handle interpolation
Keavon Mar 27, 2026
3105c46
Add the Layer > Morph menu bar entry
Keavon Mar 27, 2026
97f4a80
Fix NaN and guard against other potential NaN bugs breaking the editor
Keavon Mar 28, 2026
79b46dc
Add InterpolationDistribution parameter to Morph with weighted progre…
Keavon Mar 27, 2026
9561bc9
Add the Reverse parameter to the Morph node
Keavon Mar 28, 2026
dbc4c20
Update the order of the inputs to Blend Shapes for consistency with M…
Keavon Mar 28, 2026
1de32a0
Make Layer > Morph create the Morph Path control layer
Keavon Mar 28, 2026
85197c1
Fix migrations
Keavon Mar 29, 2026
b115a0a
Move 10 to a constant
Keavon Mar 29, 2026
b9e8522
Avoid division by 0 in the Blend Shapes node internals
Keavon Mar 29, 2026
d68ecac
Rename nodes 'Blend' -> 'Mix' and 'Blend Shapes' to 'Blend'
Keavon Mar 29, 2026
b14fccb
Fix a crash encountered while testing
Keavon Mar 29, 2026
a459ced
Final code review
Keavon Mar 30, 2026
a9a3fe1
Make domain push dupe checks debug-only and use push_unchecked in the…
Keavon Mar 30, 2026
17a6942
Pre-allocate for pushes to the vector domains
Keavon Mar 30, 2026
f594a29
Add fast path at t=0
Keavon Mar 30, 2026
ed75e6c
Inline reserve()
Keavon Apr 1, 2026
a33e9d6
Set up the control path layer above not below, and starting collapsed
Keavon Apr 1, 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 @@ -625,6 +626,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 @@ -1485,6 +1492,8 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
DeleteSelectedLayers,
DuplicateSelectedLayers,
GroupSelectedLayers,
BlendSelectedLayers,
MorphSelectedLayers,
SelectedLayersLower,
SelectedLayersLowerToBack,
SelectedLayersRaise,
Expand Down Expand Up @@ -2160,6 +2169,57 @@ 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);

// The control path layer (Blend Path / Morph Path) should start collapsed.
let instance_path = {
// Build instance path from root down to the control path layer, which is a sibling of the main layer under `parent`.
let mut instance_path: Vec<NodeId> = parent
.ancestors(network_interface.document_metadata())
.take_while(|&ancestor| ancestor != LayerNodeIdentifier::ROOT_PARENT)
.map(LayerNodeIdentifier::to_node)
.collect();
instance_path.reverse();
instance_path.push(control_path_id);
instance_path
};
responses.add(DocumentMessage::ToggleLayerExpansion { instance_path, recursive: false });

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(control_path_layer, parent, insert_index, &[]);
network_interface.move_layer_to_stack(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,61 @@ 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)),
]);

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