Skip to content

Commit

Permalink
Add CanvasPointer API (#308)
Browse files Browse the repository at this point in the history
* Add position rounding feature

Replaces previous impls. which only worked on some items, and were triggered when unexpected e.g. clicking a node that hadn't been moved.

Update test expectations

* Narrow TS types - readonly

* nit - Clean up, Doc

* nit - Clean up legacy accessors

Marks as deprecated

* Fix TS type - IContextMenuOptions.scale

* [Refactor] dist2 for use in pointer API

* Add CanvasPointer - API for pointer events

Add TS strict types
Add final click drag distance math
Add option to retain events

* nit - Rename

* nit

* Remove Subgraph - unused & not maintained

* Remove live_mode

Unused, not maintained.

* Update README

Remove live_mode reference

* Update delete selected - include reroutes & groups

* Bypass link menu if alt/shift pressed

* Remove old dragged_node interface

Incomplete impl. - unused.
Superceded by selectedItems

* Fix top/left edge of rectangles not in hitbox

* [Refactor] Match function names to interface names

* Add interface to find widgets by Point

LGraphNode.getWidgetOnPos

* Add widget search param - includeDisabled

* nit - Doc

* Rewrite canvas mouse handling

- Rewrites most pointer handling to use CanvasPointer callbacks
- All callbacks are declared ahead of time during the initial pointerdown event, logically grouped together
- Drastically simplifies the alteration or creation of new click / drag interactions
- Click events are all clicks, rather than some processed on mouse down, others on mouse up

- Functions return instead of setting and repeatedly checking multiple state vars
- Removes all lines that needed THIRTEEN tab indents

* Split middle click out from processMouseDown

* Use pointer API for link menus

* Narrow canvas event interfaces

* Fix canvas event types

Replaces original workarounds with final types

* Refactor - deprecated isInsideRectangle

* Add canvas hovering over state

- Centralises cursor set behaviour
- Provides simple downstream override

* nit

* [Refactor] Use measure functions

* Add double-click API to CanvasPointer

a

* nit - Doc

* Allow larger gap between double click events

* Rewrite double-click into CanvasPointer actions

* Improve double-click UX

Prefer down events over up events

* Add production defaults

* Add middle-click handling

* Remove debug code

* Remove redundant code

* Fix add reroute alt-click adds two undo steps

* Fix click on connected input disconnects

Old behaviour was to disconnect, then recreate a new link on drop.

* Add module export: CanvasPointer
  • Loading branch information
webfiltered authored Nov 16, 2024
1 parent f26f7db commit b29a32c
Show file tree
Hide file tree
Showing 14 changed files with 1,266 additions and 1,363 deletions.
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ Try it in the [demo site](https://tamats.com/projects/litegraph/editor).
- Customizable theme (colors, shapes, background)
- Callbacks to personalize every action/drawing/event of nodes
- Subgraphs (nodes that contain graphs themselves)
- Live mode system (hides the graph but calls nodes to render whatever they want, useful to create UIs)
- Graphs can be executed in NodeJS
- Highly customizable nodes (color, shape, slots vertical or horizontal, widgets, custom rendering)
- Easy to integrate in any JS application (one single file, no dependencies)
Expand Down
268 changes: 268 additions & 0 deletions src/CanvasPointer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
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. */
onDragStart?(): 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?.()
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)
}
}
3 changes: 2 additions & 1 deletion src/DragAndScale.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Point, Rect, Rect32 } from "./interfaces"
import type { CanvasMouseEvent } from "./types/events"
import { LiteGraph } from "./litegraph"
import { isInRect } from "./measure"

export class DragAndScale {
/** Maximum scale (zoom in) */
Expand Down Expand Up @@ -98,7 +99,7 @@ export class DragAndScale {
e.canvasy = y
e.dragging = this.dragging

const is_inside = !this.viewport || (this.viewport && x >= this.viewport[0] && x < (this.viewport[0] + this.viewport[2]) && y >= this.viewport[1] && y < (this.viewport[1] + this.viewport[3]))
const is_inside = !this.viewport || isInRect(x, y, this.viewport)

let ignore = false
if (this.onmouse) {
Expand Down
17 changes: 1 addition & 16 deletions src/LGraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -826,9 +826,6 @@ export class LGraph implements LinkNetwork, Serialisable<SerialisableGraph> {
const canvas = this.list_of_graphcanvas[i]
if (canvas.selected_nodes[node.id])
delete canvas.selected_nodes[node.id]

if (canvas.node_dragged == node)
canvas.node_dragged = null
}
}

Expand Down Expand Up @@ -1222,18 +1219,6 @@ export class LGraph implements LinkNetwork, Serialisable<SerialisableGraph> {
// @ts-expect-error
this.canvasAction(c => c.onConnectionChange?.())
}
/**
* returns if the graph is in live mode
*/
isLive(): boolean {
if (!this.list_of_graphcanvas) return false

for (let i = 0; i < this.list_of_graphcanvas.length; ++i) {
const c = this.list_of_graphcanvas[i]
if (c.live_mode) return true
}
return false
}
/**
* clears the triggered slot animation in all links (stop visual animation)
*/
Expand Down Expand Up @@ -1334,7 +1319,7 @@ export class LGraph implements LinkNetwork, Serialisable<SerialisableGraph> {

const node = this.getNodeById(link.target_id)
node?.disconnectInput(link.target_slot)

link.disconnect(this)
}

Expand Down
Loading

0 comments on commit b29a32c

Please sign in to comment.