Files
ComfyUI_frontend/src/CanvasPointer.ts
filtered c0e8b33716 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
2024-11-18 10:12:20 -05:00

272 lines
8.1 KiB
TypeScript

import type { CanvasPointerEvent } from "./types/events"
import type { LGraphCanvas } from "./LGraphCanvas"
import { dist2 } from "./measure"
/**
* Allows click and drag actions to be declared ahead of time during a pointerdown event.
*
* Depending on whether the user clicks or drags the pointer, only the appropriate callbacks are called:
* - {@link onClick}
* - {@link onDoubleClick}
* - {@link onDragStart}
* - {@link onDrag}
* - {@link onDragEnd}
* - {@link finally}
*
* @seealso
* - {@link LGraphCanvas.processMouseDown}
* - {@link LGraphCanvas.processMouseMove}
* - {@link LGraphCanvas.processMouseUp}
*/
export class CanvasPointer {
/** Maximum time in milliseconds to ignore click drift */
static bufferTime = 150
/** Maximum gap between pointerup and pointerdown events to be considered as a double click */
static doubleClickTime = 300
/** Maximum offset from click location */
static get maxClickDrift() {
return this.#maxClickDrift
}
static set maxClickDrift(value) {
this.#maxClickDrift = value
this.#maxClickDrift2 = value * value
}
static #maxClickDrift = 6
/** {@link maxClickDrift} squared. Used to calculate click drift without `sqrt`. */
static #maxClickDrift2 = this.#maxClickDrift ** 2
/** The element this PointerState should capture input against when dragging. */
element: Element
/** Pointer ID used by drag capture. */
pointerId: number
/** Set to true when if the pointer moves far enough after a down event, before the corresponding up event is fired. */
dragStarted: boolean = false
/** The {@link eUp} from the last successful click */
eLastDown?: CanvasPointerEvent
/** Used downstream for touch event support. */
isDouble: boolean = false
/** Used downstream for touch event support. */
isDown: boolean = false
/**
* If `true`, {@link eDown}, {@link eMove}, and {@link eUp} will be set to
* `null` when {@link reset} is called.
*
* Default: `true`
*/
clearEventsOnReset: boolean = true
/** The last pointerdown event for the primary button */
eDown: CanvasPointerEvent | null = null
/** The last pointermove event for the primary button */
eMove: CanvasPointerEvent | null = null
/** 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.
* @param pointer This pointer instance. Permits actions such as late binding of the finally() callback.
*/
onDragStart?(pointer: this): unknown
/**
* Called on pointermove whilst dragging.
* @param eMove The pointermove event of this ongoing drag action
*/
onDrag?(eMove: CanvasPointerEvent): unknown
/**
* Called on pointerup after dragging (i.e. not called if clicked).
* @param upEvent The pointerup or pointermove event that triggered this callback
*/
onDragEnd?(upEvent: CanvasPointerEvent): unknown
/**
* Callback that will be run once, the next time a pointerup event appears to be a normal click.
* @param upEvent The pointerup or pointermove event that triggered this callback
*/
onClick?(upEvent: CanvasPointerEvent): unknown
/**
* Callback that will be run once, the next time a pointerup event appears to be a normal click.
* @param upEvent The pointerup or pointermove event that triggered this callback
*/
onDoubleClick?(upEvent: CanvasPointerEvent): unknown
/**
* Run-once callback, called at the end of any click or drag, whether or not it was successful in any way.
*
* The setter of this callback will call the existing value before replacing it.
* Therefore, simply setting this value twice will execute the first callback.
*/
get finally() {
return this.#finally
}
set finally(value) {
try {
this.#finally?.()
} finally {
this.#finally = value
}
}
#finally?: () => unknown
constructor(element: Element) {
this.element = element
}
/**
* Callback for `pointerdown` events. To be used as the event handler (or called by it).
* @param e The `pointerdown` event
*/
down(e: CanvasPointerEvent): void {
this.reset()
this.eDown = e
this.pointerId = e.pointerId
this.element.setPointerCapture(e.pointerId)
}
/**
* Callback for `pointermove` events. To be used as the event handler (or called by it).
* @param e The `pointermove` event
*/
move(e: CanvasPointerEvent): void {
const { eDown } = this
if (!eDown) return
// No buttons down, but eDown exists - clean up & leave
if (!e.buttons) {
this.reset()
return
}
// Primary button released - treat as pointerup.
if (!(e.buttons & eDown.buttons)) {
this.#completeClick(e)
this.reset()
return
}
this.eMove = e
this.onDrag?.(e)
// Dragging, but no callback to run
if (this.dragStarted) return
const longerThanBufferTime = e.timeStamp - eDown.timeStamp > CanvasPointer.bufferTime
if (longerThanBufferTime || !this.#hasSamePosition(e, eDown)) {
this.#setDragStarted()
}
}
/**
* Callback for `pointerup` events. To be used as the event handler (or called by it).
* @param e The `pointerup` event
*/
up(e: CanvasPointerEvent): boolean {
if (e.button !== this.eDown?.button) return false
this.#completeClick(e)
const { dragStarted } = this
this.reset()
return !dragStarted
}
#completeClick(e: CanvasPointerEvent): void {
const { eDown } = this
if (!eDown) return
this.eUp = e
if (this.dragStarted) {
// A move event already started drag
this.onDragEnd?.(e)
} else if (!this.#hasSamePosition(e, eDown)) {
// Teleport without a move event (e.g. tab out, move, tab back)
this.#setDragStarted()
this.onDragEnd?.(e)
} else if (this.onDoubleClick && this.#isDoubleClick()) {
// Double-click event
this.onDoubleClick(e)
this.eLastDown = undefined
} else {
// Normal click event
this.onClick?.(e)
this.eLastDown = eDown
}
}
/**
* Checks if two events occurred near each other - not further apart than the maximum click drift.
* @param a The first event to compare
* @param b The second event to compare
* @param tolerance2 The maximum distance (squared) before the positions are considered different
* @returns `true` if the two events were no more than {@link maxClickDrift} apart, otherwise `false`
*/
#hasSamePosition(
a: PointerEvent,
b: PointerEvent,
tolerance2 = CanvasPointer.#maxClickDrift2,
): boolean {
const drift = dist2(a.clientX, a.clientY, b.clientX, b.clientY)
return drift <= tolerance2
}
/**
* Checks whether the pointer is currently past the max click drift threshold.
* @param e The latest pointer event
* @returns `true` if the latest pointer event is past the the click drift threshold
*/
#isDoubleClick(): boolean {
const { eDown, eLastDown } = this
if (!eDown || !eLastDown) return false
// Use thrice the drift distance for double-click gap
const tolerance2 = (3 * CanvasPointer.#maxClickDrift) ** 2
const diff = eDown.timeStamp - eLastDown.timeStamp
return diff > 0 &&
diff < CanvasPointer.doubleClickTime &&
this.#hasSamePosition(eDown, eLastDown, tolerance2)
}
#setDragStarted(): void {
this.dragStarted = true
this.onDragStart?.(this)
delete this.onDragStart
}
/**
* Resets the state of this {@link CanvasPointer} instance.
*
* The {@link finally} callback is first executed, then all callbacks and intra-click
* state is cleared.
*/
reset(): void {
// The setter executes the callback before clearing it
this.finally = undefined
delete this.onClick
delete this.onDoubleClick
delete this.onDragStart
delete this.onDrag
delete this.onDragEnd
this.isDown = false
this.isDouble = false
this.dragStarted = false
if (this.clearEventsOnReset) {
this.eDown = null
this.eMove = null
this.eUp = null
}
const { element, pointerId } = this
if (element.hasPointerCapture(pointerId))
element.releasePointerCapture(pointerId)
}
}