mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-21 07:14:11 +00:00
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:
177
src/LGraph.ts
177
src/LGraph.ts
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 = []
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -1215,8 +1332,10 @@ 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.
|
||||
@@ -1224,9 +1343,18 @@ export class LGraph implements Serialisable<SerialisableGraph> {
|
||||
* @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,
|
||||
@@ -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,
|
||||
@@ -1267,6 +1396,7 @@ export class LGraph implements Serialisable<SerialisableGraph> {
|
||||
groups,
|
||||
nodes,
|
||||
links,
|
||||
reroutes,
|
||||
extra
|
||||
}
|
||||
|
||||
@@ -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)) {
|
||||
@@ -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.
|
||||
|
||||
@@ -1311,6 +1459,19 @@ 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
|
||||
@@ -1318,7 +1479,7 @@ export class LGraph implements Serialisable<SerialisableGraph> {
|
||||
//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]
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,7 @@ import type { LGraph } from "./LGraph"
|
||||
import type { ISerialisedGroup } from "./types/serialisation"
|
||||
import { LiteGraph } from "./litegraph"
|
||||
import { LGraphCanvas } from "./LGraphCanvas"
|
||||
import { isInsideRectangle, containsCentre, containsRect, createBounds } from "./measure"
|
||||
import { containsCentre, containsRect, isInsideRectangle, isPointInRectangle, createBounds } from "./measure"
|
||||
import { LGraphNode } from "./LGraphNode"
|
||||
import { RenderShape, TitleMode } from "./types/globalEnums"
|
||||
|
||||
@@ -194,21 +194,26 @@ export class LGraphGroup implements Positionable, IPinnable {
|
||||
}
|
||||
|
||||
recomputeInsideNodes(): void {
|
||||
const { nodes, groups } = this.graph
|
||||
const { nodes, reroutes, groups } = this.graph
|
||||
const children = this._children
|
||||
const node_bounding = new Float32Array(4)
|
||||
this._nodes.length = 0
|
||||
children.clear()
|
||||
|
||||
// move any nodes we partially overlap
|
||||
// Move nodes we overlap the centre point of
|
||||
for (const node of nodes) {
|
||||
node.getBounding(node_bounding)
|
||||
if (containsCentre(this._bounding, node_bounding)) {
|
||||
if (containsCentre(this._bounding, node.boundingRect)) {
|
||||
this._nodes.push(node)
|
||||
children.add(node)
|
||||
}
|
||||
}
|
||||
|
||||
// Move reroutes we overlap the centre point of
|
||||
for (const reroute of reroutes.values()) {
|
||||
if (isPointInRectangle(reroute.pos, this._bounding))
|
||||
children.add(reroute)
|
||||
}
|
||||
|
||||
// Move groups we wholly contain
|
||||
for (const group of groups) {
|
||||
if (containsRect(this._bounding, group._bounding))
|
||||
children.add(group)
|
||||
@@ -283,7 +288,7 @@ export class LGraphGroup implements Positionable, IPinnable {
|
||||
}
|
||||
|
||||
isInResize(x: number, y: number): boolean {
|
||||
const b = this._bounding
|
||||
const b = this.boundingRect
|
||||
const right = b[0] + b[2]
|
||||
const bottom = b[1] + b[3]
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
54
src/LLink.ts
54
src/LLink.ts
@@ -1,15 +1,17 @@
|
||||
import type { CanvasColour, ISlotType } from "./interfaces"
|
||||
import type { CanvasColour, LinkNetwork, ISlotType, LinkSegment } from "./interfaces"
|
||||
import type { NodeId } from "./LGraphNode"
|
||||
import type { Reroute, RerouteId } from "./Reroute"
|
||||
import type { Serialisable, SerialisableLLink } from "./types/serialisation"
|
||||
|
||||
export type LinkId = number
|
||||
|
||||
export type SerialisedLLinkArray = [id: LinkId, origin_id: NodeId, origin_slot: number, target_id: NodeId, target_slot: number, type: ISlotType]
|
||||
export type SerialisedLLinkArray = [id: LinkId, origin_id: NodeId, origin_slot: number, target_id: NodeId, target_slot: number, type: ISlotType]
|
||||
|
||||
//this is the class in charge of storing link information
|
||||
export class LLink implements Serialisable<SerialisableLLink> {
|
||||
export class LLink implements LinkSegment, Serialisable<SerialisableLLink> {
|
||||
/** Link ID */
|
||||
id: LinkId
|
||||
parentId?: RerouteId
|
||||
type: ISlotType
|
||||
/** Output node ID */
|
||||
origin_id: NodeId
|
||||
@@ -19,6 +21,7 @@ export class LLink implements Serialisable<SerialisableLLink> {
|
||||
target_id: NodeId
|
||||
/** Input slot index */
|
||||
target_slot: number
|
||||
|
||||
data?: number | string | boolean | { toToolTip?(): string }
|
||||
_data?: unknown
|
||||
/** Centre point of the link, calculated during render only - can be inaccurate */
|
||||
@@ -27,6 +30,8 @@ export class LLink implements Serialisable<SerialisableLLink> {
|
||||
_last_time?: number
|
||||
/** The last canvas 2D path that was used to render this link */
|
||||
path?: Path2D
|
||||
/** @inheritdoc */
|
||||
_centreAngle?: number
|
||||
|
||||
#color?: CanvasColour
|
||||
/** Custom colour for this link only */
|
||||
@@ -35,13 +40,14 @@ export class LLink implements Serialisable<SerialisableLLink> {
|
||||
this.#color = value === "" ? null : value
|
||||
}
|
||||
|
||||
constructor(id: LinkId, type: ISlotType, origin_id: NodeId, origin_slot: number, target_id: NodeId, target_slot: number) {
|
||||
constructor(id: LinkId, type: ISlotType, origin_id: NodeId, origin_slot: number, target_id: NodeId, target_slot: number, parentId?: RerouteId) {
|
||||
this.id = id
|
||||
this.type = type
|
||||
this.origin_id = origin_id
|
||||
this.origin_slot = origin_slot
|
||||
this.target_id = target_id
|
||||
this.target_slot = target_slot
|
||||
this.parentId = parentId
|
||||
|
||||
this._data = null
|
||||
this._pos = new Float32Array(2) //center
|
||||
@@ -58,7 +64,28 @@ export class LLink implements Serialisable<SerialisableLLink> {
|
||||
* @returns A new LLink
|
||||
*/
|
||||
static create(data: SerialisableLLink): LLink {
|
||||
return new LLink(data.id, data.type, data.origin_id, data.origin_slot, data.target_id, data.target_slot)
|
||||
return new LLink(data.id, data.type, data.origin_id, data.origin_slot, data.target_id, data.target_slot, data.parentId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all reroutes from the output slot to this segment. If this segment is a reroute, it will be the last element.
|
||||
* @returns An ordered array of all reroutes from the node output to this reroute or the reroute before it. Otherwise, an empty array.
|
||||
*/
|
||||
static getReroutes(network: LinkNetwork, linkSegment: LinkSegment): Reroute[] {
|
||||
return network.reroutes.get(linkSegment.parentId)
|
||||
?.getReroutes() ?? []
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the reroute in the chain after the provided reroute ID.
|
||||
* @param network The network this link belongs to
|
||||
* @param linkSegment The starting point of the search (input side). Typically the LLink object itself, but can be any link segment.
|
||||
* @param rerouteId The matching reroute will have this set as its {@link parentId}.
|
||||
* @returns The reroute that was found, `undefined` if no reroute was found, or `null` if an infinite loop was detected.
|
||||
*/
|
||||
static findNextReroute(network: LinkNetwork, linkSegment: LinkSegment, rerouteId: RerouteId): Reroute | null | undefined {
|
||||
return network.reroutes.get(linkSegment.parentId)
|
||||
?.findNextReroute(rerouteId)
|
||||
}
|
||||
|
||||
configure(o: LLink | SerialisedLLinkArray) {
|
||||
@@ -76,9 +103,25 @@ export class LLink implements Serialisable<SerialisableLLink> {
|
||||
this.origin_slot = o.origin_slot
|
||||
this.target_id = o.target_id
|
||||
this.target_slot = o.target_slot
|
||||
this.parentId = o.parentId
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnects a link and removes it from the graph, cleaning up any reroutes that are no longer used
|
||||
* @param network The container (LGraph) where reroutes should be updated
|
||||
* @param keepReroutes If `true`, reroutes will not be garbage collected.
|
||||
*/
|
||||
disconnect(network: LinkNetwork, keepReroutes?: boolean): void {
|
||||
const reroutes = LLink.getReroutes(network, this)
|
||||
|
||||
for (const reroute of reroutes) {
|
||||
reroute.linkIds.delete(this.id)
|
||||
if (!keepReroutes && !reroute.linkIds.size) network.reroutes.delete(reroute.id)
|
||||
}
|
||||
network.links.delete(this.id)
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Prefer {@link LLink.asSerialisable} (returns an object, not an array)
|
||||
* @returns An array representing this LLink
|
||||
@@ -103,6 +146,7 @@ export class LLink implements Serialisable<SerialisableLLink> {
|
||||
target_slot: this.target_slot,
|
||||
type: this.type
|
||||
}
|
||||
if (this.parentId) copy.parentId = this.parentId
|
||||
return copy
|
||||
}
|
||||
}
|
||||
|
||||
279
src/Reroute.ts
Normal file
279
src/Reroute.ts
Normal file
@@ -0,0 +1,279 @@
|
||||
import type { CanvasColour, LinkSegment, LinkNetwork, Point, Positionable, ReadOnlyRect } from "./interfaces"
|
||||
import { LLink, type LinkId } from "./LLink"
|
||||
import type { SerialisableReroute, Serialisable } from "./types/serialisation"
|
||||
import { distance } from "./measure"
|
||||
import type { NodeId } from "./LGraphNode"
|
||||
|
||||
export type RerouteId = number
|
||||
|
||||
/**
|
||||
* Represents an additional point on the graph that a link path will travel through. Used for visual organisation only.
|
||||
*
|
||||
* Requires no disposal or clean up.
|
||||
* Stores only primitive values (IDs) to reference other items in its network, and a `WeakRef` to a {@link LinkNetwork} to resolve them.
|
||||
*/
|
||||
export class Reroute implements Positionable, LinkSegment, Serialisable<SerialisableReroute> {
|
||||
static radius: number = 10
|
||||
|
||||
#malloc = new Float32Array(8)
|
||||
|
||||
/** The network this reroute belongs to. Contains all valid links and reroutes. */
|
||||
#network: WeakRef<LinkNetwork>
|
||||
|
||||
#parentId?: RerouteId
|
||||
/** @inheritdoc */
|
||||
public get parentId(): RerouteId {
|
||||
return this.#parentId
|
||||
}
|
||||
/** Ignores attempts to create an infinite loop. @inheritdoc */
|
||||
public set parentId(value: RerouteId) {
|
||||
if (value === this.id) return
|
||||
if (this.getReroutes() === null) return
|
||||
this.#parentId = value
|
||||
}
|
||||
|
||||
#pos = this.#malloc.subarray(0, 2)
|
||||
/** @inheritdoc */
|
||||
get pos(): Point {
|
||||
return this.#pos
|
||||
}
|
||||
set pos(value: Point) {
|
||||
if (!(value?.length >= 2)) throw new TypeError("Reroute.pos is an x,y point, and expects an indexable with at least two values.")
|
||||
this.#pos[0] = value[0]
|
||||
this.#pos[1] = value[1]
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
get boundingRect(): ReadOnlyRect {
|
||||
const { radius } = Reroute
|
||||
const [x, y] = this.#pos
|
||||
return [x - radius, y - radius, 2 * radius, 2 * radius]
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
selected?: boolean
|
||||
|
||||
/** The ID ({@link LLink.id}) of every link using this reroute */
|
||||
linkIds: Set<LinkId>
|
||||
|
||||
/** The averaged angle of every link through this reroute. */
|
||||
otherAngle: number = 0
|
||||
|
||||
/** Cached cos */
|
||||
cos: number = 0
|
||||
sin: number = 0
|
||||
|
||||
/** Bezier curve control point for the "target" (input) side of the link */
|
||||
controlPoint: Point = this.#malloc.subarray(4, 6)
|
||||
|
||||
/** @inheritdoc */
|
||||
path?: Path2D
|
||||
/** @inheritdoc */
|
||||
_centreAngle?: number
|
||||
/** @inheritdoc */
|
||||
_pos: Float32Array = this.#malloc.subarray(6, 8)
|
||||
|
||||
/**
|
||||
* Used to ensure reroute angles are only executed once per frame.
|
||||
* @todo Calculate on change instead.
|
||||
*/
|
||||
#lastRenderTime: number = -Infinity
|
||||
#buffer: Point = this.#malloc.subarray(2, 4)
|
||||
|
||||
/** @inheritdoc */
|
||||
get origin_id(): NodeId | undefined {
|
||||
// if (!this.linkIds.size) return this.#network.deref()?.reroutes.get(this.parentId)
|
||||
return this.#network.deref()
|
||||
?.links.get(this.linkIds.values().next().value)
|
||||
?.origin_id
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
get origin_slot(): number | undefined {
|
||||
return this.#network.deref()
|
||||
?.links.get(this.linkIds.values().next().value)
|
||||
?.origin_slot
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialises a new link reroute object.
|
||||
* @param id Unique identifier for this reroute
|
||||
* @param network The network of links this reroute belongs to. Internally converted to a WeakRef.
|
||||
* @param pos Position in graph coordinates
|
||||
* @param linkIds Link IDs ({@link LLink.id}) of all links that use this reroute
|
||||
*/
|
||||
constructor(
|
||||
public readonly id: RerouteId,
|
||||
network: LinkNetwork,
|
||||
pos?: Point,
|
||||
parentId?: RerouteId,
|
||||
linkIds?: Iterable<LinkId>
|
||||
) {
|
||||
this.#network = new WeakRef(network)
|
||||
this.update(parentId, pos, linkIds)
|
||||
this.linkIds ??= new Set()
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a new parentId to the reroute, and optinoally a new position and linkId.
|
||||
* Primarily used for deserialisation.
|
||||
* @param parentId The ID of the reroute prior to this reroute, or `undefined` if it is the first reroute connected to a nodes output
|
||||
* @param pos The position of this reroute
|
||||
* @param linkIds All link IDs that pass through this reroute
|
||||
*/
|
||||
update(parentId: RerouteId | undefined, pos?: Point, linkIds?: Iterable<LinkId>): void {
|
||||
this.parentId = parentId
|
||||
if (pos) this.pos = pos
|
||||
if (linkIds) this.linkIds = new Set(linkIds)
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the linkIds this reroute has. Removes broken links.
|
||||
* @param links Collection of valid links
|
||||
* @returns true if any links remain after validation
|
||||
*/
|
||||
validateLinks(links: Map<LinkId, LLink>): boolean {
|
||||
const { linkIds } = this
|
||||
for (const linkId of linkIds) {
|
||||
if (!links.get(linkId)) linkIds.delete(linkId)
|
||||
}
|
||||
return linkIds.size > 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves an ordered array of all reroutes from the node output.
|
||||
* @param visited Internal. A set of reroutes that this function has already visited whilst recursing up the chain.
|
||||
* @returns An ordered array of all reroutes from the node output to this reroute, inclusive.
|
||||
* `null` if an infinite loop is detected.
|
||||
* `undefined` if the reroute chain or {@link LinkNetwork} are invalid.
|
||||
*/
|
||||
getReroutes(visited = new Set<Reroute>()): Reroute[] | null | undefined {
|
||||
// No parentId - last in the chain
|
||||
if (this.#parentId === undefined) return [this]
|
||||
// Invalid chain - looped
|
||||
if (visited.has(this)) return null
|
||||
visited.add(this)
|
||||
|
||||
const parent = this.#network.deref()?.reroutes.get(this.#parentId)
|
||||
// Invalid parent (or network) - drop silently to recover
|
||||
if (!parent) {
|
||||
this.#parentId = undefined
|
||||
return [this]
|
||||
}
|
||||
|
||||
const reroutes = parent.getReroutes(visited)
|
||||
reroutes?.push(this)
|
||||
return reroutes
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal. Called by {@link LLink.findNextReroute}. Not intended for use by itself.
|
||||
* @param withParentId The rerouteId to look for
|
||||
* @param visited A set of reroutes that have already been visited
|
||||
* @returns The reroute that was found, `undefined` if no reroute was found, or `null` if an infinite loop was detected.
|
||||
*/
|
||||
findNextReroute(withParentId: RerouteId, visited = new Set<Reroute>()): Reroute | null | undefined {
|
||||
if (this.#parentId === withParentId) return this
|
||||
if (visited.has(this)) return null
|
||||
visited.add(this)
|
||||
|
||||
return this.#network.deref()
|
||||
?.reroutes.get(this.#parentId)
|
||||
?.findNextReroute(withParentId, visited)
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
move(deltaX: number, deltaY: number) {
|
||||
this.#pos[0] += deltaX
|
||||
this.#pos[1] += deltaY
|
||||
}
|
||||
|
||||
calculateAngle(lastRenderTime: number, network: LinkNetwork, linkStart: Point): void {
|
||||
// Ensure we run once per render
|
||||
if (!(lastRenderTime > this.#lastRenderTime)) return
|
||||
this.#lastRenderTime = lastRenderTime
|
||||
|
||||
const { links } = network
|
||||
const { linkIds, id } = this
|
||||
const angles: number[] = []
|
||||
let sum = 0
|
||||
for (const linkId of linkIds) {
|
||||
const link = links.get(linkId)
|
||||
// Remove the linkId or just ignore?
|
||||
if (!link) continue
|
||||
|
||||
const pos = LLink.findNextReroute(network, link, id)?.pos ??
|
||||
network.getNodeById(link.target_id)
|
||||
?.getConnectionPos(true, link.target_slot, this.#buffer)
|
||||
if (!pos) continue
|
||||
|
||||
// TODO: Store points/angles, check if changed, skip calcs.
|
||||
const angle = Math.atan2(pos[1] - this.#pos[1], pos[0] - this.#pos[0])
|
||||
angles.push(angle)
|
||||
sum += angle
|
||||
}
|
||||
if (!angles.length) return
|
||||
|
||||
sum /= angles.length
|
||||
|
||||
const originToReroute = Math.atan2(this.#pos[1] - linkStart[1], this.#pos[0] - linkStart[0])
|
||||
let diff = (originToReroute - sum) * 0.5
|
||||
if (Math.abs(diff) > Math.PI * 0.5) diff += Math.PI
|
||||
const dist = Math.min(80, distance(linkStart, this.#pos) * 0.25)
|
||||
|
||||
// Store results
|
||||
const originDiff = originToReroute - diff
|
||||
const cos = Math.cos(originDiff)
|
||||
const sin = Math.sin(originDiff)
|
||||
|
||||
this.otherAngle = originDiff
|
||||
this.cos = cos
|
||||
this.sin = sin
|
||||
this.controlPoint[0] = dist * -cos
|
||||
this.controlPoint[1] = dist * -sin
|
||||
return
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the reroute on the canvas.
|
||||
* @param ctx Canvas context to draw on
|
||||
* @param colour Reroute colour (typically link colour)
|
||||
*
|
||||
* @remarks Leaves {@link ctx}.fillStyle, strokeStyle, and lineWidth dirty (perf.).
|
||||
*/
|
||||
draw(ctx: CanvasRenderingContext2D, colour: CanvasColour): void {
|
||||
const { pos } = this
|
||||
ctx.fillStyle = colour
|
||||
ctx.beginPath()
|
||||
ctx.arc(pos[0], pos[1], Reroute.radius, 0, 2 * Math.PI)
|
||||
ctx.fill()
|
||||
|
||||
ctx.lineWidth = 1
|
||||
ctx.strokeStyle = "rgb(0,0,0,0.5)"
|
||||
ctx.stroke()
|
||||
|
||||
ctx.fillStyle = "#ffffff55"
|
||||
ctx.strokeStyle = "rgb(0,0,0,0.3)"
|
||||
ctx.beginPath()
|
||||
ctx.arc(pos[0], pos[1], 8, 0, 2 * Math.PI)
|
||||
ctx.fill()
|
||||
ctx.stroke()
|
||||
|
||||
if (this.selected) {
|
||||
ctx.strokeStyle = "#fff"
|
||||
ctx.beginPath()
|
||||
ctx.arc(pos[0], pos[1], 12, 0, 2 * Math.PI)
|
||||
ctx.stroke()
|
||||
}
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
asSerialisable(): SerialisableReroute {
|
||||
return {
|
||||
id: this.id,
|
||||
parentId: this.parentId,
|
||||
pos: [this.pos[0], this.pos[1]],
|
||||
linkIds: [...this.linkIds]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { ContextMenu } from "./ContextMenu"
|
||||
import type { LGraphNode, NodeId } from "./LGraphNode"
|
||||
import type { LinkDirection, RenderShape } from "./types/globalEnums"
|
||||
import type { LinkId } from "./LLink"
|
||||
import type { LinkId, LLink } from "./LLink"
|
||||
import type { Reroute, RerouteId } from "./Reroute"
|
||||
|
||||
export type Dictionary<T> = { [key: string]: T }
|
||||
|
||||
@@ -12,13 +13,19 @@ export type NullableProperties<T> = {
|
||||
|
||||
export type CanvasColour = string | CanvasGradient | CanvasPattern
|
||||
|
||||
/** An object containing a set of child objects */
|
||||
export interface Parent<TChild> {
|
||||
/** All objects owned by the parent object. */
|
||||
readonly children?: ReadonlySet<TChild>
|
||||
}
|
||||
|
||||
/**
|
||||
* An object that can be positioned, selected, and moved.
|
||||
*
|
||||
* May contain other {@link Positionable} objects.
|
||||
*/
|
||||
export interface Positionable {
|
||||
id: NodeId | number
|
||||
export interface Positionable extends Parent<Positionable> {
|
||||
id: NodeId | RerouteId | number
|
||||
/** Position in graph coordinates. Default: 0,0 */
|
||||
pos: Point
|
||||
/** true if this object is part of the selection, otherwise false. */
|
||||
@@ -27,8 +34,6 @@ export interface Positionable {
|
||||
/** See {@link IPinnable.pinned} */
|
||||
readonly pinned?: boolean
|
||||
|
||||
readonly children?: ReadonlySet<Positionable>
|
||||
|
||||
/**
|
||||
* Adds a delta to the current position.
|
||||
* @param deltaX X value to add to current position
|
||||
@@ -60,6 +65,35 @@ export interface IPinnable {
|
||||
unpin(): void
|
||||
}
|
||||
|
||||
/**
|
||||
* Contains a list of links, reroutes, and nodes.
|
||||
*/
|
||||
export interface LinkNetwork {
|
||||
links: Map<LinkId, LLink>
|
||||
reroutes: Map<RerouteId, Reroute>
|
||||
getNodeById(id: NodeId): LGraphNode | null
|
||||
}
|
||||
|
||||
/** Contains a cached 2D canvas path and a centre point, with an optional forward angle. */
|
||||
export interface LinkSegment {
|
||||
/** Link / reroute ID */
|
||||
readonly id: LinkId | RerouteId
|
||||
/** The {@link id} of the reroute that this segment starts from (output side), otherwise `undefined`. */
|
||||
readonly parentId?: RerouteId
|
||||
|
||||
/** The last canvas 2D path that was used to render this segment */
|
||||
path?: Path2D
|
||||
/** Centre point of the {@link path}. Calculated during render only - can be inaccurate */
|
||||
readonly _pos: Float32Array
|
||||
/** Y-forward along the {@link path} from its centre point, in radians. `undefined` if using circles for link centres. Calculated during render only - can be inaccurate. */
|
||||
_centreAngle?: number
|
||||
|
||||
/** Output node ID */
|
||||
readonly origin_id: NodeId
|
||||
/** Output slot index */
|
||||
readonly origin_slot: number
|
||||
}
|
||||
|
||||
export interface IInputOrOutput {
|
||||
// If an input, this will be defined
|
||||
input?: INodeInputSlot
|
||||
@@ -167,6 +201,7 @@ export interface ConnectingLink extends IInputOrOutput {
|
||||
slot: number
|
||||
pos: Point
|
||||
direction?: LinkDirection
|
||||
afterRerouteId?: RerouteId
|
||||
}
|
||||
|
||||
interface IContextMenuBase {
|
||||
|
||||
@@ -23,7 +23,7 @@ export { INodeSlot, INodeInputSlot, INodeOutputSlot, ConnectingLink, CanvasColou
|
||||
export { IWidget }
|
||||
export { LGraphBadge, BadgePosition }
|
||||
export { SlotShape, LabelPosition, SlotDirection, SlotType }
|
||||
export { EaseFunction } from "./types/globalEnums"
|
||||
export { EaseFunction, LinkMarkerShape } from "./types/globalEnums"
|
||||
export type { SerialisableGraph, SerialisableLLink } from "./types/serialisation"
|
||||
export { createBounds } from "./measure"
|
||||
|
||||
|
||||
@@ -37,6 +37,16 @@ export enum LinkRenderType {
|
||||
SPLINE_LINK = 2,
|
||||
}
|
||||
|
||||
/** The marker in the middle of a link */
|
||||
export enum LinkMarkerShape {
|
||||
/** Do not display markers */
|
||||
None = 0,
|
||||
/** Circles (default) */
|
||||
Circle = 1,
|
||||
/** Directional arrows */
|
||||
Arrow = 2,
|
||||
}
|
||||
|
||||
export enum TitleMode {
|
||||
NORMAL_TITLE = 0,
|
||||
NO_TITLE = 1,
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import type { ISlotType, Dictionary, INodeFlags, INodeInputSlot, INodeOutputSlot, Point, Rect, Size } from "../interfaces"
|
||||
import type { ISlotType, Dictionary, INodeFlags, INodeInputSlot, INodeOutputSlot, Point, Size } from "../interfaces"
|
||||
import type { LGraph, LGraphState } from "../LGraph"
|
||||
import type { IGraphGroupFlags, LGraphGroup } from "../LGraphGroup"
|
||||
import type { LGraphNode, NodeId } from "../LGraphNode"
|
||||
import type { LiteGraph } from "../litegraph"
|
||||
import type { LinkId, LLink } from "../LLink"
|
||||
import type { RerouteId } from "../Reroute"
|
||||
import type { TWidgetValue } from "../types/widgets"
|
||||
import { RenderShape } from "./globalEnums"
|
||||
import type { RenderShape } from "./globalEnums"
|
||||
|
||||
/**
|
||||
* An object that implements custom pre-serialization logic via {@link Serialisable.asSerialisable}.
|
||||
@@ -27,6 +28,7 @@ export interface SerialisableGraph {
|
||||
groups?: ISerialisedGroup[]
|
||||
nodes?: ISerialisedNode[]
|
||||
links?: SerialisableLLink[]
|
||||
reroutes?: SerialisableReroute[]
|
||||
extra?: Record<any, any>
|
||||
}
|
||||
|
||||
@@ -67,7 +69,7 @@ export type ISerialisedGraph<
|
||||
groups: TGroup[]
|
||||
config: LGraph["config"]
|
||||
version: typeof LiteGraph.VERSION
|
||||
extra?: unknown
|
||||
extra?: Record<any, any>
|
||||
}
|
||||
|
||||
/** Serialised LGraphGroup */
|
||||
@@ -87,6 +89,14 @@ export interface IClipboardContents {
|
||||
nodes?: ISerialisedNode[]
|
||||
links?: TClipboardLink[]
|
||||
}
|
||||
|
||||
export interface SerialisableReroute {
|
||||
id: RerouteId
|
||||
parentId?: RerouteId
|
||||
pos: Point
|
||||
linkIds: LinkId[]
|
||||
}
|
||||
|
||||
export interface SerialisableLLink {
|
||||
/** Link ID */
|
||||
id: LinkId
|
||||
@@ -100,4 +110,6 @@ export interface SerialisableLLink {
|
||||
target_slot: number
|
||||
/** Data type of the link */
|
||||
type: ISlotType
|
||||
/** ID of the last reroute (from input to output) that this link passes through, otherwise `undefined` */
|
||||
parentId?: RerouteId
|
||||
}
|
||||
|
||||
18
src/utils/collections.ts
Normal file
18
src/utils/collections.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { Parent } from "../interfaces"
|
||||
|
||||
/**
|
||||
* Creates a flat set of all items by recursively iterating through all child items.
|
||||
* @param items The original set of items to iterate through
|
||||
* @returns All items in the original set, and recursively, their children
|
||||
*/
|
||||
export function getAllNestedItems<TParent extends Parent<TParent>>(items: ReadonlySet<TParent>): Set<TParent> {
|
||||
const allItems = new Set<TParent>()
|
||||
items?.forEach(x => addRecursively(x, allItems))
|
||||
return allItems
|
||||
|
||||
function addRecursively(item: TParent, flatSet: Set<TParent>): void {
|
||||
if (flatSet.has(item)) return
|
||||
flatSet.add(item)
|
||||
item.children?.forEach(x => addRecursively(x, flatSet))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user