Snap everything to grid (#315)

* Implement snap to grid

- Moves positioning logic to LGraph
- Simplifies code
- Adds Pointer API to alt-clone node
- Removes always_round_positions, replaced by always snap to grid (default size is 1 when always snapping)

Fix refator error

* Fix group items snapped without group

* Allow snapping of all items

- Add snapToGrid to Positionable
- Impl. on all types
- Deprecated: alignToGrid is now a wrapper

* Fix test import alias, update expectations

* Prevent desync of before / after change events

Adds ability to perform late binding of finally() during drag start.

* nit - Refactor

* Fix unwanted snap on node/group add

* nit - Doc

* Add shift key state tracking for snap to grid

Private impl., no state API as yet.

* Add snap guides rendering

Nodes, reroutes

* Optimisation - reroute rendering

 Fixes exponential redraw

* Add snap guidelines for groups
This commit is contained in:
filtered
2024-11-19 02:12:20 +11:00
committed by GitHub
parent 3e50941ce3
commit c0e8b33716
13 changed files with 291 additions and 83 deletions

View File

@@ -68,8 +68,11 @@ export class CanvasPointer {
/** The last pointerup event for the primary button */
eUp: CanvasPointerEvent | null = null
/** If set, as soon as the mouse moves outside the click drift threshold, this action is run once. */
onDragStart?(): unknown
/**
* If set, as soon as the mouse moves outside the click drift threshold, this action is run once.
* @param pointer This pointer instance. Permits actions such as late binding of the finally() callback.
*/
onDragStart?(pointer: this): unknown
/**
* Called on pointermove whilst dragging.
@@ -232,7 +235,7 @@ export class CanvasPointer {
#setDragStarted(): void {
this.dragStarted = true
this.onDragStart?.()
this.onDragStart?.(this)
delete this.onDragStart
}

View File

@@ -1,4 +1,4 @@
import type { Dictionary, IContextMenuValue, LinkNetwork, ISlotType, MethodNames, Point, LinkSegment } from "./interfaces"
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"
@@ -9,6 +9,7 @@ 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
@@ -25,6 +26,26 @@ export interface LGraphState {
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
/**
* When set to a positive number, when nodes are moved their positions will
* be rounded to the nearest multiple of this value. Half up.
* Default: `undefined`
* @todo Not implemented - see {@link LiteGraph.CANVAS_GRID_SIZE}
*/
snapToGrid?: number
/**
* If `true`, items always snap to the grid - modifier keys are ignored.
* When {@link snapToGrid} is falsy, a value of `1` is used.
* Default: `false`
*/
alwaysSnapToGrid?: boolean
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:
@@ -81,7 +102,7 @@ export class LGraph implements LinkNetwork, Serialisable<SerialisableGraph> {
filter?: string
_subgraph_node?: LGraphNode
/** Must contain serialisable values, e.g. primitive types */
config: { align_to_grid?: any; links_ontop?: any }
config: LGraphConfig
vars: Dictionary<unknown>
nodes_executing: boolean[]
nodes_actioning: (string | boolean)[]
@@ -710,6 +731,12 @@ export class LGraph implements LinkNetwork, Serialisable<SerialisableGraph> {
if (!node) return
const { state } = this
// Ensure created items are snapped
if (this.config.alwaysSnapToGrid) {
const snapTo = this.getSnapToGridSize()
if (snapTo) node.snapToGrid(snapTo)
}
// LEGACY: This was changed from constructor === LGraphGroup
//groups
if (node instanceof LGraphGroup) {
@@ -961,6 +988,36 @@ export class LGraph implements LinkNetwork, Serialisable<SerialisableGraph> {
}
}
/**
* Snaps the provided items to a grid.
*
* Item positions are reounded to the nearest multiple of {@link LiteGraph.CANVAS_GRID_SIZE}.
*
* When {@link config}.{@link LGraphConfig.alwaysSnapToGrid alwaysSnapToGrid} is enabled
* and the grid size is falsy, a default of 1 is used.
* @param items The items to snap 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 this.config.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

View File

@@ -8,14 +8,14 @@ import type { LGraph } from "./LGraph"
import type { ContextMenu } from "./ContextMenu"
import { CanvasItem, EaseFunction, LGraphEventMode, LinkDirection, LinkMarkerShape, LinkRenderType, RenderShape, TitleMode } from "./types/globalEnums"
import { LGraphGroup } from "./LGraphGroup"
import { distance, overlapBounding, isPointInRect, findPointOnCurve, containsRect, isInRectangle, createBounds, isInRect } from "./measure"
import { distance, overlapBounding, isPointInRect, findPointOnCurve, containsRect, isInRectangle, createBounds, isInRect, snapPoint } from "./measure"
import { drawSlot, LabelPosition } from "./draw"
import { DragAndScale } from "./DragAndScale"
import { LinkReleaseContextExtended, LiteGraph, clamp } from "./litegraph"
import { stringOrEmpty, stringOrNull } from "./strings"
import { alignNodes, distributeNodes, getBoundaryNodes } from "./utils/arrange"
import { Reroute, type RerouteId } from "./Reroute"
import { getAllNestedItems } from "./utils/collections"
import { getAllNestedItems, findFirstNode } from "./utils/collections"
import { CanvasPointer } from "./CanvasPointer"
interface IShowSearchOptions {
@@ -382,6 +382,12 @@ export class LGraphCanvas {
SELECTED_NODE: LGraphNode
/** @deprecated Panels */
NODEPANEL_IS_OPEN: boolean
/** Once per frame check of snap to grid value. @todo Update on change. */
#snapToGrid?: number
/** Set on keydown, keyup. @todo */
#shiftDown: boolean = false
getMenuOptions?(): IContextMenuValue[]
getExtraMenuOptions?(canvas: LGraphCanvas, options: IContextMenuValue[]): IContextMenuValue[]
static active_node: LGraphNode
@@ -1834,12 +1840,19 @@ export class LGraphCanvas {
cloned.pos[0] += 5
cloned.pos[1] += 5
graph.add(cloned, false)
if (this.allow_dragnodes) {
pointer.onDragStart = (pointer) => {
graph.add(cloned, false)
this.#startDraggingItems(cloned, pointer)
}
pointer.onDragEnd = (e) => this.#processDraggedItems(e)
} else {
// TODO: Check if before/after change are necessary here.
graph.beforeChange()
this.isDragging = true
graph.add(cloned, false)
graph.afterChange()
}
this.processSelect(cloned, e)
return
}
}
@@ -1874,11 +1887,8 @@ export class LGraphCanvas {
pointer.onClick = () => this.processSelect(reroute, e)
if (!pointer.onDragStart) {
pointer.onDragStart = () => {
this.processSelect(reroute, e, true)
this.isDragging = true
}
pointer.finally = () => this.isDragging = false
pointer.onDragStart = (pointer) => this.#startDraggingItems(reroute, pointer, true)
pointer.onDragEnd = (e) => this.#processDraggedItems(e)
}
return
}
@@ -1912,17 +1922,9 @@ export class LGraphCanvas {
return
} else if (this.reroutesEnabled && e.altKey && !e.shiftKey) {
pointer.finally = () => {
this.emitAfterChange()
this.isDragging = false
}
this.emitBeforeChange()
const newReroute = graph.createReroute([x, y], linkSegment)
pointer.onDragStart = () => {
this.processSelect(newReroute, e)
this.isDragging = true
}
pointer.onDragStart = (pointer) => this.#startDraggingItems(newReroute, pointer)
pointer.onDragEnd = (e) => this.#processDraggedItems(e)
return
}
} else if (isInRectangle(x, y, centre[0] - 4, centre[1] - 4, 8, 8)) {
@@ -1951,12 +1953,11 @@ export class LGraphCanvas {
const headerHeight = f * 1.4
if (isInRectangle(x, y, group.pos[0], group.pos[1], group.size[0], headerHeight)) {
// In title bar
pointer.onDragStart = () => {
pointer.onDragStart = (pointer) => {
group.recomputeInsideNodes()
this.processSelect(group, e, true)
this.isDragging = true
this.#startDraggingItems(group, pointer, true)
}
pointer.finally = () => this.isDragging = false
pointer.onDragEnd = (e) => this.#processDraggedItems(e)
}
}
@@ -2183,12 +2184,8 @@ export class LGraphCanvas {
return
// Drag node
pointer.onDragStart = () => {
graph.beforeChange()
this.processSelect(node, e, true)
this.isDragging = true
}
pointer.finally = () => this.isDragging = false
pointer.onDragStart = (pointer) => this.#startDraggingItems(node, pointer, true)
pointer.onDragEnd = (e) => this.#processDraggedItems(e)
}
this.dirty_canvas = true
@@ -2687,6 +2684,45 @@ export class LGraphCanvas {
e.preventDefault()
return
}
/**
* Start dragging an item, optionally including all other selected items.
*
* ** This function sets the {@link CanvasPointer.finally}() callback. **
* @param item The item that the drag event started on
* @param pointer The pointer event that initiated the drag, e.g. pointerdown
* @param sticky If `true`, the item is added to the selection - see {@link processSelect}
*/
#startDraggingItems(item: Positionable, pointer: CanvasPointer, sticky = false): void {
this.emitBeforeChange()
this.graph.beforeChange()
// Ensure that dragging is properly cleaned up, on success or failure.
pointer.finally = () => {
this.isDragging = false
this.graph.afterChange()
this.emitAfterChange()
}
this.processSelect(item, pointer.eDown, sticky)
this.isDragging = true
}
/**
* Handles shared clean up and placement after items have been dragged.
* @param e The event that completed the drag, e.g. pointerup, pointermove
*/
#processDraggedItems(e: CanvasPointerEvent): void {
const { graph } = this
if (e.shiftKey || graph.config.alwaysSnapToGrid)
graph.snapToGrid(this.selectedItems)
this.dirty_canvas = true
this.dirty_bgcanvas = true
// TODO: Replace legacy behaviour: callbacks were never extended for multiple items
this.onNodeMoved?.(findFirstNode(this.selectedItems))
}
/**
* Called when a mouse up event has to be processed
**/
@@ -2730,25 +2766,6 @@ export class LGraphCanvas {
//left button
this.selected_group = null
// Deprecated - old API for backwards compat
if (this.isDragging && this.selectedItems.size === 1) {
const val = this.selectedItems.values().next().value
if (val instanceof LGraphNode) {
this.onNodeMoved?.(val)
graph.afterChange(val)
}
}
if (this.isDragging && LiteGraph.always_round_positions) {
const selected = this.selectedItems
const allItems = getAllNestedItems(selected)
allItems.forEach(x => {
x.pos[0] = Math.round(x.pos[0])
x.pos[1] = Math.round(x.pos[1])
})
this.dirty_canvas = true
this.dirty_bgcanvas = true
}
this.isDragging = false
const x = e.canvasX
@@ -2988,6 +3005,7 @@ export class LGraphCanvas {
* process a key event
**/
processKey(e: KeyboardEvent): boolean | null {
this.#shiftDown = e.shiftKey
if (!this.graph) return
let block_default = false
@@ -3812,6 +3830,11 @@ export class LGraphCanvas {
ctx.clip()
}
// TODO: Set snapping value when changed instead of once per frame
this.#snapToGrid = this.#shiftDown || this.graph.config.alwaysSnapToGrid
? this.graph.getSnapToGridSize()
: undefined
//clear
//canvas.width = canvas.width;
if (this.clear_background) {
@@ -3844,18 +3867,23 @@ export class LGraphCanvas {
//draw nodes
const visible_nodes = this.visible_nodes
const drawSnapGuides = this.#snapToGrid && this.isDragging
for (let i = 0; i < visible_nodes.length; ++i) {
const node = visible_nodes[i]
//transform coords system
ctx.save()
// Draw snap shadow
if (drawSnapGuides && this.selectedItems.has(node))
this.drawSnapGuide(ctx, node)
// Localise co-ordinates to node position
ctx.translate(node.pos[0], node.pos[1])
//Draw
// Draw
this.drawNode(node, ctx)
//Restore
ctx.restore()
}
@@ -4673,14 +4701,13 @@ export class LGraphCanvas {
bgcolor: CanvasColour,
selected: boolean
): void {
//bg rect
// Rendering options
ctx.strokeStyle = fgcolor
ctx.fillStyle = bgcolor
const title_height = LiteGraph.NODE_TITLE_HEIGHT
const low_quality = this.ds.scale < 0.5
//render node area depending on shape
const shape = node._shape || node.constructor.shape || RenderShape.ROUND
const title_mode = node.constructor.title_mode
@@ -4696,8 +4723,7 @@ export class LGraphCanvas {
const old_alpha = ctx.globalAlpha
//full node shape
//if(node.flags.collapsed)
// Draw node background (shape)
{
ctx.beginPath()
if (shape == RenderShape.BOX || low_quality) {
@@ -5004,9 +5030,59 @@ export class LGraphCanvas {
ctx.globalAlpha = 1
}
/**
* Draws a snap guide for a {@link Positionable} item.
*
* Initial design was a simple white rectangle representing the location the
* item would land if dropped.
* @param ctx The 2D canvas context to draw on
* @param item The item to draw a snap guide for
* @param snapTo The grid size to snap to
* @todo Update to align snapping with boundingRect
* @todo Shapes
*/
drawSnapGuide(ctx: CanvasRenderingContext2D, item: Positionable, shape = RenderShape.ROUND) {
const snapGuide = LGraphCanvas.#temp
snapGuide.set(item.boundingRect)
// Not all items have pos equal to top-left of bounds
const { pos } = item
const offsetX = pos[0] - snapGuide[0]
const offsetY = pos[1] - snapGuide[1]
// Normalise boundingRect to pos to snap
snapGuide[0] += offsetX
snapGuide[1] += offsetY
snapPoint(snapGuide, this.#snapToGrid)
snapGuide[0] -= offsetX
snapGuide[1] -= offsetY
const { globalAlpha } = ctx
ctx.globalAlpha = 1
ctx.beginPath()
const [x, y, w, h] = snapGuide
if (shape === RenderShape.CIRCLE) {
const midX = x + (w * 0.5)
const midY = y + (h * 0.5)
const radius = Math.min(w * 0.5, h * 0.5)
ctx.arc(midX, midY, radius, 0, Math.PI * 2)
} else {
ctx.rect(x, y, w, h)
}
ctx.lineWidth = 0.5
ctx.strokeStyle = "#FFFFFF66"
ctx.fillStyle = "#FFFFFF22"
ctx.fill()
ctx.stroke()
ctx.globalAlpha = globalAlpha
}
drawConnections(ctx: CanvasRenderingContext2D): void {
const rendered = this.renderedPaths
rendered.clear()
const visibleReroutes: Reroute[] = []
const now = LiteGraph.getTime()
const visible_area = this.visible_area
LGraphCanvas.#margin_area[0] = visible_area[0] - 20
@@ -5079,8 +5155,13 @@ export class LGraphCanvas {
for (let j = 0; j < l; j++) {
const reroute = reroutes[j]
// Only render once
if (!rendered.has(reroute)) {
rendered.add(reroute)
visibleReroutes.push(reroute)
reroute._colour = link.color ||
LGraphCanvas.link_type_colors[link.type] ||
this.default_link_color
const prevReroute = this.graph.reroutes.get(reroute.parentId)
const startPos = prevReroute?.pos ?? start_node_slotpos
@@ -5123,12 +5204,6 @@ export class LGraphCanvas {
end_dir,
{ startControl },
)
// Render the reroute circles
const defaultColor = LGraphCanvas.link_type_colors[link.type] || this.default_link_color
for (const reroute of reroutes) {
reroute.draw(ctx, link.color || defaultColor)
}
} else {
this.renderLink(
ctx,
@@ -5164,6 +5239,13 @@ export class LGraphCanvas {
}
}
}
// Render the reroute circles
for (const reroute of visibleReroutes) {
if (this.#snapToGrid && this.isDragging && this.selectedItems.has(reroute))
this.drawSnapGuide(ctx, reroute, RenderShape.CIRCLE)
reroute.draw(ctx)
}
ctx.globalAlpha = 1
}
@@ -5834,6 +5916,7 @@ export class LGraphCanvas {
ctx.save()
ctx.globalAlpha = 0.5 * this.editor_alpha
const drawSnapGuides = this.#snapToGrid && this.isDragging
for (let i = 0; i < groups.length; ++i) {
const group = groups[i]
@@ -5842,6 +5925,10 @@ export class LGraphCanvas {
continue
} //out of the visible area
// Draw snap shadow
if (drawSnapGuides && this.selectedItems.has(group))
this.drawSnapGuide(ctx, group)
group.draw(this, ctx)
}

View File

@@ -3,7 +3,7 @@ import type { LGraph } from "./LGraph"
import type { ISerialisedGroup } from "./types/serialisation"
import { LiteGraph } from "./litegraph"
import { LGraphCanvas } from "./LGraphCanvas"
import { containsCentre, containsRect, isInRectangle, isPointInRect, createBounds } from "./measure"
import { containsCentre, containsRect, isInRectangle, isPointInRect, createBounds, snapPoint } from "./measure"
import { LGraphNode } from "./LGraphNode"
import { RenderShape, TitleMode } from "./types/globalEnums"
@@ -192,6 +192,11 @@ export class LGraphGroup implements Positionable, IPinnable {
}
}
/** @inheritdoc */
snapToGrid(snapTo: number): boolean {
return this.pinned ? false : snapPoint(this.pos, snapTo)
}
recomputeInsideNodes(): void {
const { nodes, reroutes, groups } = this.graph
const children = this._children
@@ -233,8 +238,8 @@ export class LGraphGroup implements Positionable, IPinnable {
* @param padding Value in graph units to add to all sides of the group. Default: 10
*/
resizeTo(objects: Iterable<Positionable>, padding: number = 10): void {
const boundingBox = createBounds(objects, padding);
if(boundingBox === null) return
const boundingBox = createBounds(objects, padding)
if (boundingBox === null) return
this.pos[0] = boundingBox[0]
this.pos[1] = boundingBox[1] - this.titleHeight

View File

@@ -9,7 +9,7 @@ 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"
import { isInRectangle, isInRect } from "./measure"
import { isInRectangle, isInRect, snapPoint } from "./measure"
import { LLink } from "./LLink"
export type NodeId = number | string
@@ -2295,10 +2295,14 @@ export class LGraphNode implements Positionable, IPinnable {
return out
}
/* Force align to grid */
/** @inheritdoc */
snapToGrid(snapTo: number): boolean {
return this.pinned ? false : snapPoint(this.pos, snapTo)
}
/** @see {@link snapToGrid} */
alignToGrid(): void {
this.pos[0] = LiteGraph.CANVAS_GRID_SIZE * Math.round(this.pos[0] / LiteGraph.CANVAS_GRID_SIZE)
this.pos[1] = LiteGraph.CANVAS_GRID_SIZE * Math.round(this.pos[1] / LiteGraph.CANVAS_GRID_SIZE)
this.snapToGrid(LiteGraph.CANVAS_GRID_SIZE)
}
/* Console output */

View File

@@ -132,8 +132,6 @@ export class LiteGraphGlobal {
ctrl_alt_click_do_break_link = true // [true!] who accidentally ctrl-alt-clicks on an in/output? nobody! that's who!
snaps_for_comfy = true // [true!] snaps links when dragging connections over valid targets
snap_highlights_node = true // [true!] renders a partial border to highlight when a dragged link is snapped to a node
/** After moving items on the canvas, their positions will be rounded. Effectively "snap to grid" with a grid size of 1. */
always_round_positions = false
search_hide_on_mouse_leave = true // [false on mobile] better true if not touch device, TODO add an helper/listener to close if false
search_filter_enabled = false // [true!] enable filtering slots type in the search widget, !requires auto_load_slot_types or manual set registered_slot_[in/out]_types and slot_types_[in/out]

View File

@@ -73,6 +73,9 @@ export class Reroute implements Positionable, LinkSegment, Serialisable<Serialis
/** @inheritdoc */
_pos: Float32Array = this.#malloc.subarray(6, 8)
/** Colour of the first link that rendered this reroute */
_colour?: CanvasColour
/**
* Used to ensure reroute angles are only executed once per frame.
* @todo Calculate on change instead.
@@ -188,6 +191,16 @@ export class Reroute implements Positionable, LinkSegment, Serialisable<Serialis
this.#pos[1] += deltaY
}
/** @inheritdoc */
snapToGrid(snapTo: number): boolean {
if (!snapTo) return false
const { pos } = this
pos[0] = snapTo * Math.round(pos[0] / snapTo)
pos[1] = snapTo * Math.round(pos[1] / snapTo)
return true
}
calculateAngle(lastRenderTime: number, network: LinkNetwork, linkStart: Point): void {
// Ensure we run once per render
if (!(lastRenderTime > this.#lastRenderTime)) return
@@ -241,9 +254,9 @@ export class Reroute implements Positionable, LinkSegment, Serialisable<Serialis
*
* @remarks Leaves {@link ctx}.fillStyle, strokeStyle, and lineWidth dirty (perf.).
*/
draw(ctx: CanvasRenderingContext2D, colour: CanvasColour): void {
draw(ctx: CanvasRenderingContext2D): void {
const { pos } = this
ctx.fillStyle = colour
ctx.fillStyle = this._colour
ctx.beginPath()
ctx.arc(pos[0], pos[1], Reroute.radius, 0, 2 * Math.PI)
ctx.fill()

View File

@@ -42,6 +42,15 @@ export interface Positionable extends Parent<Positionable> {
*/
move(deltaX: number, deltaY: number, skipChildren?: boolean): void
/**
* Snaps this item to a grid.
*
* Position values are rounded to the nearest multiple of {@link snapTo}.
* @param snapTo The size of the grid to align to
* @returns `true` if it moved, or `false` if the snap was rejected (e.g. `pinned`)
*/
snapToGrid(snapTo: number): boolean
/**
* Cached position & size as `x, y, width, height`.
* @readonly See {@link move}

View File

@@ -310,4 +310,19 @@ export function createBounds(objects: Iterable<Positionable>, padding: number =
bounds[2] - bounds[0] + (2 * padding),
bounds[3] - bounds[1] + (2 * padding)
]
}
}
/**
* Snaps the provided {@link Point} or {@link Rect} ({@link pos}) to a grid of size {@link snapTo}.
* @param pos The point that will be snapped
* @param snapTo The value to round up/down by (multiples thereof)
* @returns `true` if snapTo is truthy, otherwise `false`
* @remarks `NaN` propagates through this function and does not affect return value.
*/
export function snapPoint(pos: Point | Rect, snapTo: number): boolean {
if (!snapTo) return false
pos[0] = snapTo * Math.round(pos[0] / snapTo)
pos[1] = snapTo * Math.round(pos[1] / snapTo)
return true
}

View File

@@ -6,13 +6,19 @@ export enum NodeSlotType {
/** Shape that an object will render as - used by nodes and slots */
export enum RenderShape {
/** Rectangle with square corners */
BOX = 1,
/** Rounded rectangle */
ROUND = 2,
/** Circle is circle */
CIRCLE = 3,
/** Two rounded corners: top left & bottom right */
CARD = 4,
/** Slot shape: Arrow */
ARROW = 5,
/** intended for slot arrays */
/** Slot shape: Grid */
GRID = 6,
/** Slot shape: Hollow circle */
HollowCircle = 7,
}

View File

@@ -1,4 +1,5 @@
import type { Parent } from "../interfaces"
import type { Parent, Positionable } from "../interfaces"
import { LGraphNode } from "@/LGraphNode"
/**
* Creates a flat set of all items by recursively iterating through all child items.
@@ -16,3 +17,14 @@ export function getAllNestedItems<TParent extends Parent<TParent>>(items: Readon
item.children?.forEach(x => addRecursively(x, flatSet))
}
}
/**
* Iterates through a collection of {@link Positionable} items, returning the first {@link LGraphNode}.
* @param items The items to search through
* @returns The first node found in {@link items}, otherwise `undefined`
*/
export function findFirstNode(items: Iterable<Positionable>): LGraphNode | undefined {
for (const item of items) {
if (item instanceof LGraphNode) return item
}
}

View File

@@ -134,7 +134,6 @@ LiteGraphGlobal {
"allow_multi_output_for_events": true,
"allow_scripts": false,
"alt_drag_do_clone_nodes": false,
"always_round_positions": false,
"auto_load_slot_types": false,
"auto_sort_node_types": false,
"catch_exceptions": true,

View File

@@ -31,7 +31,7 @@ export default defineConfig({
alias: { '@': '/src' },
},
test: {
alias: { '@': '../src' },
alias: { '@/': path.resolve(__dirname, './src/') },
environment: 'jsdom',
},
})