Add shaking input gesture to disconnect a node being dragged (#2889)
* Add shaking input gesture to disconnect a node being dragged * Improve shake detection algorithm * Fix reconnection * Improve shake reconnect logic * Fix history --------- Co-authored-by: Adam <adamgerhant@gmail.com>
This commit is contained in:
parent
e4ec67d852
commit
f299497090
|
|
@ -19,5 +19,6 @@ pub enum InputMapperMessage {
|
|||
|
||||
// Messages
|
||||
PointerMove,
|
||||
PointerShake,
|
||||
WheelScroll,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,14 +54,15 @@ pub fn input_mappings() -> Mapping {
|
|||
entry!(KeyDown(KeyZ); modifiers=[Accel, MouseLeft], action_dispatch=DocumentMessage::Noop),
|
||||
//
|
||||
// NodeGraphMessage
|
||||
entry!(KeyDown(MouseLeft); action_dispatch=NodeGraphMessage::PointerDown {shift_click: false, control_click: false, alt_click: false, right_click: false}),
|
||||
entry!(KeyDown(MouseLeft); modifiers=[Shift], action_dispatch=NodeGraphMessage::PointerDown {shift_click: true, control_click: false, alt_click: false, right_click: false}),
|
||||
entry!(KeyDown(MouseLeft); modifiers=[Accel], action_dispatch=NodeGraphMessage::PointerDown {shift_click: false, control_click: true, alt_click: false, right_click: false}),
|
||||
entry!(KeyDown(MouseLeft); modifiers=[Shift, Accel], action_dispatch=NodeGraphMessage::PointerDown {shift_click: true, control_click: true, alt_click: false, right_click: false}),
|
||||
entry!(KeyDown(MouseLeft); modifiers=[Alt], action_dispatch=NodeGraphMessage::PointerDown {shift_click: false, control_click: false, alt_click: true, right_click: false}),
|
||||
entry!(KeyDown(MouseRight); action_dispatch=NodeGraphMessage::PointerDown {shift_click: false, control_click: false, alt_click: false, right_click: true}),
|
||||
entry!(KeyDown(MouseLeft); action_dispatch=NodeGraphMessage::PointerDown { shift_click: false, control_click: false, alt_click: false, right_click: false }),
|
||||
entry!(KeyDown(MouseLeft); modifiers=[Shift], action_dispatch=NodeGraphMessage::PointerDown { shift_click: true, control_click: false, alt_click: false, right_click: false }),
|
||||
entry!(KeyDown(MouseLeft); modifiers=[Accel], action_dispatch=NodeGraphMessage::PointerDown { shift_click: false, control_click: true, alt_click: false, right_click: false }),
|
||||
entry!(KeyDown(MouseLeft); modifiers=[Shift, Accel], action_dispatch=NodeGraphMessage::PointerDown { shift_click: true, control_click: true, alt_click: false, right_click: false }),
|
||||
entry!(KeyDown(MouseLeft); modifiers=[Alt], action_dispatch=NodeGraphMessage::PointerDown { shift_click: false, control_click: false, alt_click: true, right_click: false }),
|
||||
entry!(KeyDown(MouseRight); action_dispatch=NodeGraphMessage::PointerDown { shift_click: false, control_click: false, alt_click: false, right_click: true }),
|
||||
entry!(DoubleClick(MouseButton::Left); action_dispatch=NodeGraphMessage::EnterNestedNetwork),
|
||||
entry!(PointerMove; refresh_keys=[Shift], action_dispatch=NodeGraphMessage::PointerMove {shift: Shift}),
|
||||
entry!(PointerMove; refresh_keys=[Shift], action_dispatch=NodeGraphMessage::PointerMove { shift: Shift }),
|
||||
entry!(PointerShake; action_dispatch=NodeGraphMessage::ShakeNode),
|
||||
entry!(KeyUp(MouseLeft); action_dispatch=NodeGraphMessage::PointerUp),
|
||||
entry!(KeyDown(Delete); modifiers=[Accel], action_dispatch=NodeGraphMessage::DeleteSelectedNodes { delete_children: false }),
|
||||
entry!(KeyDown(Backspace); modifiers=[Accel], action_dispatch=NodeGraphMessage::DeleteSelectedNodes { delete_children: false }),
|
||||
|
|
@ -417,7 +418,7 @@ pub fn input_mappings() -> Mapping {
|
|||
entry!(KeyDown(Tab); modifiers=[Control], action_dispatch=PortfolioMessage::NextDocument),
|
||||
entry!(KeyDown(Tab); modifiers=[Control, Shift], action_dispatch=PortfolioMessage::PrevDocument),
|
||||
entry!(KeyDown(KeyW); modifiers=[Accel], action_dispatch=PortfolioMessage::CloseActiveDocumentWithConfirmation),
|
||||
entry!(KeyDown(KeyW); modifiers=[Accel,Alt], action_dispatch=PortfolioMessage::CloseAllDocumentsWithConfirmation),
|
||||
entry!(KeyDown(KeyW); modifiers=[Accel, Alt], action_dispatch=PortfolioMessage::CloseAllDocumentsWithConfirmation),
|
||||
entry!(KeyDown(KeyO); modifiers=[Accel], action_dispatch=PortfolioMessage::OpenDocument),
|
||||
entry!(KeyDown(KeyI); modifiers=[Accel], action_dispatch=PortfolioMessage::Import),
|
||||
entry!(KeyDown(KeyX); modifiers=[Accel], action_dispatch=PortfolioMessage::Cut { clipboard: Clipboard::Device }),
|
||||
|
|
@ -440,7 +441,7 @@ pub fn input_mappings() -> Mapping {
|
|||
entry!(KeyDown(Space); modifiers=[Shift], action_dispatch=AnimationMessage::ToggleLivePreview),
|
||||
entry!(KeyDown(Home); modifiers=[Shift], action_dispatch=AnimationMessage::RestartAnimation),
|
||||
];
|
||||
let (mut key_up, mut key_down, mut key_up_no_repeat, mut key_down_no_repeat, mut double_click, mut wheel_scroll, mut pointer_move) = mappings;
|
||||
let (mut key_up, mut key_down, mut key_up_no_repeat, mut key_down_no_repeat, mut double_click, mut wheel_scroll, mut pointer_move, mut pointer_shake) = mappings;
|
||||
|
||||
let sort = |list: &mut KeyMappingEntries| list.0.sort_by(|a, b| b.modifiers.count_ones().cmp(&a.modifiers.count_ones()));
|
||||
// Sort the sublists of `key_up`, `key_down`, `key_up_no_repeat`, and `key_down_no_repeat`
|
||||
|
|
@ -457,6 +458,8 @@ pub fn input_mappings() -> Mapping {
|
|||
sort(&mut wheel_scroll);
|
||||
// Sort `pointer_move`
|
||||
sort(&mut pointer_move);
|
||||
// Sort `pointer_shake`
|
||||
sort(&mut pointer_shake);
|
||||
|
||||
Mapping {
|
||||
key_up,
|
||||
|
|
@ -466,6 +469,7 @@ pub fn input_mappings() -> Mapping {
|
|||
double_click,
|
||||
wheel_scroll,
|
||||
pointer_move,
|
||||
pointer_shake,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -90,6 +90,7 @@ macro_rules! mapping {
|
|||
let mut double_click = KeyMappingEntries::mouse_buttons_arrays();
|
||||
let mut wheel_scroll = KeyMappingEntries::new();
|
||||
let mut pointer_move = KeyMappingEntries::new();
|
||||
let mut pointer_shake = KeyMappingEntries::new();
|
||||
|
||||
$(
|
||||
// Each of the many entry slices, one specified per action
|
||||
|
|
@ -104,6 +105,7 @@ macro_rules! mapping {
|
|||
InputMapperMessage::DoubleClick(key) => &mut double_click[key as usize],
|
||||
InputMapperMessage::WheelScroll => &mut wheel_scroll,
|
||||
InputMapperMessage::PointerMove => &mut pointer_move,
|
||||
InputMapperMessage::PointerShake => &mut pointer_shake,
|
||||
};
|
||||
// Push each entry to the corresponding `KeyMappingEntries` list for its input type
|
||||
corresponding_list.push(entry.clone());
|
||||
|
|
@ -111,7 +113,7 @@ macro_rules! mapping {
|
|||
}
|
||||
)*
|
||||
|
||||
(key_up, key_down, key_up_no_repeat, key_down_no_repeat, double_click, wheel_scroll, pointer_move)
|
||||
(key_up, key_down, key_up_no_repeat, key_down_no_repeat, double_click, wheel_scroll, pointer_move, pointer_shake)
|
||||
}};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ pub struct Mapping {
|
|||
pub double_click: [KeyMappingEntries; NUMBER_OF_MOUSE_BUTTONS],
|
||||
pub wheel_scroll: KeyMappingEntries,
|
||||
pub pointer_move: KeyMappingEntries,
|
||||
pub pointer_shake: KeyMappingEntries,
|
||||
}
|
||||
|
||||
impl Default for Mapping {
|
||||
|
|
@ -47,6 +48,7 @@ impl Mapping {
|
|||
InputMapperMessage::DoubleClick(key) => &self.double_click[*key as usize],
|
||||
InputMapperMessage::WheelScroll => &self.wheel_scroll,
|
||||
InputMapperMessage::PointerMove => &self.pointer_move,
|
||||
InputMapperMessage::PointerShake => &self.pointer_shake,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -59,6 +61,7 @@ impl Mapping {
|
|||
InputMapperMessage::DoubleClick(key) => &mut self.double_click[*key as usize],
|
||||
InputMapperMessage::WheelScroll => &mut self.wheel_scroll,
|
||||
InputMapperMessage::PointerMove => &mut self.pointer_move,
|
||||
InputMapperMessage::PointerShake => &mut self.pointer_shake,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ pub enum InputPreprocessorMessage {
|
|||
PointerDown { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys },
|
||||
PointerMove { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys },
|
||||
PointerUp { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys },
|
||||
PointerShake { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys },
|
||||
CurrentTime { timestamp: u64 },
|
||||
WheelScroll { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys },
|
||||
}
|
||||
|
|
|
|||
|
|
@ -97,6 +97,14 @@ impl MessageHandler<InputPreprocessorMessage, InputPreprocessorMessageContext> f
|
|||
|
||||
self.translate_mouse_event(mouse_state, false, responses);
|
||||
}
|
||||
InputPreprocessorMessage::PointerShake { editor_mouse_state, modifier_keys } => {
|
||||
self.update_states_of_modifier_keys(modifier_keys, keyboard_platform, responses);
|
||||
|
||||
let mouse_state = editor_mouse_state.to_mouse_state(&self.viewport_bounds);
|
||||
self.mouse.position = mouse_state.position;
|
||||
|
||||
responses.add(InputMapperMessage::PointerShake);
|
||||
}
|
||||
InputPreprocessorMessage::CurrentTime { timestamp } => {
|
||||
responses.add(AnimationMessage::SetTime { time: timestamp as f64 });
|
||||
self.time = timestamp;
|
||||
|
|
|
|||
|
|
@ -82,6 +82,9 @@ pub enum NodeGraphMessage {
|
|||
node_id: NodeId,
|
||||
parent: LayerNodeIdentifier,
|
||||
},
|
||||
SetChainPosition {
|
||||
node_id: NodeId,
|
||||
},
|
||||
PasteNodes {
|
||||
serialized_nodes: String,
|
||||
},
|
||||
|
|
@ -98,6 +101,7 @@ pub enum NodeGraphMessage {
|
|||
PointerOutsideViewport {
|
||||
shift: Key,
|
||||
},
|
||||
ShakeNode,
|
||||
RemoveImport {
|
||||
import_index: usize,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ use crate::messages::portfolio::document::node_graph::utility_types::{ContextMen
|
|||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use crate::messages::portfolio::document::utility_types::misc::GroupFolderType;
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::{
|
||||
self, InputConnector, NodeNetworkInterface, NodeTemplate, NodeTypePersistentMetadata, OutputConnector, Previewing, TypeSource,
|
||||
self, FlowType, InputConnector, NodeNetworkInterface, NodeTemplate, NodeTypePersistentMetadata, OutputConnector, Previewing, TypeSource,
|
||||
};
|
||||
use crate::messages::portfolio::document::utility_types::nodes::{CollapsedLayers, LayerPanelEntry};
|
||||
use crate::messages::portfolio::document::utility_types::wires::{GraphWireStyle, WirePath, WirePathUpdate, build_vector_wire};
|
||||
|
|
@ -56,6 +56,8 @@ pub struct NodeGraphMessageHandler {
|
|||
/// If dragging the selected nodes, this stores the starting position both in viewport and node graph coordinates,
|
||||
/// plus a flag indicating if it has been dragged since the mousedown began.
|
||||
pub drag_start: Option<(DragStart, bool)>,
|
||||
// Store the selected chain nodes on drag start so they can be reconnected if shaken
|
||||
pub drag_start_chain_nodes: Vec<NodeId>,
|
||||
/// If dragging the background to create a box selection, this stores its starting point in node graph coordinates,
|
||||
/// plus a flag indicating if it has been dragged since the mousedown began.
|
||||
box_selection_start: Option<(DVec2, bool)>,
|
||||
|
|
@ -601,6 +603,9 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
|||
NodeGraphMessage::MoveNodeToChainStart { node_id, parent } => {
|
||||
network_interface.move_node_to_chain_start(&node_id, parent, selection_network_path);
|
||||
}
|
||||
NodeGraphMessage::SetChainPosition { node_id } => {
|
||||
network_interface.set_chain_position(&node_id, selection_network_path);
|
||||
}
|
||||
NodeGraphMessage::PasteNodes { serialized_nodes } => {
|
||||
let data = match serde_json::from_str::<Vec<(NodeId, NodeTemplate)>>(&serialized_nodes) {
|
||||
Ok(d) => d,
|
||||
|
|
@ -854,6 +859,20 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
|||
};
|
||||
|
||||
self.drag_start = Some((drag_start, false));
|
||||
let selected_chain_nodes = updated_selected
|
||||
.iter()
|
||||
.filter(|node_id| network_interface.is_chain(node_id, selection_network_path))
|
||||
.copied()
|
||||
.collect::<Vec<_>>();
|
||||
self.drag_start_chain_nodes = selected_chain_nodes
|
||||
.iter()
|
||||
.flat_map(|selected| {
|
||||
network_interface
|
||||
.upstream_flow_back_from_nodes(vec![*selected], selection_network_path, FlowType::PrimaryFlow)
|
||||
.skip(1)
|
||||
.filter(|node_id| network_interface.is_chain(node_id, selection_network_path))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
self.begin_dragging = true;
|
||||
self.node_has_moved_in_drag = false;
|
||||
self.update_node_graph_hints(responses);
|
||||
|
|
@ -1221,6 +1240,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
|||
{
|
||||
return None;
|
||||
}
|
||||
log::debug!("preferences.graph_wire_style: {:?}", preferences.graph_wire_style);
|
||||
let (wire, is_stack) = network_interface.vector_wire_from_input(&input, preferences.graph_wire_style, selection_network_path)?;
|
||||
wire.rectangle_intersections_exist(bounding_box[0], bounding_box[1]).then_some((input, is_stack))
|
||||
})
|
||||
|
|
@ -1303,6 +1323,135 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
|||
self.auto_panning.stop(&messages, responses);
|
||||
}
|
||||
}
|
||||
NodeGraphMessage::ShakeNode => {
|
||||
let Some(drag_start) = &self.drag_start else {
|
||||
log::error!("Drag start should be initialized when shaking a node");
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(network_metadata) = network_interface.network_metadata(selection_network_path) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let viewport_location = ipp.mouse.position;
|
||||
let point = network_metadata
|
||||
.persistent_metadata
|
||||
.navigation_metadata
|
||||
.node_graph_to_viewport
|
||||
.inverse()
|
||||
.transform_point2(viewport_location);
|
||||
|
||||
// Collect the distance to move the shaken nodes after the undo
|
||||
let graph_delta = IVec2::new(((point.x - drag_start.0.start_x) / 24.).round() as i32, ((point.y - drag_start.0.start_y) / 24.).round() as i32);
|
||||
|
||||
// Undo to the state of the graph before shaking
|
||||
responses.add(DocumentMessage::AbortTransaction);
|
||||
|
||||
// Add a history step to abort to the state before shaking if right clicked
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
|
||||
let Some(selected_nodes) = network_interface.selected_nodes_in_nested_network(selection_network_path) else {
|
||||
log::error!("Could not get selected nodes in ShakeNode");
|
||||
return;
|
||||
};
|
||||
|
||||
let mut all_selected_nodes = selected_nodes.0.iter().copied().collect::<HashSet<_>>();
|
||||
for selected_layer in selected_nodes
|
||||
.0
|
||||
.iter()
|
||||
.filter(|selected_node| network_interface.is_layer(selected_node, selection_network_path))
|
||||
.copied()
|
||||
.collect::<Vec<_>>()
|
||||
{
|
||||
for sole_dependent in network_interface.upstream_nodes_below_layer(&selected_layer, selection_network_path) {
|
||||
all_selected_nodes.insert(sole_dependent);
|
||||
}
|
||||
}
|
||||
|
||||
for selected_node in &all_selected_nodes {
|
||||
// Handle inputs of selected node
|
||||
for input_index in 0..network_interface.number_of_inputs(selected_node, selection_network_path) {
|
||||
let input_connector = InputConnector::node(*selected_node, input_index);
|
||||
// Only disconnect inputs to non selected nodes
|
||||
if network_interface
|
||||
.upstream_output_connector(&input_connector, selection_network_path)
|
||||
.and_then(|connector| connector.node_id())
|
||||
.is_some_and(|node_id| !all_selected_nodes.contains(&node_id))
|
||||
{
|
||||
responses.add(NodeGraphMessage::DisconnectInput { input_connector });
|
||||
}
|
||||
}
|
||||
|
||||
let number_of_outputs = network_interface.number_of_outputs(selected_node, selection_network_path);
|
||||
let first_deselected_upstream_node = network_interface
|
||||
.upstream_flow_back_from_nodes(vec![*selected_node], selection_network_path, FlowType::PrimaryFlow)
|
||||
.find(|upstream_node| !all_selected_nodes.contains(upstream_node));
|
||||
let Some(outward_wires) = network_interface.outward_wires(selection_network_path) else {
|
||||
log::error!("Could not get output wires in shake input");
|
||||
continue;
|
||||
};
|
||||
|
||||
// Disconnect output wires to non selected nodes
|
||||
for output_index in 0..number_of_outputs {
|
||||
let output_connector = OutputConnector::node(*selected_node, output_index);
|
||||
if let Some(downstream_connections) = outward_wires.get(&output_connector) {
|
||||
for &input_connector in downstream_connections {
|
||||
if input_connector.node_id().is_some_and(|downstream_node| !all_selected_nodes.contains(&downstream_node)) {
|
||||
responses.add(NodeGraphMessage::DisconnectInput { input_connector });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle reconnection
|
||||
// Find first non selected upstream node by primary flow
|
||||
if let Some(first_deselected_upstream_node) = first_deselected_upstream_node {
|
||||
let Some(downstream_connections_to_first_output) = outward_wires.get(&OutputConnector::node(*selected_node, 0)).cloned() else {
|
||||
log::error!("Could not get downstream_connections_to_first_output in shake node");
|
||||
return;
|
||||
};
|
||||
// Reconnect only if all downstream outputs are not selected
|
||||
if !downstream_connections_to_first_output
|
||||
.iter()
|
||||
.any(|connector| connector.node_id().is_some_and(|node_id| all_selected_nodes.contains(&node_id)))
|
||||
{
|
||||
// Find what output on the deselected upstream node to reconnect to
|
||||
for output_index in 0..network_interface.number_of_outputs(&first_deselected_upstream_node, selection_network_path) {
|
||||
let output_connector = &OutputConnector::node(first_deselected_upstream_node, output_index);
|
||||
let Some(outward_wires) = network_interface.outward_wires(selection_network_path) else {
|
||||
log::error!("Could not get output wires in shake input");
|
||||
continue;
|
||||
};
|
||||
if let Some(inputs) = outward_wires.get(output_connector) {
|
||||
// This can only run once
|
||||
if inputs.iter().any(|input_connector| {
|
||||
input_connector
|
||||
.node_id()
|
||||
.is_some_and(|upstream_node| all_selected_nodes.contains(&upstream_node) && input_connector.input_index() == 0)
|
||||
}) {
|
||||
// Output index is the output of the deselected upstream node to reconnect to
|
||||
for downstream_connections_to_first_output in &downstream_connections_to_first_output {
|
||||
responses.add(NodeGraphMessage::CreateWire {
|
||||
output_connector: OutputConnector::node(first_deselected_upstream_node, output_index),
|
||||
input_connector: *downstream_connections_to_first_output,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set all chain nodes back to chain position
|
||||
// TODO: Fix
|
||||
// for chain_node_to_reset in std::mem::take(&mut self.drag_start_chain_nodes) {
|
||||
// responses.add(NodeGraphMessage::SetChainPosition { node_id: chain_node_to_reset });
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
responses.add(NodeGraphMessage::ShiftSelectedNodesByAmount { graph_delta, rubber_band: false });
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
responses.add(NodeGraphMessage::SendGraph);
|
||||
}
|
||||
NodeGraphMessage::RemoveImport { import_index: usize } => {
|
||||
network_interface.remove_import(usize, selection_network_path);
|
||||
responses.add(NodeGraphMessage::SendGraph);
|
||||
|
|
@ -1823,6 +1972,12 @@ impl NodeGraphMessageHandler {
|
|||
));
|
||||
}
|
||||
|
||||
if self.drag_start.is_some() {
|
||||
common.extend(actions!(NodeGraphMessageDiscriminant;
|
||||
ShakeNode,
|
||||
));
|
||||
}
|
||||
|
||||
common
|
||||
}
|
||||
|
||||
|
|
@ -2597,6 +2752,7 @@ impl Default for NodeGraphMessageHandler {
|
|||
node_has_moved_in_drag: false,
|
||||
shift_without_push: false,
|
||||
box_selection_start: None,
|
||||
drag_start_chain_nodes: Vec::new(),
|
||||
selection_before_pointer_down: Vec::new(),
|
||||
disconnecting: None,
|
||||
initial_disconnecting: false,
|
||||
|
|
|
|||
|
|
@ -5103,12 +5103,45 @@ impl NodeNetworkInterface {
|
|||
else {
|
||||
log::error!("Could not set chain position for layer node {node_id}");
|
||||
}
|
||||
// let previous_upstream_node = self.upstream_output_connector(&InputConnector::node(*node_id, 0), network_path).and_then(|output| output.node_id());
|
||||
// let Some(previous_upstream_node_position) = previous_upstream_node.and_then(|upstream| self.position_from_downstream_node(&upstream, network_path)) else {
|
||||
// log::error!("Could not get previous_upstream_node_position");
|
||||
// return;
|
||||
// };
|
||||
self.unload_upstream_node_click_targets(vec![*node_id], network_path);
|
||||
// Reload click target of the layer which encapsulate the chain
|
||||
if let Some(downstream_layer) = self.downstream_layer_for_chain_node(node_id, network_path) {
|
||||
self.unload_node_click_targets(&downstream_layer, network_path);
|
||||
}
|
||||
self.unload_all_nodes_bounding_box(network_path);
|
||||
|
||||
// let Some(new_upstream_node_position) = previous_upstream_node.and_then(|upstream| self.position_from_downstream_node(&upstream, network_path)) else {
|
||||
// log::error!("Could not get new_upstream_node_position");
|
||||
// return;
|
||||
// };
|
||||
// if let Some(previous_upstream_node) = {
|
||||
// let x_delta = new_upstream_node_position.x - previous_upstream_node_position.x;
|
||||
// // Upstream node got shifted to left, so shift all upstream absolute sole dependents
|
||||
// if x_delta != 0 {
|
||||
// let upstream_absolute_nodes = SelectedNodes(
|
||||
// self.upstream_flow_back_from_nodes(vec![previous_upstream_node], network_path, FlowType::UpstreamFlow)
|
||||
// .into_iter()
|
||||
// .filter(|node_id| self.is_absolute(node_id, network_path))
|
||||
// .collect::<Vec<_>>(),
|
||||
// );
|
||||
// let old_selected_nodes = std::mem::replace(self.selected_nodes_mut(network_path).unwrap(), upstream_absolute_nodes);
|
||||
// if x_delta < 0 {
|
||||
// for _ in 0..x_delta.abs() {
|
||||
// self.shift_selected_nodes(Direction::Left, false, network_path);
|
||||
// }
|
||||
// } else {
|
||||
// for _ in 0..x_delta.abs() {
|
||||
// self.shift_selected_nodes(Direction::Right, false, network_path);
|
||||
// }
|
||||
// }
|
||||
// let _ = std::mem::replace(self.selected_nodes_mut(network_path).unwrap(), old_selected_nodes);
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
fn valid_upstream_chain_nodes(&mut self, input_connector: &InputConnector, network_path: &[NodeId]) -> Vec<NodeId> {
|
||||
|
|
@ -5965,31 +5998,6 @@ impl NodeNetworkInterface {
|
|||
self.create_wire(&OutputConnector::node(*node_id, 0), &InputConnector::node(parent.to_node(), 1), network_path);
|
||||
self.set_chain_position(node_id, network_path);
|
||||
} else {
|
||||
// TODO: Implement a more robust horizontal shift system when inserting a node into a chain.
|
||||
// This should be done by breaking the chain and shifting the sole dependents for each node upstream of the insertion.
|
||||
// Before inserting the node, shift the layer right 7 units so that all sole dependents are also shifted
|
||||
// let input_connector = InputConnector::node(parent.to_node(), 0);
|
||||
// let old_upstream = self.upstream_output_connector(&input_connector, network_path);
|
||||
// This also needs to disconnect from the downstream layer
|
||||
// self.disconnect_input(&input_connector, network_path);
|
||||
// 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![parent.to_node()]);
|
||||
|
||||
// for _ in 0..7 {
|
||||
// self.shift_selected_nodes(Direction::Left, false, network_path);
|
||||
// }
|
||||
// // Grip drag it back to the right
|
||||
// for _ in 0..7 {
|
||||
// self.shift_selected_nodes(Direction::Right, true, network_path);
|
||||
// }
|
||||
// let _ = self.selected_nodes_mut(network_path).unwrap().replace_with(old_selected_nodes);
|
||||
// if let Some(old_upstream) = old_upstream {
|
||||
// self.create_wire(&old_upstream, &input_connector, network_path);
|
||||
// }
|
||||
|
||||
// Insert the node in the gap and set the upstream to a chain
|
||||
self.insert_node_between(node_id, &InputConnector::node(parent.to_node(), 1), 0, network_path);
|
||||
self.force_set_upstream_to_chain(node_id, network_path);
|
||||
|
|
|
|||
|
|
@ -36,6 +36,8 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
|
|||
let textToolInteractiveInputElement = undefined as undefined | HTMLDivElement;
|
||||
let canvasFocused = true;
|
||||
let inPointerLock = false;
|
||||
const shakeSamples: { x: number; y: number; time: number }[] = [];
|
||||
let lastShakeTime = 0;
|
||||
|
||||
// Event listeners
|
||||
|
||||
|
|
@ -159,6 +161,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
|
|||
if (!viewportPointerInteractionOngoing && (inFloatingMenu || inGraphOverlay)) return;
|
||||
|
||||
const modifiers = makeKeyboardModifiersBitfield(e);
|
||||
if (detectShake(e)) editor.handle.onMouseShake(e.clientX, e.clientY, e.buttons, modifiers);
|
||||
editor.handle.onMouseMove(e.clientX, e.clientY, e.buttons, modifiers);
|
||||
}
|
||||
|
||||
|
|
@ -331,6 +334,71 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
|
|||
});
|
||||
}
|
||||
|
||||
function detectShake(e: PointerEvent | MouseEvent): boolean {
|
||||
const SENSITIVITY_DIRECTION_CHANGES = 3;
|
||||
const SENSITIVITY_DISTANCE_TO_DISPLACEMENT_RATIO = 0.1;
|
||||
const DETECTION_WINDOW_MS = 500;
|
||||
const DEBOUNCE_MS = 1000;
|
||||
|
||||
// Add the current mouse position and time to our list of samples
|
||||
const now = Date.now();
|
||||
shakeSamples.push({ x: e.clientX, y: e.clientY, time: now });
|
||||
|
||||
// Remove samples that are older than our time window
|
||||
while (shakeSamples.length > 0 && now - shakeSamples[0].time > DETECTION_WINDOW_MS) {
|
||||
shakeSamples.shift();
|
||||
}
|
||||
|
||||
// We can't be shaking if it's too early in terms of samples or debounce time
|
||||
if (shakeSamples.length <= 3 || now - lastShakeTime <= DEBOUNCE_MS) return false;
|
||||
|
||||
// Calculate the total distance traveled
|
||||
let totalDistanceSquared = 0;
|
||||
for (let i = 1; i < shakeSamples.length; i += 1) {
|
||||
const p1 = shakeSamples[i - 1];
|
||||
const p2 = shakeSamples[i];
|
||||
totalDistanceSquared += (p2.x - p1.x) ** 2 + (p2.y - p1.y) ** 2;
|
||||
}
|
||||
|
||||
// Count the number of times the mouse changes direction significantly, and the average position of the mouse
|
||||
let directionChanges = 0;
|
||||
const averagePoint = { x: 0, y: 0 };
|
||||
let averagePointCount = 0;
|
||||
for (let i = 0; i < shakeSamples.length - 2; i += 1) {
|
||||
const p1 = shakeSamples[i];
|
||||
const p2 = shakeSamples[i + 1];
|
||||
const p3 = shakeSamples[i + 2];
|
||||
|
||||
const vector1 = { x: p2.x - p1.x, y: p2.y - p1.y };
|
||||
const vector2 = { x: p3.x - p2.x, y: p3.y - p2.y };
|
||||
|
||||
// Check if the dot product is negative, which indicates the angle between vectors is > 90 degrees
|
||||
if (vector1.x * vector2.x + vector1.y * vector2.y < 0) directionChanges += 1;
|
||||
|
||||
averagePoint.x += p2.x;
|
||||
averagePoint.y += p2.y;
|
||||
averagePointCount += 1;
|
||||
}
|
||||
if (averagePointCount > 0) {
|
||||
averagePoint.x /= averagePointCount;
|
||||
averagePoint.y /= averagePointCount;
|
||||
}
|
||||
|
||||
// Calculate the displacement (the distance between the first and last mouse positions)
|
||||
const lastPoint = shakeSamples[shakeSamples.length - 1];
|
||||
const displacementSquared = (lastPoint.x - averagePoint.x) ** 2 + (lastPoint.y - averagePoint.y) ** 2;
|
||||
|
||||
// A shake is detected if the mouse has traveled a lot but not moved far, and has changed direction enough times
|
||||
if (SENSITIVITY_DISTANCE_TO_DISPLACEMENT_RATIO * totalDistanceSquared >= displacementSquared && directionChanges >= SENSITIVITY_DIRECTION_CHANGES) {
|
||||
lastShakeTime = now;
|
||||
shakeSamples.length = 0;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Frontend message subscriptions
|
||||
|
||||
editor.subscriptions.subscribeJsMessage(TriggerPaste, async () => {
|
||||
|
|
|
|||
|
|
@ -384,6 +384,17 @@ impl EditorHandle {
|
|||
self.dispatch(message);
|
||||
}
|
||||
|
||||
/// Mouse shaken
|
||||
#[wasm_bindgen(js_name = onMouseShake)]
|
||||
pub fn on_mouse_shake(&self, x: f64, y: f64, mouse_keys: u8, modifiers: u8) {
|
||||
let editor_mouse_state = EditorMouseState::from_keys_and_editor_position(mouse_keys, (x, y).into());
|
||||
|
||||
let modifier_keys = ModifierKeys::from_bits(modifiers).expect("Invalid modifier keys");
|
||||
|
||||
let message = InputPreprocessorMessage::PointerShake { editor_mouse_state, modifier_keys };
|
||||
self.dispatch(message);
|
||||
}
|
||||
|
||||
/// Mouse double clicked
|
||||
#[wasm_bindgen(js_name = onDoubleClick)]
|
||||
pub fn on_double_click(&self, x: f64, y: f64, mouse_keys: u8, modifiers: u8) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue