mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-02 14:27:40 +00:00
* 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
272 lines
8.1 KiB
TypeScript
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)
|
|
}
|
|
}
|