mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-08-30 09:47:22 +00:00
Add grid snapping to graph imports/exports; improve layer panel drag into/between insertion; better preserve graph space on reordering (#1911)
* Fix disconnecting root node when previewing * Final previewing fixes * Improve positioning when moving layer to stack * Improve layer panel * Import/Export edge grid snapping * Fix layer ordering and positioning * Small bug fixes and improvements * Fix copy and paste position * Nit * Align imports/edges when using scrolling * Fix misaligned exports in demo artwork --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
parent
60707c0369
commit
193f757c45
16 changed files with 494 additions and 190 deletions
|
@ -1,5 +1,7 @@
|
|||
// Graph
|
||||
pub const GRID_SIZE: u32 = 24;
|
||||
pub const EXPORTS_TO_EDGE_PIXEL_GAP: u32 = 120;
|
||||
pub const IMPORTS_TO_EDGE_PIXEL_GAP: u32 = 120;
|
||||
|
||||
// Viewport
|
||||
pub const VIEWPORT_ZOOM_WHEEL_RATE: f64 = (1. / 600.) * 3.;
|
||||
|
|
|
@ -31,6 +31,7 @@ impl MessageHandler<InputPreprocessorMessage, InputPreprocessorMessageData> for
|
|||
self.viewport_bounds = bounds;
|
||||
|
||||
responses.add(NavigationMessage::CanvasPan { delta: DVec2::ZERO });
|
||||
responses.add(NodeGraphMessage::SetGridAlignedEdges);
|
||||
}
|
||||
}
|
||||
InputPreprocessorMessage::DoubleClick { editor_mouse_state, modifier_keys } => {
|
||||
|
|
|
@ -415,6 +415,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
|
|||
self.selection_network_path.clone_from(&self.breadcrumb_network_path);
|
||||
}
|
||||
responses.add(DocumentMessage::PTZUpdate);
|
||||
responses.add(NodeGraphMessage::SetGridAlignedEdges);
|
||||
responses.add(NodeGraphMessage::SendGraph);
|
||||
}
|
||||
DocumentMessage::FlipSelectedLayers { flip_axis } => {
|
||||
|
@ -443,6 +444,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
|
|||
// Update the tilt menu bar buttons to be disabled when the graph is open
|
||||
responses.add(MenuBarMessage::SendLayout);
|
||||
if open {
|
||||
responses.add(NodeGraphMessage::SetGridAlignedEdges);
|
||||
responses.add(NodeGraphMessage::SendGraph);
|
||||
responses.add(NavigationMessage::CanvasTiltSet { angle_radians: 0. });
|
||||
}
|
||||
|
@ -585,13 +587,37 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
|
|||
return;
|
||||
}
|
||||
|
||||
let layers_to_move = self.network_interface.shallowest_unique_layers(&self.selection_network_path).collect::<Vec<_>>();
|
||||
let layers_to_move = self.network_interface.shallowest_unique_layers_sorted(&self.selection_network_path);
|
||||
// Offset the index for layers to move that are below another layer to move. For example when moving 1 and 2 between 3 and 4, 2 should be inserted at the same index as 1 since 1 is moved first.
|
||||
let layers_to_move_with_insert_offset: Vec<(LayerNodeIdentifier, usize)> = layers_to_move
|
||||
.iter()
|
||||
.map(|layer| {
|
||||
if layer.parent(self.metadata()) != Some(parent) {
|
||||
(*layer, 0)
|
||||
} else {
|
||||
let upstream_selected_siblings = layer
|
||||
.downstream_siblings(self.network_interface.document_metadata())
|
||||
.filter(|sibling| {
|
||||
sibling != layer
|
||||
&& layers_to_move.iter().any(|layer| {
|
||||
layer == sibling
|
||||
&& layer
|
||||
.parent(self.metadata())
|
||||
.is_some_and(|parent| parent.children(self.metadata()).position(|child| child == *layer) < Some(insert_index))
|
||||
})
|
||||
})
|
||||
.count();
|
||||
(*layer, upstream_selected_siblings)
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for layer_to_move in layers_to_move.into_iter().rev() {
|
||||
for (layer_index, (layer_to_move, insert_offset)) in layers_to_move_with_insert_offset.into_iter().enumerate() {
|
||||
let calculated_insert_index = insert_index + layer_index - insert_offset;
|
||||
responses.add(NodeGraphMessage::MoveLayerToStack {
|
||||
layer: layer_to_move,
|
||||
parent,
|
||||
insert_index,
|
||||
insert_index: calculated_insert_index,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -600,15 +626,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
|
|||
}
|
||||
DocumentMessage::MoveSelectedLayersToGroup { parent } => {
|
||||
// Group all shallowest unique selected layers in order
|
||||
let all_layers_to_group = self.network_interface.shallowest_unique_layers(&self.selection_network_path).collect::<Vec<_>>();
|
||||
|
||||
// Ensure nodes are grouped in the correct order
|
||||
let mut all_layers_to_group_sorted = Vec::new();
|
||||
for descendant in LayerNodeIdentifier::ROOT_PARENT.descendants(self.metadata()) {
|
||||
if all_layers_to_group.contains(&descendant) {
|
||||
all_layers_to_group_sorted.push(descendant);
|
||||
};
|
||||
}
|
||||
let all_layers_to_group_sorted = self.network_interface.shallowest_unique_layers_sorted(&self.selection_network_path);
|
||||
|
||||
for layer_to_group in all_layers_to_group_sorted.into_iter().rev() {
|
||||
responses.add(NodeGraphMessage::MoveLayerToStack {
|
||||
|
@ -1044,11 +1062,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
|
|||
let transform = self
|
||||
.navigation_handler
|
||||
.calculate_offset_transform(ipp.viewport_bounds.center(), &network_metadata.persistent_metadata.navigation_metadata.node_graph_ptz);
|
||||
self.network_interface.set_transform(
|
||||
transform,
|
||||
DVec2::new(ipp.viewport_bounds.bottom_right.x - ipp.viewport_bounds.top_left.x, 0.),
|
||||
&self.breadcrumb_network_path,
|
||||
);
|
||||
self.network_interface.set_transform(transform, &self.breadcrumb_network_path);
|
||||
let imports = self.network_interface.frontend_imports(&self.breadcrumb_network_path).unwrap_or_default();
|
||||
let exports = self.network_interface.frontend_exports(&self.breadcrumb_network_path).unwrap_or_default();
|
||||
responses.add(DocumentMessage::RenderRulers);
|
||||
|
@ -1261,12 +1275,6 @@ impl DocumentMessageHandler {
|
|||
Ok(document_message_handler)
|
||||
}
|
||||
|
||||
pub fn with_name_and_content(name: String, serialized_content: String) -> Result<Self, EditorError> {
|
||||
let mut document = Self::deserialize_document(&serialized_content)?;
|
||||
document.name = name;
|
||||
Ok(document)
|
||||
}
|
||||
|
||||
/// Called recursively by the entry function [`serialize_root`].
|
||||
fn serialize_structure(&self, folder: LayerNodeIdentifier, structure_section: &mut Vec<u64>, data_section: &mut Vec<u64>, path: &mut Vec<LayerNodeIdentifier>) {
|
||||
let mut space = 0;
|
||||
|
|
|
@ -71,9 +71,6 @@ impl MessageHandler<NavigationMessage, NavigationMessageData<'_>> for Navigation
|
|||
|
||||
match message {
|
||||
NavigationMessage::BeginCanvasPan => {
|
||||
let Some(ptz) = get_ptz(document_ptz, network_interface, graph_view_overlay_open, breadcrumb_network_path) else {
|
||||
return;
|
||||
};
|
||||
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Grabbing });
|
||||
|
||||
responses.add(FrontendMessage::UpdateInputHints {
|
||||
|
@ -81,6 +78,9 @@ impl MessageHandler<NavigationMessage, NavigationMessageData<'_>> for Navigation
|
|||
});
|
||||
|
||||
self.mouse_position = ipp.mouse.position;
|
||||
let Some(ptz) = get_ptz(document_ptz, network_interface, graph_view_overlay_open, breadcrumb_network_path) else {
|
||||
return;
|
||||
};
|
||||
self.navigation_operation = NavigationOperation::Pan { pan_original_for_abort: ptz.pan };
|
||||
}
|
||||
NavigationMessage::BeginCanvasTilt { was_dispatched_from_menu } => {
|
||||
|
@ -172,6 +172,7 @@ impl MessageHandler<NavigationMessage, NavigationMessageData<'_>> for Navigation
|
|||
true => (-ipp.mouse.scroll_delta.y, 0.).into(),
|
||||
} * VIEWPORT_SCROLL_RATE;
|
||||
responses.add(NavigationMessage::CanvasPan { delta });
|
||||
responses.add(NodeGraphMessage::SetGridAlignedEdges);
|
||||
}
|
||||
NavigationMessage::CanvasTiltResetAndZoomTo100Percent => {
|
||||
let Some(ptz) = get_ptz_mut(document_ptz, network_interface, graph_view_overlay_open, breadcrumb_network_path) else {
|
||||
|
@ -182,6 +183,7 @@ impl MessageHandler<NavigationMessage, NavigationMessageData<'_>> for Navigation
|
|||
ptz.set_zoom(1.);
|
||||
responses.add(PortfolioMessage::UpdateDocumentWidgets);
|
||||
responses.add(DocumentMessage::PTZUpdate);
|
||||
responses.add(NodeGraphMessage::SetGridAlignedEdges);
|
||||
}
|
||||
NavigationMessage::CanvasTiltSet { angle_radians } => {
|
||||
let Some(ptz) = get_ptz_mut(document_ptz, network_interface, graph_view_overlay_open, breadcrumb_network_path) else {
|
||||
|
@ -252,6 +254,7 @@ impl MessageHandler<NavigationMessage, NavigationMessageData<'_>> for Navigation
|
|||
ptz.set_zoom(zoom);
|
||||
responses.add(PortfolioMessage::UpdateDocumentWidgets);
|
||||
responses.add(DocumentMessage::PTZUpdate);
|
||||
responses.add(NodeGraphMessage::SetGridAlignedEdges);
|
||||
}
|
||||
NavigationMessage::EndCanvasPTZ { abort_transform } => {
|
||||
let Some(ptz) = get_ptz_mut(document_ptz, network_interface, graph_view_overlay_open, breadcrumb_network_path) else {
|
||||
|
@ -272,14 +275,13 @@ impl MessageHandler<NavigationMessage, NavigationMessageData<'_>> for Navigation
|
|||
ptz.set_zoom(zoom_original_for_abort);
|
||||
}
|
||||
}
|
||||
|
||||
responses.add(DocumentMessage::PTZUpdate);
|
||||
}
|
||||
|
||||
// Final chance to apply snapping if the key was pressed during this final frame
|
||||
ptz.tilt = self.snapped_tilt(ptz.tilt);
|
||||
ptz.set_zoom(self.snapped_zoom(ptz.zoom()));
|
||||
|
||||
responses.add(DocumentMessage::PTZUpdate);
|
||||
responses.add(NodeGraphMessage::SetGridAlignedEdges);
|
||||
// Reset the navigation operation now that it's done
|
||||
self.navigation_operation = NavigationOperation::None;
|
||||
|
||||
|
@ -336,6 +338,7 @@ impl MessageHandler<NavigationMessage, NavigationMessageData<'_>> for Navigation
|
|||
|
||||
responses.add(PortfolioMessage::UpdateDocumentWidgets);
|
||||
responses.add(DocumentMessage::PTZUpdate);
|
||||
responses.add(NodeGraphMessage::SetGridAlignedEdges);
|
||||
}
|
||||
NavigationMessage::FitViewportToSelection => {
|
||||
if let Some(bounds) = selection_bounds {
|
||||
|
@ -420,6 +423,7 @@ impl MessageHandler<NavigationMessage, NavigationMessageData<'_>> for Navigation
|
|||
};
|
||||
|
||||
responses.add(NavigationMessage::CanvasZoomSet { zoom_factor: ptz.zoom() });
|
||||
responses.add(NodeGraphMessage::SetGridAlignedEdges);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -40,6 +40,7 @@ pub enum NodeGraphMessage {
|
|||
DisconnectInput {
|
||||
input_connector: InputConnector,
|
||||
},
|
||||
DisconnectRootNode,
|
||||
EnterNestedNetwork,
|
||||
DuplicateSelectedNodes,
|
||||
ExposeInput {
|
||||
|
@ -92,6 +93,7 @@ pub enum NodeGraphMessage {
|
|||
SendClickTargets,
|
||||
EndSendClickTargets,
|
||||
SendGraph,
|
||||
SetGridAlignedEdges,
|
||||
SetInputValue {
|
||||
node_id: NodeId,
|
||||
input_index: usize,
|
||||
|
|
|
@ -226,6 +226,9 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
|
|||
NodeGraphMessage::DisconnectInput { input_connector } => {
|
||||
network_interface.disconnect_input(&input_connector, selection_network_path);
|
||||
}
|
||||
NodeGraphMessage::DisconnectRootNode => {
|
||||
network_interface.start_previewing_without_restore(selection_network_path);
|
||||
}
|
||||
NodeGraphMessage::DuplicateSelectedNodes => {
|
||||
let all_selected_nodes = network_interface.upstream_chain_nodes(selection_network_path);
|
||||
|
||||
|
@ -435,10 +438,12 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
|
|||
self.initial_disconnecting = true;
|
||||
self.disconnecting = Some(clicked_input.clone());
|
||||
|
||||
let Some(output_connector) = network_interface.upstream_output_connector(clicked_input, selection_network_path) else {
|
||||
log::error!("Could not get upstream node from {clicked_input:?} when moving existing wire");
|
||||
return;
|
||||
let output_connector = if *clicked_input == InputConnector::Export(0) {
|
||||
network_interface.root_node(selection_network_path).map(|root_node| root_node.to_connector())
|
||||
} else {
|
||||
network_interface.upstream_output_connector(clicked_input, selection_network_path)
|
||||
};
|
||||
let Some(output_connector) = output_connector else { return };
|
||||
self.wire_in_progress_from_connector = network_interface.output_position(&output_connector, selection_network_path);
|
||||
return;
|
||||
}
|
||||
|
@ -553,9 +558,19 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
|
|||
// Disconnect if the wire was previously connected to an input
|
||||
if let Some(disconnecting) = &self.disconnecting {
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
responses.add(NodeGraphMessage::DisconnectInput {
|
||||
input_connector: disconnecting.clone(),
|
||||
});
|
||||
let mut disconnect_root_node = false;
|
||||
if let Previewing::Yes { root_node_to_restore } = network_interface.previewing(selection_network_path) {
|
||||
if root_node_to_restore.is_some() && *disconnecting == InputConnector::Export(0) {
|
||||
disconnect_root_node = true;
|
||||
}
|
||||
}
|
||||
if disconnect_root_node {
|
||||
responses.add(NodeGraphMessage::DisconnectRootNode);
|
||||
} else {
|
||||
responses.add(NodeGraphMessage::DisconnectInput {
|
||||
input_connector: disconnecting.clone(),
|
||||
});
|
||||
}
|
||||
// Update the frontend that the node is disconnected
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
responses.add(NodeGraphMessage::SendGraph);
|
||||
|
@ -946,6 +961,15 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
|
|||
responses.add(NodeGraphMessage::SendSelectedNodes);
|
||||
}
|
||||
}
|
||||
NodeGraphMessage::SetGridAlignedEdges => {
|
||||
if graph_view_overlay_open {
|
||||
network_interface.set_grid_aligned_edges(DVec2::new(ipp.viewport_bounds.bottom_right.x - ipp.viewport_bounds.top_left.x, 0.), breadcrumb_network_path);
|
||||
// Send the new edges to the frontend
|
||||
let imports = network_interface.frontend_imports(breadcrumb_network_path).unwrap_or_default();
|
||||
let exports = network_interface.frontend_exports(breadcrumb_network_path).unwrap_or_default();
|
||||
responses.add(FrontendMessage::UpdateImportsExports { imports, exports });
|
||||
}
|
||||
}
|
||||
NodeGraphMessage::SetInputValue { node_id, input_index, value } => {
|
||||
let input = NodeInput::value(value, false);
|
||||
responses.add(NodeGraphMessage::SetInput {
|
||||
|
@ -988,9 +1012,13 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
|
|||
continue;
|
||||
}
|
||||
|
||||
let mut is_downstream_from_selected_absolute_layer = false;
|
||||
// Deselect stack nodes upstream from a selected layer
|
||||
let mut is_upstream_from_selected_absolute_layer = false;
|
||||
let mut current_node = *selected_node;
|
||||
loop {
|
||||
if network_interface.is_absolute(selected_node, selection_network_path) {
|
||||
break;
|
||||
}
|
||||
let Some(outward_wires) = network_interface.outward_wires(selection_network_path) else {
|
||||
break;
|
||||
};
|
||||
|
@ -1009,16 +1037,16 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
|
|||
if !network_interface.is_layer(&downstream_node, selection_network_path) {
|
||||
break;
|
||||
}
|
||||
if network_interface.is_absolute(&downstream_node, selection_network_path) {
|
||||
is_downstream_from_selected_absolute_layer = node_ids.contains(&downstream_node);
|
||||
// Break the iteration if the layer is absolute(top of stack), or it is selected
|
||||
if network_interface.is_absolute(&downstream_node, selection_network_path) || node_ids.contains(&downstream_node) {
|
||||
is_upstream_from_selected_absolute_layer = node_ids.contains(&downstream_node);
|
||||
break;
|
||||
}
|
||||
current_node = downstream_node;
|
||||
}
|
||||
if is_downstream_from_selected_absolute_layer {
|
||||
continue;
|
||||
if !is_upstream_from_selected_absolute_layer {
|
||||
filtered_node_ids.push(*selected_node)
|
||||
}
|
||||
filtered_node_ids.push(*selected_node)
|
||||
}
|
||||
|
||||
for node_id in filtered_node_ids {
|
||||
|
@ -1052,7 +1080,6 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
|
|||
if is_layer && !network_interface.is_eligible_to_be_layer(&node_id, selection_network_path) {
|
||||
return;
|
||||
}
|
||||
|
||||
network_interface.set_to_node_or_layer(&node_id, selection_network_path, is_layer);
|
||||
|
||||
self.context_menu = None;
|
||||
|
|
|
@ -178,4 +178,6 @@ pub struct FrontendClickTargets {
|
|||
pub visibility_click_targets: Vec<String>,
|
||||
#[serde(rename = "allNodesBoundingBox")]
|
||||
pub all_nodes_bounding_box: String,
|
||||
#[serde(rename = "importExportsBoundingBox")]
|
||||
pub import_exports_bounding_box: String,
|
||||
}
|
||||
|
|
|
@ -284,10 +284,10 @@ impl LayerNodeIdentifier {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn upstream_siblings(self, metadata: &DocumentMetadata) -> AxisIter {
|
||||
pub fn downstream_siblings(self, metadata: &DocumentMetadata) -> AxisIter {
|
||||
AxisIter {
|
||||
layer_node: Some(self),
|
||||
next_node: Self::next_sibling,
|
||||
next_node: Self::previous_sibling,
|
||||
metadata,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use super::document_metadata::{DocumentMetadata, LayerNodeIdentifier, NodeRelations};
|
||||
use super::misc::PTZ;
|
||||
use super::nodes::SelectedNodes;
|
||||
use crate::consts::{EXPORTS_TO_EDGE_PIXEL_GAP, IMPORTS_TO_EDGE_PIXEL_GAP};
|
||||
use crate::messages::portfolio::document::graph_operation::utility_types::ModifyInputsContext;
|
||||
use crate::messages::portfolio::document::node_graph::utility_types::{FrontendClickTargets, FrontendGraphDataType, FrontendGraphInput, FrontendGraphOutput};
|
||||
|
||||
|
@ -299,10 +300,44 @@ impl NodeNetworkInterface {
|
|||
};
|
||||
match &mut node_template.persistent_node_metadata.node_type_metadata {
|
||||
// TODO: Remove 2x2 offset and replace with layout system to find space for new node
|
||||
NodeTypePersistentMetadata::Layer(layer_metadata) => layer_metadata.position = LayerPosition::Absolute(position + IVec2::new(2, 2)),
|
||||
NodeTypePersistentMetadata::Node(node_metadata) => node_metadata.position = NodePosition::Absolute(position + IVec2::new(2, 2)),
|
||||
NodeTypePersistentMetadata::Layer(layer_metadata) => layer_metadata.position = LayerPosition::Absolute(position),
|
||||
NodeTypePersistentMetadata::Node(node_metadata) => node_metadata.position = NodePosition::Absolute(position),
|
||||
};
|
||||
}
|
||||
|
||||
// Ensure a chain node has a selected downstream layer
|
||||
if let NodeTypePersistentMetadata::Node(node_metadata) = &node_template.persistent_node_metadata.node_type_metadata {
|
||||
if matches!(node_metadata.position, NodePosition::Chain) {
|
||||
let Some(downstream_layer) = self.downstream_layer(node_id, network_path) else {
|
||||
log::error!("Could not get downstream layer in copy_nodes");
|
||||
return None;
|
||||
};
|
||||
if new_ids.keys().all(|key| *key != downstream_layer.to_node()) {
|
||||
let Some(position) = self.position(node_id, network_path) else {
|
||||
log::error!("Could not get position in create_node_template");
|
||||
return None;
|
||||
};
|
||||
node_template.persistent_node_metadata.node_type_metadata = NodeTypePersistentMetadata::Node(NodePersistentMetadata {
|
||||
position: NodePosition::Absolute(position + IVec2::new(2, 2)),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Shift all absolute nodes 2 to the right and 2 down
|
||||
match &mut node_template.persistent_node_metadata.node_type_metadata {
|
||||
NodeTypePersistentMetadata::Layer(layer_metadata) => {
|
||||
if let LayerPosition::Absolute(position) = &mut layer_metadata.position {
|
||||
*position += IVec2::new(2, 2)
|
||||
}
|
||||
}
|
||||
NodeTypePersistentMetadata::Node(node_metadata) => {
|
||||
if let NodePosition::Absolute(position) = &mut node_metadata.position {
|
||||
*position += IVec2::new(2, 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some((new, node_id, node_template))
|
||||
})
|
||||
})
|
||||
|
@ -961,6 +996,18 @@ impl NodeNetworkInterface {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn shallowest_unique_layers_sorted(&self, network_path: &[NodeId]) -> Vec<LayerNodeIdentifier> {
|
||||
let all_layers_to_group = self.shallowest_unique_layers(network_path).collect::<Vec<_>>();
|
||||
// Ensure nodes are grouped in the correct order
|
||||
let mut all_layers_to_group_sorted = Vec::new();
|
||||
for descendant in LayerNodeIdentifier::ROOT_PARENT.descendants(self.document_metadata()) {
|
||||
if all_layers_to_group.contains(&descendant) {
|
||||
all_layers_to_group_sorted.push(descendant);
|
||||
};
|
||||
}
|
||||
all_layers_to_group_sorted
|
||||
}
|
||||
|
||||
/// Ancestor that is shared by all layers and that is deepest (more nested). Default may be the root. Skips selected non-folder, non-artboard layers
|
||||
pub fn deepest_common_ancestor(&self, network_path: &[NodeId], include_self: bool) -> Option<LayerNodeIdentifier> {
|
||||
if !network_path.is_empty() {
|
||||
|
@ -1283,30 +1330,29 @@ impl NodeNetworkInterface {
|
|||
return;
|
||||
};
|
||||
let mut import_export_ports = Ports::new();
|
||||
|
||||
let exports_to_edge_distance = network_metadata.persistent_metadata.navigation_metadata.exports_to_edge_distance;
|
||||
let viewport_top_right = network_metadata
|
||||
.persistent_metadata
|
||||
.navigation_metadata
|
||||
.node_graph_to_viewport
|
||||
.inverse()
|
||||
.transform_point2(network_metadata.persistent_metadata.navigation_metadata.node_graph_top_right + DVec2::new(-120., 0.));
|
||||
|
||||
.transform_point2(DVec2::new(exports_to_edge_distance, 0.));
|
||||
let bounding_box_top_right = DVec2::new((all_nodes_bounding_box[1].x / 24. + 0.5).floor() * 24., (all_nodes_bounding_box[0].y / 24. + 0.5).floor() * 24.) + DVec2::new(4. * 24., 0.);
|
||||
|
||||
let export_top_right = DVec2::new(viewport_top_right.x.max(bounding_box_top_right.x), bounding_box_top_right.y);
|
||||
for input_index in 0..network.exports.len() {
|
||||
import_export_ports.insert_input_port_at_center(input_index, export_top_right + DVec2::new(0., input_index as f64 * 24.));
|
||||
}
|
||||
|
||||
let imports_to_edge_distance = network_metadata.persistent_metadata.navigation_metadata.imports_to_edge_distance;
|
||||
let viewport_top_left = network_metadata
|
||||
.persistent_metadata
|
||||
.navigation_metadata
|
||||
.node_graph_to_viewport
|
||||
.inverse()
|
||||
.transform_point2(DVec2::new(120., 0.));
|
||||
|
||||
let bounding_box_top_left = DVec2::new((all_nodes_bounding_box[0].x / 24. + 0.5).floor() * 24., (all_nodes_bounding_box[0].y / 24. + 0.5).floor() * 24.) + DVec2::new(-4. * 24., 0.);
|
||||
.transform_point2(DVec2::new(imports_to_edge_distance, 0.));
|
||||
let bounding_box_top_left = DVec2::new((all_nodes_bounding_box[0].x / 24. + 0.5).floor() * 24., (all_nodes_bounding_box[0].y / 24. + 0.5).floor() * 24.) + DVec2::new(-96., 0.);
|
||||
let import_top_left = DVec2::new(viewport_top_left.x.min(bounding_box_top_left.x), bounding_box_top_left.y);
|
||||
|
||||
for output_index in 0..self.number_of_displayed_imports(network_path) {
|
||||
import_export_ports.insert_output_port_at_center(output_index, import_top_left + DVec2::new(0., output_index as f64 * 24.));
|
||||
}
|
||||
|
@ -1828,17 +1874,43 @@ impl NodeNetworkInterface {
|
|||
node_click_targets.push(path);
|
||||
}
|
||||
});
|
||||
|
||||
let bounds = self.all_nodes_bounding_box(network_path).cloned().unwrap_or([DVec2::ZERO, DVec2::ZERO]);
|
||||
let rect = bezier_rs::Subpath::<PointId>::new_rect(bounds[0], bounds[1]);
|
||||
let mut all_nodes_bounding_box = String::new();
|
||||
let _ = rect.subpath_to_svg(&mut all_nodes_bounding_box, DAffine2::IDENTITY);
|
||||
|
||||
let Some(network_metadata) = self.network_metadata(network_path) else {
|
||||
log::error!("Could not get nested network_metadata in collect_front_end_click_targets");
|
||||
return FrontendClickTargets::default();
|
||||
};
|
||||
let import_exports_viewport_top_left = DVec2::new(network_metadata.persistent_metadata.navigation_metadata.imports_to_edge_distance, -10.);
|
||||
let import_exports_viewport_bottom_right = DVec2::new(network_metadata.persistent_metadata.navigation_metadata.exports_to_edge_distance, 2000.);
|
||||
|
||||
let node_graph_top_left = network_metadata
|
||||
.persistent_metadata
|
||||
.navigation_metadata
|
||||
.node_graph_to_viewport
|
||||
.inverse()
|
||||
.transform_point2(import_exports_viewport_top_left);
|
||||
let node_graph_bottom_right = network_metadata
|
||||
.persistent_metadata
|
||||
.navigation_metadata
|
||||
.node_graph_to_viewport
|
||||
.inverse()
|
||||
.transform_point2(import_exports_viewport_bottom_right);
|
||||
|
||||
let import_exports_target = bezier_rs::Subpath::<PointId>::new_rect(node_graph_top_left, node_graph_bottom_right);
|
||||
let mut import_exports_bounding_box = String::new();
|
||||
let _ = import_exports_target.subpath_to_svg(&mut import_exports_bounding_box, DAffine2::IDENTITY);
|
||||
|
||||
FrontendClickTargets {
|
||||
node_click_targets,
|
||||
layer_click_targets,
|
||||
port_click_targets,
|
||||
visibility_click_targets,
|
||||
all_nodes_bounding_box,
|
||||
import_exports_bounding_box,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2231,13 +2303,39 @@ impl NodeNetworkInterface {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn set_transform(&mut self, transform: DAffine2, node_graph_top_right: DVec2, network_path: &[NodeId]) {
|
||||
pub fn set_transform(&mut self, transform: DAffine2, network_path: &[NodeId]) {
|
||||
let Some(network_metadata) = self.network_metadata_mut(network_path) else {
|
||||
log::error!("Could not get nested network in set_transform");
|
||||
return;
|
||||
};
|
||||
network_metadata.persistent_metadata.navigation_metadata.node_graph_to_viewport = transform;
|
||||
network_metadata.persistent_metadata.navigation_metadata.node_graph_top_right = node_graph_top_right;
|
||||
self.unload_import_export_ports(network_path);
|
||||
}
|
||||
|
||||
// This should be run whenever the pan ends, a zoom occurs, or the network is opened
|
||||
pub fn set_grid_aligned_edges(&mut self, node_graph_top_right: DVec2, network_path: &[NodeId]) {
|
||||
let Some(network_metadata) = self.network_metadata_mut(network_path) else {
|
||||
log::error!("Could not get nested network in set_grid_aligned_edges");
|
||||
return;
|
||||
};
|
||||
// When setting the edges to be grid aligned, update the pixel offset to ensure the next pan starts from the snapped import/export position
|
||||
let node_graph_to_viewport = network_metadata.persistent_metadata.navigation_metadata.node_graph_to_viewport;
|
||||
|
||||
let target_exports_distance = node_graph_to_viewport
|
||||
.inverse()
|
||||
.transform_point2(DVec2::new(node_graph_top_right.x - EXPORTS_TO_EDGE_PIXEL_GAP as f64, 0.));
|
||||
|
||||
let target_imports_distance = node_graph_to_viewport.inverse().transform_point2(DVec2::new(IMPORTS_TO_EDGE_PIXEL_GAP as f64, 0.));
|
||||
|
||||
let rounded_exports_distance = (target_exports_distance.x / 24. + 0.5).floor() * 24.;
|
||||
let rounded_imports_distance = (target_imports_distance.x / 24. + 0.5).floor() * 24.;
|
||||
|
||||
let rounded_viewport_exports_distance = node_graph_to_viewport.transform_point2(DVec2::new(rounded_exports_distance, 0.));
|
||||
let rounded_viewport_imports_distance = node_graph_to_viewport.transform_point2(DVec2::new(rounded_imports_distance, 0.));
|
||||
|
||||
network_metadata.persistent_metadata.navigation_metadata.exports_to_edge_distance = rounded_viewport_exports_distance.x;
|
||||
network_metadata.persistent_metadata.navigation_metadata.imports_to_edge_distance = rounded_viewport_imports_distance.x;
|
||||
|
||||
self.unload_import_export_ports(network_path);
|
||||
}
|
||||
|
||||
|
@ -2402,12 +2500,8 @@ impl NodeNetworkInterface {
|
|||
}
|
||||
}
|
||||
|
||||
let previous_metadata = if let NodeInput::Node { node_id, output_index, .. } = &previous_input {
|
||||
let previous_position = self.position(node_id, network_path).map(|position| (*node_id, position));
|
||||
let previous_outward_wires = self
|
||||
.outward_wires(network_path)
|
||||
.and_then(|outward_wires| outward_wires.get(&OutputConnector::node(*node_id, *output_index)).map(|outward_wires| outward_wires.len()));
|
||||
previous_position.zip(previous_outward_wires)
|
||||
let previous_metadata = if let NodeInput::Node { node_id, .. } = &previous_input {
|
||||
self.position(node_id, network_path).map(|position| (*node_id, position))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
@ -2483,7 +2577,7 @@ impl NodeNetworkInterface {
|
|||
match &input_connector {
|
||||
InputConnector::Export(_) => {
|
||||
// If a layer is connected to the exports, it should be set to absolute position without being moved.
|
||||
self.set_absolute_position(upstream_node_id, network_path, current_node_position)
|
||||
self.set_absolute_position(upstream_node_id, current_node_position, network_path)
|
||||
}
|
||||
InputConnector::Node {
|
||||
node_id: downstream_node_id,
|
||||
|
@ -2499,18 +2593,14 @@ impl NodeNetworkInterface {
|
|||
NodeTypePersistentMetadata::Layer(_) => {
|
||||
// If the layer feeds into the bottom input of layer, set its position to stack at its previous y position
|
||||
if *input_index == 0 {
|
||||
let Some(downstream_node_position) = self.position(downstream_node_id, network_path) else {
|
||||
log::error!("Could not get downstream node position in set_input for node {downstream_node_id}");
|
||||
return;
|
||||
};
|
||||
self.set_stack_position(upstream_node_id, (current_node_position.y - downstream_node_position.y - 3).max(0) as u32, network_path);
|
||||
self.set_stack_position_calculated_offset(upstream_node_id, downstream_node_id, network_path);
|
||||
} else {
|
||||
self.set_absolute_position(upstream_node_id, network_path, current_node_position);
|
||||
self.set_absolute_position(upstream_node_id, current_node_position, network_path);
|
||||
}
|
||||
}
|
||||
NodeTypePersistentMetadata::Node(_) => {
|
||||
// If the layer feeds into a node, set its y offset to 0
|
||||
self.set_absolute_position(upstream_node_id, network_path, current_node_position);
|
||||
self.set_absolute_position(upstream_node_id, current_node_position, network_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2532,22 +2622,27 @@ impl NodeNetworkInterface {
|
|||
}
|
||||
// If a node is disconnected.
|
||||
(NodeInput::Node { .. }, NodeInput::Value { .. } | NodeInput::Scope { .. } | NodeInput::Inline { .. }) => {
|
||||
self.unload_outward_wires(network_path);
|
||||
// If a node was previously connected, and it is no longer connected to any nodes, then set its position to absolute at its previous position
|
||||
if let Some(((old_upstream_node_id, previous_position), previous_outward_wires_len)) = previous_metadata {
|
||||
if let Some((old_upstream_node_id, previous_position)) = previous_metadata {
|
||||
let mut set_to_absolute = true;
|
||||
// Do not set to absolute if the node is connected to the same node
|
||||
if let NodeInput::Node { node_id: new_upstream_node_id, .. } = &new_input {
|
||||
if *new_upstream_node_id == old_upstream_node_id {
|
||||
set_to_absolute = false;
|
||||
// Do not set to absolute if the node is being disconnected, but still has another connection to a layer node
|
||||
if matches!(new_input, NodeInput::Value { .. }) {
|
||||
if let Some(outward_wires) = self
|
||||
.outward_wires(network_path)
|
||||
.and_then(|outward_wires| outward_wires.get(&OutputConnector::node(old_upstream_node_id, 0)))
|
||||
{
|
||||
if outward_wires.len() == 1
|
||||
&& outward_wires[0].input_index() == 0
|
||||
&& outward_wires[0].node_id().is_some_and(|downstream_node| self.is_layer(&downstream_node, network_path))
|
||||
{
|
||||
set_to_absolute = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Do not set to absolute if the node is being disconnected, but still has another connection
|
||||
if matches!(new_input, NodeInput::Value { .. }) && previous_outward_wires_len > 1 {
|
||||
set_to_absolute = false;
|
||||
}
|
||||
|
||||
if set_to_absolute {
|
||||
self.set_absolute_position(&old_upstream_node_id, network_path, previous_position);
|
||||
self.set_absolute_position(&old_upstream_node_id, previous_position, network_path);
|
||||
}
|
||||
}
|
||||
// Load structure if the change is to the document network and to the first or second
|
||||
|
@ -2561,7 +2656,6 @@ impl NodeNetworkInterface {
|
|||
}
|
||||
}
|
||||
}
|
||||
self.unload_outward_wires(network_path);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
@ -2578,32 +2672,47 @@ impl NodeNetworkInterface {
|
|||
return;
|
||||
}
|
||||
|
||||
// If the node upstream from the disconnected input is a chain, then break the chain by setting it to absolute positioning
|
||||
if let NodeInput::Node { node_id: upstream_node_id, .. } = ¤t_input {
|
||||
if let NodeInput::Node {
|
||||
node_id: upstream_node_id,
|
||||
output_index,
|
||||
..
|
||||
} = ¤t_input
|
||||
{
|
||||
// If the node upstream from the disconnected input is a chain, then break the chain by setting it to absolute positioning
|
||||
if self.is_chain(upstream_node_id, network_path) {
|
||||
self.set_upstream_chain_to_absolute(upstream_node_id, network_path);
|
||||
}
|
||||
// If the node upstream from the disconnected input has an outward wire to the bottom of a layer, set it back to stack positioning
|
||||
if self.is_layer(upstream_node_id, network_path) {
|
||||
let Some(outward_wires) = self
|
||||
.outward_wires(network_path)
|
||||
.and_then(|outward_wires| outward_wires.get(&OutputConnector::node(*upstream_node_id, *output_index)))
|
||||
else {
|
||||
log::error!("Could not get outward wires in disconnect_input");
|
||||
return;
|
||||
};
|
||||
let mut other_outward_wires = outward_wires.iter().filter(|outward_wire| *outward_wire != input_connector);
|
||||
if let Some(other_outward_wire) = other_outward_wires.next().cloned() {
|
||||
if other_outward_wires.next().is_none() {
|
||||
if let InputConnector::Node {
|
||||
node_id: downstream_node_id,
|
||||
input_index,
|
||||
} = other_outward_wire
|
||||
{
|
||||
if self.is_layer(&downstream_node_id, network_path) && input_index == 0 {
|
||||
self.set_stack_position_calculated_offset(upstream_node_id, &downstream_node_id, network_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let tagged_value = TaggedValue::from_type(&self.input_type(input_connector, network_path));
|
||||
|
||||
let value_input = NodeInput::value(tagged_value, true);
|
||||
|
||||
if matches!(input_connector, InputConnector::Node { .. }) {
|
||||
self.set_input(input_connector, value_input, network_path);
|
||||
} else {
|
||||
// TODO: Allow the user to drag the solid line when previewing
|
||||
// This causes a crash since when ending preview the NodeInput::Node to the previewed node needs to be disconnected.
|
||||
// Since it is only possible to drag the solid line, if previewing then there must be a dashed connection, which becomes the new export
|
||||
// if matches!(self.previewing(network_path), Previewing::Yes { .. }) {
|
||||
// self.start_previewing_without_restore(network_path);
|
||||
// }
|
||||
// // If there is no preview, then disconnect
|
||||
// else {
|
||||
// self.set_input(input_connector, value_input, network_path);
|
||||
// }
|
||||
self.set_input(input_connector, value_input, network_path);
|
||||
}
|
||||
self.set_input(input_connector, value_input, network_path);
|
||||
}
|
||||
|
||||
pub fn create_wire(&mut self, output_connector: &OutputConnector, input_connector: &InputConnector, network_path: &[NodeId]) {
|
||||
|
@ -2808,7 +2917,7 @@ impl NodeNetworkInterface {
|
|||
}
|
||||
}
|
||||
// If the node is not a layer or an upstream layer is not found, reconnect to the first upstream node
|
||||
if reconnect_to_input.is_none() && (matches!(node.inputs.first(), Some(NodeInput::Node { .. })) || matches!(node.inputs.first(), Some(NodeInput::Network { .. }))) {
|
||||
if reconnect_to_input.is_none() && (matches!(node.inputs.first(), Some(NodeInput::Node { .. }) | Some(NodeInput::Network { .. }))) {
|
||||
reconnect_to_input = Some(node.inputs[0].clone());
|
||||
}
|
||||
}
|
||||
|
@ -2828,8 +2937,31 @@ impl NodeNetworkInterface {
|
|||
}
|
||||
|
||||
for input_to_disconnect in &downstream_inputs_to_disconnect {
|
||||
if let Some(reconnect_input) = reconnect_to_input.take() {
|
||||
self.set_input(input_to_disconnect, reconnect_input, network_path)
|
||||
// Prevent reconnecting export to import until https://github.com/GraphiteEditor/Graphite/issues/1762 is solved
|
||||
if matches!(reconnect_to_input, Some(NodeInput::Network { .. })) && matches!(input_to_disconnect, InputConnector::Export(_)) {
|
||||
self.disconnect_input(input_to_disconnect, network_path);
|
||||
} else if let Some(reconnect_input) = reconnect_to_input.take() {
|
||||
self.set_input(input_to_disconnect, reconnect_input.clone(), network_path);
|
||||
if let Some(node_metadata) = self.node_metadata(deleting_node_id, network_path) {
|
||||
if let NodeTypePersistentMetadata::Layer(layer_metadata) = &node_metadata.persistent_metadata.node_type_metadata {
|
||||
if let LayerPosition::Stack(deleted_layer_offset) = layer_metadata.position {
|
||||
if let NodeInput::Node { node_id, .. } = reconnect_input {
|
||||
self.set_stack_position(&node_id, deleted_layer_offset, network_path);
|
||||
self.unload_upstream_node_click_targets(vec![node_id], network_path);
|
||||
}
|
||||
} else {
|
||||
// Move upstream layer to the top of the stack
|
||||
let Some(deleted_node_position) = self.position(deleting_node_id, network_path) else {
|
||||
log::error!("Could not get position in remove_references_from_network");
|
||||
return false;
|
||||
};
|
||||
if let NodeInput::Node { node_id, .. } = reconnect_input {
|
||||
self.set_absolute_position(&node_id, deleted_node_position, network_path);
|
||||
self.unload_upstream_node_click_targets(vec![node_id], network_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.disconnect_input(input_to_disconnect, network_path);
|
||||
}
|
||||
|
@ -2837,7 +2969,7 @@ impl NodeNetworkInterface {
|
|||
true
|
||||
}
|
||||
|
||||
fn start_previewing_without_restore(&mut self, network_path: &[NodeId]) {
|
||||
pub fn start_previewing_without_restore(&mut self, network_path: &[NodeId]) {
|
||||
// Some logic will have to be performed to prevent the graph positions from being completely changed when the export changes to some previewed node
|
||||
let Some(network_metadata) = self.network_metadata_mut(network_path) else {
|
||||
log::error!("Could not get nested network_metadata in start_previewing_without_restore");
|
||||
|
@ -2950,18 +3082,14 @@ impl NodeNetworkInterface {
|
|||
log::error!("Could not get upstream sibling position in set_to_node_or_layer");
|
||||
return;
|
||||
};
|
||||
self.set_absolute_position(&upstream_sibling_id, network_path, upstream_sibling_position);
|
||||
self.set_absolute_position(&upstream_sibling_id, upstream_sibling_position, network_path);
|
||||
}
|
||||
}
|
||||
(false, true) => {
|
||||
// If a node is set to a layer
|
||||
if let Some(upstream_sibling_id) = upstream_sibling_id {
|
||||
if self.is_layer(&upstream_sibling_id, network_path) {
|
||||
let (Some(toggled_node_position), Some(upstream_node_position)) = (self.position(node_id, network_path), self.position(&upstream_sibling_id, network_path)) else {
|
||||
log::error!("Could not get downstream node position in set_to_node_or_layer");
|
||||
return;
|
||||
};
|
||||
self.set_stack_position(&upstream_sibling_id, (upstream_node_position.y - toggled_node_position.y - 3).max(0) as u32, network_path);
|
||||
self.set_stack_position_calculated_offset(&upstream_sibling_id, node_id, network_path);
|
||||
} else {
|
||||
self.set_upstream_chain_to_absolute(&upstream_sibling_id, network_path);
|
||||
}
|
||||
|
@ -3013,9 +3141,27 @@ impl NodeNetworkInterface {
|
|||
if is_layer {
|
||||
self.try_set_upstream_to_chain(&InputConnector::node(*node_id, 1), network_path);
|
||||
// Reload click target of the layer which used to encapsulate the node
|
||||
if let Some(downstream_layer) = self.downstream_layer(node_id, network_path) {
|
||||
self.unload_node_click_targets(&downstream_layer.to_node(), network_path);
|
||||
|
||||
let mut downstream_layer = Some(*node_id);
|
||||
while let Some(downstream_layer_id) = downstream_layer {
|
||||
if downstream_layer_id == *node_id || !self.is_layer(&downstream_layer_id, network_path) {
|
||||
let Some(outward_wires) = self.outward_wires(network_path) else {
|
||||
log::error!("Could not get outward wires in set_to_node_or_layer");
|
||||
downstream_layer = None;
|
||||
break;
|
||||
};
|
||||
downstream_layer = outward_wires
|
||||
.get(&OutputConnector::node(downstream_layer_id, 0))
|
||||
.and_then(|outward_wires| if outward_wires.len() == 1 { outward_wires[0].node_id() } else { None });
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if let Some(downstream_layer) = downstream_layer {
|
||||
self.unload_node_click_targets(&downstream_layer, network_path);
|
||||
}
|
||||
} else {
|
||||
self.try_set_upstream_to_chain(&InputConnector::node(*node_id, 0), network_path);
|
||||
}
|
||||
|
||||
self.unload_upstream_node_click_targets(vec![*node_id], network_path);
|
||||
|
@ -3065,6 +3211,11 @@ impl NodeNetworkInterface {
|
|||
if root_node_to_restore.node_id == toggle_id {
|
||||
new_export = Some(OutputConnector::node(toggle_id, 0));
|
||||
new_previewing_state = Previewing::Yes { root_node_to_restore: None };
|
||||
} else {
|
||||
// Root node to restore does not change
|
||||
new_previewing_state = Previewing::Yes {
|
||||
root_node_to_restore: Some(root_node_to_restore),
|
||||
};
|
||||
}
|
||||
}
|
||||
// There is a dashed line without a solid line.
|
||||
|
@ -3106,7 +3257,7 @@ impl NodeNetworkInterface {
|
|||
}
|
||||
|
||||
/// Sets the position of a node to an absolute position
|
||||
fn set_absolute_position(&mut self, node_id: &NodeId, network_path: &[NodeId], position: IVec2) {
|
||||
fn set_absolute_position(&mut self, node_id: &NodeId, position: IVec2, network_path: &[NodeId]) {
|
||||
let Some(node_metadata) = self.node_metadata_mut(node_id, network_path) else {
|
||||
log::error!("Could not get node_metadata for node {node_id}");
|
||||
return;
|
||||
|
@ -3131,6 +3282,20 @@ impl NodeNetworkInterface {
|
|||
}
|
||||
}
|
||||
|
||||
/// Sets the position of a node to a stack position without changing its y offset
|
||||
pub fn set_stack_position_calculated_offset(&mut self, node_id: &NodeId, downstream_layer: &NodeId, network_path: &[NodeId]) {
|
||||
let Some(node_position) = self.position(node_id, network_path) else {
|
||||
log::error!("Could not get node position for node {node_id}");
|
||||
return;
|
||||
};
|
||||
let Some(downstream_position) = self.position(downstream_layer, network_path) else {
|
||||
log::error!("Could not get downstream position for node {downstream_layer}");
|
||||
return;
|
||||
};
|
||||
|
||||
self.set_stack_position(node_id, (node_position.y - downstream_position.y - 3).max(0) as u32, network_path);
|
||||
}
|
||||
|
||||
/// Sets the position of a node to a chain position
|
||||
pub fn set_chain_position(&mut self, node_id: &NodeId, network_path: &[NodeId]) {
|
||||
let Some(node_metadata) = self.node_metadata_mut(node_id, network_path) else {
|
||||
|
@ -3221,7 +3386,7 @@ impl NodeNetworkInterface {
|
|||
};
|
||||
// Set any chain nodes to absolute positioning
|
||||
if self.is_chain(upstream_id, network_path) {
|
||||
self.set_absolute_position(upstream_id, network_path, previous_position);
|
||||
self.set_absolute_position(upstream_id, previous_position, network_path);
|
||||
}
|
||||
// If there is an upstream layer then stop breaking the chain
|
||||
else {
|
||||
|
@ -3289,8 +3454,9 @@ impl NodeNetworkInterface {
|
|||
}
|
||||
|
||||
// TODO: Run the auto layout system to make space for the new nodes
|
||||
// Disconnect the layers primary output and the input to the last non layer node feeding into it through primary flow, reconnects, then moves the layer to the new layer and stack index
|
||||
/// Disconnect the layers primary output and the input to the last non layer node feeding into it through primary flow, reconnects, then moves the layer to the new layer and stack index
|
||||
pub fn move_layer_to_stack(&mut self, layer: LayerNodeIdentifier, mut parent: LayerNodeIdentifier, mut insert_index: usize, network_path: &[NodeId]) {
|
||||
// A layer is considered to be the height of that layer plus the height to the upstream layer sibling
|
||||
// If a non artboard layer is attempted to be connected to the exports, and there is already an artboard connected, then connect the layer to the artboard.
|
||||
if let Some(first_layer) = LayerNodeIdentifier::ROOT_PARENT.children(&self.document_metadata).next() {
|
||||
if parent == LayerNodeIdentifier::ROOT_PARENT
|
||||
|
@ -3302,7 +3468,72 @@ impl NodeNetworkInterface {
|
|||
}
|
||||
}
|
||||
|
||||
// If the moved layer is a child of the new parent, then get its index
|
||||
// Disconnecting top of stack:
|
||||
// 1. Get vertical offset 1 between the top of stack and the upstream layer
|
||||
// 2. Set the position of the upstream layer to the top of stack
|
||||
// 3. Remove the top of stack
|
||||
// 4. Move top of stack to destination
|
||||
|
||||
// Disconnecting layer from stack:
|
||||
// 1. Get vertical offset 1 between the layer and the upstream layer sibling
|
||||
// 2. Get vertical offset 2 between the layer and the downstream layer sibling
|
||||
// 3. Set the offset of the upstream layer to offset 2
|
||||
// 4. Remove the layer from the stack
|
||||
// 5. Move the layer to the destination
|
||||
|
||||
let previous_upstream_layer = self
|
||||
.upstream_flow_back_from_nodes(vec![layer.to_node()], network_path, FlowType::PrimaryFlow)
|
||||
.skip(1)
|
||||
.find(|node_id| self.is_layer(node_id, network_path));
|
||||
|
||||
let vertical_offset_1 = if let Some(previous_upstream_layer) = previous_upstream_layer {
|
||||
let Some(node_metadata) = self.node_metadata(&previous_upstream_layer, network_path) else {
|
||||
log::error!("Could not get node_metadata in move_layer_to_stack");
|
||||
return;
|
||||
};
|
||||
if let NodeTypePersistentMetadata::Layer(LayerPersistentMetadata { position }) = &node_metadata.persistent_metadata.node_type_metadata {
|
||||
match position {
|
||||
LayerPosition::Stack(y_offset) => *y_offset,
|
||||
_ => 0,
|
||||
}
|
||||
} else {
|
||||
let (Some(moved_layer_position), Some(previous_upstream_layer_position)) = (self.position(&layer.to_node(), network_path), self.position(&previous_upstream_layer, network_path))
|
||||
else {
|
||||
log::error!("Could not get moved layer position in move_layer_to_stack");
|
||||
return;
|
||||
};
|
||||
(previous_upstream_layer_position.y - moved_layer_position.y - 3).max(0) as u32
|
||||
}
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let Some(moved_layer_metadata) = self.node_metadata(&layer.to_node(), network_path) else {
|
||||
log::error!("Could not get node_metadata in move_layer_to_stack");
|
||||
return;
|
||||
};
|
||||
|
||||
let NodeTypePersistentMetadata::Layer(LayerPersistentMetadata { position }) = &moved_layer_metadata.persistent_metadata.node_type_metadata else {
|
||||
log::error!("Could not get layer metadata for layer in move_layer_to_stack");
|
||||
return;
|
||||
};
|
||||
|
||||
match &position {
|
||||
LayerPosition::Stack(offset) => {
|
||||
if let Some(previous_upstream_layer) = previous_upstream_layer {
|
||||
self.set_stack_position(&previous_upstream_layer, *offset, network_path);
|
||||
self.unload_upstream_node_click_targets(vec![previous_upstream_layer], network_path);
|
||||
}
|
||||
}
|
||||
LayerPosition::Absolute(stack_top_position) => {
|
||||
if let Some(previous_upstream_layer) = previous_upstream_layer {
|
||||
self.set_absolute_position(&previous_upstream_layer, *stack_top_position, network_path);
|
||||
self.unload_upstream_node_click_targets(vec![previous_upstream_layer], network_path);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// If the moved layer is a child of the new parent, then get its index after the disconnect
|
||||
if let Some(moved_layer_previous_index) = parent.children(&self.document_metadata).position(|child| child == layer) {
|
||||
// Adjust the insert index if the layer's previous index is less than the insert index
|
||||
if moved_layer_previous_index < insert_index {
|
||||
|
@ -3310,30 +3541,21 @@ impl NodeNetworkInterface {
|
|||
}
|
||||
}
|
||||
|
||||
let previous_upstream_layer = self
|
||||
.upstream_flow_back_from_nodes(vec![layer.to_node()], network_path, FlowType::PrimaryFlow)
|
||||
.skip(1)
|
||||
.find(|node_id| self.is_layer(node_id, network_path));
|
||||
|
||||
let Some(layer_to_move_position) = self.position(&layer.to_node(), network_path) else {
|
||||
log::error!("Could not get layer to move position in move_layer_to_stack");
|
||||
return;
|
||||
};
|
||||
|
||||
// Get the distance between the layer to move and its upstream layer sibling
|
||||
// Disconnect layer to move
|
||||
self.remove_references_from_network(&layer.to_node(), true, network_path);
|
||||
self.disconnect_input(&InputConnector::node(layer.to_node(), 0), network_path);
|
||||
|
||||
// Shift the previous upstream layer to the moved layers position
|
||||
if let Some(previous_upstream_layer) = previous_upstream_layer {
|
||||
let Some(previous_upstream_layer_position) = self.position(&previous_upstream_layer, network_path) else {
|
||||
log::error!("Could not get previous upstream layer position in move_layer_to_stack");
|
||||
return;
|
||||
};
|
||||
let y_offset = layer_to_move_position.y - previous_upstream_layer_position.y;
|
||||
self.shift_node(&previous_upstream_layer, IVec2::new(0, y_offset), network_path);
|
||||
}
|
||||
// Moving layer to top of stack:
|
||||
// 1. Get the position of the top of the stack. If a new stack is created, then offset by (-8, 3) from the parent
|
||||
// 2. Move layer to the top of the stack
|
||||
// 3. Connect layer to the top of stack
|
||||
// 3. Set upstream layer to stack position with vertical offset 1
|
||||
|
||||
// Moving layer to stack:
|
||||
// 1. Insert the layer into the stack
|
||||
// 2. Get vertical offset 3 of the upstream layer
|
||||
// 3. Set the stack offset of the moved layer to vertical offset 3
|
||||
// 4. Set the stack offset of the upstream layer to vertical offset 1
|
||||
|
||||
let post_node = ModifyInputsContext::get_post_node_with_index(self, parent, insert_index);
|
||||
|
||||
|
@ -3343,8 +3565,8 @@ impl NodeNetworkInterface {
|
|||
return;
|
||||
};
|
||||
|
||||
// Connect the layer to a parent layer/node at the top of the stack
|
||||
if post_node.input_index() == 1 || matches!(post_node, InputConnector::Export(_)) {
|
||||
// Connect the layer to a parent layer/node at the top of the stack, or a non layer node midway down the stack
|
||||
if post_node.input_index() == 1 || matches!(post_node, InputConnector::Export(_)) || !post_node.node_id().is_some_and(|post_node_id| self.is_layer(&post_node_id, network_path)) {
|
||||
match post_node_input {
|
||||
// Create a new stack
|
||||
NodeInput::Value { .. } | NodeInput::Scope(_) | NodeInput::Inline(_) => {
|
||||
|
@ -3359,7 +3581,7 @@ impl NodeNetworkInterface {
|
|||
return;
|
||||
};
|
||||
let offset = IVec2::new(-8, 3);
|
||||
self.set_absolute_position(&layer.to_node(), network_path, post_node_position + offset);
|
||||
self.set_absolute_position(&layer.to_node(), post_node_position + offset, network_path);
|
||||
}
|
||||
// Move to the top of a stack
|
||||
NodeInput::Node { node_id, .. } => {
|
||||
|
@ -3367,29 +3589,46 @@ impl NodeNetworkInterface {
|
|||
log::error!("Could not get top of stack position in move_layer_to_stack");
|
||||
return;
|
||||
};
|
||||
self.set_absolute_position(&layer.to_node(), network_path, top_of_stack_position - IVec2::new(0, 3));
|
||||
self.shift_node(&layer.to_node(), IVec2::new(0, 3), network_path);
|
||||
self.set_absolute_position(&layer.to_node(), top_of_stack_position, network_path);
|
||||
self.unload_upstream_node_click_targets(vec![layer.to_node()], network_path);
|
||||
self.insert_node_between(&layer.to_node(), &post_node, 0, network_path);
|
||||
self.set_stack_position(&node_id, vertical_offset_1, network_path);
|
||||
}
|
||||
NodeInput::Network { .. } => {
|
||||
log::error!("Cannot move post node to parent which connects to the imports")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Create a new stack
|
||||
match post_node_input {
|
||||
// Move to the bottom of the stack
|
||||
NodeInput::Value { .. } | NodeInput::Scope(_) | NodeInput::Inline(_) => {
|
||||
self.create_wire(&OutputConnector::node(layer.to_node(), 0), &post_node, network_path);
|
||||
self.set_stack_position(&layer.to_node(), 0, network_path);
|
||||
}
|
||||
NodeInput::Node { .. } => {
|
||||
// Insert into the stack
|
||||
NodeInput::Node { node_id: upstream_node_id, .. } => {
|
||||
let Some(upstream_node_metadata) = self.node_metadata(&upstream_node_id, network_path) else {
|
||||
log::error!("Could not get upstream node metadata in move_layer_to_stack");
|
||||
return;
|
||||
};
|
||||
let NodeTypePersistentMetadata::Layer(LayerPersistentMetadata { position }) = &upstream_node_metadata.persistent_metadata.node_type_metadata else {
|
||||
log::error!("Could not get upstream node metadata in move_layer_to_stack");
|
||||
return;
|
||||
};
|
||||
let LayerPosition::Stack(vertical_offset_3) = position.clone() else {
|
||||
log::error!("Could not get vertical offset 3 in move_layer_to_stack");
|
||||
return;
|
||||
};
|
||||
self.insert_node_between(&layer.to_node(), &post_node, 0, network_path);
|
||||
self.set_stack_position(&layer.to_node(), vertical_offset_3, network_path);
|
||||
self.set_stack_position(&upstream_node_id, vertical_offset_1, network_path);
|
||||
}
|
||||
NodeInput::Network { .. } => {
|
||||
log::error!("Cannot move post node to parent which connects to the imports")
|
||||
}
|
||||
}
|
||||
self.set_stack_position(&layer.to_node(), 0, network_path);
|
||||
}
|
||||
self.unload_upstream_node_click_targets(vec![layer.to_node()], network_path);
|
||||
}
|
||||
|
||||
// Insert a node onto a wire. Ensure insert_node_input_index is an exposed input
|
||||
|
@ -3957,8 +4196,12 @@ pub struct NavigationMetadata {
|
|||
// TODO: Remove and replace with calculate_offset_transform from the node_graph_ptz. This will be difficult since it requires both the navigation message handler and the IPP
|
||||
/// Transform from node graph space to viewport space.
|
||||
pub node_graph_to_viewport: DAffine2,
|
||||
/// The top right of the node graph in viewport space
|
||||
pub node_graph_top_right: DVec2,
|
||||
/// The viewport pixel distance distance between the left edge of the node graph and the exports. Rounded to nearest grid space when the panning ends.
|
||||
#[serde(default)]
|
||||
pub exports_to_edge_distance: f64,
|
||||
/// The viewport pixel distance between the left edge of the node graph and the imports. Rounded to nearest grid space when the panning ends.
|
||||
#[serde(default)]
|
||||
pub imports_to_edge_distance: f64,
|
||||
}
|
||||
|
||||
impl Default for NavigationMetadata {
|
||||
|
@ -3967,7 +4210,8 @@ impl Default for NavigationMetadata {
|
|||
NavigationMetadata {
|
||||
node_graph_ptz: PTZ::default(),
|
||||
node_graph_to_viewport: DAffine2::IDENTITY,
|
||||
node_graph_top_right: DVec2::ZERO,
|
||||
exports_to_edge_distance: 0.,
|
||||
imports_to_edge_distance: 0.,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -380,7 +380,11 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
|
|||
let upgrade_vector_manipulation_format = document_serialized_content.contains("ManipulatorGroupIds") && !document_name.contains("__DO_NOT_UPGRADE__");
|
||||
let document_name = document_name.replace("__DO_NOT_UPGRADE__", "");
|
||||
|
||||
let document = DocumentMessageHandler::with_name_and_content(document_name.clone(), document_serialized_content);
|
||||
let document = DocumentMessageHandler::deserialize_document(&document_serialized_content).map(|mut document| {
|
||||
document.name = document_name.clone();
|
||||
document
|
||||
});
|
||||
|
||||
let mut document = match document {
|
||||
Ok(document) => document,
|
||||
Err(e) => {
|
||||
|
@ -549,15 +553,16 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
|
|||
};
|
||||
// If the downstream node is a layer and the input is the first input and the current layer is not in a stack
|
||||
if input_index == 0 && document.network_interface.is_layer(&downstream_node, &[]) && document.network_interface.is_absolute(&layer.to_node(), &[]) {
|
||||
// Ensure the layer is horizontally aligned with the downstream layer to prevent changing the layout of old files
|
||||
let (Some(layer_position), Some(downstream_position)) =
|
||||
(document.network_interface.position(&layer.to_node(), &[]), document.network_interface.position(&downstream_node, &[]))
|
||||
else {
|
||||
log::error!("Could not get downstream node position in PortfolioMessage::OpenDocumentFileWithId");
|
||||
return;
|
||||
log::error!("Could not get position for layer {:?} or downstream node {} when opening file", layer.to_node(), downstream_node);
|
||||
continue;
|
||||
};
|
||||
document
|
||||
.network_interface
|
||||
.set_stack_position(&layer.to_node(), (layer_position.y - downstream_position.y - 3).max(0) as u32, &[]);
|
||||
if layer_position.x == downstream_position.x {
|
||||
document.network_interface.set_stack_position_calculated_offset(&layer.to_node(), &downstream_node, &[]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -514,6 +514,7 @@
|
|||
handlePosition={scrollbarPos.x}
|
||||
on:handlePosition={({ detail }) => panCanvasX(detail)}
|
||||
on:pressTrack={({ detail }) => pageX(detail)}
|
||||
on:pointerup={() => editor.handle.setGridAlignedEdges()}
|
||||
/>
|
||||
</LayoutRow>
|
||||
</LayoutCol>
|
||||
|
|
|
@ -21,9 +21,6 @@
|
|||
entry: LayerPanelEntry;
|
||||
};
|
||||
|
||||
const RANGE_TO_INSERT_WITHIN_BOTTOM_FOLDER_NOT_ROOT = 20;
|
||||
const INSERT_MARK_OFFSET = 2;
|
||||
|
||||
type DraggingData = {
|
||||
select?: () => void;
|
||||
insertParentId: bigint | undefined;
|
||||
|
@ -207,9 +204,6 @@
|
|||
const treeChildren = tree.div()?.children;
|
||||
const treeOffset = tree.div()?.getBoundingClientRect().top;
|
||||
|
||||
// Closest distance to the middle of the row along the Y axis
|
||||
let closest = Infinity;
|
||||
|
||||
// Folder to insert into
|
||||
let insertParentId: bigint | undefined = undefined;
|
||||
let insertDepth = 0;
|
||||
|
@ -221,30 +215,26 @@
|
|||
let highlightFolder = false;
|
||||
|
||||
let markerHeight = 0;
|
||||
let previousHeight: number | undefined = undefined;
|
||||
|
||||
if (treeChildren !== undefined && treeOffset !== undefined) {
|
||||
Array.from(treeChildren).forEach((treeChild, index) => {
|
||||
const layerPanel = document.querySelector("[data-layer-panel]"); // Selects the element with the data-layer-panel attribute
|
||||
if (layerPanel !== null && treeChildren !== undefined && treeOffset !== undefined) {
|
||||
let layerPanelTop = layerPanel.getBoundingClientRect().top;
|
||||
Array.from(treeChildren).forEach((treeChild) => {
|
||||
const indexAttribute = treeChild.getAttribute("data-index");
|
||||
if (!indexAttribute) return;
|
||||
const { folderIndex, entry: layer } = layers[parseInt(indexAttribute, 10)];
|
||||
|
||||
const rect = treeChild.getBoundingClientRect();
|
||||
const position = rect.top + rect.height / 2;
|
||||
const distance = position - clientY;
|
||||
|
||||
// Inserting above current row
|
||||
if (distance > 0 && distance < closest) {
|
||||
insertParentId = layer.parentId;
|
||||
insertDepth = layer.depth - 1;
|
||||
insertIndex = folderIndex;
|
||||
highlightFolder = false;
|
||||
closest = distance;
|
||||
markerHeight = previousHeight || treeOffset + INSERT_MARK_OFFSET;
|
||||
if (rect.top > clientY || rect.bottom < clientY) {
|
||||
return;
|
||||
}
|
||||
// Inserting below current row
|
||||
else if (distance > -closest && distance > -RANGE_TO_INSERT_WITHIN_BOTTOM_FOLDER_NOT_ROOT && distance < 0) {
|
||||
if (layer.childrenAllowed) {
|
||||
const pointerPercentage = (clientY - rect.top) / rect.height;
|
||||
if (layer.childrenAllowed) {
|
||||
if (pointerPercentage < 0.25) {
|
||||
insertParentId = layer.parentId;
|
||||
insertDepth = layer.depth - 1;
|
||||
insertIndex = folderIndex;
|
||||
markerHeight = rect.top - layerPanelTop;
|
||||
} else if (pointerPercentage < 0.75 || (layer.childrenPresent && layer.expanded)) {
|
||||
insertParentId = layer.id;
|
||||
insertDepth = layer.depth;
|
||||
insertIndex = 0;
|
||||
|
@ -253,24 +243,33 @@
|
|||
insertParentId = layer.parentId;
|
||||
insertDepth = layer.depth - 1;
|
||||
insertIndex = folderIndex + 1;
|
||||
highlightFolder = false;
|
||||
markerHeight = rect.bottom - layerPanelTop;
|
||||
}
|
||||
} else {
|
||||
if (pointerPercentage < 0.5) {
|
||||
insertParentId = layer.parentId;
|
||||
insertDepth = layer.depth - 1;
|
||||
insertIndex = folderIndex;
|
||||
markerHeight = rect.top - layerPanelTop;
|
||||
} else {
|
||||
insertParentId = layer.parentId;
|
||||
insertDepth = layer.depth - 1;
|
||||
insertIndex = folderIndex + 1;
|
||||
markerHeight = rect.bottom - layerPanelTop;
|
||||
}
|
||||
|
||||
closest = -distance;
|
||||
markerHeight = index === treeChildren.length - 1 ? rect.bottom - INSERT_MARK_OFFSET : rect.bottom;
|
||||
}
|
||||
// Inserting with no nesting at the end of the panel
|
||||
else if (closest === Infinity) {
|
||||
if (layer.parentId === undefined) insertIndex = folderIndex + 1;
|
||||
|
||||
markerHeight = rect.bottom - INSERT_MARK_OFFSET;
|
||||
}
|
||||
previousHeight = rect.bottom;
|
||||
});
|
||||
// Dragging to the empty space below all layers
|
||||
let lastLayer = treeChildren[treeChildren.length - 1];
|
||||
if (lastLayer.getBoundingClientRect().bottom < clientY) {
|
||||
const numberRootLayers = layers.filter((layer) => layer.entry.depth === 1).length;
|
||||
insertParentId = undefined;
|
||||
insertDepth = 0;
|
||||
insertIndex = numberRootLayers;
|
||||
markerHeight = lastLayer.getBoundingClientRect().bottom - layerPanelTop;
|
||||
}
|
||||
}
|
||||
|
||||
markerHeight -= treeOffset || 0;
|
||||
|
||||
return {
|
||||
select,
|
||||
insertParentId,
|
||||
|
@ -370,7 +369,7 @@
|
|||
<WidgetLayout layout={layersPanelOptionsLayout} />
|
||||
</LayoutRow>
|
||||
<LayoutRow class="list-area" scrollableY={true}>
|
||||
<LayoutCol class="list" bind:this={list} on:click={() => deselectAllLayers()} on:dragover={(e) => draggable && updateInsertLine(e)} on:dragend={() => draggable && drop()}>
|
||||
<LayoutCol class="list" data-layer-panel bind:this={list} on:click={() => deselectAllLayers()} on:dragover={(e) => draggable && updateInsertLine(e)} on:dragend={() => draggable && drop()}>
|
||||
{#each layers as listing, index}
|
||||
<LayoutRow
|
||||
class="layer"
|
||||
|
|
|
@ -449,6 +449,7 @@
|
|||
<path class="visibility" d={pathString} />
|
||||
{/each}
|
||||
<path class="all-nodes-bounding-box" d={$nodeGraph.clickTargets.allNodesBoundingBox} />
|
||||
<path class="all-nodes-bounding-box" d={$nodeGraph.clickTargets.importExportsBoundingBox} />
|
||||
</svg>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
|
||||
const pointerPosition = (direction: ScrollbarDirection, e: PointerEvent): number => (direction === "Vertical" ? e.clientY : e.clientX);
|
||||
|
||||
const dispatch = createEventDispatcher<{ handlePosition: number; pressTrack: number }>();
|
||||
const dispatch = createEventDispatcher<{ handlePosition: number; pressTrack: number; pointerup }>();
|
||||
|
||||
export let direction: ScrollbarDirection = "Vertical";
|
||||
export let handlePosition = 0.5;
|
||||
|
@ -102,7 +102,7 @@
|
|||
});
|
||||
</script>
|
||||
|
||||
<div class={`scrollbar-input ${direction.toLowerCase()}`}>
|
||||
<div class={`scrollbar-input ${direction.toLowerCase()}`} on:pointerup={() => dispatch("pointerup")}>
|
||||
<button class="arrow decrease" on:pointerdown={() => changePosition(-50)} tabindex="-1" />
|
||||
<div class="scroll-track" bind:this={scrollTrack} on:pointerdown={grabArea}>
|
||||
<div class="scroll-thumb" on:pointerdown={grabHandle} class:dragging style:top={thumbTop} style:bottom={thumbBottom} style:left={thumbLeft} style:right={thumbRight} />
|
||||
|
|
|
@ -175,6 +175,7 @@ export type FrontendClickTargets = {
|
|||
readonly portClickTargets: string[];
|
||||
readonly visibilityClickTargets: string[];
|
||||
readonly allNodesBoundingBox: string;
|
||||
readonly importExportsBoundingBox: string;
|
||||
};
|
||||
|
||||
export type ContextMenuInformation = {
|
||||
|
|
|
@ -564,6 +564,13 @@ impl EditorHandle {
|
|||
self.dispatch(message);
|
||||
}
|
||||
|
||||
/// Snaps the import/export edges to a grid space when the scroll bar is released
|
||||
#[wasm_bindgen(js_name = setGridAlignedEdges)]
|
||||
pub fn set_grid_aligned_edges(&self) {
|
||||
let message = NodeGraphMessage::SetGridAlignedEdges;
|
||||
self.dispatch(message);
|
||||
}
|
||||
|
||||
/// Creates a new document node in the node graph
|
||||
#[wasm_bindgen(js_name = createNode)]
|
||||
pub fn create_node(&self, node_type: String, x: i32, y: i32) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue