mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-02 14:27:40 +00:00
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:
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -31,7 +31,7 @@ export default defineConfig({
|
||||
alias: { '@': '/src' },
|
||||
},
|
||||
test: {
|
||||
alias: { '@': '../src' },
|
||||
alias: { '@/': path.resolve(__dirname, './src/') },
|
||||
environment: 'jsdom',
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user