diff --git a/src/litegraph.d.ts b/src/litegraph.d.ts index 88702086b..7f3c74751 100755 --- a/src/litegraph.d.ts +++ b/src/litegraph.d.ts @@ -154,12 +154,24 @@ export type LinkReleaseContext = { type_filter_out?: string; }; +export type ConnectingLink = { + node: LGraphNode; + slot: number; + input: INodeInputSlot | null; + output: INodeOutputSlot | null; + pos: Vector2; +}; + +export type LinkReleaseContextExtended = { + links: ConnectingLink[]; +}; + export type LiteGraphCanvasEventType = "empty-release" | "empty-double-click"; export type LiteGraphCanvasEvent = CustomEvent<{ subType: string; originalEvent: Event, - linkReleaseContext?: LinkReleaseContext; + linkReleaseContext?: LinkReleaseContextExtended; }>; export const LiteGraph: { diff --git a/src/litegraph.js b/src/litegraph.js index 8059750f0..9360f1ccb 100755 --- a/src/litegraph.js +++ b/src/litegraph.js @@ -112,6 +112,7 @@ shift_click_do_break_link_from: false, // [false!] prefer false if results too easy to break links - implement with ALT or TODO custom keys click_do_break_link_to: false, // [false!]prefer false, way too easy to break links + ctrl_alt_click_do_break_link: true, // [true!] who accidentally ctrl-alt-clicks on an in/output? nobody! that's who! search_hide_on_mouse_leave: true, // [false on mobile] better true if not touch device, TODO add an helper/listener to close if false search_filter_enabled: false, // [true!] enable filtering slots type in the search widget, !requires auto_load_slot_types or manual set registered_slot_[in/out]_types and slot_types_[in/out] @@ -5340,6 +5341,7 @@ LGraphNode.prototype.executeAction = function(action) this.last_mouse_position = [0, 0]; this.visible_area = this.ds.visible_area; this.visible_links = []; + this.connecting_links = null; // Explicitly null-checked this.viewport = options.viewport || null; //to constraint render area to a portion of the canvas @@ -5392,7 +5394,7 @@ LGraphNode.prototype.executeAction = function(action) this.node_dragged = null; this.node_over = null; this.node_capturing_input = null; - this.connecting_node = null; + this.connecting_links = null; this.highlighted_links = {}; this.dragging_canvas = false; @@ -5907,7 +5909,7 @@ LGraphNode.prototype.executeAction = function(action) //left button mouse / single finger if (e.which == 1 && !this.pointer_is_double) { - if (e.ctrlKey) + if (e.ctrlKey && !e.altKey) { this.dragging_rectangle = new Float32Array(4); this.dragging_rectangle[0] = e.canvasX; @@ -5918,7 +5920,7 @@ LGraphNode.prototype.executeAction = function(action) } // clone node ALT dragging - if (LiteGraph.alt_drag_do_clone_nodes && e.altKey && node && this.allow_interaction && !skip_action && !this.read_only) + if (LiteGraph.alt_drag_do_clone_nodes && e.altKey && !e.ctrlKey && node && this.allow_interaction && !skip_action && !this.read_only) { const cloned = node.clone() if (cloned) { @@ -5949,7 +5951,7 @@ LGraphNode.prototype.executeAction = function(action) } //if it wasn't selected? //not dragging mouse to connect two slots - if ( this.allow_interaction && !this.connecting_node && !node.flags.collapsed && !this.live_mode ) { + if ( this.allow_interaction && !this.connecting_links && !node.flags.collapsed && !this.live_mode ) { //Search for corner for resize if ( !skip_action && node.resizable !== false && node.inResizeCorner(e.canvasX, e.canvasY) @@ -5974,16 +5976,51 @@ LGraphNode.prototype.executeAction = function(action) 20 ) ) { - this.connecting_node = node; - this.connecting_output = output; - this.connecting_output.slot_index = i; - this.connecting_pos = node.getConnectionPos( false, i ); - this.connecting_slot = i; + // Drag multiple output links + if (e.shiftKey) { + if (output.links?.length > 0) { + + this.connecting_links = []; + for (const linkId of output.links) { + const link = this.graph.links[linkId]; + const slot = link.target_slot; + const linked_node = this.graph._nodes_by_id[link.target_id]; + const input = linked_node.inputs[slot]; + const pos = linked_node.getConnectionPos(true, slot); + + this.connecting_links.push({ + node: linked_node, + slot: slot, + input: input, + output: null, + pos: pos, + }); + } + + skip_action = true; + break; + } + } + + output.slot_index = i; + this.connecting_links = [ + { + node: node, + slot: i, + input: null, + output: output, + pos: link_pos, + } + ] if (LiteGraph.shift_click_do_break_link_from){ if (e.shiftKey) { node.disconnectOutput(i); } + } else if (LiteGraph.ctrl_alt_click_do_break_link) { + if (e.ctrlKey && e.altKey && !e.shiftKey) { + node.disconnectOutput(i); + } } if (is_double_click) { @@ -6031,15 +6068,9 @@ LGraphNode.prototype.executeAction = function(action) var link_info = this.graph.links[ input.link ]; //before disconnecting - if (LiteGraph.click_do_break_link_to){ + if (LiteGraph.click_do_break_link_to || (LiteGraph.ctrl_alt_click_do_break_link && e.ctrlKey && e.altKey && !e.shiftKey)){ node.disconnectInput(i); - this.dirty_bgcanvas = true; - skip_action = true; - }else{ - // do same action as has not node ? - } - - if ( + } else if ( this.allow_reconnect_links || //this.move_destination_link_without_shift || e.shiftKey @@ -6047,18 +6078,22 @@ LGraphNode.prototype.executeAction = function(action) if (!LiteGraph.click_do_break_link_to){ node.disconnectInput(i); } - this.connecting_node = this.graph._nodes_by_id[ - link_info.origin_id - ]; - this.connecting_slot = - link_info.origin_slot; - this.connecting_output = this.connecting_node.outputs[ - this.connecting_slot - ]; - this.connecting_pos = this.connecting_node.getConnectionPos( false, this.connecting_slot ); + const linked_node = this.graph._nodes_by_id[link_info.origin_id]; + const slot = link_info.origin_slot; + this.connecting_links = [ + { + node: linked_node, + slot: slot, + input: null, + output: linked_node.outputs[slot], + pos: linked_node.getConnectionPos( false, slot ), + } + ] this.dirty_bgcanvas = true; skip_action = true; + }else{ + // do same action as has not node ? } @@ -6068,15 +6103,21 @@ LGraphNode.prototype.executeAction = function(action) if (!skip_action){ // connect from in to out, from to to from - this.connecting_node = node; - this.connecting_input = input; - this.connecting_input.slot_index = i; - this.connecting_pos = node.getConnectionPos( true, i ); - this.connecting_slot = i; + this.connecting_links = [ + { + node: node, + slot: i, + input: input, + output: null, + pos: link_pos, + } + ] this.dirty_bgcanvas = true; skip_action = true; } + + break; } } } @@ -6218,7 +6259,7 @@ LGraphNode.prototype.executeAction = function(action) if (node && this.allow_interaction && !skip_action && !this.read_only){ //not dragging mouse to connect two slots if ( - !this.connecting_node && + !this.connecting_links && !node.flags.collapsed && !this.live_mode ) { @@ -6424,7 +6465,7 @@ LGraphNode.prototype.executeAction = function(action) this.dirty_canvas = true; this.dirty_bgcanvas = true; } else if ((this.allow_interaction || (node && node.flags.allow_interaction)) && !this.read_only) { - if (this.connecting_node) { + if (this.connecting_links) { this.dirty_canvas = true; } @@ -6465,9 +6506,10 @@ LGraphNode.prototype.executeAction = function(action) } //if dragging a link - if (this.connecting_node) { + if (this.connecting_links) { + const firstLink = this.connecting_links[0]; - if (this.connecting_output){ + if (firstLink.output) { var pos = this._highlight_input || [0, 0]; //to store the output of isOverNodeInput @@ -6479,7 +6521,7 @@ LGraphNode.prototype.executeAction = function(action) var slot = this.isOverNodeInput( node, e.canvasX, e.canvasY, pos ); if (slot != -1 && node.inputs[slot]) { var slot_type = node.inputs[slot].type; - if ( LiteGraph.isValidConnection( this.connecting_output.type, slot_type ) ) { + if ( LiteGraph.isValidConnection( firstLink.output.type, slot_type ) ) { this._highlight_input = pos; this._highlight_input_slot = node.inputs[slot]; // XXX CHECK THIS } @@ -6489,7 +6531,7 @@ LGraphNode.prototype.executeAction = function(action) } } - }else if(this.connecting_input){ + }else if(firstLink.input){ var pos = this._highlight_output || [0, 0]; //to store the output of isOverNodeOutput @@ -6501,7 +6543,7 @@ LGraphNode.prototype.executeAction = function(action) var slot = this.isOverNodeOutput( node, e.canvasX, e.canvasY, pos ); if (slot != -1 && node.outputs[slot]) { var slot_type = node.outputs[slot].type; - if ( LiteGraph.isValidConnection( this.connecting_input.type, slot_type ) ) { + if ( LiteGraph.isValidConnection( firstLink.input.type, slot_type ) ) { this._highlight_output = pos; } } else { @@ -6725,34 +6767,33 @@ LGraphNode.prototype.executeAction = function(action) } this.dragging_rectangle = null; - } else if (this.connecting_node) { - //dragging a connection - this.dirty_canvas = true; - this.dirty_bgcanvas = true; - - var connInOrOut = this.connecting_output || this.connecting_input; - var connType = connInOrOut.type; + } else if (this.connecting_links) { //node below mouse if (node) { - - /* no need to condition on event type.. just another type - if ( - connType == LiteGraph.EVENT && - this.isOverNodeBox(node, e.canvasX, e.canvasY) - ) { - - this.connecting_node.connect( - this.connecting_slot, - node, - LiteGraph.EVENT - ); + for (const link of this.connecting_links) { + + //dragging a connection + this.dirty_canvas = true; + this.dirty_bgcanvas = true; - } else {*/ + /* no need to condition on event type.. just another type + if ( + connType == LiteGraph.EVENT && + this.isOverNodeBox(node, e.canvasX, e.canvasY) + ) { + + this.connecting_node.connect( + this.connecting_slot, + node, + LiteGraph.EVENT + ); + + } else {*/ //slot below mouse? connect - if (this.connecting_output){ + if (link.output){ var slot = this.isOverNodeInput( node, @@ -6760,13 +6801,13 @@ LGraphNode.prototype.executeAction = function(action) e.canvasY ); if (slot != -1) { - this.connecting_node.connect(this.connecting_slot, node, slot); + link.node.connect(link.slot, node, slot); } else { //not on top of an input // look for a good slot - this.connecting_node.connectByType(this.connecting_slot,node,connType); + link.node.connectByType(link.slot, node, link.output.type); } - } else if (this.connecting_input) { + } else if (link.input) { var slot = this.isOverNodeOutput( node, e.canvasX, @@ -6774,31 +6815,36 @@ LGraphNode.prototype.executeAction = function(action) ); if (slot != -1) { - node.connect(slot, this.connecting_node, this.connecting_slot); // this is inverted has output-input nature like + node.connect(slot, link.node, link.slot); // this is inverted has output-input nature like } else { //not on top of an input // look for a good slot - this.connecting_node.connectByTypeOutput(this.connecting_slot,node,connType); + link.node.connectByTypeOutput(link.slot, node, link.input.type); } } + } } else { - const linkReleaseContext = this.connecting_output ? { - node_from: this.connecting_node, - slot_from: this.connecting_output, - type_filter_in: this.connecting_output.type + const firstLink = this.connecting_links[0]; + const linkReleaseContext = firstLink.output ? { + node_from: firstLink.node, + slot_from: firstLink.output, + type_filter_in: firstLink.output.type } : { - node_to: this.connecting_node, - slot_from: this.connecting_input, - type_filter_out: this.connecting_input.type + node_to: firstLink.node, + slot_from: firstLink.input, + type_filter_out: firstLink.input.type + }; + // For external event only. + const linkReleaseContextExtended = { + links: this.connecting_links, }; - this.canvas.dispatchEvent(new CustomEvent( "litegraph:canvas", { bubbles: true, detail: { subType: "empty-release", originalEvent: e, - linkReleaseContext, + linkReleaseContextExtended, }, } )); @@ -6809,20 +6855,16 @@ LGraphNode.prototype.executeAction = function(action) this.showSearchBox(e, linkReleaseContext); } }else{ - if(this.connecting_output){ - this.showConnectionMenu({nodeFrom: this.connecting_node, slotFrom: this.connecting_output, e: e}); - }else if(this.connecting_input){ - this.showConnectionMenu({nodeTo: this.connecting_node, slotTo: this.connecting_input, e: e}); + if(firstLink.output){ + this.showConnectionMenu({nodeFrom: firstLink.node, slotFrom: firstLink.output, e: e}); + }else if(firstLink.input){ + this.showConnectionMenu({nodeTo: firstLink.node, slotTo: firstLink.input, e: e}); } } } } - this.connecting_output = null; - this.connecting_input = null; - this.connecting_pos = null; - this.connecting_node = null; - this.connecting_slot = -1; + this.connecting_links = null; } //not dragging connection else if (this.resizing_node) { this.dirty_canvas = true; @@ -7867,127 +7909,129 @@ LGraphNode.prototype.executeAction = function(action) } } - //current connection (the one being dragged by the mouse) - if (this.connecting_pos != null) { - ctx.lineWidth = this.connections_width; - var link_color = null; - - var connInOrOut = this.connecting_output || this.connecting_input; - - var connType = connInOrOut.type; - var connDir = connInOrOut.dir; - if(connDir == null) - { - if (this.connecting_output) - connDir = this.connecting_node.horizontal ? LiteGraph.DOWN : LiteGraph.RIGHT; - else - connDir = this.connecting_node.horizontal ? LiteGraph.UP : LiteGraph.LEFT; - } - var connShape = connInOrOut.shape; - - switch (connType) { - case LiteGraph.EVENT: - link_color = LiteGraph.EVENT_LINK_COLOR; - break; - default: - link_color = LiteGraph.CONNECTING_LINK_COLOR; - } - - //the connection being dragged by the mouse - this.renderLink( - ctx, - this.connecting_pos, - [this.graph_mouse[0], this.graph_mouse[1]], - null, - false, - null, - link_color, - connDir, - LiteGraph.CENTER - ); + if (this.connecting_links) { + //current connection (the one being dragged by the mouse) + for (const link of this.connecting_links) { + ctx.lineWidth = this.connections_width; + var link_color = null; + + var connInOrOut = link.output || link.input; + + var connType = connInOrOut.type; + var connDir = connInOrOut.dir; + if(connDir == null) + { + if (link.output) + connDir = link.node.horizontal ? LiteGraph.DOWN : LiteGraph.RIGHT; + else + connDir = link.node.horizontal ? LiteGraph.UP : LiteGraph.LEFT; + } + var connShape = connInOrOut.shape; + + switch (connType) { + case LiteGraph.EVENT: + link_color = LiteGraph.EVENT_LINK_COLOR; + break; + default: + link_color = LiteGraph.CONNECTING_LINK_COLOR; + } - ctx.beginPath(); - if ( - connType === LiteGraph.EVENT || - connShape === LiteGraph.BOX_SHAPE - ) { - ctx.rect( - this.connecting_pos[0] - 6 + 0.5, - this.connecting_pos[1] - 5 + 0.5, - 14, - 10 - ); - ctx.fill(); - ctx.beginPath(); - ctx.rect( - this.graph_mouse[0] - 6 + 0.5, - this.graph_mouse[1] - 5 + 0.5, - 14, - 10 - ); - } else if (connShape === LiteGraph.ARROW_SHAPE) { - ctx.moveTo(this.connecting_pos[0] + 8, this.connecting_pos[1] + 0.5); - ctx.lineTo(this.connecting_pos[0] - 4, this.connecting_pos[1] + 6 + 0.5); - ctx.lineTo(this.connecting_pos[0] - 4, this.connecting_pos[1] - 6 + 0.5); - ctx.closePath(); - } - else { - ctx.arc( - this.connecting_pos[0], - this.connecting_pos[1], - 4, - 0, - Math.PI * 2 - ); - ctx.fill(); - ctx.beginPath(); - ctx.arc( - this.graph_mouse[0], - this.graph_mouse[1], - 4, - 0, - Math.PI * 2 + //the connection being dragged by the mouse + this.renderLink( + ctx, + link.pos, + [this.graph_mouse[0], this.graph_mouse[1]], + null, + false, + null, + link_color, + connDir, + LiteGraph.CENTER ); - } - ctx.fill(); - ctx.fillStyle = "#ffcc00"; - if (this._highlight_input) { ctx.beginPath(); - var shape = this._highlight_input_slot.shape; - if (shape === LiteGraph.ARROW_SHAPE) { - ctx.moveTo(this._highlight_input[0] + 8, this._highlight_input[1] + 0.5); - ctx.lineTo(this._highlight_input[0] - 4, this._highlight_input[1] + 6 + 0.5); - ctx.lineTo(this._highlight_input[0] - 4, this._highlight_input[1] - 6 + 0.5); + if ( + connType === LiteGraph.EVENT || + connShape === LiteGraph.BOX_SHAPE + ) { + ctx.rect( + link.pos[0] - 6 + 0.5, + link.pos[1] - 5 + 0.5, + 14, + 10 + ); + ctx.fill(); + ctx.beginPath(); + ctx.rect( + this.graph_mouse[0] - 6 + 0.5, + this.graph_mouse[1] - 5 + 0.5, + 14, + 10 + ); + } else if (connShape === LiteGraph.ARROW_SHAPE) { + ctx.moveTo(link.pos[0] + 8, link.pos[1] + 0.5); + ctx.lineTo(link.pos[0] - 4, link.pos[1] + 6 + 0.5); + ctx.lineTo(link.pos[0] - 4, link.pos[1] - 6 + 0.5); ctx.closePath(); - } else { + } + else { ctx.arc( - this._highlight_input[0], - this._highlight_input[1], - 6, + link.pos[0], + link.pos[1], + 4, 0, Math.PI * 2 ); - } - ctx.fill(); - } - if (this._highlight_output) { - ctx.beginPath(); - if (shape === LiteGraph.ARROW_SHAPE) { - ctx.moveTo(this._highlight_output[0] + 8, this._highlight_output[1] + 0.5); - ctx.lineTo(this._highlight_output[0] - 4, this._highlight_output[1] + 6 + 0.5); - ctx.lineTo(this._highlight_output[0] - 4, this._highlight_output[1] - 6 + 0.5); - ctx.closePath(); - } else { + ctx.fill(); + ctx.beginPath(); ctx.arc( - this._highlight_output[0], - this._highlight_output[1], - 6, + this.graph_mouse[0], + this.graph_mouse[1], + 4, 0, Math.PI * 2 ); } ctx.fill(); + + ctx.fillStyle = "#ffcc00"; + if (this._highlight_input) { + ctx.beginPath(); + var shape = this._highlight_input_slot.shape; + if (shape === LiteGraph.ARROW_SHAPE) { + ctx.moveTo(this._highlight_input[0] + 8, this._highlight_input[1] + 0.5); + ctx.lineTo(this._highlight_input[0] - 4, this._highlight_input[1] + 6 + 0.5); + ctx.lineTo(this._highlight_input[0] - 4, this._highlight_input[1] - 6 + 0.5); + ctx.closePath(); + } else { + ctx.arc( + this._highlight_input[0], + this._highlight_input[1], + 6, + 0, + Math.PI * 2 + ); + } + ctx.fill(); + } + if (this._highlight_output) { + ctx.beginPath(); + if (shape === LiteGraph.ARROW_SHAPE) { + ctx.moveTo(this._highlight_output[0] + 8, this._highlight_output[1] + 0.5); + ctx.lineTo(this._highlight_output[0] - 4, this._highlight_output[1] + 6 + 0.5); + ctx.lineTo(this._highlight_output[0] - 4, this._highlight_output[1] - 6 + 0.5); + ctx.closePath(); + } else { + ctx.arc( + this._highlight_output[0], + this._highlight_output[1], + 6, + 0, + Math.PI * 2 + ); + } + ctx.fill(); + } } } @@ -8574,8 +8618,8 @@ LGraphNode.prototype.executeAction = function(action) var render_text = !low_quality; - var out_slot = this.connecting_output; - var in_slot = this.connecting_input; + var out_slot = this.connecting_links ? this.connecting_links[0].output : null; + var in_slot = this.connecting_links ? this.connecting_links[0].input : null; ctx.lineWidth = 1; var max_y = 0; @@ -8593,7 +8637,7 @@ LGraphNode.prototype.executeAction = function(action) ctx.globalAlpha = editor_alpha; //change opacity of incompatible slots when dragging a connection - if ( this.connecting_output && !LiteGraph.isValidConnection( slot.type , out_slot.type) ) { + if ( out_slot && !LiteGraph.isValidConnection( slot.type , out_slot.type) ) { ctx.globalAlpha = 0.4 * editor_alpha; } @@ -8692,7 +8736,7 @@ LGraphNode.prototype.executeAction = function(action) var slot_shape = slot.shape; //change opacity of incompatible slots when dragging a connection - if (this.connecting_input && !LiteGraph.isValidConnection( slot_type , in_slot.type) ) { + if (in_slot && !LiteGraph.isValidConnection( slot_type , in_slot.type) ) { ctx.globalAlpha = 0.4 * editor_alpha; }