Skip to content

Commit

Permalink
Add Reroutes (#301)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
webfiltered authored Nov 12, 2024
1 parent c6d7a44 commit 3d6adf0
Show file tree
Hide file tree
Showing 11 changed files with 1,083 additions and 331 deletions.
177 changes: 169 additions & 8 deletions src/LGraph.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import type { Dictionary, IContextMenuValue, ISlotType, MethodNames, Point } from "./interfaces"
import type { ISerialisedGraph, Serialisable, SerialisableGraph } from "./types/serialisation"
import type { Dictionary, IContextMenuValue, LinkNetwork, ISlotType, MethodNames, Point, LinkSegment } from "./interfaces"
import type { ISerialisedGraph, Serialisable, SerialisableGraph, SerialisableReroute } from "./types/serialisation"
import { Reroute, RerouteId } from "./Reroute"
import { LGraphEventMode } from "./types/globalEnums"
import { LiteGraph } from "./litegraph"
import { LGraphCanvas } from "./LGraphCanvas"
import { LGraphGroup } from "./LGraphGroup"
import { type NodeId, LGraphNode } from "./LGraphNode"
import { type LinkId, LLink, type SerialisedLLinkArray } from "./LLink"
import { type LinkId, LLink } from "./LLink"
import { MapProxyHandler } from "./MapProxyHandler"
import { isSortaInsideOctagon } from "./measure"

interface IGraphInput {
name: string
Expand All @@ -30,7 +32,7 @@ type ParamsArray<T extends Record<any, any>, K extends MethodNames<T>> = Paramet
+ onNodeRemoved: when a node inside this graph is removed
+ onNodeConnectionChange: some connection has changed in the graph (connected or disconnected)
*/
export class LGraph implements Serialisable<SerialisableGraph> {
export class LGraph implements LinkNetwork, Serialisable<SerialisableGraph> {
static serialisedSchemaVersion = 1 as const

//default supported types
Expand Down Expand Up @@ -88,6 +90,28 @@ export class LGraph implements Serialisable<SerialisableGraph> {
inputs: Dictionary<IGraphInput>
outputs: Dictionary<IGraphInput>

#reroutes = new Map<RerouteId, Reroute>()
/** All reroutes in this graph. */
public get reroutes(): Map<RerouteId, Reroute> {
return this.#reroutes
}
public set reroutes(value: Map<RerouteId, Reroute>) {
if (!value) throw new TypeError("Attempted to set LGraph.reroutes to a falsy value.")

const reroutes = this.#reroutes
if (value.size === 0) {
reroutes.clear()
return
}

for (const rerouteId of reroutes.keys()) {
if (!value.has(rerouteId)) reroutes.delete(rerouteId)
}
for (const [id, reroute] of value) {
reroutes.set(id, reroute)
}
}

/** @deprecated See {@link state}.{@link LGraphState.lastNodeId lastNodeId} */
get last_node_id() {
return this.state.lastNodeId
Expand Down Expand Up @@ -185,7 +209,9 @@ export class LGraph implements Serialisable<SerialisableGraph> {
this._nodes_by_id = {}
this._nodes_in_order = [] //nodes sorted in execution order
this._nodes_executable = null //nodes that contain onExecute sorted in execution order

this._links.clear()
this.reroutes.clear()

//other scene stuff
this._groups = []
Expand Down Expand Up @@ -913,6 +939,31 @@ export class LGraph implements Serialisable<SerialisableGraph> {
return this._groups.toReversed().find(g => g.isPointInside(x, y))
}

/**
* Returns the top-most group with a titlebar in the provided position.
* @param x The x coordinate in canvas space
* @param y The y coordinate in canvas space
* @return The group or null
*/
getGroupTitlebarOnPos(x: number, y: number): LGraphGroup | undefined {
return this._groups.toReversed().find(g => g.isPointInTitlebar(x, y))
}

/**
* Finds a reroute a the given graph point
* @param x X co-ordinate in graph space
* @param y Y co-ordinate in graph space
* @returns The first reroute under the given co-ordinates, or undefined
*/
getRerouteOnPos(x: number, y: number): Reroute | undefined {
for (const reroute of this.reroutes.values()) {
const pos = reroute.pos

if (isSortaInsideOctagon(x - pos[0], y - pos[1], 20))
return reroute
}
}

/**
* Checks that the node type matches the node type registered, used when replacing a nodetype by a newer version during execution
* this replaces the ones using the old version with the new version
Expand Down Expand Up @@ -1205,6 +1256,72 @@ export class LGraph implements Serialisable<SerialisableGraph> {
setDirtyCanvas(fg: boolean, bg?: boolean): void {
this.canvasAction(c => c.setDirty(fg, bg))
}

/**
* Configures a reroute on the graph where ID is already known (probably deserialisation).
* Creates the object if it does not exist.
* @param id Reroute ID
* @param pos Position in graph space
* @param linkIds IDs of links that pass through this reroute
*/
setReroute({ id, parentId, pos, linkIds }: SerialisableReroute): Reroute {
if (id > this.state.lastRerouteId) this.state.lastRerouteId = id
const reroute = this.reroutes.get(id) ?? new Reroute(id, this)
reroute.update(parentId, pos, linkIds)
this.reroutes.set(id, reroute)
return reroute
}

/**
* Creates a new reroute and adds it to the graph.
* @param pos Position in graph space
* @param links The links that will use this reroute (e.g. if from an output with multiple outputs, and all will use it)
* @param afterRerouteId If set, this reroute will be shown after the specified ID. Otherwise, the reroute will be added as the last on the link.
* @returns The newly created reroute - typically ignored.
*/
createReroute(pos: Point, before: LinkSegment): Reroute {
const rerouteId = ++this.state.lastRerouteId
const linkIds = before instanceof Reroute
? before.linkIds
: [before.id]
const reroute = new Reroute(rerouteId, this, pos, before.parentId, linkIds)
this.reroutes.set(rerouteId, reroute)
for (const linkId of linkIds) {
const link = this._links.get(linkId)
if (!link) continue
if (link.parentId === before.parentId) link.parentId = rerouteId
LLink.getReroutes(this, link)
?.filter(x => x.parentId === before.parentId)
.forEach(x => x.parentId = rerouteId)
}

return reroute
}

/**
* Removes a reroute from the graph
* @param id ID of reroute to remove
*/
removeReroute(id: RerouteId): void {
const { reroutes } = this
const reroute = reroutes.get(id)
if (!reroute) return

// Extract reroute from the reroute chain
const { parentId, linkIds } = reroute
for (const reroute of reroutes.values()) {
if (reroute.parentId === id) reroute.parentId = parentId
}

for (const linkId of linkIds) {
const link = this._links.get(linkId)
if (link && link.parentId === id) link.parentId = parentId
}

reroutes.delete(id)
this.setDirtyCanvas(false, true)
}

/**
* Destroys a link
* @param {Number} link_id
Expand All @@ -1215,18 +1332,29 @@ export class LGraph implements Serialisable<SerialisableGraph> {

const node = this.getNodeById(link.target_id)
node?.disconnectInput(link.target_slot)

link.disconnect(this)
}
//save and recover app state ***************************************

/**
* Creates a Object containing all the info about this graph, it can be serialized
* @deprecated Use {@link asSerialisable}, which returns the newer schema version.
*
* @return {Object} value of the node
*/
serialize(option?: { sortNodes: boolean }): ISerialisedGraph {
const { config, state, groups, nodes, extra } = this.asSerialisable(option)
const links = [...this._links.values()].map(x => x.serialize())
const { config, state, groups, nodes, reroutes, extra } = this.asSerialisable(option)
const linkArray = [...this._links.values()]
const links = linkArray.map(x => x.serialize())

if (reroutes.length) {
extra.reroutes = reroutes

// Link parent IDs cannot go in 0.4 schema arrays
extra.linkExtensions = linkArray
.filter(x => x.parentId !== undefined)
.map(x => ({ id: x.id, parentId: x.parentId }))
}
return {
last_node_id: state.lastNodeId,
last_link_id: state.lastLinkId,
Expand Down Expand Up @@ -1259,6 +1387,7 @@ export class LGraph implements Serialisable<SerialisableGraph> {
const groups = this._groups.map(x => x.serialize())

const links = [...this._links.values()].map(x => x.asSerialisable())
const reroutes = [...this.reroutes.values()].map(x => x.asSerialisable())

const data: SerialisableGraph = {
version: LGraph.serialisedSchemaVersion,
Expand All @@ -1267,6 +1396,7 @@ export class LGraph implements Serialisable<SerialisableGraph> {
groups,
nodes,
links,
reroutes,
extra
}

Expand All @@ -1284,6 +1414,10 @@ export class LGraph implements Serialisable<SerialisableGraph> {
if (!data) return
if (!keep_old) this.clear()

const { extra } = data
let reroutes: SerialisableReroute[] | undefined

// TODO: Determine whether this should this fall back to 0.4.
if (data.version === 0.4) {
// Deprecated - old schema version, links are arrays
if (Array.isArray(data.links)) {
Expand All @@ -1292,6 +1426,20 @@ export class LGraph implements Serialisable<SerialisableGraph> {
this._links.set(link.id, link)
}
}
//#region `extra` embeds for v0.4

// LLink parentIds
if (Array.isArray(extra?.linkExtensions)) {
for (const linkEx of extra.linkExtensions) {
const link = this._links.get(linkEx.id)
if (link) link.parentId = linkEx.parentId
}
}

// Reroutes
reroutes = extra?.reroutes

//#endregion `extra` embeds for v0.4
} else {
// New schema - one version so far, no check required.

Expand All @@ -1311,14 +1459,27 @@ export class LGraph implements Serialisable<SerialisableGraph> {
this._links.set(link.id, link)
}
}

reroutes = data.reroutes
}

// Reroutes
if (Array.isArray(reroutes)) {
for (const rerouteData of reroutes) {
const reroute = this.setReroute(rerouteData)

// Drop broken links, and ignore reroutes with no valid links
if (!reroute.validateLinks(this._links))
this.reroutes.delete(rerouteData.id)
}
}

const nodesData = data.nodes

//copy all stored fields
for (const i in data) {
//links must be accepted
if (i == "nodes" || i == "groups" || i == "links" || i === "state")
if (i == "nodes" || i == "groups" || i == "links" || i === "state" || i === "reroutes")
continue
this[i] = data[i]
}
Expand Down
Loading

0 comments on commit 3d6adf0

Please sign in to comment.