diff --git a/src/LGraph.ts b/src/LGraph.ts index e4418840d..42fed51b9 100644 --- a/src/LGraph.ts +++ b/src/LGraph.ts @@ -1,6 +1,6 @@ import type { Dictionary, IContextMenuValue, ISlotType, MethodNames, Point } from "./interfaces" -import type { ISerialisedGraph } from "./types/serialisation" -import { LGraphEventMode, TitleMode } from "./types/globalEnums" +import type { ISerialisedGraph, Serialisable, SerialisableGraph } from "./types/serialisation" +import { LGraphEventMode } from "./types/globalEnums" import { LiteGraph } from "./litegraph" import { LGraphCanvas } from "./LGraphCanvas" import { LGraphGroup } from "./LGraphGroup" @@ -14,6 +14,13 @@ interface IGraphInput { value?: unknown } +export interface LGraphState { + lastGroupId: number + lastNodeId: number + lastLinkId: number + lastRerouteId: number +} + type ParamsArray, K extends MethodNames> = Parameters[1] extends undefined ? Parameters | Parameters[0] : Parameters /** @@ -23,8 +30,9 @@ type ParamsArray, K extends MethodNames> = 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 { + static serialisedSchemaVersion = 1 as const -export class LGraph { //default supported types static supported_types = ["number", "string", "boolean"] static STATUS_STOPPED = 1 @@ -47,11 +55,9 @@ export class LGraph { links: Map & Record list_of_graphcanvas: LGraphCanvas[] | null status: number - last_node_id: number - last_link_id: number - /** The largest ID created by this graph */ - last_reroute_id: number - lastGroupId: number = -1 + + state: LGraphState + _nodes: LGraphNode[] _nodes_by_id: Record _nodes_in_order: LGraphNode[] @@ -72,6 +78,7 @@ export class LGraph { _last_trigger_time?: number filter?: string _subgraph_node?: LGraphNode + /** Must contain serialisable values, e.g. primitive types */ config: { align_to_grid?: any; links_ontop?: any } vars: Dictionary nodes_executing: boolean[] @@ -80,6 +87,22 @@ export class LGraph { extra: Record inputs: Dictionary outputs: Dictionary + + /** @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 @@ -102,8 +125,8 @@ export class LGraph { onAfterChange?(graph: LGraph, info?: LGraphNode): void onConnectionChange?(node: LGraphNode): void on_change?(graph: LGraph): void - onSerialize?(data: ISerialisedGraph): void - onConfigure?(data: ISerialisedGraph): 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 @@ -113,7 +136,7 @@ export class LGraph { * See {@link LGraph} * @param o data from previous serialization [optional] */ - constructor(o?: ISerialisedGraph) { + constructor(o?: ISerialisedGraph | SerialisableGraph) { if (LiteGraph.debug) console.log("Graph created") /** @see MapProxyHandler */ @@ -141,8 +164,12 @@ export class LGraph { this.stop() this.status = LGraph.STATUS_STOPPED - this.last_node_id = 0 - this.last_link_id = 0 + this.state = { + lastGroupId: 0, + lastNodeId: 0, + lastLinkId: 0, + lastRerouteId: 0 + } this._version = -1 //used to detect changes @@ -158,6 +185,7 @@ export class LGraph { 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() //other scene stuff this._groups = [] @@ -654,13 +682,14 @@ export class LGraph { */ add(node: LGraphNode | LGraphGroup, skip_compute_order?: boolean): LGraphNode | null | undefined { if (!node) return + const { state } = this // LEGACY: This was changed from constructor === LGraphGroup //groups if (node instanceof LGraphGroup) { // Assign group ID - if (node.id == null || node.id === -1) node.id = ++this.lastGroupId - if (node.id > this.lastGroupId) this.lastGroupId = node.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) @@ -677,7 +706,7 @@ export class LGraph { ) node.id = LiteGraph.use_uuids ? LiteGraph.uuidv4() - : ++this.last_node_id + : ++state.lastNodeId } if (this._nodes.length >= LiteGraph.MAX_NUMBER_OF_NODES) { @@ -691,9 +720,9 @@ export class LGraph { } else { if (node.id == null || node.id == -1) { - node.id = ++this.last_node_id - } else if (typeof node.id === "number" && this.last_node_id < node.id) { - this.last_node_id = node.id + node.id = ++state.lastNodeId + } else if (typeof node.id === "number" && state.lastNodeId < node.id) { + state.lastNodeId = node.id } } @@ -1190,67 +1219,106 @@ export class LGraph { //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 nodes = !LiteGraph.use_uuids && option?.sortNodes + const { config, state, groups, nodes, extra } = this.asSerialisable(option) + const links = [...this._links.values()].map(x => x.serialize()) + + 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_info = nodes.map(node => node.serialize()) - //pack link info into a non-verbose format - const links: SerialisedLLinkArray[] = [] - for (const link of this._links.values()) { - links.push(link.serialize()) - } + const nodes = nodeList.map(node => node.serialize()) + const groups = this._groups.map(x => x.serialize()) - const groups_info = [] - for (let i = 0; i < this._groups.length; ++i) { - groups_info.push(this._groups[i].serialize()) - } + const links = [...this._links.values()].map(x => x.asSerialisable()) - const data: ISerialisedGraph = { - last_node_id: this.last_node_id, - last_link_id: this.last_link_id, - nodes: nodes_info, - links: links, - groups: groups_info, - config: this.config, - extra: this.extra, - version: LiteGraph.VERSION + const data: SerialisableGraph = { + version: LGraph.serialisedSchemaVersion, + config, + state, + groups, + nodes, + links, + extra } this.onSerialize?.(data) return data } + /** * Configure a graph from a JSON string * @param {String} str configure a graph from a JSON string * @param {Boolean} returns if there was any error parsing */ - configure(data: ISerialisedGraph, keep_old?: boolean): boolean | undefined { + configure(data: ISerialisedGraph | SerialisableGraph, keep_old?: boolean): boolean | undefined { // TODO: Finish typing configure() if (!data) return - if (!keep_old) this.clear() - const nodesData = data.nodes + 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) + } + } + } else { + // New schema - one version so far, no check required. - // LEGACY: This was changed from constructor === Array - //decode links info (they are very verbose) - if (Array.isArray(data.links)) { - this._links.clear() - for (const link_data of data.links) { - const link = LLink.createFromArray(link_data) - this._links.set(link.id, link) + // 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) + } } } + const nodesData = data.nodes + //copy all stored fields for (const i in data) { //links must be accepted - if (i == "nodes" || i == "groups" || i == "links") + if (i == "nodes" || i == "groups" || i == "links" || i === "state") continue this[i] = data[i] } diff --git a/src/LGraphNode.ts b/src/LGraphNode.ts index f8e8da3dd..cc74b9454 100644 --- a/src/LGraphNode.ts +++ b/src/LGraphNode.ts @@ -1929,8 +1929,8 @@ export class LGraphNode implements Positionable, IPinnable { } // UUID: LinkIds - // const nextId = LiteGraph.use_uuids ? LiteGraph.uuidv4() : ++graph.last_link_id - const nextId = ++graph.last_link_id + // const nextId = LiteGraph.use_uuids ? LiteGraph.uuidv4() : ++graph.state.lastLinkId + const nextId = ++graph.state.lastLinkId //create link class link_info = new LLink( diff --git a/src/LiteGraphGlobal.ts b/src/LiteGraphGlobal.ts index e844d6769..ac523aefc 100644 --- a/src/LiteGraphGlobal.ts +++ b/src/LiteGraphGlobal.ts @@ -21,7 +21,8 @@ export class LiteGraphGlobal { SlotType = SlotType LabelPosition = LabelPosition - VERSION = 0.4 + /** Used in serialised graphs at one point. */ + VERSION = 0.4 as const CANVAS_GRID_SIZE = 10 diff --git a/src/litegraph.ts b/src/litegraph.ts index 5754cb8d7..e4a52fd1f 100644 --- a/src/litegraph.ts +++ b/src/litegraph.ts @@ -24,6 +24,7 @@ export { IWidget } export { LGraphBadge, BadgePosition } export { SlotShape, LabelPosition, SlotDirection, SlotType } export { EaseFunction } from "./types/globalEnums" +export type { SerialisableGraph, SerialisableLLink } from "./types/serialisation" export function clamp(v: number, a: number, b: number): number { return a > v ? a : b < v ? b : v diff --git a/src/types/serialisation.ts b/src/types/serialisation.ts index 397388fea..6b828dbe8 100644 --- a/src/types/serialisation.ts +++ b/src/types/serialisation.ts @@ -1,5 +1,5 @@ import type { ISlotType, Dictionary, INodeFlags, INodeInputSlot, INodeOutputSlot, Point, Rect, Size } from "../interfaces" -import type { LGraph } from "../LGraph" +import type { LGraph, LGraphState } from "../LGraph" import type { IGraphGroupFlags, LGraphGroup } from "../LGraphGroup" import type { LGraphNode, NodeId } from "../LGraphNode" import type { LiteGraph } from "../litegraph" @@ -19,6 +19,17 @@ export interface Serialisable { asSerialisable(): SerialisableObject } +export interface SerialisableGraph { + /** Schema version. @remarks Version bump should add to const union, which is used to narrow type during deserialise. */ + version: 0 | 1 + config: LGraph["config"] + state: LGraphState + groups?: ISerialisedGroup[] + nodes?: ISerialisedNode[] + links?: SerialisableLLink[] + extra?: Record +} + /** Serialised LGraphNode */ export interface ISerialisedNode { title?: string @@ -40,15 +51,17 @@ export interface ISerialisedNode { widgets_values?: TWidgetValue[] } -/** Contains serialised graph elements */ +/** + * Original implementation from static litegraph.d.ts + * Maintained for backwards compat + */ export type ISerialisedGraph< TNode = ReturnType, TLink = ReturnType, TGroup = ReturnType > = { - last_node_id: LGraph["last_node_id"] - last_link_id: LGraph["last_link_id"] - last_reroute_id?: LGraph["last_reroute_id"] + last_node_id: NodeId + last_link_id: number nodes: TNode[] links: TLink[] groups: TGroup[]