Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Reroutes #301

Merged
merged 40 commits into from
Nov 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
cbeff24
Add Reroute
webfiltered Oct 31, 2024
e4760a4
Narrow TS type of schema v0.4 extras
webfiltered Nov 9, 2024
48bd7d0
Add reroutes to schema 0.4
webfiltered Nov 9, 2024
9d52c16
Add Reroute POC to LLink
webfiltered Nov 4, 2024
2afeb18
Add Reroute rendering
webfiltered Nov 4, 2024
b2f8a09
Add Reroute context menu - Delete Reroute
webfiltered Nov 4, 2024
d34b460
Update delete selected - include reroutes & groups
webfiltered Oct 27, 2024
183b7d8
Add Reroute select & move
webfiltered Nov 4, 2024
592abe2
Include reroutes in area-select
webfiltered Nov 4, 2024
c819ca0
Move disconnect link logic to LLink
webfiltered Oct 17, 2024
838a0d3
Add Reroute connect
webfiltered Nov 4, 2024
ba00edf
nit
webfiltered Oct 21, 2024
7c1106c
Add Reroute support - connecting links
webfiltered Nov 4, 2024
e6fb6bf
Add Add Reroute from link menu (menu)
webfiltered Nov 4, 2024
a1b2fba
nit
webfiltered Nov 4, 2024
cbc60b7
Add shift-drag from reroute to add new link
webfiltered Nov 4, 2024
f7e1376
Prevent Reroutes from disappearing
webfiltered Nov 8, 2024
1ba0bf3
Add fourth param to connectInputToOutput
webfiltered Nov 8, 2024
2c181bf
Allow both connecting in/out to be null
webfiltered Oct 25, 2024
6050a8f
Move ConnectingLink start pos to Reroute
webfiltered Nov 4, 2024
fa73c5d
Add link render options
webfiltered Nov 4, 2024
ecd04e5
Refactor renderLink - spline / bezier
webfiltered Oct 18, 2024
7cadc2e
Refactor renderLink - linear, straight
webfiltered Oct 18, 2024
d0c5651
Fix centre points on all link types
webfiltered Oct 18, 2024
6b2ed9f
[Refactor] Generic recursive interface / flat set
webfiltered Nov 4, 2024
e18a1b1
nit
webfiltered Nov 4, 2024
6e7e8a4
Allow Reroutes to be members of groups
webfiltered Nov 4, 2024
140e565
Start links from the closest reroute
webfiltered Nov 4, 2024
d49c2a5
Add Reroutes using alt-click on link paths
webfiltered Oct 27, 2024
705b669
nit - Refactor
webfiltered Nov 4, 2024
8d9c7aa
nit - Refactor
webfiltered Nov 11, 2024
223002c
Fix reroute deselect UX
webfiltered Nov 4, 2024
030e25a
Add Reroute link centre-marker handling
webfiltered Nov 4, 2024
02d20d4
Add optional link arrow markers
webfiltered Nov 4, 2024
9af1b27
Add module export: LinkMarkerShape
webfiltered Nov 11, 2024
2e2965b
Add link arrow direction for all link types
webfiltered Nov 11, 2024
3da941d
Add Reroute auto-swivel with custom curves
webfiltered Nov 4, 2024
4776e4f
Add state switch to disable reroutes
webfiltered Nov 11, 2024
8c6e228
Fix cannot deselect when reroutes disabled
webfiltered Nov 12, 2024
047455f
Include reroutes in select-all
webfiltered Nov 12, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading