Add Reroutes (#301)

* Add Reroute

- Initial Reroute implementation
- LLink and Reroute both implement the new LinkSegment interface
- LinkSegments can have a parentId, which always points to a Reroute

* Narrow TS type of schema v0.4 extras

* Add reroutes to schema 0.4

Use extras.reroutes to store additional data

* Add Reroute POC to LLink

* Add Reroute rendering

* Add Reroute context menu - Delete Reroute

* Update delete selected - include reroutes & groups

* Add Reroute select & move

* Include reroutes in area-select

* Move disconnect link logic to LLink

* Add Reroute connect

* nit

* Add Reroute support - connecting links

* Add Add Reroute from link menu (menu)

* nit

* Add shift-drag from reroute to add new link

* Prevent Reroutes from disappearing

Add keepReroutes option to prevent Reroute GC

* Add fourth param to connectInputToOutput

* Allow both connecting in/out to be null

* Move ConnectingLink start pos to Reroute

* Add link render options

* Refactor renderLink - spline / bezier

* Refactor renderLink - linear, straight

* Fix centre points on all link types

Improves link render time

* [Refactor] Generic recursive interface / flat set

* nit

* Allow Reroutes to be members of groups

* Start links from the closest reroute

For the "shift-click drag link from link" feature

* Add Reroutes using alt-click on link paths

* nit - Refactor

* nit - Refactor

* Fix reroute deselect UX

Temporary workaround

* Add Reroute link centre-marker handling

* Add optional link arrow markers

Add enum for link markers
-> Pointing the way forward ->
Set default centre marker to arrow

* Add module export: LinkMarkerShape

* Add link arrow direction for all link types

* Add Reroute auto-swivel with custom curves

* Add state switch to disable reroutes

Works at root of all canvas interactions, should leave existing reroutes untouched but invisible until e.g. links are edited / changed.

* Fix cannot deselect when reroutes disabled

* Include reroutes in select-all
This commit is contained in:
filtered
2024-11-13 03:18:48 +11:00
committed by GitHub
parent c6d7a446f2
commit 3d6adf0225
11 changed files with 1104 additions and 352 deletions

View File

@@ -5,6 +5,7 @@ import type { ISerialisedNode } from "./types/serialisation"
import type { LGraphCanvas } from "./LGraphCanvas"
import type { CanvasMouseEvent } from "./types/events"
import type { DragAndScale } from "./DragAndScale"
import type { Reroute, RerouteId } from "./Reroute"
import { LGraphEventMode, NodeSlotType, TitleMode, RenderShape } from "./types/globalEnums"
import { BadgePosition, LGraphBadge } from "./LGraphBadge"
import { type LGraphNodeConstructor, LiteGraph } from "./litegraph"
@@ -36,6 +37,8 @@ interface ConnectByTypeOptions {
wildcardToTyped?: boolean
/** Allow our typed slot to connect to wildcard slots on remote node. Default: true */
typedToWildcard?: boolean
/** The {@link Reroute.id} that the connection is being dragged from. */
afterRerouteId?: RerouteId
}
/** Internal type used for type safety when implementing generic checks for inputs & outputs */
@@ -1291,7 +1294,7 @@ export class LGraphNode implements Positionable, IPinnable {
if (this.widgets?.length) {
for (let i = 0, l = this.widgets.length; i < l; ++i) {
const widget = this.widgets[i]
if (widget.hidden || (widget.advanced && !this.showAdvanced)) continue;
if (widget.hidden || (widget.advanced && !this.showAdvanced)) continue
widgets_height += widget.computeSize
? widget.computeSize(size[0])[1] + 4
@@ -1795,7 +1798,7 @@ export class LGraphNode implements Positionable, IPinnable {
*/
connectByType(slot: number | string, target_node: LGraphNode, target_slotType: ISlotType, optsIn?: ConnectByTypeOptions): LLink | null {
const slotIndex = this.findConnectByTypeSlot(true, target_node, target_slotType, optsIn)
if (slotIndex !== null) return this.connect(slot, target_node, slotIndex)
if (slotIndex !== null) return this.connect(slot, target_node, slotIndex, optsIn?.afterRerouteId)
console.debug("[connectByType]: no way to connect type: ", target_slotType, " to node: ", target_node)
return null
@@ -1816,7 +1819,7 @@ export class LGraphNode implements Positionable, IPinnable {
if ("generalTypeInCase" in optsIn) optsIn.typedToWildcard = !!optsIn.generalTypeInCase
}
const slotIndex = this.findConnectByTypeSlot(false, source_node, source_slotType, optsIn)
if (slotIndex !== null) return source_node.connect(slotIndex, this, slot)
if (slotIndex !== null) return source_node.connect(slotIndex, this, slot, optsIn?.afterRerouteId)
console.debug("[connectByType]: no way to connect type: ", source_slotType, " to node: ", source_node)
return null
@@ -1829,7 +1832,7 @@ export class LGraphNode implements Positionable, IPinnable {
* @param {number | string} target_slot the input slot of the target node (could be the number of the slot or the string with the name of the slot, or -1 to connect a trigger)
* @return {Object} the link_info is created, otherwise null
*/
connect(slot: number | string, target_node: LGraphNode, target_slot: ISlotType): LLink | null {
connect(slot: number | string, target_node: LGraphNode, target_slot: ISlotType, afterRerouteId?: RerouteId): LLink | null {
// Allow legacy API support for searching target_slot by string, without mutating the input variables
let targetIndex: number
@@ -1919,7 +1922,7 @@ export class LGraphNode implements Positionable, IPinnable {
//if there is something already plugged there, disconnect
if (target_node.inputs[targetIndex]?.link != null) {
graph.beforeChange()
target_node.disconnectInput(targetIndex)
target_node.disconnectInput(targetIndex, true)
changed = true
}
if (output.links?.length) {
@@ -1942,7 +1945,8 @@ export class LGraphNode implements Positionable, IPinnable {
this.id,
slot,
target_node.id,
targetIndex
targetIndex,
afterRerouteId
)
//add to graph links list
@@ -1953,6 +1957,10 @@ export class LGraphNode implements Positionable, IPinnable {
output.links.push(link_info.id)
//connect in input
target_node.inputs[targetIndex].link = link_info.id
// Reroutes
LLink.getReroutes(graph, link_info)
.forEach(x => x?.linkIds.add(nextId))
graph._version++
//link_info has been created now, so its updated
@@ -2108,9 +2116,10 @@ export class LGraphNode implements Positionable, IPinnable {
/**
* Disconnect one input
* @param slot Input slot index, or the name of the slot
* @param keepReroutes If `true`, reroutes will not be garbage collected.
* @return true if disconnected successfully or already disconnected, otherwise false
*/
disconnectInput(slot: number | string): boolean {
disconnectInput(slot: number | string, keepReroutes?: boolean): boolean {
// Allow search by string
if (typeof slot === "string") {
slot = this.findInputSlot(slot)
@@ -2150,7 +2159,7 @@ export class LGraphNode implements Positionable, IPinnable {
}
}
this.graph._links.delete(link_id)
link_info.disconnect(this.graph, keepReroutes)
if (this.graph) this.graph._version++
this.onConnectionsChange?.(
@@ -2453,8 +2462,7 @@ export class LGraphNode implements Positionable, IPinnable {
const outNode = graph.getNodeById(outLink.target_id)
if (!outNode) return
// TODO: Add 4th param (afterRerouteId: inLink.parentId) when reroutes are merged.
const result = inNode.connect(inLink.origin_slot, outNode, outLink.target_slot)
const result = inNode.connect(inLink.origin_slot, outNode, outLink.target_slot, inLink.parentId)
madeAnyConnections ||= !!result
}
}