mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 22:28:10 +08:00
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:
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
@@ -1812,7 +1790,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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -222,24 +220,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 {
|
||||
|
||||
@@ -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] {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
@@ -1252,24 +1267,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -324,11 +324,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;
|
||||
|
||||
Reference in New Issue
Block a user