mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-02 14:27:40 +00:00
1682 lines
46 KiB
TypeScript
1682 lines
46 KiB
TypeScript
// @ts-strict-ignore
|
|
import type {
|
|
Dictionary,
|
|
IContextMenuValue,
|
|
LinkNetwork,
|
|
ISlotType,
|
|
MethodNames,
|
|
Point,
|
|
LinkSegment,
|
|
Positionable,
|
|
} 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 } from "./LLink"
|
|
import { MapProxyHandler } from "./MapProxyHandler"
|
|
import { isSortaInsideOctagon } from "./measure"
|
|
import { getAllNestedItems } from "./utils/collections"
|
|
|
|
interface IGraphInput {
|
|
name: string
|
|
type: string
|
|
value?: unknown
|
|
}
|
|
|
|
export interface LGraphState {
|
|
lastGroupId: number
|
|
lastNodeId: number
|
|
lastLinkId: number
|
|
lastRerouteId: number
|
|
}
|
|
|
|
type ParamsArray<T extends Record<any, any>, K extends MethodNames<T>> =
|
|
Parameters<T[K]>[1] extends undefined
|
|
? Parameters<T[K]> | Parameters<T[K]>[0]
|
|
: Parameters<T[K]>
|
|
|
|
/** Configuration used by {@link LGraph} `config`. */
|
|
export interface LGraphConfig {
|
|
/** @deprecated Legacy config - unused */
|
|
align_to_grid?: any
|
|
links_ontop?: any
|
|
}
|
|
|
|
/**
|
|
* LGraph is the class that contain a full graph. We instantiate one and add nodes to it, and then we can run the execution loop.
|
|
* supported callbacks:
|
|
* + onNodeAdded: when a new node is added to the graph
|
|
* + onNodeRemoved: when a node inside this graph is removed
|
|
* + onNodeConnectionChange: some connection has changed in the graph (connected or disconnected)
|
|
*/
|
|
export class LGraph implements LinkNetwork, Serialisable<SerialisableGraph> {
|
|
static serialisedSchemaVersion = 1 as const
|
|
|
|
// default supported types
|
|
static supported_types = ["number", "string", "boolean"]
|
|
static STATUS_STOPPED = 1
|
|
static STATUS_RUNNING = 2
|
|
|
|
_version: number
|
|
/** The backing store for links. Keys are wrapped in String() */
|
|
_links: Map<LinkId, LLink> = new Map()
|
|
/**
|
|
* Indexed property access is deprecated.
|
|
* Backwards compatibility with a Proxy has been added, but will eventually be removed.
|
|
*
|
|
* Use {@link Map} methods:
|
|
* ```
|
|
* const linkId = 123
|
|
* const link = graph.links.get(linkId)
|
|
* // Deprecated: const link = graph.links[linkId]
|
|
* ```
|
|
*/
|
|
links: Map<LinkId, LLink> & Record<LinkId, LLink>
|
|
list_of_graphcanvas: LGraphCanvas[] | null
|
|
status: number
|
|
|
|
state: LGraphState
|
|
|
|
_nodes: LGraphNode[]
|
|
_nodes_by_id: Record<NodeId, LGraphNode>
|
|
_nodes_in_order: LGraphNode[]
|
|
_nodes_executable: LGraphNode[] | null
|
|
_groups: LGraphGroup[]
|
|
iteration: number
|
|
globaltime: number
|
|
runningtime: number
|
|
fixedtime: number
|
|
fixedtime_lapse: number
|
|
elapsed_time: number
|
|
last_update_time: number
|
|
starttime: number
|
|
catch_errors: boolean
|
|
execution_timer_id: number | null
|
|
errors_in_execution: boolean
|
|
execution_time: number
|
|
_last_trigger_time?: number
|
|
filter?: string
|
|
/** Must contain serialisable values, e.g. primitive types */
|
|
config: LGraphConfig
|
|
vars: Dictionary<unknown>
|
|
nodes_executing: boolean[]
|
|
nodes_actioning: (string | boolean)[]
|
|
nodes_executedAction: string[]
|
|
extra: Record<any, any>
|
|
inputs: Dictionary<IGraphInput>
|
|
outputs: Dictionary<IGraphInput>
|
|
|
|
/** @returns Whether the graph has no items */
|
|
get empty(): boolean {
|
|
return this._nodes.length + this._groups.length + this.reroutes.size === 0
|
|
}
|
|
|
|
/** @returns All items on the canvas that can be selected */
|
|
*positionableItems(): Generator<LGraphNode | LGraphGroup | Reroute> {
|
|
for (const node of this._nodes) yield node
|
|
for (const group of this._groups) yield group
|
|
for (const reroute of this.reroutes.values()) yield reroute
|
|
return
|
|
}
|
|
|
|
#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
|
|
}
|
|
|
|
set last_node_id(value) {
|
|
this.state.lastNodeId = value
|
|
}
|
|
|
|
/** @deprecated See {@link state}.{@link LGraphState.lastLinkId lastLinkId} */
|
|
get last_link_id() {
|
|
return this.state.lastLinkId
|
|
}
|
|
|
|
set last_link_id(value) {
|
|
this.state.lastLinkId = value
|
|
}
|
|
|
|
onInputsOutputsChange?(): void
|
|
onInputAdded?(name: string, type: string): void
|
|
onAfterStep?(): void
|
|
onBeforeStep?(): void
|
|
onPlayEvent?(): void
|
|
onStopEvent?(): void
|
|
onAfterExecute?(): void
|
|
onExecuteStep?(): void
|
|
onNodeAdded?(node: LGraphNode): void
|
|
onNodeRemoved?(node: LGraphNode): void
|
|
onTrigger?(action: string, param: unknown): void
|
|
onInputRenamed?(old_name: string, name: string): void
|
|
onInputTypeChanged?(name: string, type: string): void
|
|
onInputRemoved?(name: string): void
|
|
onOutputAdded?(name: string, type: string): void
|
|
onOutputRenamed?(old_name: string, name: string): void
|
|
onOutputTypeChanged?(name: string, type: string): void
|
|
onOutputRemoved?(name: string): void
|
|
onBeforeChange?(graph: LGraph, info?: LGraphNode): void
|
|
onAfterChange?(graph: LGraph, info?: LGraphNode): void
|
|
onConnectionChange?(node: LGraphNode): void
|
|
on_change?(graph: LGraph): void
|
|
onSerialize?(data: ISerialisedGraph | SerialisableGraph): void
|
|
onConfigure?(data: ISerialisedGraph | SerialisableGraph): void
|
|
onGetNodeMenuOptions?(options: IContextMenuValue[], node: LGraphNode): void
|
|
onNodeConnectionChange?(
|
|
nodeSlotType: ISlotType,
|
|
targetNode: LGraphNode,
|
|
slotIndex: number,
|
|
sourceNode?: LGraphNode,
|
|
sourceSlotIndex?: number,
|
|
): void
|
|
|
|
private _input_nodes?: LGraphNode[]
|
|
|
|
/**
|
|
* See {@link LGraph}
|
|
* @param o data from previous serialization [optional]
|
|
*/
|
|
constructor(o?: ISerialisedGraph | SerialisableGraph) {
|
|
if (LiteGraph.debug) console.log("Graph created")
|
|
|
|
/** @see MapProxyHandler */
|
|
const links = this._links
|
|
MapProxyHandler.bindAllMethods(links)
|
|
const handler = new MapProxyHandler<LLink>()
|
|
this.links = new Proxy(links, handler) as Map<LinkId, LLink> & Record<LinkId, LLink>
|
|
|
|
this.list_of_graphcanvas = null
|
|
this.clear()
|
|
|
|
if (o) this.configure(o)
|
|
}
|
|
|
|
// TODO: Remove
|
|
// used to know which types of connections support this graph (some graphs do not allow certain types)
|
|
getSupportedTypes(): string[] {
|
|
// @ts-expect-error
|
|
return this.supported_types || LGraph.supported_types
|
|
}
|
|
|
|
/**
|
|
* Removes all nodes from this graph
|
|
*/
|
|
clear(): void {
|
|
this.stop()
|
|
this.status = LGraph.STATUS_STOPPED
|
|
|
|
this.state = {
|
|
lastGroupId: 0,
|
|
lastNodeId: 0,
|
|
lastLinkId: 0,
|
|
lastRerouteId: 0,
|
|
}
|
|
|
|
this._version = -1 // used to detect changes
|
|
|
|
// safe clear
|
|
if (this._nodes) {
|
|
for (let i = 0; i < this._nodes.length; ++i) {
|
|
this._nodes[i].onRemoved?.()
|
|
}
|
|
}
|
|
|
|
// nodes
|
|
this._nodes = []
|
|
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 = []
|
|
|
|
// iterations
|
|
this.iteration = 0
|
|
|
|
// custom data
|
|
this.config = {}
|
|
this.vars = {}
|
|
this.extra = {} // to store custom data
|
|
|
|
// timing
|
|
this.globaltime = 0
|
|
this.runningtime = 0
|
|
this.fixedtime = 0
|
|
this.fixedtime_lapse = 0.01
|
|
this.elapsed_time = 0.01
|
|
this.last_update_time = 0
|
|
this.starttime = 0
|
|
|
|
this.catch_errors = true
|
|
|
|
this.nodes_executing = []
|
|
this.nodes_actioning = []
|
|
this.nodes_executedAction = []
|
|
|
|
this.inputs = {}
|
|
this.outputs = {}
|
|
|
|
// notify canvas to redraw
|
|
this.change()
|
|
|
|
this.canvasAction(c => c.clear())
|
|
}
|
|
|
|
get nodes() {
|
|
return this._nodes
|
|
}
|
|
|
|
get groups() {
|
|
return this._groups
|
|
}
|
|
|
|
/**
|
|
* Attach Canvas to this graph
|
|
*/
|
|
attachCanvas(graphcanvas: LGraphCanvas): void {
|
|
if (graphcanvas.constructor != LGraphCanvas)
|
|
throw "attachCanvas expects a LGraphCanvas instance"
|
|
if (graphcanvas.graph != this)
|
|
graphcanvas.graph?.detachCanvas(graphcanvas)
|
|
|
|
graphcanvas.graph = this
|
|
|
|
this.list_of_graphcanvas ||= []
|
|
this.list_of_graphcanvas.push(graphcanvas)
|
|
}
|
|
|
|
/**
|
|
* Detach Canvas from this graph
|
|
*/
|
|
detachCanvas(graphcanvas: LGraphCanvas): void {
|
|
if (!this.list_of_graphcanvas) return
|
|
|
|
const pos = this.list_of_graphcanvas.indexOf(graphcanvas)
|
|
if (pos == -1) return
|
|
|
|
graphcanvas.graph = null
|
|
this.list_of_graphcanvas.splice(pos, 1)
|
|
}
|
|
|
|
/**
|
|
* Starts running this graph every interval milliseconds.
|
|
* @param interval amount of milliseconds between executions, if 0 then it renders to the monitor refresh rate
|
|
*/
|
|
start(interval?: number): void {
|
|
if (this.status == LGraph.STATUS_RUNNING) return
|
|
this.status = LGraph.STATUS_RUNNING
|
|
|
|
this.onPlayEvent?.()
|
|
this.sendEventToAllNodes("onStart")
|
|
|
|
// launch
|
|
this.starttime = LiteGraph.getTime()
|
|
this.last_update_time = this.starttime
|
|
interval ||= 0
|
|
const that = this
|
|
|
|
// execute once per frame
|
|
if (
|
|
interval == 0 &&
|
|
typeof window != "undefined" &&
|
|
window.requestAnimationFrame
|
|
) {
|
|
function on_frame() {
|
|
if (that.execution_timer_id != -1) return
|
|
|
|
window.requestAnimationFrame(on_frame)
|
|
that.onBeforeStep?.()
|
|
that.runStep(1, !that.catch_errors)
|
|
that.onAfterStep?.()
|
|
}
|
|
this.execution_timer_id = -1
|
|
on_frame()
|
|
} else {
|
|
// execute every 'interval' ms
|
|
// @ts-expect-error
|
|
this.execution_timer_id = setInterval(function () {
|
|
// execute
|
|
that.onBeforeStep?.()
|
|
that.runStep(1, !that.catch_errors)
|
|
that.onAfterStep?.()
|
|
}, interval)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Stops the execution loop of the graph
|
|
*/
|
|
stop(): void {
|
|
if (this.status == LGraph.STATUS_STOPPED) return
|
|
|
|
this.status = LGraph.STATUS_STOPPED
|
|
|
|
this.onStopEvent?.()
|
|
|
|
if (this.execution_timer_id != null) {
|
|
if (this.execution_timer_id != -1) {
|
|
clearInterval(this.execution_timer_id)
|
|
}
|
|
this.execution_timer_id = null
|
|
}
|
|
|
|
this.sendEventToAllNodes("onStop")
|
|
}
|
|
|
|
/**
|
|
* Run N steps (cycles) of the graph
|
|
* @param num number of steps to run, default is 1
|
|
* @param do_not_catch_errors [optional] if you want to try/catch errors
|
|
* @param limit max number of nodes to execute (used to execute from start to a node)
|
|
*/
|
|
runStep(num: number, do_not_catch_errors: boolean, limit?: number): void {
|
|
num = num || 1
|
|
|
|
const start = LiteGraph.getTime()
|
|
this.globaltime = 0.001 * (start - this.starttime)
|
|
|
|
const nodes = this._nodes_executable
|
|
? this._nodes_executable
|
|
: this._nodes
|
|
if (!nodes) return
|
|
|
|
limit = limit || nodes.length
|
|
|
|
if (do_not_catch_errors) {
|
|
// iterations
|
|
for (let i = 0; i < num; i++) {
|
|
for (let j = 0; j < limit; ++j) {
|
|
const node = nodes[j]
|
|
// FIXME: Looks like copy/paste broken logic - checks for "on", executes "do"
|
|
if (node.mode == LGraphEventMode.ALWAYS && node.onExecute) {
|
|
// wrap node.onExecute();
|
|
node.doExecute?.()
|
|
}
|
|
}
|
|
|
|
this.fixedtime += this.fixedtime_lapse
|
|
this.onExecuteStep?.()
|
|
}
|
|
|
|
this.onAfterExecute?.()
|
|
} else {
|
|
try {
|
|
// iterations
|
|
for (let i = 0; i < num; i++) {
|
|
for (let j = 0; j < limit; ++j) {
|
|
const node = nodes[j]
|
|
if (node.mode == LGraphEventMode.ALWAYS) {
|
|
node.onExecute?.()
|
|
}
|
|
}
|
|
|
|
this.fixedtime += this.fixedtime_lapse
|
|
this.onExecuteStep?.()
|
|
}
|
|
|
|
this.onAfterExecute?.()
|
|
this.errors_in_execution = false
|
|
} catch (err) {
|
|
this.errors_in_execution = true
|
|
if (LiteGraph.throw_errors) throw err
|
|
|
|
if (LiteGraph.debug) console.log("Error during execution: " + err)
|
|
this.stop()
|
|
}
|
|
}
|
|
|
|
const now = LiteGraph.getTime()
|
|
let elapsed = now - start
|
|
if (elapsed == 0) elapsed = 1
|
|
|
|
this.execution_time = 0.001 * elapsed
|
|
this.globaltime += 0.001 * elapsed
|
|
this.iteration += 1
|
|
this.elapsed_time = (now - this.last_update_time) * 0.001
|
|
this.last_update_time = now
|
|
this.nodes_executing = []
|
|
this.nodes_actioning = []
|
|
this.nodes_executedAction = []
|
|
}
|
|
|
|
/**
|
|
* Updates the graph execution order according to relevance of the nodes (nodes with only outputs have more relevance than
|
|
* nodes with only inputs.
|
|
*/
|
|
updateExecutionOrder(): void {
|
|
this._nodes_in_order = this.computeExecutionOrder(false)
|
|
this._nodes_executable = []
|
|
for (let i = 0; i < this._nodes_in_order.length; ++i) {
|
|
if (this._nodes_in_order[i].onExecute) {
|
|
this._nodes_executable.push(this._nodes_in_order[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
// This is more internal, it computes the executable nodes in order and returns it
|
|
computeExecutionOrder(
|
|
only_onExecute: boolean,
|
|
set_level?: boolean,
|
|
): LGraphNode[] {
|
|
const L: LGraphNode[] = []
|
|
const S: LGraphNode[] = []
|
|
const M: Dictionary<LGraphNode> = {}
|
|
const visited_links: Record<NodeId, boolean> = {} // to avoid repeating links
|
|
const remaining_links: Record<NodeId, number> = {} // to a
|
|
|
|
// search for the nodes without inputs (starting nodes)
|
|
for (let i = 0, l = this._nodes.length; i < l; ++i) {
|
|
const node = this._nodes[i]
|
|
if (only_onExecute && !node.onExecute) {
|
|
continue
|
|
}
|
|
|
|
M[node.id] = node // add to pending nodes
|
|
|
|
let num = 0 // num of input connections
|
|
if (node.inputs) {
|
|
for (let j = 0, l2 = node.inputs.length; j < l2; j++) {
|
|
if (node.inputs[j]?.link != null) {
|
|
num += 1
|
|
}
|
|
}
|
|
}
|
|
|
|
if (num == 0) {
|
|
// is a starting node
|
|
S.push(node)
|
|
if (set_level) node._level = 1
|
|
} else {
|
|
// num of input links
|
|
if (set_level) node._level = 0
|
|
remaining_links[node.id] = num
|
|
}
|
|
}
|
|
|
|
while (true) {
|
|
// get an starting node
|
|
const node = S.shift()
|
|
if (node === undefined) break
|
|
|
|
L.push(node) // add to ordered list
|
|
delete M[node.id] // remove from the pending nodes
|
|
|
|
if (!node.outputs) continue
|
|
|
|
// for every output
|
|
for (let i = 0; i < node.outputs.length; i++) {
|
|
const output = node.outputs[i]
|
|
// not connected
|
|
// TODO: Confirm functionality, clean condition
|
|
if (output?.links == null || output.links.length == 0)
|
|
continue
|
|
|
|
// for every connection
|
|
for (let j = 0; j < output.links.length; j++) {
|
|
const link_id = output.links[j]
|
|
const link = this._links.get(link_id)
|
|
if (!link) continue
|
|
|
|
// already visited link (ignore it)
|
|
if (visited_links[link.id]) continue
|
|
|
|
const target_node = this.getNodeById(link.target_id)
|
|
if (target_node == null) {
|
|
visited_links[link.id] = true
|
|
continue
|
|
}
|
|
|
|
if (set_level && (!target_node._level || target_node._level <= node._level)) {
|
|
target_node._level = node._level + 1
|
|
}
|
|
|
|
// mark as visited
|
|
visited_links[link.id] = true
|
|
// reduce the number of links remaining
|
|
remaining_links[target_node.id] -= 1
|
|
|
|
// if no more links, then add to starters array
|
|
if (remaining_links[target_node.id] == 0) S.push(target_node)
|
|
}
|
|
}
|
|
}
|
|
|
|
// the remaining ones (loops)
|
|
for (const i in M) {
|
|
L.push(M[i])
|
|
}
|
|
|
|
if (L.length != this._nodes.length && LiteGraph.debug)
|
|
console.warn("something went wrong, nodes missing")
|
|
|
|
/** Ensure type is set */
|
|
type OrderedLGraphNode = LGraphNode & { order: number }
|
|
|
|
/** Sets the order property of each provided node to its index in {@link nodes}. */
|
|
function setOrder(nodes: LGraphNode[]): asserts nodes is OrderedLGraphNode[] {
|
|
const l = nodes.length
|
|
for (let i = 0; i < l; ++i) {
|
|
nodes[i].order = i
|
|
}
|
|
}
|
|
|
|
// save order number in the node
|
|
setOrder(L)
|
|
|
|
// sort now by priority
|
|
L.sort(function (A, B) {
|
|
// @ts-expect-error ctor props
|
|
const Ap = A.constructor.priority || A.priority || 0
|
|
// @ts-expect-error ctor props
|
|
const Bp = B.constructor.priority || B.priority || 0
|
|
// if same priority, sort by order
|
|
|
|
return Ap == Bp
|
|
? A.order - B.order
|
|
: Ap - Bp
|
|
})
|
|
|
|
// save order number in the node, again...
|
|
setOrder(L)
|
|
|
|
return L
|
|
}
|
|
|
|
/**
|
|
* Returns all the nodes that could affect this one (ancestors) by crawling all the inputs recursively.
|
|
* It doesn't include the node itself
|
|
* @returns an array with all the LGraphNodes that affect this node, in order of execution
|
|
*/
|
|
getAncestors(node: LGraphNode): LGraphNode[] {
|
|
const ancestors: LGraphNode[] = []
|
|
const pending = [node]
|
|
const visited: Dictionary<boolean> = {}
|
|
|
|
while (pending.length) {
|
|
const current = pending.shift()
|
|
if (!current?.inputs) continue
|
|
|
|
if (!visited[current.id] && current != node) {
|
|
visited[current.id] = true
|
|
ancestors.push(current)
|
|
}
|
|
|
|
for (let i = 0; i < current.inputs.length; ++i) {
|
|
const input = current.getInputNode(i)
|
|
if (input && ancestors.indexOf(input) == -1) {
|
|
pending.push(input)
|
|
}
|
|
}
|
|
}
|
|
|
|
ancestors.sort(function (a, b) {
|
|
return a.order - b.order
|
|
})
|
|
return ancestors
|
|
}
|
|
|
|
/**
|
|
* Positions every node in a more readable manner
|
|
*/
|
|
arrange(margin?: number, layout?: string): void {
|
|
margin = margin || 100
|
|
|
|
const nodes = this.computeExecutionOrder(false, true)
|
|
const columns: LGraphNode[][] = []
|
|
for (let i = 0; i < nodes.length; ++i) {
|
|
const node = nodes[i]
|
|
const col = node._level || 1
|
|
columns[col] ||= []
|
|
columns[col].push(node)
|
|
}
|
|
|
|
let x = margin
|
|
|
|
for (let i = 0; i < columns.length; ++i) {
|
|
const column = columns[i]
|
|
if (!column) continue
|
|
|
|
let max_size = 100
|
|
let y = margin + LiteGraph.NODE_TITLE_HEIGHT
|
|
for (let j = 0; j < column.length; ++j) {
|
|
const node = column[j]
|
|
node.pos[0] = layout == LiteGraph.VERTICAL_LAYOUT ? y : x
|
|
node.pos[1] = layout == LiteGraph.VERTICAL_LAYOUT ? x : y
|
|
const max_size_index = layout == LiteGraph.VERTICAL_LAYOUT ? 1 : 0
|
|
if (node.size[max_size_index] > max_size) {
|
|
max_size = node.size[max_size_index]
|
|
}
|
|
const node_size_index = layout == LiteGraph.VERTICAL_LAYOUT ? 0 : 1
|
|
y += node.size[node_size_index] + margin + LiteGraph.NODE_TITLE_HEIGHT
|
|
}
|
|
x += max_size + margin
|
|
}
|
|
|
|
this.setDirtyCanvas(true, true)
|
|
}
|
|
|
|
/**
|
|
* Returns the amount of time the graph has been running in milliseconds
|
|
* @returns number of milliseconds the graph has been running
|
|
*/
|
|
getTime(): number {
|
|
return this.globaltime
|
|
}
|
|
|
|
/**
|
|
* Returns the amount of time accumulated using the fixedtime_lapse var.
|
|
* This is used in context where the time increments should be constant
|
|
* @returns number of milliseconds the graph has been running
|
|
*/
|
|
getFixedTime(): number {
|
|
return this.fixedtime
|
|
}
|
|
|
|
/**
|
|
* Returns the amount of time it took to compute the latest iteration.
|
|
* Take into account that this number could be not correct
|
|
* if the nodes are using graphical actions
|
|
* @returns number of milliseconds it took the last cycle
|
|
*/
|
|
getElapsedTime(): number {
|
|
return this.elapsed_time
|
|
}
|
|
|
|
/**
|
|
* Sends an event to all the nodes, useful to trigger stuff
|
|
* @param eventname the name of the event (function to be called)
|
|
* @param params parameters in array format
|
|
*/
|
|
sendEventToAllNodes(
|
|
eventname: string,
|
|
params?: object | object[],
|
|
mode?: LGraphEventMode,
|
|
): void {
|
|
mode = mode || LGraphEventMode.ALWAYS
|
|
|
|
const nodes = this._nodes_in_order ? this._nodes_in_order : this._nodes
|
|
if (!nodes) return
|
|
|
|
for (let j = 0, l = nodes.length; j < l; ++j) {
|
|
const node = nodes[j]
|
|
|
|
if (!node[eventname] || node.mode != mode) continue
|
|
if (params === undefined) {
|
|
node[eventname]()
|
|
} else if (params && params.constructor === Array) {
|
|
node[eventname].apply(node, params)
|
|
} else {
|
|
node[eventname](params)
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Runs an action on every canvas registered to this graph.
|
|
* @param action Action to run for every canvas
|
|
*/
|
|
canvasAction(action: (canvas: LGraphCanvas) => void): void {
|
|
this.list_of_graphcanvas?.forEach(action)
|
|
}
|
|
|
|
/** @deprecated See {@link LGraph.canvasAction} */
|
|
sendActionToCanvas<T extends MethodNames<LGraphCanvas>>(
|
|
action: T,
|
|
params?: ParamsArray<LGraphCanvas, T>,
|
|
): void {
|
|
if (!this.list_of_graphcanvas) return
|
|
|
|
for (let i = 0; i < this.list_of_graphcanvas.length; ++i) {
|
|
const c = this.list_of_graphcanvas[i]
|
|
c[action]?.apply(c, params)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Adds a new node instance to this graph
|
|
* @param node the instance of the node
|
|
*/
|
|
add(
|
|
node: LGraphNode | LGraphGroup,
|
|
skip_compute_order?: boolean,
|
|
): LGraphNode | null | undefined {
|
|
if (!node) return
|
|
const { state } = this
|
|
|
|
// Ensure created items are snapped
|
|
if (LiteGraph.alwaysSnapToGrid) {
|
|
const snapTo = this.getSnapToGridSize()
|
|
if (snapTo) node.snapToGrid(snapTo)
|
|
}
|
|
|
|
// LEGACY: This was changed from constructor === LGraphGroup
|
|
// groups
|
|
if (node instanceof LGraphGroup) {
|
|
// Assign group ID
|
|
if (node.id == null || node.id === -1) node.id = ++state.lastGroupId
|
|
if (node.id > state.lastGroupId) state.lastGroupId = node.id
|
|
|
|
this._groups.push(node)
|
|
this.setDirtyCanvas(true)
|
|
this.change()
|
|
node.graph = this
|
|
this._version++
|
|
return
|
|
}
|
|
|
|
// nodes
|
|
if (node.id != -1 && this._nodes_by_id[node.id] != null) {
|
|
console.warn(
|
|
"LiteGraph: there is already a node with this ID, changing it",
|
|
)
|
|
node.id = LiteGraph.use_uuids
|
|
? LiteGraph.uuidv4()
|
|
: ++state.lastNodeId
|
|
}
|
|
|
|
if (this._nodes.length >= LiteGraph.MAX_NUMBER_OF_NODES) {
|
|
throw "LiteGraph: max number of nodes in a graph reached"
|
|
}
|
|
|
|
// give him an id
|
|
if (LiteGraph.use_uuids) {
|
|
if (node.id == null || node.id == -1)
|
|
node.id = LiteGraph.uuidv4()
|
|
} else {
|
|
if (node.id == null || node.id == -1) {
|
|
node.id = ++state.lastNodeId
|
|
} else if (typeof node.id === "number" && state.lastNodeId < node.id) {
|
|
state.lastNodeId = node.id
|
|
}
|
|
}
|
|
|
|
node.graph = this
|
|
this._version++
|
|
|
|
this._nodes.push(node)
|
|
this._nodes_by_id[node.id] = node
|
|
|
|
node.onAdded?.(this)
|
|
|
|
if (this.config.align_to_grid) node.alignToGrid()
|
|
|
|
if (!skip_compute_order) this.updateExecutionOrder()
|
|
|
|
this.onNodeAdded?.(node)
|
|
|
|
this.setDirtyCanvas(true)
|
|
this.change()
|
|
|
|
return node // to chain actions
|
|
}
|
|
|
|
/**
|
|
* Removes a node from the graph
|
|
* @param node the instance of the node
|
|
*/
|
|
remove(node: LGraphNode | LGraphGroup): void {
|
|
// LEGACY: This was changed from constructor === LiteGraph.LGraphGroup
|
|
if (node instanceof LGraphGroup) {
|
|
const index = this._groups.indexOf(node)
|
|
if (index != -1) {
|
|
this._groups.splice(index, 1)
|
|
}
|
|
node.graph = null
|
|
this._version++
|
|
this.setDirtyCanvas(true, true)
|
|
this.change()
|
|
return
|
|
}
|
|
|
|
// not found
|
|
if (this._nodes_by_id[node.id] == null) return
|
|
// cannot be removed
|
|
if (node.ignore_remove) return
|
|
|
|
this.beforeChange() // sure? - almost sure is wrong
|
|
|
|
// disconnect inputs
|
|
if (node.inputs) {
|
|
for (let i = 0; i < node.inputs.length; i++) {
|
|
const slot = node.inputs[i]
|
|
if (slot.link != null) node.disconnectInput(i)
|
|
}
|
|
}
|
|
|
|
// disconnect outputs
|
|
if (node.outputs) {
|
|
for (let i = 0; i < node.outputs.length; i++) {
|
|
const slot = node.outputs[i]
|
|
if (slot.links?.length) node.disconnectOutput(i)
|
|
}
|
|
}
|
|
|
|
// callback
|
|
node.onRemoved?.()
|
|
|
|
node.graph = null
|
|
this._version++
|
|
|
|
// remove from canvas render
|
|
if (this.list_of_graphcanvas) {
|
|
for (let i = 0; i < this.list_of_graphcanvas.length; ++i) {
|
|
const canvas = this.list_of_graphcanvas[i]
|
|
if (canvas.selected_nodes[node.id])
|
|
delete canvas.selected_nodes[node.id]
|
|
}
|
|
}
|
|
|
|
// remove from containers
|
|
const pos = this._nodes.indexOf(node)
|
|
if (pos != -1) this._nodes.splice(pos, 1)
|
|
|
|
delete this._nodes_by_id[node.id]
|
|
|
|
this.onNodeRemoved?.(node)
|
|
|
|
// close panels
|
|
this.canvasAction(c => c.checkPanels())
|
|
|
|
this.setDirtyCanvas(true, true)
|
|
this.afterChange() // sure? - almost sure is wrong
|
|
this.change()
|
|
|
|
this.updateExecutionOrder()
|
|
}
|
|
|
|
/**
|
|
* Returns a node by its id.
|
|
*/
|
|
getNodeById(id: NodeId): LGraphNode | null {
|
|
return id != null
|
|
? this._nodes_by_id[id]
|
|
: null
|
|
}
|
|
|
|
/**
|
|
* Returns a list of nodes that matches a class
|
|
* @param classObject the class itself (not an string)
|
|
* @returns a list with all the nodes of this type
|
|
*/
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
|
findNodesByClass(classObject: Function, result?: LGraphNode[]): LGraphNode[] {
|
|
result = result || []
|
|
result.length = 0
|
|
for (let i = 0, l = this._nodes.length; i < l; ++i) {
|
|
if (this._nodes[i].constructor === classObject)
|
|
result.push(this._nodes[i])
|
|
}
|
|
return result
|
|
}
|
|
|
|
/**
|
|
* Returns a list of nodes that matches a type
|
|
* @param type the name of the node type
|
|
* @returns a list with all the nodes of this type
|
|
*/
|
|
findNodesByType(type: string, result: LGraphNode[]): LGraphNode[] {
|
|
const matchType = type.toLowerCase()
|
|
result = result || []
|
|
result.length = 0
|
|
for (let i = 0, l = this._nodes.length; i < l; ++i) {
|
|
if (this._nodes[i].type?.toLowerCase() == matchType)
|
|
result.push(this._nodes[i])
|
|
}
|
|
return result
|
|
}
|
|
|
|
/**
|
|
* Returns the first node that matches a name in its title
|
|
* @param title the name of the node to search
|
|
* @returns the node or null
|
|
*/
|
|
findNodeByTitle(title: string): LGraphNode | null {
|
|
for (let i = 0, l = this._nodes.length; i < l; ++i) {
|
|
if (this._nodes[i].title == title)
|
|
return this._nodes[i]
|
|
}
|
|
return null
|
|
}
|
|
|
|
/**
|
|
* Returns a list of nodes that matches a name
|
|
* @param title the name of the node to search
|
|
* @returns a list with all the nodes with this name
|
|
*/
|
|
findNodesByTitle(title: string): LGraphNode[] {
|
|
const result: LGraphNode[] = []
|
|
for (let i = 0, l = this._nodes.length; i < l; ++i) {
|
|
if (this._nodes[i].title == title)
|
|
result.push(this._nodes[i])
|
|
}
|
|
return result
|
|
}
|
|
|
|
/**
|
|
* Returns the top-most node in this position of the canvas
|
|
* @param x the x coordinate in canvas space
|
|
* @param y the y coordinate in canvas space
|
|
* @param nodeList a list with all the nodes to search from, by default is all the nodes in the graph
|
|
* @returns the node at this position or null
|
|
*/
|
|
getNodeOnPos(
|
|
x: number,
|
|
y: number,
|
|
nodeList?: LGraphNode[],
|
|
): LGraphNode | null {
|
|
const nodes = nodeList || this._nodes
|
|
let i = nodes.length
|
|
while (--i >= 0) {
|
|
const node = nodes[i]
|
|
if (node.isPointInside(x, y)) return node
|
|
}
|
|
return null
|
|
}
|
|
|
|
/**
|
|
* Returns the top-most group in that position
|
|
* @param x The x coordinate in canvas space
|
|
* @param y The y coordinate in canvas space
|
|
* @returns The group or null
|
|
*/
|
|
getGroupOnPos(x: number, y: number): LGraphGroup | undefined {
|
|
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
|
|
* @returns 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
|
|
|
|
if (isSortaInsideOctagon(x - pos[0], y - pos[1], 2 * Reroute.radius))
|
|
return reroute
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Snaps the provided items to a grid.
|
|
*
|
|
* Item positions are reounded to the nearest multiple of {@link LiteGraph.CANVAS_GRID_SIZE}.
|
|
*
|
|
* When {@link LiteGraph.alwaysSnapToGrid} is enabled
|
|
* and the grid size is falsy, a default of 1 is used.
|
|
* @param items The items to be snapped to the grid
|
|
* @todo Currently only snaps nodes.
|
|
*/
|
|
snapToGrid(items: Set<Positionable>): void {
|
|
const snapTo = this.getSnapToGridSize()
|
|
if (!snapTo) return
|
|
|
|
getAllNestedItems(items).forEach((item) => {
|
|
if (!item.pinned) item.snapToGrid(snapTo)
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Finds the size of the grid that items should be snapped to when moved.
|
|
* @returns The size of the grid that items should be snapped to
|
|
*/
|
|
getSnapToGridSize(): number {
|
|
// Default to 1 when always snapping
|
|
return LiteGraph.alwaysSnapToGrid
|
|
? LiteGraph.CANVAS_GRID_SIZE || 1
|
|
: LiteGraph.CANVAS_GRID_SIZE
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
checkNodeTypes() {
|
|
for (let i = 0; i < this._nodes.length; i++) {
|
|
const node = this._nodes[i]
|
|
const ctor = LiteGraph.registered_node_types[node.type]
|
|
if (node.constructor == ctor) continue
|
|
|
|
console.log("node being replaced by newer version: " + node.type)
|
|
const newnode = LiteGraph.createNode(node.type)
|
|
this._nodes[i] = newnode
|
|
newnode.configure(node.serialize())
|
|
newnode.graph = this
|
|
this._nodes_by_id[newnode.id] = newnode
|
|
|
|
if (node.inputs) newnode.inputs = node.inputs.concat()
|
|
if (node.outputs) newnode.outputs = node.outputs.concat()
|
|
}
|
|
this.updateExecutionOrder()
|
|
}
|
|
|
|
// ********** GLOBALS *****************
|
|
onAction(
|
|
action: string,
|
|
param: unknown,
|
|
options: { action_call?: string },
|
|
): void {
|
|
this._input_nodes = this.findNodesByClass(
|
|
// @ts-expect-error Never impl.
|
|
LiteGraph.GraphInput,
|
|
this._input_nodes,
|
|
)
|
|
for (let i = 0; i < this._input_nodes.length; ++i) {
|
|
const node = this._input_nodes[i]
|
|
if (node.properties.name != action) continue
|
|
|
|
// wrap node.onAction(action, param);
|
|
node.actionDo(action, param, options)
|
|
break
|
|
}
|
|
}
|
|
|
|
trigger(action: string, param: unknown) {
|
|
this.onTrigger?.(action, param)
|
|
}
|
|
|
|
/**
|
|
* Tell this graph it has a global graph input of this type
|
|
*/
|
|
addInput(name: string, type: string, value?: unknown): void {
|
|
const input = this.inputs[name]
|
|
// already exist
|
|
if (input) return
|
|
|
|
this.beforeChange()
|
|
this.inputs[name] = { name: name, type: type, value: value }
|
|
this._version++
|
|
this.afterChange()
|
|
|
|
this.onInputAdded?.(name, type)
|
|
this.onInputsOutputsChange?.()
|
|
}
|
|
|
|
/**
|
|
* Assign a data to the global graph input
|
|
*/
|
|
setInputData(name: string, data: unknown): void {
|
|
const input = this.inputs[name]
|
|
if (!input) return
|
|
input.value = data
|
|
}
|
|
|
|
/**
|
|
* Returns the current value of a global graph input
|
|
*/
|
|
getInputData(name: string): unknown {
|
|
const input = this.inputs[name]
|
|
return input
|
|
? input.value
|
|
: null
|
|
}
|
|
|
|
/**
|
|
* Changes the name of a global graph input
|
|
*/
|
|
renameInput(old_name: string, name: string): boolean | undefined {
|
|
if (name == old_name) return
|
|
|
|
if (!this.inputs[old_name]) return false
|
|
|
|
if (this.inputs[name]) {
|
|
console.error("there is already one input with that name")
|
|
return false
|
|
}
|
|
|
|
this.inputs[name] = this.inputs[old_name]
|
|
delete this.inputs[old_name]
|
|
this._version++
|
|
|
|
this.onInputRenamed?.(old_name, name)
|
|
this.onInputsOutputsChange?.()
|
|
}
|
|
|
|
/**
|
|
* Changes the type of a global graph input
|
|
*/
|
|
changeInputType(name: string, type: string): boolean | undefined {
|
|
if (!this.inputs[name]) return false
|
|
|
|
if (
|
|
this.inputs[name].type &&
|
|
String(this.inputs[name].type).toLowerCase() == String(type).toLowerCase()
|
|
) {
|
|
return
|
|
}
|
|
|
|
this.inputs[name].type = type
|
|
this._version++
|
|
this.onInputTypeChanged?.(name, type)
|
|
}
|
|
|
|
/**
|
|
* Removes a global graph input
|
|
*/
|
|
removeInput(name: string): boolean {
|
|
if (!this.inputs[name]) return false
|
|
|
|
delete this.inputs[name]
|
|
this._version++
|
|
|
|
this.onInputRemoved?.(name)
|
|
this.onInputsOutputsChange?.()
|
|
return true
|
|
}
|
|
|
|
/**
|
|
* Creates a global graph output
|
|
*/
|
|
addOutput(name: string, type: string, value: unknown): void {
|
|
this.outputs[name] = { name: name, type: type, value: value }
|
|
this._version++
|
|
|
|
this.onOutputAdded?.(name, type)
|
|
|
|
this.onInputsOutputsChange?.()
|
|
}
|
|
|
|
/**
|
|
* Assign a data to the global output
|
|
*/
|
|
setOutputData(name: string, value: unknown): void {
|
|
const output = this.outputs[name]
|
|
if (!output) return
|
|
output.value = value
|
|
}
|
|
|
|
/**
|
|
* Returns the current value of a global graph output
|
|
*/
|
|
getOutputData(name: string): unknown {
|
|
const output = this.outputs[name]
|
|
if (!output) return null
|
|
return output.value
|
|
}
|
|
|
|
/**
|
|
* Renames a global graph output
|
|
*/
|
|
renameOutput(old_name: string, name: string): boolean | undefined {
|
|
if (!this.outputs[old_name]) return false
|
|
|
|
if (this.outputs[name]) {
|
|
console.error("there is already one output with that name")
|
|
return false
|
|
}
|
|
|
|
this.outputs[name] = this.outputs[old_name]
|
|
delete this.outputs[old_name]
|
|
this._version++
|
|
|
|
this.onOutputRenamed?.(old_name, name)
|
|
|
|
this.onInputsOutputsChange?.()
|
|
}
|
|
|
|
/**
|
|
* Changes the type of a global graph output
|
|
*/
|
|
changeOutputType(name: string, type: string): boolean | undefined {
|
|
if (!this.outputs[name]) return false
|
|
|
|
if (
|
|
this.outputs[name].type &&
|
|
String(this.outputs[name].type).toLowerCase() == String(type).toLowerCase()
|
|
) {
|
|
return
|
|
}
|
|
|
|
this.outputs[name].type = type
|
|
this._version++
|
|
this.onOutputTypeChanged?.(name, type)
|
|
}
|
|
|
|
/**
|
|
* Removes a global graph output
|
|
*/
|
|
removeOutput(name: string): boolean {
|
|
if (!this.outputs[name]) return false
|
|
|
|
delete this.outputs[name]
|
|
this._version++
|
|
|
|
this.onOutputRemoved?.(name)
|
|
|
|
this.onInputsOutputsChange?.()
|
|
return true
|
|
}
|
|
|
|
/** @todo Clean up - never implemented. */
|
|
triggerInput(name: string, value: any): void {
|
|
const nodes = this.findNodesByTitle(name)
|
|
for (let i = 0; i < nodes.length; ++i) {
|
|
// @ts-expect-error
|
|
nodes[i].onTrigger(value)
|
|
}
|
|
}
|
|
|
|
/** @todo Clean up - never implemented. */
|
|
setCallback(name: string, func: any): void {
|
|
const nodes = this.findNodesByTitle(name)
|
|
for (let i = 0; i < nodes.length; ++i) {
|
|
// @ts-expect-error
|
|
nodes[i].setTrigger(func)
|
|
}
|
|
}
|
|
|
|
// used for undo, called before any change is made to the graph
|
|
beforeChange(info?: LGraphNode): void {
|
|
this.onBeforeChange?.(this, info)
|
|
this.canvasAction(c => c.onBeforeChange?.(this))
|
|
}
|
|
|
|
// used to resend actions, called after any change is made to the graph
|
|
afterChange(info?: LGraphNode): void {
|
|
this.onAfterChange?.(this, info)
|
|
this.canvasAction(c => c.onAfterChange?.(this))
|
|
}
|
|
|
|
connectionChange(node: LGraphNode): void {
|
|
this.updateExecutionOrder()
|
|
this.onConnectionChange?.(node)
|
|
this._version++
|
|
// TODO: Interface never implemented - any consumers?
|
|
// @ts-expect-error
|
|
this.canvasAction(c => c.onConnectionChange?.())
|
|
}
|
|
|
|
/**
|
|
* clears the triggered slot animation in all links (stop visual animation)
|
|
*/
|
|
clearTriggeredSlots(): void {
|
|
for (const link_info of this._links.values()) {
|
|
if (!link_info) continue
|
|
|
|
if (link_info._last_time) link_info._last_time = 0
|
|
}
|
|
}
|
|
|
|
/* Called when something visually changed (not the graph!) */
|
|
change(): void {
|
|
if (LiteGraph.debug) {
|
|
console.log("Graph changed")
|
|
}
|
|
this.canvasAction(c => c.setDirty(true, true))
|
|
this.on_change?.(this)
|
|
}
|
|
|
|
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 serialisedReroute See {@link SerialisableReroute}
|
|
*/
|
|
setReroute({ id, parentId, pos, linkIds }: SerialisableReroute): Reroute {
|
|
id ??= ++this.state.lastRerouteId
|
|
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 before The existing link segment (reroute, link) that will be after this reroute,
|
|
* going from the node output to input.
|
|
* @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
|
|
*/
|
|
removeLink(link_id: LinkId): void {
|
|
const link = this._links.get(link_id)
|
|
if (!link) return
|
|
|
|
const node = this.getNodeById(link.target_id)
|
|
node?.disconnectInput(link.target_slot)
|
|
|
|
link.disconnect(this)
|
|
}
|
|
|
|
/**
|
|
* Creates a Object containing all the info about this graph, it can be serialized
|
|
* @deprecated Use {@link asSerialisable}, which returns the newer schema version.
|
|
* @returns value of the node
|
|
*/
|
|
serialize(option?: { sortNodes: boolean }): ISerialisedGraph {
|
|
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,
|
|
nodes,
|
|
links,
|
|
groups,
|
|
config,
|
|
extra,
|
|
version: LiteGraph.VERSION,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Prepares a shallow copy of this object for immediate serialisation or structuredCloning.
|
|
* The return value should be discarded immediately.
|
|
* @param options Serialise options = currently `sortNodes: boolean`, whether to sort nodes by ID.
|
|
* @returns A shallow copy of parts of this graph, with shallow copies of its serialisable objects.
|
|
* Mutating the properties of the return object may result in changes to your graph.
|
|
* It is intended for use with {@link structuredClone} or {@link JSON.stringify}.
|
|
*/
|
|
asSerialisable(options?: { sortNodes: boolean }): SerialisableGraph {
|
|
const { config, state, extra } = this
|
|
|
|
const nodeList = !LiteGraph.use_uuids && options?.sortNodes
|
|
// @ts-expect-error If LiteGraph.use_uuids is false, ids are numbers.
|
|
? [...this._nodes].sort((a, b) => a.id - b.id)
|
|
: this._nodes
|
|
|
|
const nodes = nodeList.map(node => node.serialize())
|
|
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,
|
|
config,
|
|
state,
|
|
groups,
|
|
nodes,
|
|
links,
|
|
reroutes,
|
|
extra,
|
|
}
|
|
|
|
this.onSerialize?.(data)
|
|
return data
|
|
}
|
|
|
|
/**
|
|
* Configure a graph from a JSON string
|
|
* @param data The deserialised object to configure this graph from
|
|
* @param keep_old If `true`, the graph will not be cleared prior to
|
|
* adding the configuration.
|
|
*/
|
|
configure(
|
|
data: ISerialisedGraph | SerialisableGraph,
|
|
keep_old?: boolean,
|
|
): boolean | undefined {
|
|
// TODO: Finish typing configure()
|
|
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)) {
|
|
for (const linkData of data.links) {
|
|
const link = LLink.createFromArray(linkData)
|
|
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.
|
|
|
|
// State
|
|
if (data.state) {
|
|
const { state: { lastGroupId, lastLinkId, lastNodeId, lastRerouteId } } = data
|
|
if (lastGroupId != null) this.state.lastGroupId = lastGroupId
|
|
if (lastLinkId != null) this.state.lastLinkId = lastLinkId
|
|
if (lastNodeId != null) this.state.lastNodeId = lastNodeId
|
|
if (lastRerouteId != null) this.state.lastRerouteId = lastRerouteId
|
|
}
|
|
|
|
// Links
|
|
if (Array.isArray(data.links)) {
|
|
for (const linkData of data.links) {
|
|
const link = LLink.create(linkData)
|
|
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" ||
|
|
i === "reroutes"
|
|
)
|
|
continue
|
|
this[i] = data[i]
|
|
}
|
|
|
|
let error = false
|
|
|
|
// create nodes
|
|
this._nodes = []
|
|
if (nodesData) {
|
|
for (let i = 0, l = nodesData.length; i < l; ++i) {
|
|
const n_info = nodesData[i] // stored info
|
|
let node = LiteGraph.createNode(n_info.type, n_info.title)
|
|
if (!node) {
|
|
if (LiteGraph.debug) console.log("Node not found or has errors: " + n_info.type)
|
|
|
|
// in case of error we create a replacement node to avoid losing info
|
|
node = new LGraphNode(undefined)
|
|
node.last_serialization = n_info
|
|
node.has_errors = true
|
|
error = true
|
|
// continue;
|
|
}
|
|
|
|
node.id = n_info.id // id it or it will create a new id
|
|
this.add(node, true) // add before configure, otherwise configure cannot create links
|
|
}
|
|
|
|
// configure nodes afterwards so they can reach each other
|
|
for (let i = 0, l = nodesData.length; i < l; ++i) {
|
|
const n_info = nodesData[i]
|
|
const node = this.getNodeById(n_info.id)
|
|
node?.configure(n_info)
|
|
}
|
|
}
|
|
|
|
// groups
|
|
this._groups.length = 0
|
|
if (data.groups) {
|
|
for (let i = 0; i < data.groups.length; ++i) {
|
|
// TODO: Search/remove these global object refs
|
|
const group = new LiteGraph.LGraphGroup()
|
|
group.configure(data.groups[i])
|
|
this.add(group)
|
|
}
|
|
}
|
|
|
|
this.updateExecutionOrder()
|
|
|
|
this.extra = data.extra || {}
|
|
|
|
this.onConfigure?.(data)
|
|
this._version++
|
|
this.setDirtyCanvas(true, true)
|
|
return error
|
|
}
|
|
|
|
load(url: string | Blob | URL | File, callback: () => void) {
|
|
const that = this
|
|
|
|
// LEGACY: This was changed from constructor === File/Blob
|
|
// from file
|
|
if (url instanceof Blob || url instanceof File) {
|
|
const reader = new FileReader()
|
|
reader.addEventListener("load", function (event) {
|
|
const data = JSON.parse(event.target.result.toString())
|
|
that.configure(data)
|
|
callback?.()
|
|
})
|
|
|
|
reader.readAsText(url)
|
|
return
|
|
}
|
|
|
|
// is a string, then an URL
|
|
const req = new XMLHttpRequest()
|
|
req.open("GET", url, true)
|
|
req.send(null)
|
|
req.onload = function () {
|
|
if (req.status !== 200) {
|
|
console.error("Error loading graph:", req.status, req.response)
|
|
return
|
|
}
|
|
const data = JSON.parse(req.response)
|
|
that.configure(data)
|
|
callback?.()
|
|
}
|
|
req.onerror = function (err) {
|
|
console.error("Error loading graph:", err)
|
|
}
|
|
}
|
|
}
|