Move session state out of the network interface caches and replace the selection hijacks with explicit parameters (#4379)

* Move drag offsets out of the stack dependents cache into dedicated drag session state

* Replace the move_layer_to_stack selection hijack with a seeded stack dependents load

* Move text measurement into a neutral utility module out of the overlays layer

* Deduplicate the document bounds queries and name the artboard clip input index

* Replace the remove_references_from_network selection hijack with an explicit shift set

* Extract shared import and export wire disconnection used by the expose handlers

* Extract the shared epilogue of the import and export removal operations

* Move the post node lookup into the network interface to fix the layering inversion

* Clear drag offsets when the stack dependents cache unloads so programmatic shifts cannot replay them

* Use the thread-local text context for text measurement instead of a process-global mutex

* Resolve the post node lookup within the network being edited instead of the document root
This commit is contained in:
Keavon Chambers
2026-07-26 01:09:14 -07:00
committed by Dennis Kobert
parent 8b3a6a350f
commit bc360aa910
14 changed files with 291 additions and 296 deletions

View File

@@ -1,7 +1,7 @@
use super::transform_utils;
use crate::messages::portfolio::document::node_graph::document_node_definitions::{DefinitionIdentifier, resolve_document_node_type, resolve_network_node_type, resolve_proto_node_type};
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::{self, FlowType, InputConnector, NodeNetworkInterface, OutputConnector};
use crate::messages::portfolio::document::utility_types::network_interface::{self, FlowType, InputConnector, NodeNetworkInterface};
use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::graph_modification_utils::{get_fill_input_node_id, get_upstream_gradient_value_node_id, gradient_chain_target_input};
use glam::{DAffine2, DVec2, IVec2};
@@ -58,72 +58,6 @@ impl<'a> ModifyInputsContext<'a> {
Some(document)
}
/// Starts at any folder, or the output, and skips layer nodes based on insert_index. Non layer nodes are always skipped. Returns the post node InputConnector and pre node OutputConnector
/// Non layer nodes directly upstream of a layer are treated as part of that layer. See insert_index == 2 in the diagram
/// -----> Post node
/// | if insert_index == 0, return (Post node, Some(Layer1))
/// -> Layer1
/// ↑ if insert_index == 1, return (Layer1, Some(Layer2))
/// -> Layer2
/// ↑
/// -> NonLayerNode
/// ↑ if insert_index == 2, return (NonLayerNode, Some(Layer3))
/// -> Layer3
/// if insert_index == 3, return (Layer3, None)
pub fn get_post_node_with_index(network_interface: &NodeNetworkInterface, parent: LayerNodeIdentifier, insert_index: usize) -> InputConnector {
let mut post_node_input_connector = if parent == LayerNodeIdentifier::ROOT_PARENT {
InputConnector::Export(0)
} else {
InputConnector::node(parent.to_node(), 1)
};
// Skip layers based on skip_layer_nodes, which inserts the new layer at a certain index of the layer stack.
let mut current_index = 0;
// Set the post node to the layer node at insert_index
loop {
if current_index == insert_index {
break;
}
let next_node_in_stack_id = network_interface
.input_from_connector(&post_node_input_connector, &[])
.and_then(|input_from_connector| if let NodeInput::Node { node_id, .. } = input_from_connector { Some(node_id) } else { None });
if let Some(next_node_in_stack_id) = next_node_in_stack_id {
// Only increment index for layer nodes
if network_interface.is_layer(next_node_in_stack_id, &[]) {
current_index += 1;
}
// Input as a sibling to the Layer node above
post_node_input_connector = InputConnector::node(*next_node_in_stack_id, 0);
} else {
log::error!("Error getting post node: insert_index out of bounds");
break;
};
}
let layer_input_connector = post_node_input_connector;
// Sink post_node down to the end of the non layer chain that feeds into post_node, such that pre_node is the layer node at insert_index + 1, or None if insert_index is the last layer
loop {
let pre_node_output_connector = network_interface.upstream_output_connector(&post_node_input_connector, &[]);
match pre_node_output_connector {
Some(OutputConnector::Node { node_id: pre_node_id, .. }) if !network_interface.is_layer(&pre_node_id, &[]) => {
// Update post_node_input_connector for the next iteration
post_node_input_connector = InputConnector::node(pre_node_id, 0);
// Insert directly under layer if moving to the end of a layer stack that ends with a non layer node that does not have an exposed primary input
let primary_is_exposed = network_interface.input_from_connector(&post_node_input_connector, &[]).is_some_and(|input| input.is_exposed());
if !primary_is_exposed {
return layer_input_connector;
}
}
_ => break, // Break if pre_node_output_connector is None or if pre_node_id is a layer
}
}
post_node_input_connector
}
/// Creates a new layer and adds it to the document network. network_interface.move_layer_to_stack should be called after
pub fn create_layer(&mut self, new_id: NodeId) -> LayerNodeIdentifier {
let new_merge_node = resolve_network_node_type("Merge").expect("Merge node").default_node_template();

View File

@@ -530,19 +530,8 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
network_interface.set_input(&encapsulating_connector, input, network_path);
let Some(outward_wires) = network_interface.outward_wires(breadcrumb_network_path) else {
log::error!("Could not get outward wires in remove_import");
return;
};
let Some(downstream_connections) = outward_wires.get(&OutputConnector::Import(0)).cloned() else {
log::error!("Could not get outward wires for import in remove_import");
return;
};
// Disconnect all connections in the encapsulating network
for downstream_connection in &downstream_connections {
network_interface.disconnect_input(downstream_connection, breadcrumb_network_path);
}
// Disconnect all connections fed by the hidden import in the nested network
network_interface.disconnect_import_wires(0, breadcrumb_network_path);
responses.add(NodeGraphMessage::UpdateImportsExports);
responses.add(NodeGraphMessage::SendWires);
@@ -565,18 +554,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
// Disconnect all connections in the encapsulating network
if let Some((encapsulating_node, encapsulating_path)) = breadcrumb_network_path.split_last() {
let Some(outward_wires) = network_interface.outward_wires(encapsulating_path) else {
log::error!("Could not get outward wires in remove_import");
return;
};
let Some(downstream_connections) = outward_wires.get(&OutputConnector::node(*encapsulating_node, 0)).cloned() else {
log::error!("Could not get outward wires for import in remove_import");
return;
};
for downstream_connection in &downstream_connections {
network_interface.disconnect_input(downstream_connection, encapsulating_path);
}
network_interface.disconnect_output_wires(&OutputConnector::node(*encapsulating_node, 0), encapsulating_path);
}
responses.add(NodeGraphMessage::UpdateImportsExports);
@@ -1352,7 +1330,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
self.shift_without_push = false;
// Reset all offsets to end the rubber banding while dragging
network_interface.unload_stack_dependents_y_offset(selection_network_path);
network_interface.clear_drag_offsets(selection_network_path);
let Some(selected_nodes) = network_interface.selected_nodes_in_nested_network(selection_network_path) else {
log::error!("Could not get selected nodes in PointerUp");
return;
@@ -1813,7 +1791,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
network_interface.shift_selected_nodes(direction, self.shift_without_push, selection_network_path);
if !rubber_band {
network_interface.unload_stack_dependents_y_offset(selection_network_path);
network_interface.clear_drag_offsets(selection_network_path);
}
if graph_view_overlay_open {

View File

@@ -2,16 +2,14 @@ use super::utility_types::{DrawHandles, OverlayContext};
use crate::consts::HIDE_HANDLE_DISTANCE;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface;
use crate::messages::portfolio::fonts::FALLBACK_FONT_RESOURCE;
pub use crate::messages::portfolio::document::utility_types::text_metrics::text_width;
use crate::messages::tool::common_functionality::shape_editor::{SelectedLayerState, ShapeState};
use crate::messages::tool::tool_messages::tool_prelude::DocumentMessageHandler;
use glam::{DAffine2, DVec2};
use graphene_std::subpath::{Bezier, BezierHandles};
use graphene_std::text::{TextAlign, TextContext, TypesettingConfig};
use graphene_std::vector::misc::ManipulatorPointId;
use graphene_std::vector::{PointId, SegmentId, Vector};
use std::collections::HashMap;
use std::sync::{LazyLock, Mutex};
#[cfg(target_family = "wasm")]
use wasm_bindgen::JsCast;
@@ -220,24 +218,6 @@ pub fn path_endpoint_overlays(document: &DocumentMessageHandler, shape_editor: &
}
}
pub static GLOBAL_TEXT_CONTEXT: LazyLock<Mutex<TextContext>> = LazyLock::new(|| Mutex::new(TextContext::default()));
pub fn text_width(text: &str, font_size: f64) -> f64 {
let typesetting = TypesettingConfig {
font_size,
line_height_ratio: 1.2,
letter_spacing: 0.,
letter_tilt: 0.,
max_width: None,
max_height: None,
align: TextAlign::AlignLeft,
};
let mut text_context = GLOBAL_TEXT_CONTEXT.lock().expect("Failed to lock global text context");
let bounds = text_context.bounding_box(text, &FALLBACK_FONT_RESOURCE, typesetting, false);
bounds.x
}
pub fn hex_to_rgba_u8(hex: &str) -> [u8; 4] {
let hex = hex.trim().trim_start_matches('#');
if hex.len() != 6 && hex.len() != 8 {

View File

@@ -3,7 +3,7 @@ use crate::consts::{
COLOR_OVERLAY_YELLOW_DULL, COMPASS_ROSE_ARROW_SIZE, COMPASS_ROSE_HOVER_RING_DIAMETER, COMPASS_ROSE_MAIN_RING_DIAMETER, COMPASS_ROSE_RING_INNER_DIAMETER, DOWEL_PIN_RADIUS,
GRADIENT_MIDPOINT_DIAMOND_RADIUS, MANIPULATOR_GROUP_MARKER_SIZE, PIVOT_CROSSHAIR_LENGTH, PIVOT_CROSSHAIR_THICKNESS, PIVOT_DIAMETER, RESIZE_HANDLE_SIZE, SKEW_TRIANGLE_OFFSET, SKEW_TRIANGLE_SIZE,
};
use crate::messages::portfolio::document::overlays::utility_functions::{GLOBAL_TEXT_CONTEXT, hex_to_rgba_u8};
use crate::messages::portfolio::document::overlays::utility_functions::hex_to_rgba_u8;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::fonts::FALLBACK_FONT_RESOURCE;
use crate::messages::prelude::Message;
@@ -16,7 +16,7 @@ use graphene_std::ATTR_TRANSFORM;
use graphene_std::list::List;
use graphene_std::math::quad::Quad;
use graphene_std::subpath::{self, Subpath};
use graphene_std::text::{TextAlign, TypesettingConfig};
use graphene_std::text::{TextAlign, TextContext, TypesettingConfig};
use graphene_std::vector::click_target::ClickTargetType;
use graphene_std::vector::misc::point_to_dvec2;
use graphene_std::vector::{PointId, SegmentId, Vector};
@@ -1117,17 +1117,17 @@ impl OverlayContextInternal {
align: TextAlign::AlignLeft,
};
// Get text dimensions directly from layout
let mut text_context = GLOBAL_TEXT_CONTEXT.lock().expect("Failed to lock global text context");
let text_size = text_context.bounding_box(text, &FALLBACK_FONT_RESOURCE, typesetting, false);
// Lay out the text once, taking its dimensions and vector paths from the same thread-local context pass
let (text_size, text_list) = TextContext::with_thread_local(|text_context| {
let text_size = text_context.bounding_box(text, &FALLBACK_FONT_RESOURCE, typesetting, false);
let text_list = text_context.to_path(text, &FALLBACK_FONT_RESOURCE, typesetting, false);
(text_size, text_list)
});
let text_width = text_size.x;
let text_height = text_size.y;
// Create a rect from the size (assuming text starts at origin)
let text_bounds = kurbo::Rect::new(0., 0., text_width, text_height);
// Convert text to vector paths for rendering
let text_list = text_context.to_path(text, &FALLBACK_FONT_RESOURCE, typesetting, false);
// Calculate position based on pivot
let mut position = DVec2::ZERO;
match pivot[0] {

View File

@@ -3,5 +3,6 @@ pub mod error;
pub mod misc;
pub mod network_interface;
pub mod nodes;
pub mod text_metrics;
pub mod transformation;
pub mod wires;

View File

@@ -27,11 +27,10 @@ use crate::consts::{
EXPORTS_TO_RIGHT_EDGE_PIXEL_GAP, EXPORTS_TO_TOP_EDGE_PIXEL_GAP, GRID_SIZE, HALF_GRID_SIZE, IMPORTS_TO_LEFT_EDGE_PIXEL_GAP, IMPORTS_TO_TOP_EDGE_PIXEL_GAP, LAYER_INDENT_OFFSET, NODE_CHAIN_WIDTH,
STACK_VERTICAL_GAP,
};
use crate::messages::portfolio::document::graph_operation::utility_types::ModifyInputsContext;
use crate::messages::portfolio::document::node_graph::document_node_definitions::{DefinitionIdentifier, resolve_document_node_type};
use crate::messages::portfolio::document::node_graph::utility_types::{Direction, FrontendClickTargets, FrontendGraphDataType, FrontendGraphInput, FrontendGraphOutput};
use crate::messages::portfolio::document::overlays::utility_functions::text_width;
use crate::messages::portfolio::document::utility_types::network_interface::resolved_types::ResolvedDocumentNodeTypes;
use crate::messages::portfolio::document::utility_types::text_metrics::text_width;
use crate::messages::portfolio::document::utility_types::wires::{GraphWireStyle, WirePath, WirePathUpdate, build_thick_wire_center_line, build_vector_wire};
use crate::messages::tool::common_functionality::graph_modification_utils;
use crate::messages::tool::tool_messages::tool_prelude::NumberInputMode;

View File

@@ -113,8 +113,12 @@ impl NodeNetworkInterface {
log::error!("Could not get selected nodes in load_stack_dependents");
return;
};
self.load_stack_dependents_for_nodes(selected_nodes.selected_nodes().cloned().collect(), network_path);
}
let mut selected_layers = selected_nodes.selected_nodes().filter(|node_id| self.is_layer(node_id, network_path)).cloned().collect::<HashSet<_>>();
/// Builds the stack dependents as if `seed_nodes` were the selection, for shifts driven by a node set other than the selection.
pub(crate) fn load_stack_dependents_for_nodes(&self, seed_nodes: Vec<NodeId>, network_path: &[NodeId]) {
let mut selected_layers = seed_nodes.iter().filter(|node_id| self.is_layer(node_id, network_path)).copied().collect::<HashSet<_>>();
// Deselect all layers that are upstream of other selected layers
let mut removed_layers = Vec::new();
@@ -222,7 +226,7 @@ impl NodeNetworkInterface {
for sole_dependent in sole_dependents {
if !owned_sole_dependents.contains(&sole_dependent) {
stack_dependents.insert(sole_dependent, LayerOwner::None(0));
stack_dependents.insert(sole_dependent, LayerOwner::None);
}
}
}
@@ -241,22 +245,32 @@ impl NodeNetworkInterface {
return;
};
network_metadata.transient_metadata.stack_dependents.unload();
// Drag offsets are only meaningful relative to the stack dependents snapshot they were accumulated against, so they must not outlive it
network_metadata.transient_metadata.drag_offsets.borrow_mut().clear();
}
/// Resets all the offsets for nodes with no LayerOwner when the drag ends
pub fn unload_stack_dependents_y_offset(&mut self, network_path: &[NodeId]) {
let Some(network_metadata) = self.network_metadata_mut(network_path) else {
log::error!("Could not get nested network_metadata in unload_stack_dependents_y_offset");
/// The vertical distance the node has been pushed from its resting position during the current drag.
pub(crate) fn drag_offset(&self, node_id: &NodeId, network_path: &[NodeId]) -> i32 {
self.network_metadata(network_path)
.map_or(0, |network_metadata| network_metadata.transient_metadata.drag_offsets.borrow().get(node_id).copied().unwrap_or(0))
}
pub(crate) fn add_drag_offset(&self, node_id: &NodeId, delta: i32, network_path: &[NodeId]) {
let Some(network_metadata) = self.network_metadata(network_path) else {
log::error!("Could not get nested network_metadata in add_drag_offset");
return;
};
*network_metadata.transient_metadata.drag_offsets.borrow_mut().entry(*node_id).or_insert(0) += delta;
}
if let Some(stack_dependents) = network_metadata.transient_metadata.stack_dependents.get_loaded_mut() {
for layer_owner in stack_dependents.values_mut() {
if let LayerOwner::None(offset) = layer_owner {
*offset = 0;
}
}
}
/// Discards all drag offsets when the drag ends.
pub fn clear_drag_offsets(&self, network_path: &[NodeId]) {
let Some(network_metadata) = self.network_metadata(network_path) else {
log::error!("Could not get nested network_metadata in clear_drag_offsets");
return;
};
network_metadata.transient_metadata.drag_offsets.borrow_mut().clear();
}
pub fn import_export_ports(&mut self, network_path: &[NodeId]) -> Option<&Ports> {
@@ -1081,7 +1095,7 @@ impl NodeNetworkInterface {
let name_left = node_top_left.x + NAME_LEFT_OFFSET;
let icons_reserve = VISIBILITY_INSET_FROM_LAYER_RIGHT + icons_width + GRIP_WIDTH;
let name_right_max = node_top_left.x + width as f64 - icons_reserve;
let text_w = crate::messages::portfolio::document::overlays::utility_functions::text_width(&display_name, FONT_SIZE);
let text_w = text_width(&display_name, FONT_SIZE);
let name_right = (name_left + text_w).min(name_right_max);
if name_right > name_left {
// The 1-grid-tall name strip is centered vertically in the 2-grid-tall layer.

View File

@@ -1,4 +1,5 @@
use super::{InputConnector, OutputConnector, Previewing, RootNode, TransactionStatus};
use crate::messages::portfolio::document::node_graph::utility_types::Direction;
use crate::test_utils::test_prelude::*;
use graph_craft::document::NodeInput;
use graph_craft::document::value::TaggedValue;
@@ -187,26 +188,13 @@ fn merge_definition() -> DefinitionIdentifier {
DefinitionIdentifier::Network("Merge".to_string())
}
async fn create_node_at(editor: &mut EditorTestUtils, node_type: DefinitionIdentifier, x: i32, y: i32) -> NodeId {
let node_id = NodeId::new();
editor
.handle_message(NodeGraphMessage::CreateNodeFromContextMenu {
node_id: Some(node_id),
node_type,
xy: Some((x, y)),
add_transaction: true,
})
.await;
node_id
}
#[tokio::test]
async fn layer_stacking_follows_wiring() {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
let upper = create_node_at(&mut editor, merge_definition(), 20, 10).await;
let lower = create_node_at(&mut editor, merge_definition(), 20, 16).await;
let upper = editor.create_node_by_name_at(merge_definition(), 20, 10).await;
let lower = editor.create_node_by_name_at(merge_definition(), 20, 16).await;
let network_interface = &mut editor.active_document_mut().network_interface;
network_interface.set_to_node_or_layer(&upper, &[], true);
@@ -234,8 +222,8 @@ async fn chain_membership_follows_wiring() {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
let layer = create_node_at(&mut editor, merge_definition(), 20, 10).await;
let node = create_node_at(&mut editor, rectangle_definition(), 15, 10).await;
let layer = editor.create_node_by_name_at(merge_definition(), 20, 10).await;
let node = editor.create_node_by_name_at(rectangle_definition(), 15, 10).await;
let network_interface = &mut editor.active_document_mut().network_interface;
network_interface.set_to_node_or_layer(&layer, &[], true);
@@ -265,8 +253,8 @@ async fn move_layer_to_stack_builds_the_layer_stack() {
let artboard = NodeId::new();
editor.handle_message(new_artboard_message(artboard)).await;
let first = create_node_at(&mut editor, merge_definition(), 0, 30).await;
let second = create_node_at(&mut editor, merge_definition(), 0, 40).await;
let first = editor.create_node_by_name_at(merge_definition(), 0, 30).await;
let second = editor.create_node_by_name_at(merge_definition(), 0, 40).await;
let network_interface = &mut editor.active_document_mut().network_interface;
network_interface.set_to_node_or_layer(&first, &[], true);
@@ -288,12 +276,60 @@ async fn move_layer_to_stack_builds_the_layer_stack() {
assert_invariants(&editor, "after moving two layers into an artboard");
}
#[tokio::test]
async fn drag_offsets_do_not_outlive_programmatic_shifts() {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
let artboard = NodeId::new();
editor.handle_message(new_artboard_message(artboard)).await;
let first = editor.create_node_by_name_at(merge_definition(), 0, 30).await;
let second = editor.create_node_by_name_at(merge_definition(), 0, 40).await;
let third = editor.create_node_by_name_at(merge_definition(), 0, 50).await;
let network_interface = &mut editor.active_document_mut().network_interface;
network_interface.set_to_node_or_layer(&first, &[], true);
network_interface.set_to_node_or_layer(&second, &[], true);
network_interface.set_to_node_or_layer(&third, &[], true);
let artboard_layer = LayerNodeIdentifier::new(artboard, network_interface);
network_interface.move_layer_to_stack(LayerNodeIdentifier::new(first, network_interface), artboard_layer, 0, &[]);
network_interface.move_layer_to_stack(LayerNodeIdentifier::new(second, network_interface), artboard_layer, 1, &[]);
network_interface.move_layer_to_stack(LayerNodeIdentifier::new(third, network_interface), artboard_layer, 2, &[]);
// Making room while inserting into the stack pushes neighbors with drag-offset bookkeeping that must not outlive the operation
for node_id in [first, second, third] {
assert_eq!(network_interface.drag_offset(&node_id, &[]), 0, "Inserting into a stack should not leave a drag offset on {node_id}");
}
// Deleting the middle layer collapses the stack upward, which must also leave no offsets behind
network_interface.delete_nodes(vec![second], false, &[]);
assert_eq!(network_interface.drag_offset(&third, &[]), 0, "Collapsing the deleted layer's space should not leave a drag offset");
// A later nudge must move the survivor by exactly one unit, without replaying any restore toward its pre-collapse position
editor.handle_message(NodeGraphMessage::SelectedNodesSet { nodes: vec![third] }).await;
let network_interface = &editor.active_document().network_interface;
let before = network_interface.position(&third, &[]).expect("Surviving layer should have a position");
editor
.handle_message(NodeGraphMessage::ShiftSelectedNodes {
direction: Direction::Down,
rubber_band: false,
})
.await;
let network_interface = &editor.active_document().network_interface;
let after = network_interface.position(&third, &[]).expect("Surviving layer should have a position");
assert_eq!(after.y - before.y, 1, "A single nudge should move the layer exactly one unit");
assert_invariants(&editor, "after stack pushes, a delete collapse, and a nudge");
}
#[tokio::test]
async fn signature_edits_keep_parallel_metadata_in_sync() {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
let merge = create_node_at(&mut editor, merge_definition(), 0, 0).await;
let merge = editor.create_node_by_name_at(merge_definition(), 0, 0).await;
let path = vec![merge];
let network_interface = &mut editor.active_document_mut().network_interface;
@@ -331,7 +367,7 @@ async fn toggle_preview_transitions_with_a_connected_export() {
let artboard = NodeId::new();
editor.handle_message(new_artboard_message(artboard)).await;
let node = create_node_at(&mut editor, rectangle_definition(), 0, 20).await;
let node = editor.create_node_by_name_at(rectangle_definition(), 0, 20).await;
let network_interface = &mut editor.active_document_mut().network_interface;
let export_node = |network_interface: &super::NodeNetworkInterface| network_interface.input_from_connector(&InputConnector::Export(0), &[]).and_then(|input| input.as_node());

View File

@@ -297,13 +297,18 @@ impl NodeNetworkInterface {
}
pub fn shift_selected_nodes(&mut self, direction: Direction, shift_without_push: bool, network_path: &[NodeId]) {
let Some(mut node_ids) = self
let Some(node_ids) = self
.selected_nodes_in_nested_network(network_path)
.map(|selected_nodes| selected_nodes.selected_nodes().cloned().collect::<HashSet<_>>())
else {
log::error!("Could not get selected nodes in shift_selected_nodes");
return;
};
self.shift_nodes(node_ids, direction, shift_without_push, network_path);
}
pub(crate) fn shift_nodes(&mut self, mut node_ids: HashSet<NodeId>, direction: Direction, shift_without_push: bool, network_path: &[NodeId]) {
let seed_nodes = node_ids.clone();
if !shift_without_push {
// The owned nodes of each layer are populated by the stack dependents load, which otherwise may not run until after this filter
self.try_load_stack_dependents(network_path);
@@ -432,16 +437,9 @@ impl NodeNetworkInterface {
shifted_nodes.insert(*node_id);
self.shift_node(node_id, IVec2::new(0, shift_sign), network_path);
let Some(network_metadata) = self.network_metadata_mut(network_path) else {
log::error!("Could not get nested network_metadata in export_ports");
continue;
};
if let Some(stack_dependents) = network_metadata.transient_metadata.stack_dependents.get_loaded_mut()
&& let Some(LayerOwner::None(offset)) = stack_dependents.get_mut(node_id)
{
*offset += shift_sign;
self.transaction_modified();
};
if self.with_stack_dependents(network_path, |stack_dependents| matches!(stack_dependents.get(node_id), Some(LayerOwner::None))) == Some(true) {
self.add_drag_offset(node_id, shift_sign, network_path);
}
// Shift the upstream layer so that it stays in the same place
if self.is_layer(node_id, network_path) {
@@ -470,24 +468,24 @@ impl NodeNetworkInterface {
let mut stack_dependents_with_position = stack_dependents
.iter()
.filter_map(|(node_id, owner)| {
let LayerOwner::None(offset) = owner else {
let LayerOwner::None = owner else {
return None;
};
if *offset == 0 {
let offset = self.drag_offset(node_id, network_path);
if offset == 0 {
return None;
}
if self.selected_nodes_in_nested_network(network_path).is_some_and(|selected_nodes| {
selected_nodes
.selected_nodes()
.any(|selected_node| selected_node == node_id || self.with_owned_nodes(node_id, network_path, |owned_nodes| owned_nodes.contains(selected_node)) == Some(true))
}) {
if seed_nodes
.iter()
.any(|seed_node| seed_node == node_id || self.with_owned_nodes(node_id, network_path, |owned_nodes| owned_nodes.contains(seed_node)) == Some(true))
{
return None;
};
let Some(position) = self.position(node_id, network_path) else {
log::error!("Could not get position for node {node_id} in shift_selected_nodes");
return None;
};
Some((*node_id, *offset, position.y))
Some((*node_id, offset, position.y))
})
.collect::<Vec<(NodeId, i32, i32)>>();
@@ -533,29 +531,11 @@ impl NodeNetworkInterface {
self.shift_node(node_id, IVec2::new(0, shift_sign), network_path);
let Some(network_metadata) = self.network_metadata_mut(network_path) else {
log::error!("Could not get nested network_metadata in export_ports");
return;
};
let Some(stack_dependents) = network_metadata.transient_metadata.stack_dependents.get_loaded_mut() else {
log::error!("Stack dependents should be loaded in vertical_shift_with_push");
return;
};
let mut default_layer_owner = LayerOwner::None(0);
let layer_owner = stack_dependents.get_mut(node_id).unwrap_or_else(|| {
log::error!("Could not get layer owner in vertical_shift_with_push for node {node_id}");
&mut default_layer_owner
});
match layer_owner {
LayerOwner::None(offset) => {
*offset += shift_sign;
self.transaction_modified();
}
LayerOwner::Layer(_) => {
log::error!("Node being shifted with a push should not be owned");
}
match self.with_stack_dependents(network_path, |stack_dependents| stack_dependents.get(node_id).cloned()) {
Some(Some(LayerOwner::None)) => self.add_drag_offset(node_id, shift_sign, network_path),
Some(Some(LayerOwner::Layer(_))) => log::error!("Node being shifted with a push should not be owned"),
Some(None) => log::error!("Could not get layer owner in vertical_shift_with_push for node {node_id}"),
None => log::error!("Stack dependents should be loaded in vertical_shift_with_push"),
}
// Shift the upstream layer so that it stays in the same place
@@ -652,7 +632,7 @@ impl NodeNetworkInterface {
let layer_owner = *layer_owner;
self.shift_node_or_parent(&layer_owner, shift_sign, shifted_nodes, network_path)
}
LayerOwner::None(_) => self.vertical_shift_with_push(node_id, shift_sign, shifted_nodes, network_path),
LayerOwner::None => self.vertical_shift_with_push(node_id, shift_sign, shifted_nodes, network_path),
}
}
@@ -723,7 +703,7 @@ impl NodeNetworkInterface {
insert_index = 0;
}
let post_node = ModifyInputsContext::get_post_node_with_index(self, parent, insert_index);
let post_node = self.post_node_with_index(parent, insert_index, network_path);
let Some(post_node_input) = self.input_from_connector(&post_node, network_path).cloned() else {
log::error!("Could not get previous input in move_layer_to_stack_for_import");
return;
@@ -811,7 +791,7 @@ impl NodeNetworkInterface {
// Disconnect layer to move
self.remove_references_from_network(&layer.to_node(), network_path);
let post_node = ModifyInputsContext::get_post_node_with_index(self, parent, insert_index);
let post_node = self.post_node_with_index(parent, insert_index, network_path);
// Get the previous input to the post node before inserting the layer
let Some(post_node_input) = self.input_from_connector(&post_node, network_path).cloned() else {
@@ -872,12 +852,9 @@ impl NodeNetworkInterface {
// If there is an upstream node in the new location for the layer, create space for the moved layer by shifting the upstream node down
if let Some(upstream_node_id) = post_node_input.as_node() {
// Select the layer to move to ensure the shifting works correctly
let Some(selected_nodes) = self.selected_nodes_mut(network_path) else {
log::error!("Could not get selected nodes in move_layer_to_stack");
return;
};
let old_selected_nodes = selected_nodes.replace_with(vec![upstream_node_id]);
// Build the stack dependents from the upstream node rather than the selection so the shifting works correctly
self.unload_stack_dependents(network_path);
self.load_stack_dependents_for_nodes(vec![upstream_node_id], network_path);
// Create the minimum amount space for the moved layer
for _ in 0..STACK_VERTICAL_GAP {
@@ -886,6 +863,7 @@ impl NodeNetworkInterface {
let Some(stack_position) = self.position(&upstream_node_id, network_path) else {
log::error!("Could not get stack position in move_layer_to_stack");
self.unload_stack_dependents(network_path);
return;
};
@@ -896,7 +874,7 @@ impl NodeNetworkInterface {
self.vertical_shift_with_push(&upstream_node_id, 1, &mut HashSet::new(), network_path);
}
let _ = self.selected_nodes_mut(network_path).unwrap().replace_with(old_selected_nodes);
self.unload_stack_dependents(network_path);
}
// If true, this node should be inserted before the post node (toward root from the layer), and all outward wires from the pre node should be moved to its output.

View File

@@ -226,6 +226,55 @@ impl NodeNetworkInterface {
self.unload_modify_import_export(network_path);
}
/// Disconnects every wire fed by the given import within the network.
pub(crate) fn disconnect_import_wires(&mut self, import_index: usize, network_path: &[NodeId]) {
let Some(wires_for_import) = self.with_outward_wires(network_path, |outward_wires| outward_wires.get(&OutputConnector::Import(import_index)).cloned()) else {
log::error!("Could not get outward wires in disconnect_import_wires");
return;
};
let Some(wires_for_import) = wires_for_import else {
log::error!("Could not get outward wires for import in disconnect_import_wires");
return;
};
for downstream_connection in wires_for_import {
self.disconnect_input(&downstream_connection, network_path);
}
}
/// Disconnects every wire fed by the given output within the network.
pub(crate) fn disconnect_output_wires(&mut self, output_connector: &OutputConnector, network_path: &[NodeId]) {
let Some(downstream_connections) = self.with_outward_wires(network_path, |outward_wires| outward_wires.get(output_connector).cloned()) else {
log::error!("Could not get outward wires in disconnect_output_wires");
return;
};
let Some(downstream_connections) = downstream_connections else {
log::error!("Could not get downstream connections in disconnect_output_wires");
return;
};
for downstream_connection in downstream_connections {
self.disconnect_input(&downstream_connection, network_path);
}
}
/// Refreshes the metadata invalidated when the encapsulating node's signature changes, demoting it from a layer if it is no longer eligible.
fn finish_signature_edit(&mut self, parent_id: NodeId, encapsulating_network_path: &[NodeId], network_path: &[NodeId]) {
// Update the metadata for the encapsulating node
self.unload_outward_wires(encapsulating_network_path);
self.unload_node_click_targets(&parent_id, encapsulating_network_path);
self.unload_all_nodes_bounding_box(encapsulating_network_path);
if !self.is_eligible_to_be_layer(&parent_id, encapsulating_network_path) && self.is_layer(&parent_id, encapsulating_network_path) {
self.set_to_node_or_layer(&parent_id, encapsulating_network_path, false);
}
if encapsulating_network_path.is_empty() {
self.load_structure();
}
// Unload the metadata for the nested network
self.unload_outward_wires(network_path);
self.unload_import_export_ports(network_path);
self.unload_modify_import_export(network_path);
}
// First disconnects the export, then removes it
pub fn remove_export(&mut self, export_index: usize, network_path: &[NodeId]) {
let mut encapsulating_network_path = network_path.to_vec();
@@ -271,21 +320,7 @@ impl NodeNetworkInterface {
network_metadata.persistent_metadata.reference = None;
}
// Update the metadata for the encapsulating node
self.unload_outward_wires(&encapsulating_network_path);
self.unload_node_click_targets(&parent_id, &encapsulating_network_path);
self.unload_all_nodes_bounding_box(&encapsulating_network_path);
if !self.is_eligible_to_be_layer(&parent_id, &encapsulating_network_path) && self.is_layer(&parent_id, &encapsulating_network_path) {
self.set_to_node_or_layer(&parent_id, &encapsulating_network_path, false);
}
if encapsulating_network_path.is_empty() {
self.load_structure();
}
// Unload the metadata for the nested network
self.unload_outward_wires(network_path);
self.unload_import_export_ports(network_path);
self.unload_modify_import_export(network_path);
self.finish_signature_edit(parent_id, &encapsulating_network_path, network_path);
}
// First disconnects the import, then removes it
@@ -300,10 +335,6 @@ impl NodeNetworkInterface {
log::error!("Could not get outward wires in remove_import");
return;
};
let Some(outward_wires_for_import) = outward_wires.get(&OutputConnector::Import(import_index)).cloned() else {
log::error!("Could not get outward wires for import in remove_import");
return;
};
let mut new_import_mapping = Vec::new();
for i in (import_index + 1)..number_of_inputs {
let Some(outward_wires_for_import) = outward_wires.get(&OutputConnector::Import(i)).cloned() else {
@@ -316,9 +347,7 @@ impl NodeNetworkInterface {
}
// Disconnect all upstream connections
for outward_wire in outward_wires_for_import {
self.disconnect_input(&outward_wire, network_path);
}
self.disconnect_import_wires(import_index, network_path);
// Shift inputs connected to to imports at a higher index down one
for (output_connector, input_wire) in new_import_mapping {
self.create_wire(&output_connector, &input_wire, network_path);
@@ -347,21 +376,7 @@ impl NodeNetworkInterface {
network_metadata.persistent_metadata.reference = None;
}
// Update the metadata for the encapsulating node
self.unload_outward_wires(encapsulating_network_path);
self.unload_node_click_targets(parent_id, encapsulating_network_path);
self.unload_all_nodes_bounding_box(encapsulating_network_path);
if !self.is_eligible_to_be_layer(parent_id, encapsulating_network_path) && self.is_layer(parent_id, encapsulating_network_path) {
self.set_to_node_or_layer(parent_id, encapsulating_network_path, false);
}
if encapsulating_network_path.is_empty() {
self.load_structure();
}
// Unload the metadata for the nested network
self.unload_outward_wires(network_path);
self.unload_import_export_ports(network_path);
self.unload_modify_import_export(network_path);
self.finish_signature_edit(*parent_id, encapsulating_network_path, network_path);
}
/// The end index is before the export is removed, so moving to the end is the length of the current exports
@@ -1240,24 +1255,20 @@ impl NodeNetworkInterface {
};
let max_shift_distance = reconnected_node_position.y - disconnected_node_position.y;
let upstream_nodes = self.upstream_flow_back_from_nodes(vec![*reconnect_node], network_path, FlowType::PrimaryFlow).collect::<Vec<_>>();
let upstream_nodes = self.upstream_flow_back_from_nodes(vec![*reconnect_node], network_path, FlowType::PrimaryFlow).collect::<HashSet<_>>();
// Select the reconnect node to move to ensure the shifting works correctly
let Some(selected_nodes) = self.selected_nodes_mut(network_path) else {
log::error!("Could not get selected nodes in remove_references_from_network");
return false;
};
let old_selected_nodes = selected_nodes.replace_with(upstream_nodes);
// Build the stack dependents from the reconnected flow rather than the selection so the shifting works correctly
self.unload_stack_dependents(network_path);
self.load_stack_dependents_for_nodes(upstream_nodes.iter().copied().collect(), network_path);
// Shift up until there is either a collision or the disconnected node position is reached
let mut current_shift_distance = 0;
while self.check_collision_with_stack_dependents(reconnect_node, -1, network_path).is_empty() && max_shift_distance > current_shift_distance {
self.shift_selected_nodes(Direction::Up, false, network_path);
self.shift_nodes(upstream_nodes.clone(), Direction::Up, false, network_path);
current_shift_distance += 1;
}
let _ = self.selected_nodes_mut(network_path).unwrap().replace_with(old_selected_nodes);
self.unload_stack_dependents(network_path);
}
true

View File

@@ -1,5 +1,8 @@
use super::*;
/// Index of the Artboard definition's Clip input, which must match the input order authored in document_node_definitions.rs.
pub(crate) const ARTBOARD_CLIP_INPUT_INDEX: usize = 5;
impl NodeNetworkInterface {
/// Runs a query against a resolved [`NetworkView`], logging any error at this message-boundary wrapper and mapping it to None.
pub(crate) fn query<'a, 'p, T>(&'a self, network_path: &'p [NodeId], caller: &str, query: impl FnOnce(NetworkView<'a, 'p>) -> Result<T, NetworkError>) -> Option<T> {
@@ -619,6 +622,61 @@ impl NodeNetworkInterface {
true
}
/// The input connector into which a layer should be inserted for the given parent and stack index.
pub(crate) fn post_node_with_index(&self, parent: LayerNodeIdentifier, insert_index: usize, network_path: &[NodeId]) -> InputConnector {
let mut post_node_input_connector = if parent == LayerNodeIdentifier::ROOT_PARENT {
InputConnector::Export(0)
} else {
InputConnector::node(parent.to_node(), 1)
};
// Skip layers based on skip_layer_nodes, which inserts the new layer at a certain index of the layer stack.
let mut current_index = 0;
// Set the post node to the layer node at insert_index
loop {
if current_index == insert_index {
break;
}
let next_node_in_stack_id = self
.input_from_connector(&post_node_input_connector, network_path)
.and_then(|input_from_connector| if let NodeInput::Node { node_id, .. } = input_from_connector { Some(node_id) } else { None });
if let Some(next_node_in_stack_id) = next_node_in_stack_id {
// Only increment index for layer nodes
if self.is_layer(next_node_in_stack_id, network_path) {
current_index += 1;
}
// Input as a sibling to the Layer node above
post_node_input_connector = InputConnector::node(*next_node_in_stack_id, 0);
} else {
log::error!("Error getting post node: insert_index out of bounds");
break;
};
}
let layer_input_connector = post_node_input_connector;
// Sink post_node down to the end of the non layer chain that feeds into post_node, such that pre_node is the layer node at insert_index + 1, or None if insert_index is the last layer
loop {
let pre_node_output_connector = self.upstream_output_connector(&post_node_input_connector, network_path);
match pre_node_output_connector {
Some(OutputConnector::Node { node_id: pre_node_id, .. }) if !self.is_layer(&pre_node_id, network_path) => {
// Update post_node_input_connector for the next iteration
post_node_input_connector = InputConnector::node(pre_node_id, 0);
// Insert directly under layer if moving to the end of a layer stack that ends with a non layer node that does not have an exposed primary input
let primary_is_exposed = self.input_from_connector(&post_node_input_connector, network_path).is_some_and(|input| input.is_exposed());
if !primary_is_exposed {
return layer_input_connector;
}
}
_ => break, // Break if pre_node_output_connector is None or if pre_node_id is a layer
}
}
post_node_input_connector
}
// All chain nodes and branches from the chain which are sole dependents of the layer
pub fn upstream_nodes_below_layer(&self, node_id: &NodeId, network_path: &[NodeId]) -> HashSet<NodeId> {
// Every upstream node below layer must be a sole dependent
@@ -885,27 +943,30 @@ impl NodeNetworkInterface {
/// Calculates the document bounds in document space
pub fn document_bounds_document_space(&self, include_artboards: bool) -> Option<[DVec2; 2]> {
self.combined_document_bounds(include_artboards, |metadata, layer| metadata.bounding_box_document(layer))
}
fn combined_document_bounds(&self, include_artboards: bool, layer_bounds: impl Fn(&DocumentMetadata, LayerNodeIdentifier) -> Option<[DVec2; 2]>) -> Option<[DVec2; 2]> {
self.document_metadata
.all_layers()
.filter(|layer| include_artboards || !self.is_artboard(&layer.to_node(), &[]))
.filter_map(|layer| {
// A layer clipped by its artboard contributes only the intersection of the two bounds
if !self.is_artboard(&layer.to_node(), &[])
&& let Some(artboard_node_identifier) = layer
.ancestors(self.document_metadata())
.find(|ancestor| *ancestor != LayerNodeIdentifier::ROOT_PARENT && self.is_artboard(&ancestor.to_node(), &[]))
&& let Some(artboard) = self.document_node(&artboard_node_identifier.to_node(), &[])
&& let Some(clip_input) = artboard.inputs.get(ARTBOARD_CLIP_INPUT_INDEX)
&& let NodeInput::Value { tagged_value, .. } = clip_input
&& tagged_value.clone().deref() == &TaggedValue::Bool(true)
{
let artboard = self.document_node(&artboard_node_identifier.to_node(), &[]);
let clip_input = artboard.unwrap().inputs.get(5).unwrap();
if let NodeInput::Value { tagged_value, .. } = clip_input
&& tagged_value.clone().deref() == &TaggedValue::Bool(true)
{
return Some(Quad::clip(
self.document_metadata.bounding_box_document(layer).unwrap_or_default(),
self.document_metadata.bounding_box_document(artboard_node_identifier).unwrap_or_default(),
));
}
return Some(Quad::clip(
layer_bounds(&self.document_metadata, layer).unwrap_or_default(),
self.document_metadata.bounding_box_document(artboard_node_identifier).unwrap_or_default(),
));
}
self.document_metadata.bounding_box_document(layer)
layer_bounds(&self.document_metadata, layer)
})
// Skip any layer bounds containing NaN to avoid poisoning the combined result
.filter(|[min, max]| min.is_finite() && max.is_finite())
@@ -922,29 +983,7 @@ impl NodeNetworkInterface {
/// Calculates the document bounds in document space, expanding vector layer bounds to include the rendered
/// stroke width. Used for export so the output canvas captures strokes that overflow the path geometry.
pub fn document_bounds_document_space_with_stroke(&self, include_artboards: bool) -> Option<[DVec2; 2]> {
self.document_metadata
.all_layers()
.filter(|layer| include_artboards || !self.is_artboard(&layer.to_node(), &[]))
.filter_map(|layer| {
if !self.is_artboard(&layer.to_node(), &[])
&& let Some(artboard_node_identifier) = layer
.ancestors(self.document_metadata())
.find(|ancestor| *ancestor != LayerNodeIdentifier::ROOT_PARENT && self.is_artboard(&ancestor.to_node(), &[]))
&& let Some(artboard) = self.document_node(&artboard_node_identifier.to_node(), &[])
&& let Some(clip_input) = artboard.inputs.get(5)
&& let NodeInput::Value { tagged_value, .. } = clip_input
&& tagged_value.clone().deref() == &TaggedValue::Bool(true)
{
return Some(Quad::clip(
self.document_metadata.bounding_box_document_with_stroke(layer).unwrap_or_default(),
self.document_metadata.bounding_box_document(artboard_node_identifier).unwrap_or_default(),
));
}
self.document_metadata.bounding_box_document_with_stroke(layer)
})
// Skip any layer bounds containing NaN to avoid poisoning the combined result
.filter(|[min, max]| min.is_finite() && max.is_finite())
.reduce(Quad::combine_bounds)
self.combined_document_bounds(include_artboards, |metadata, layer| metadata.bounding_box_document_with_stroke(layer))
}
/// Calculates the selected layer bounds in document space

View File

@@ -437,6 +437,8 @@ pub struct NodeNetworkTransientMetadata {
/// Cached wire SVG paths per input connector, where an entry's presence means that wire is loaded.
pub(crate) wires: std::cell::RefCell<HashMap<InputConnector, WirePathUpdate>>,
/// Drag-session state: how far each unowned stack dependent has been pushed vertically from its resting position. Cleared when the drag ends.
pub(crate) drag_offsets: std::cell::RefCell<HashMap<NodeId, i32>>,
}
#[derive(Debug, Clone)]
@@ -459,8 +461,7 @@ pub struct NetworkEdgeDistance {
pub enum LayerOwner {
// Used to get the layer that should be shifted when there is a collision.
Layer(NodeId),
// The vertical offset of a node from the start of its shift. Should be reset when the drag ends.
None(i32),
None,
}
#[derive(Debug, Default, serde::Serialize, serde::Deserialize)]

View File

@@ -0,0 +1,16 @@
use crate::messages::portfolio::fonts::FALLBACK_FONT_RESOURCE;
use graphene_std::text::{TextAlign, TextContext, TypesettingConfig};
pub fn text_width(text: &str, font_size: f64) -> f64 {
let typesetting = TypesettingConfig {
font_size,
line_height_ratio: 1.2,
letter_spacing: 0.,
letter_tilt: 0.,
max_width: None,
max_height: None,
align: TextAlign::AlignLeft,
};
TextContext::with_thread_local(|text_context| text_context.bounding_box(text, &FALLBACK_FONT_RESOURCE, typesetting, false).x)
}

View File

@@ -345,11 +345,19 @@ impl EditorTestUtils {
}
pub async fn create_node_by_name(&mut self, node_type: DefinitionIdentifier) -> NodeId {
self.create_node(node_type, None).await
}
pub async fn create_node_by_name_at(&mut self, node_type: DefinitionIdentifier, x: i32, y: i32) -> NodeId {
self.create_node(node_type, Some((x, y))).await
}
async fn create_node(&mut self, node_type: DefinitionIdentifier, xy: Option<(i32, i32)>) -> NodeId {
let node_id = NodeId::new();
self.handle_message(NodeGraphMessage::CreateNodeFromContextMenu {
node_id: Some(node_id),
node_type,
xy: None,
xy,
add_transaction: true,
})
.await;