diff --git a/src/DragAndScale.ts b/src/DragAndScale.ts index 8d5654b..f54d2aa 100644 --- a/src/DragAndScale.ts +++ b/src/DragAndScale.ts @@ -29,6 +29,11 @@ export class DragAndScale { dragging?: boolean viewport?: Rect + /** Auto canvas movement multiplier */ + move_factor: number + /** Threshold to trigger auto canvas movement RANGE[0,1] */ + move_threshold: number + onredraw?(das: DragAndScale): void /** @deprecated */ onmouse?(e: unknown): boolean @@ -62,6 +67,9 @@ export class DragAndScale { this.element = null this.visible_area = new Float32Array(4) + this.move_factor = 350 + this.move_threshold = 0.95 + if (element) { this.element = element if (!skip_events) { diff --git a/src/LGraphCanvas.ts b/src/LGraphCanvas.ts index 1a204d1..c8e2413 100644 --- a/src/LGraphCanvas.ts +++ b/src/LGraphCanvas.ts @@ -56,6 +56,7 @@ import { createBounds, isInRect, snapPoint, + getVector2Clamped, } from "./measure" import { strokeShape } from "./draw" import { DragAndScale } from "./DragAndScale" @@ -519,6 +520,17 @@ export class LGraphCanvas implements ConnectionColorContext { /** The start position of the drag zoom. */ #dragZoomStart: { pos: Point, scale: number } | null = null + /** If true, enable automatic panning of the viewport when interacting close to canvas edges. */ + auto_panning_enabled: boolean = false + /** + * Threshold to trigger automatic panning. Works based of the percent distance between the center of the canvas and its edges + * @remarks A value of 1.0 basically disables automatic panning. + */ + auto_panning_threshold: number = 0.95 + /** Defines the speed in which the viewport will pan automatic */ + auto_panning_speed: number = 350 + auto_panning_event: ReturnType | null = null + getMenuOptions?(): IContextMenuValue[] getExtraMenuOptions?( canvas: LGraphCanvas, @@ -1930,6 +1942,51 @@ export class LGraphCanvas implements ConnectionColorContext { this.last_mouseclick = 0 } + /** + * Automatically moves the canvas to follow the mouse. + * @param deltaT Time difference since last frame + * @remarks Does not require items to be selected to work properly. Could add ramp up/down for smoother movement transition. + * */ + autoPanCanvas(deltaT: number): void { + const moveDirection = getVector2Clamped( + this.mouse, + [this.canvas.clientWidth / 2, this.canvas.clientHeight / 2], + ) + if (Math.abs(moveDirection[0]) > this.auto_panning_threshold || Math.abs(moveDirection[1]) > this.auto_panning_threshold) { + const { scale } = this.ds + const newOffsetX: number = (moveDirection[0] * deltaT * this.auto_panning_speed) / scale + const newOffsetY: number = (moveDirection[1] * deltaT * this.auto_panning_speed) / scale + + this.ds.offset[0] -= newOffsetX + this.ds.offset[1] -= newOffsetY + + this.#dirty() + if (this.connecting_links) { // Prevent selected Positionables from moving while dragging a link + // Update graph_mouse to prevent link from staying behind while dragging + this.graph_mouse[0] += newOffsetX + this.graph_mouse[1] += newOffsetY + return + } + for (const item of this.selectedItems) { + item.move(newOffsetX, newOffsetY, false) + } + } + } + + setAutoPanCanvas(enabled: boolean, interval: number = 30) { // 30 ms == roughly 33 fps + if (!this.auto_panning_enabled) return + if (enabled) { + if (this.auto_panning_event) clearInterval(this.auto_panning_event) + this.auto_panning_event = setInterval(() => { + this.autoPanCanvas(interval * 0.001) + }, interval) + return + } else { + clearInterval(this.auto_panning_event) + this.auto_panning_event = null + } + } + /** * Gets the widget at the current cursor position * @param node Optional node to check for widgets under cursor @@ -2434,6 +2491,9 @@ export class LGraphCanvas implements ConnectionColorContext { } } + // enable canvas auto panning for link output + this.setAutoPanCanvas(true) + // TODO: Move callbacks to the start of this closure (onInputClick is already correct). pointer.onDoubleClick = () => node.onOutputDblClick?.(i, e) pointer.onClick = () => node.onOutputClick?.(i, e) @@ -2481,6 +2541,9 @@ export class LGraphCanvas implements ConnectionColorContext { } this.dirty_bgcanvas = true + + // enable canvas auto panning for disconnected link input + this.setAutoPanCanvas(true) } } if (!pointer.onDragStart) { @@ -2495,6 +2558,9 @@ export class LGraphCanvas implements ConnectionColorContext { pointer.onDragStart = () => connecting.input = input this.dirty_bgcanvas = true + + // enable canvas auto panning for link input + this.setAutoPanCanvas(true) } // pointer.finally = () => this.connecting_links = null @@ -2971,10 +3037,12 @@ export class LGraphCanvas implements ConnectionColorContext { this.isDragging = false this.graph.afterChange() this.emitAfterChange() + this.setAutoPanCanvas(false) } this.processSelect(item, pointer.eDown, sticky) this.isDragging = true + this.setAutoPanCanvas(true) } /** @@ -3125,6 +3193,8 @@ export class LGraphCanvas implements ConnectionColorContext { } } } + // disable canvas auto panning for links + this.setAutoPanCanvas(false) } else { this.dirty_canvas = true diff --git a/src/measure.ts b/src/measure.ts index 6117e79..45cc9af 100644 --- a/src/measure.ts +++ b/src/measure.ts @@ -367,3 +367,19 @@ export function snapPoint(pos: Point | Rect, snapTo: number): boolean { pos[1] = snapTo * Math.round(pos[1] / snapTo) return true } + +/** + * Calculates the clamped vector between two points. + * @param pointA The first point + * @param pointB The second point + * @returns Vector2 {@link Point} of pointA to pointB, clamped to the range [-1, 1] + * @remarks Calculates a pseudo "normalized" Vector2 by clamping it in range. Faster than using SQRT for truly normalized vectors. DO NOT use as a true unit vector. + * */ +export function getVector2Clamped(pointA: Point, pointB: Point): Point { + const x: number = (pointA[0] - pointB[0]) / pointB[0] + const y: number = (pointA[1] - pointB[1]) / pointB[1] + return [ + Math.max(-1, Math.min(1, x)), + Math.max(-1, Math.min(1, y)), + ] +}