@elicdavis/node-flow 0.1.12 → 0.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +29 -2
- package/dist/index.d.ts +29 -2
- package/dist/index.js +332 -95
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +331 -95
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/connection.ts +33 -4
- package/src/contextMenu.ts +35 -3
- package/src/graph.ts +81 -37
- package/src/index.ts +3 -0
- package/src/input.ts +183 -74
- package/src/node.ts +19 -18
- package/src/nodes/subsystem.ts +43 -0
- package/src/theme.ts +1 -0
- package/dist/web/NodeFlow.js +0 -8
- package/dist/web/NodeFlow.js.map +0 -7
package/package.json
CHANGED
package/src/connection.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { FlowNode } from "./node";
|
|
|
2
2
|
import { Port } from "./port";
|
|
3
3
|
import { BoxCenter, InBox } from "./types/box";
|
|
4
4
|
import { Vector2, Zero } from "./types/vector2";
|
|
5
|
+
import { Clamp01 } from "./utils/math";
|
|
5
6
|
|
|
6
7
|
export interface ConnectionRendererParams {
|
|
7
8
|
ctx: CanvasRenderingContext2D;
|
|
@@ -18,6 +19,16 @@ export interface ConnectionRendererParams {
|
|
|
18
19
|
|
|
19
20
|
export type ConnectionRenderer = (params: ConnectionRendererParams) => void
|
|
20
21
|
|
|
22
|
+
/** Horizontal stub (screen px) at full strength on each port when output is right of input. */
|
|
23
|
+
const BACKWARD_PORT_STUB = 30;
|
|
24
|
+
/** Screen px of output-over-input overlap over which stubs ramp from 0 to full. */
|
|
25
|
+
const BACKWARD_STUB_RAMP = 80;
|
|
26
|
+
|
|
27
|
+
function smoothstep(t: number): number {
|
|
28
|
+
const x = Clamp01(t);
|
|
29
|
+
return x * x * (3 - 2 * x);
|
|
30
|
+
}
|
|
31
|
+
|
|
21
32
|
export function DefaultConnectionRenderer(connectionSize: number, connectionColor: string | undefined, mouseOverSize: number, mouseOverColor: string | undefined): ConnectionRenderer {
|
|
22
33
|
return (params: ConnectionRendererParams) => {
|
|
23
34
|
|
|
@@ -47,11 +58,29 @@ export function DefaultConnectionRenderer(connectionSize: number, connectionColo
|
|
|
47
58
|
params.ctx.strokeStyle = color;
|
|
48
59
|
params.ctx.lineWidth = lineSize;
|
|
49
60
|
|
|
50
|
-
//
|
|
61
|
+
// start = input port, end = output port
|
|
62
|
+
const inX = params.start.x;
|
|
63
|
+
const inY = params.start.y;
|
|
64
|
+
const outX = params.end.x;
|
|
65
|
+
const outY = params.end.y;
|
|
66
|
+
|
|
67
|
+
const overlap = outX - inX;
|
|
68
|
+
const fullStub = BACKWARD_PORT_STUB * params.graphScale;
|
|
69
|
+
const ramp = BACKWARD_STUB_RAMP * params.graphScale;
|
|
70
|
+
const stub = overlap <= 0 ? 0 : fullStub * smoothstep(overlap / ramp);
|
|
71
|
+
const curveStartX = inX - stub;
|
|
72
|
+
const curveEndX = outX + stub;
|
|
73
|
+
|
|
51
74
|
params.ctx.beginPath();
|
|
52
|
-
params.ctx.moveTo(
|
|
53
|
-
|
|
54
|
-
|
|
75
|
+
params.ctx.moveTo(inX, inY);
|
|
76
|
+
if (stub > 0) {
|
|
77
|
+
params.ctx.lineTo(curveStartX, inY);
|
|
78
|
+
}
|
|
79
|
+
const midX = (curveStartX + curveEndX) / 2;
|
|
80
|
+
params.ctx.bezierCurveTo(midX, inY, midX, outY, curveEndX, outY);
|
|
81
|
+
if (stub > 0) {
|
|
82
|
+
params.ctx.lineTo(outX, outY);
|
|
83
|
+
}
|
|
55
84
|
params.ctx.stroke();
|
|
56
85
|
params.ctx.shadowBlur = 0;
|
|
57
86
|
}
|
package/src/contextMenu.ts
CHANGED
|
@@ -8,11 +8,24 @@ import { CopyVector2, Vector2, Zero } from './types/vector2';
|
|
|
8
8
|
const contextEntryHeight = 30;
|
|
9
9
|
const contextEntryWidth = 250;
|
|
10
10
|
|
|
11
|
+
export enum ContextMenuItemState {
|
|
12
|
+
Enabled = 0,
|
|
13
|
+
Disabled = 1,
|
|
14
|
+
Hidden = 2,
|
|
15
|
+
}
|
|
16
|
+
|
|
11
17
|
export interface ContextMenuItemConfig {
|
|
12
18
|
name?: string;
|
|
13
19
|
textStyle?: TextStyleConfig;
|
|
14
20
|
group?: string;
|
|
15
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Evaluated when the menu is opened. Defaults to Enabled when omitted.
|
|
24
|
+
* Hidden items are omitted from the menu; Disabled items are shown muted
|
|
25
|
+
* and are not clickable.
|
|
26
|
+
*/
|
|
27
|
+
enabled?: () => ContextMenuItemState;
|
|
28
|
+
|
|
16
29
|
callback?: () => void;
|
|
17
30
|
}
|
|
18
31
|
|
|
@@ -33,6 +46,8 @@ export class ContextMenuItem {
|
|
|
33
46
|
|
|
34
47
|
#textStyle: TextStyle;
|
|
35
48
|
|
|
49
|
+
#state: ContextMenuItemState;
|
|
50
|
+
|
|
36
51
|
public group?: string;
|
|
37
52
|
|
|
38
53
|
constructor(config?: ContextMenuItemConfig) {
|
|
@@ -40,14 +55,21 @@ export class ContextMenuItem {
|
|
|
40
55
|
this.#callback = config?.callback;
|
|
41
56
|
this.#textStyle = new TextStyle(config?.textStyle);
|
|
42
57
|
this.group = config?.group;
|
|
58
|
+
this.#state = config?.enabled === undefined
|
|
59
|
+
? ContextMenuItemState.Enabled
|
|
60
|
+
: config.enabled();
|
|
43
61
|
}
|
|
44
62
|
|
|
45
63
|
getName(): string {
|
|
46
64
|
return this.#name;
|
|
47
65
|
}
|
|
48
66
|
|
|
67
|
+
state(): ContextMenuItemState {
|
|
68
|
+
return this.#state;
|
|
69
|
+
}
|
|
70
|
+
|
|
49
71
|
execute(): void {
|
|
50
|
-
if (this.#callback === undefined) {
|
|
72
|
+
if (this.#state !== ContextMenuItemState.Enabled || this.#callback === undefined) {
|
|
51
73
|
return;
|
|
52
74
|
}
|
|
53
75
|
this.#callback();
|
|
@@ -136,7 +158,11 @@ export class ContextMenu {
|
|
|
136
158
|
|
|
137
159
|
if (config?.items !== undefined) {
|
|
138
160
|
for (let i = 0; i < config?.items.length; i++) {
|
|
139
|
-
|
|
161
|
+
const item = new ContextMenuItem(config?.items[i]);
|
|
162
|
+
if (item.state() === ContextMenuItemState.Hidden) {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
this.#items.push(item);
|
|
140
166
|
}
|
|
141
167
|
}
|
|
142
168
|
|
|
@@ -312,7 +338,10 @@ export class ContextMenu {
|
|
|
312
338
|
}
|
|
313
339
|
}
|
|
314
340
|
|
|
315
|
-
|
|
341
|
+
const itemDisabled = entry.item !== undefined
|
|
342
|
+
&& entry.item.state() === ContextMenuItemState.Disabled;
|
|
343
|
+
|
|
344
|
+
if (!itemDisabled && (entryMousedOver || (this.#openSubMenu !== undefined && entry.subMenu === this.#openSubMenu))) {
|
|
316
345
|
ctx.fillStyle = Theme.ContextMenu.HighlightColor;
|
|
317
346
|
ctx.beginPath();
|
|
318
347
|
ctx.roundRect(
|
|
@@ -326,6 +355,9 @@ export class ContextMenu {
|
|
|
326
355
|
}
|
|
327
356
|
|
|
328
357
|
this.#textStyle.setupStyle(ctx, menuScale);
|
|
358
|
+
if (itemDisabled) {
|
|
359
|
+
ctx.fillStyle = Theme.ContextMenu.DisabledFontColor;
|
|
360
|
+
}
|
|
329
361
|
ctx.fillText(entry.text, position.x + (scaledEntryHeight / 5), this.#tempBox.Position.y + (scaledEntryHeight / 2))
|
|
330
362
|
|
|
331
363
|
// Render arrows
|
package/src/graph.ts
CHANGED
|
@@ -195,9 +195,9 @@ export class NodeFlowGraph {
|
|
|
195
195
|
this.#contextMenuConfig = CombineContextMenus({
|
|
196
196
|
items: [
|
|
197
197
|
{
|
|
198
|
-
name: "
|
|
198
|
+
name: "Center on Graph",
|
|
199
199
|
group: contextMenuGroup,
|
|
200
|
-
callback: this
|
|
200
|
+
callback: this.centerOnGraph.bind(this)
|
|
201
201
|
},
|
|
202
202
|
],
|
|
203
203
|
}, config?.contextMenu);
|
|
@@ -222,23 +222,18 @@ export class NodeFlowGraph {
|
|
|
222
222
|
|
|
223
223
|
window.requestAnimationFrame(this.#render.bind(this));
|
|
224
224
|
|
|
225
|
-
this.#canvas.addEventListener('wheel', (event) => {
|
|
226
|
-
event.preventDefault();
|
|
227
|
-
this.zoom(Math.sign(event.deltaY));
|
|
228
|
-
}, false);
|
|
229
|
-
|
|
230
225
|
new MouseObserver(this.#canvas,
|
|
231
226
|
this.#mouseDragEvent.bind(this),
|
|
232
|
-
|
|
233
|
-
// Mouse move event
|
|
234
227
|
(mousePosition) => {
|
|
235
228
|
this.#mousePosition = mousePosition;
|
|
236
229
|
},
|
|
237
|
-
|
|
238
230
|
this.#clickStart.bind(this),
|
|
239
231
|
this.#clickEnd.bind(this),
|
|
240
232
|
this.#openContextMenu.bind(this),
|
|
241
|
-
this.#fileDrop.bind(this)
|
|
233
|
+
this.#fileDrop.bind(this),
|
|
234
|
+
(amount, anchor) => {
|
|
235
|
+
this.zoom(amount, anchor);
|
|
236
|
+
}
|
|
242
237
|
);
|
|
243
238
|
|
|
244
239
|
document.addEventListener(
|
|
@@ -331,20 +326,25 @@ export class NodeFlowGraph {
|
|
|
331
326
|
}
|
|
332
327
|
|
|
333
328
|
|
|
334
|
-
zoom(amount: number): void {
|
|
329
|
+
zoom(amount: number, anchor?: Vector2): void {
|
|
330
|
+
const anchorPos = anchor ?? this.#mousePosition;
|
|
335
331
|
|
|
336
332
|
let oldPos: Vector2 | undefined = undefined;
|
|
337
|
-
if (
|
|
338
|
-
oldPos = this.#sceenPositionToGraphPosition(
|
|
333
|
+
if (anchorPos !== undefined) {
|
|
334
|
+
oldPos = this.#sceenPositionToGraphPosition(anchorPos);
|
|
339
335
|
}
|
|
340
336
|
|
|
341
337
|
this.#camera.zoom += amount * this.#camera.zoom * 0.05;
|
|
342
338
|
|
|
343
|
-
if (!oldPos ||
|
|
339
|
+
if (!oldPos || anchorPos === undefined) {
|
|
344
340
|
return;
|
|
345
341
|
}
|
|
346
|
-
|
|
347
|
-
|
|
342
|
+
|
|
343
|
+
if (anchor !== undefined) {
|
|
344
|
+
this.#mousePosition = anchor;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const newPos = this.#sceenPositionToGraphPosition(anchorPos);
|
|
348
348
|
this.#camera.position.x += (newPos.x - oldPos.x) * this.#camera.zoom;
|
|
349
349
|
this.#camera.position.y += (newPos.y - oldPos.y) * this.#camera.zoom;
|
|
350
350
|
}
|
|
@@ -380,6 +380,70 @@ export class NodeFlowGraph {
|
|
|
380
380
|
return this.#mainNodeSubsystem.getNodes();
|
|
381
381
|
}
|
|
382
382
|
|
|
383
|
+
/**
|
|
384
|
+
* Returns currently selected nodes in stable graph order (same selection
|
|
385
|
+
* state used by Ctrl+click and box-select styling via FlowNode.selected()).
|
|
386
|
+
*/
|
|
387
|
+
getSelectedNodes(): Array<FlowNode> {
|
|
388
|
+
return this.#mainNodeSubsystem.getSelectedNodes();
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Adjusts camera position and zoom so all nodes fit in view (~10% padding).
|
|
393
|
+
* Falls back to camera.reset() when there are no nodes.
|
|
394
|
+
*/
|
|
395
|
+
centerOnGraph(): void {
|
|
396
|
+
const nodes = this.getNodes();
|
|
397
|
+
if (nodes.length === 0) {
|
|
398
|
+
this.#camera.reset();
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Identity camera so calculateBounds returns graph-space sizes.
|
|
403
|
+
const measureCamera = new Camera();
|
|
404
|
+
let minX = Infinity;
|
|
405
|
+
let minY = Infinity;
|
|
406
|
+
let maxX = -Infinity;
|
|
407
|
+
let maxY = -Infinity;
|
|
408
|
+
|
|
409
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
410
|
+
const bounds = nodes[i].calculateBounds(this.#ctx, measureCamera);
|
|
411
|
+
minX = Math.min(minX, bounds.Position.x);
|
|
412
|
+
minY = Math.min(minY, bounds.Position.y);
|
|
413
|
+
maxX = Math.max(maxX, bounds.Position.x + bounds.Size.x);
|
|
414
|
+
maxY = Math.max(maxY, bounds.Position.y + bounds.Size.y);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
let width = maxX - minX;
|
|
418
|
+
let height = maxY - minY;
|
|
419
|
+
if (width <= 0 || height <= 0) {
|
|
420
|
+
this.#camera.reset();
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const padX = width * 0.05;
|
|
425
|
+
const padY = height * 0.05;
|
|
426
|
+
minX -= padX;
|
|
427
|
+
minY -= padY;
|
|
428
|
+
width += padX * 2;
|
|
429
|
+
height += padY * 2;
|
|
430
|
+
|
|
431
|
+
const canvasWidth = this.#canvas.clientWidth;
|
|
432
|
+
const canvasHeight = this.#canvas.clientHeight;
|
|
433
|
+
if (canvasWidth <= 0 || canvasHeight <= 0) {
|
|
434
|
+
this.#camera.reset();
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const zoom = Math.min(canvasWidth / width, canvasHeight / height);
|
|
439
|
+
const centerX = minX + width / 2;
|
|
440
|
+
const centerY = minY + height / 2;
|
|
441
|
+
|
|
442
|
+
this.#camera.zoom = zoom;
|
|
443
|
+
this.#camera.position.x = canvasWidth / 2 - centerX * zoom;
|
|
444
|
+
this.#camera.position.y = canvasHeight / 2 - centerY * zoom;
|
|
445
|
+
}
|
|
446
|
+
|
|
383
447
|
connectedInputsNodeReferences(nodeIndex: number): Array<FlowNode> {
|
|
384
448
|
return this.#mainNodeSubsystem.connectedInputsNodeReferencesByIndex(nodeIndex);
|
|
385
449
|
}
|
|
@@ -447,26 +511,6 @@ export class NodeFlowGraph {
|
|
|
447
511
|
};
|
|
448
512
|
}
|
|
449
513
|
|
|
450
|
-
|
|
451
|
-
// Somethings wrong here. Needs more testing
|
|
452
|
-
// #fitView(): void {
|
|
453
|
-
// if (this.#nodes.length === 0) {
|
|
454
|
-
// return;
|
|
455
|
-
// }
|
|
456
|
-
|
|
457
|
-
// const curBox: Box = {
|
|
458
|
-
// Position: { x: 0, y: 0 },
|
|
459
|
-
// Size: { x: 0, y: 0 }
|
|
460
|
-
// };
|
|
461
|
-
// CopyBox(curBox, this.#nodes[0].calculateBounds(this.#ctx, this.#graphState.position, this.#graphState.scale));
|
|
462
|
-
|
|
463
|
-
// for (let i = 1; i < this.#nodes.length; i++) {
|
|
464
|
-
// ExpandBox(curBox, this.#nodes[i].calculateBounds(this.#ctx, this.#graphState.position, this.#graphState.scale));
|
|
465
|
-
// }
|
|
466
|
-
|
|
467
|
-
// CopyVector2(this.#graphState.position, curBox.Position);
|
|
468
|
-
// }
|
|
469
|
-
|
|
470
514
|
#clickEnd(): void {
|
|
471
515
|
this.currentView().clickEnd();
|
|
472
516
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
export * from './graph';
|
|
2
2
|
export * from './node';
|
|
3
3
|
|
|
4
|
+
import { ContextMenuConfig, ContextMenuItemConfig, ContextMenuItemState } from './contextMenu';
|
|
5
|
+
export { ContextMenuConfig, ContextMenuItemConfig, ContextMenuItemState };
|
|
6
|
+
|
|
4
7
|
import { Theme } from './theme';
|
|
5
8
|
export { Theme };
|
|
6
9
|
|
package/src/input.ts
CHANGED
|
@@ -1,20 +1,29 @@
|
|
|
1
|
-
import { CopyVector2, SubVector2, Vector2, Zero } from "./types/vector2";
|
|
1
|
+
import { CopyVector2, Distance, SubVector2, Vector2, Zero } from "./types/vector2";
|
|
2
|
+
|
|
3
|
+
const LONG_PRESS_MS = 500;
|
|
4
|
+
const LONG_PRESS_SLOP = 12;
|
|
5
|
+
const PINCH_SENSITIVITY = 50;
|
|
6
|
+
const SUPPRESS_MOUSE_MS = 600;
|
|
2
7
|
|
|
3
8
|
export class MouseObserver {
|
|
4
9
|
|
|
5
10
|
#clicked: boolean;
|
|
6
|
-
|
|
7
11
|
#lastTouch: Vector2;
|
|
8
|
-
|
|
9
|
-
#ele: HTMLElement
|
|
10
|
-
|
|
12
|
+
#ele: HTMLElement;
|
|
11
13
|
#lastMousePosition: Vector2;
|
|
12
14
|
|
|
13
|
-
#dragCallback: (delta: Vector2) => void
|
|
14
|
-
#moveCallback: (position: Vector2) => void
|
|
15
|
-
#clickStart: (position: Vector2, shiftOrCtrl: boolean) => void
|
|
16
|
-
#clickStop: () => void
|
|
17
|
-
#contextMenu: (position: Vector2) => void
|
|
15
|
+
#dragCallback: (delta: Vector2) => void;
|
|
16
|
+
#moveCallback: (position: Vector2) => void;
|
|
17
|
+
#clickStart: (position: Vector2, shiftOrCtrl: boolean) => void;
|
|
18
|
+
#clickStop: () => void;
|
|
19
|
+
#contextMenu: (position: Vector2) => void;
|
|
20
|
+
#zoomCallback: (amount: number, anchor: Vector2) => void;
|
|
21
|
+
|
|
22
|
+
#longPressTimer: ReturnType<typeof setTimeout> | null = null;
|
|
23
|
+
#longPressOrigin: Vector2 = Zero();
|
|
24
|
+
#longPressFired: boolean = false;
|
|
25
|
+
#pinchDist: number = 0;
|
|
26
|
+
#lastTouchAt: number = 0;
|
|
18
27
|
|
|
19
28
|
constructor(
|
|
20
29
|
ele: HTMLElement,
|
|
@@ -23,7 +32,8 @@ export class MouseObserver {
|
|
|
23
32
|
clickStart: (position: Vector2, shiftOrCtrl: boolean) => void,
|
|
24
33
|
clickStop: () => void,
|
|
25
34
|
contextMenu: (position: Vector2) => void,
|
|
26
|
-
fileDrop: (file: File) => void
|
|
35
|
+
fileDrop: (file: File) => void,
|
|
36
|
+
zoomCallback: (amount: number, anchor: Vector2) => void,
|
|
27
37
|
) {
|
|
28
38
|
this.#ele = ele;
|
|
29
39
|
this.#dragCallback = dragCallback;
|
|
@@ -31,123 +41,222 @@ export class MouseObserver {
|
|
|
31
41
|
this.#clickStart = clickStart;
|
|
32
42
|
this.#clickStop = clickStop;
|
|
33
43
|
this.#contextMenu = contextMenu;
|
|
44
|
+
this.#zoomCallback = zoomCallback;
|
|
34
45
|
|
|
35
46
|
this.#clicked = false;
|
|
36
47
|
this.#lastTouch = Zero();
|
|
37
48
|
this.#lastMousePosition = Zero();
|
|
38
49
|
|
|
39
|
-
|
|
40
|
-
ele.addEventListener('mousedown', this.#down.bind(this), false);
|
|
41
|
-
ele.addEventListener('touchstart', this.#touchDown.bind(this), false);
|
|
50
|
+
ele.style.touchAction = "none";
|
|
42
51
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
document.addEventListener('touchend', this.#up.bind(this), false);
|
|
52
|
+
ele.addEventListener("mousedown", this.#down.bind(this), false);
|
|
53
|
+
ele.addEventListener("touchstart", this.#touchStart.bind(this), { passive: false });
|
|
46
54
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
55
|
+
document.addEventListener("mouseup", this.#up.bind(this), false);
|
|
56
|
+
document.addEventListener("touchend", this.#touchEnd.bind(this), false);
|
|
57
|
+
document.addEventListener("touchcancel", this.#touchEnd.bind(this), false);
|
|
50
58
|
|
|
51
|
-
ele.addEventListener(
|
|
52
|
-
|
|
53
|
-
|
|
59
|
+
ele.addEventListener("mousemove", this.#move.bind(this), false);
|
|
60
|
+
ele.addEventListener("touchmove", this.#touchMove.bind(this), { passive: false });
|
|
61
|
+
|
|
62
|
+
ele.addEventListener("wheel", this.#wheel.bind(this), { passive: false });
|
|
54
63
|
|
|
64
|
+
ele.addEventListener("drop", (ev) => {
|
|
65
|
+
ev.preventDefault();
|
|
55
66
|
if (ev.dataTransfer?.items) {
|
|
56
|
-
// Use DataTransferItemList interface to access the file(s)
|
|
57
67
|
[...ev.dataTransfer.items].forEach((item, i) => {
|
|
58
|
-
// If dropped items aren't files, reject them
|
|
59
68
|
if (item.kind === "file") {
|
|
60
69
|
const file = item.getAsFile();
|
|
61
70
|
if (file) {
|
|
62
71
|
fileDrop(file);
|
|
63
|
-
console.log(file)
|
|
64
|
-
console.log(`… file[${i}].name = ${file.name}`);
|
|
65
72
|
}
|
|
66
73
|
}
|
|
67
74
|
});
|
|
68
|
-
} else if (ev.dataTransfer) {
|
|
69
|
-
// Use DataTransfer interface to access the file(s)
|
|
70
|
-
[...ev.dataTransfer.files].forEach((file, i) => {
|
|
71
|
-
console.log(`… file[${i}].name = ${file.name}`);
|
|
72
|
-
});
|
|
73
75
|
}
|
|
74
76
|
});
|
|
75
77
|
|
|
76
|
-
ele.addEventListener(
|
|
78
|
+
ele.addEventListener("dragover", (ev) => {
|
|
77
79
|
ev.preventDefault();
|
|
78
|
-
this.#moveCallback(this.#
|
|
79
|
-
// console.log(ev)
|
|
80
|
+
this.#moveCallback(this.#mousePos(ev));
|
|
80
81
|
});
|
|
81
82
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
83
|
+
ele.addEventListener("contextmenu", (evt) => {
|
|
84
|
+
evt.preventDefault();
|
|
85
|
+
// Touch uses long-press; ignore synthetic browser contextmenu after touch.
|
|
86
|
+
if (Date.now() - this.#lastTouchAt < SUPPRESS_MOUSE_MS) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
this.#contextMenu(this.#mousePos(evt));
|
|
86
90
|
}, false);
|
|
87
91
|
}
|
|
88
92
|
|
|
89
|
-
#
|
|
90
|
-
|
|
93
|
+
#mousePos(event: MouseEvent | DragEvent | WheelEvent): Vector2 {
|
|
94
|
+
const rect = this.#ele.getBoundingClientRect();
|
|
91
95
|
return {
|
|
92
96
|
x: event.clientX - rect.left,
|
|
93
|
-
y: event.clientY - rect.top
|
|
97
|
+
y: event.clientY - rect.top,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
#touchPos(touch: Touch): Vector2 {
|
|
102
|
+
const rect = this.#ele.getBoundingClientRect();
|
|
103
|
+
return {
|
|
104
|
+
x: touch.clientX - rect.left,
|
|
105
|
+
y: touch.clientY - rect.top,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
#fromTouch(): boolean {
|
|
110
|
+
return Date.now() - this.#lastTouchAt < SUPPRESS_MOUSE_MS;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
#clearLongPress(): void {
|
|
114
|
+
if (this.#longPressTimer !== null) {
|
|
115
|
+
clearTimeout(this.#longPressTimer);
|
|
116
|
+
this.#longPressTimer = null;
|
|
94
117
|
}
|
|
95
118
|
}
|
|
96
119
|
|
|
97
|
-
#
|
|
98
|
-
|
|
120
|
+
#wheel(event: WheelEvent): void {
|
|
121
|
+
event.preventDefault();
|
|
122
|
+
let dy = event.deltaY;
|
|
123
|
+
if (event.deltaMode === WheelEvent.DOM_DELTA_LINE) {
|
|
124
|
+
dy *= 16;
|
|
125
|
+
} else if (event.deltaMode === WheelEvent.DOM_DELTA_PAGE) {
|
|
126
|
+
dy *= this.#ele.clientHeight;
|
|
127
|
+
}
|
|
128
|
+
// Scroll up zooms in (matches Figma / Maps).
|
|
129
|
+
const amount = -dy / 100;
|
|
130
|
+
if (amount === 0) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const anchor = this.#mousePos(event);
|
|
134
|
+
this.#moveCallback(anchor);
|
|
135
|
+
this.#zoomCallback(amount, anchor);
|
|
136
|
+
}
|
|
99
137
|
|
|
138
|
+
#move(event: MouseEvent): void {
|
|
139
|
+
if (this.#fromTouch()) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const pos = this.#mousePos(event);
|
|
100
143
|
if (this.#clicked) {
|
|
101
144
|
const delta = Zero();
|
|
102
145
|
SubVector2(delta, pos, this.#lastMousePosition);
|
|
103
|
-
this.#dragCallback(delta)
|
|
146
|
+
this.#dragCallback(delta);
|
|
104
147
|
}
|
|
105
|
-
|
|
106
148
|
this.#moveCallback(pos);
|
|
107
149
|
CopyVector2(this.#lastMousePosition, pos);
|
|
108
150
|
}
|
|
109
151
|
|
|
110
|
-
#
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
152
|
+
#down(event: MouseEvent): void {
|
|
153
|
+
if (event.button !== 0 || this.#fromTouch()) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
this.#clicked = true;
|
|
157
|
+
this.#clickStart(this.#mousePos(event), event.ctrlKey || event.shiftKey);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
#up(event: MouseEvent): void {
|
|
161
|
+
if (event.button !== 0 || this.#fromTouch()) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
this.#clicked = false;
|
|
165
|
+
this.#clickStop();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
#touchStart(event: TouchEvent): void {
|
|
169
|
+
this.#lastTouchAt = Date.now();
|
|
170
|
+
event.preventDefault();
|
|
171
|
+
|
|
172
|
+
if (event.touches.length >= 2) {
|
|
173
|
+
this.#clearLongPress();
|
|
174
|
+
this.#clicked = false;
|
|
175
|
+
this.#pinchDist = Distance(
|
|
176
|
+
this.#touchPos(event.touches[0]),
|
|
177
|
+
this.#touchPos(event.touches[1]),
|
|
178
|
+
);
|
|
179
|
+
return;
|
|
115
180
|
}
|
|
181
|
+
|
|
182
|
+
if (event.touches.length !== 1) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const pos = this.#touchPos(event.touches[0]);
|
|
187
|
+
CopyVector2(this.#longPressOrigin, pos);
|
|
188
|
+
CopyVector2(this.#lastTouch, pos);
|
|
189
|
+
this.#longPressFired = false;
|
|
190
|
+
this.#pinchDist = 0;
|
|
191
|
+
|
|
192
|
+
this.#clearLongPress();
|
|
193
|
+
const x = pos.x;
|
|
194
|
+
const y = pos.y;
|
|
195
|
+
this.#longPressTimer = setTimeout(() => {
|
|
196
|
+
this.#longPressTimer = null;
|
|
197
|
+
this.#longPressFired = true;
|
|
198
|
+
this.#clicked = false;
|
|
199
|
+
this.#clickStop();
|
|
200
|
+
this.#contextMenu({ x, y });
|
|
201
|
+
}, LONG_PRESS_MS);
|
|
202
|
+
|
|
203
|
+
this.#clicked = true;
|
|
116
204
|
this.#moveCallback(pos);
|
|
205
|
+
this.#clickStart(pos, false);
|
|
206
|
+
}
|
|
117
207
|
|
|
118
|
-
|
|
208
|
+
#touchMove(event: TouchEvent): void {
|
|
209
|
+
this.#lastTouchAt = Date.now();
|
|
210
|
+
event.preventDefault();
|
|
211
|
+
|
|
212
|
+
if (event.touches.length >= 2) {
|
|
213
|
+
this.#clearLongPress();
|
|
214
|
+
this.#clicked = false;
|
|
215
|
+
const a = this.#touchPos(event.touches[0]);
|
|
216
|
+
const b = this.#touchPos(event.touches[1]);
|
|
217
|
+
const dist = Distance(a, b);
|
|
218
|
+
const anchor = { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
|
|
219
|
+
if (this.#pinchDist > 0) {
|
|
220
|
+
this.#moveCallback(anchor);
|
|
221
|
+
this.#zoomCallback((dist - this.#pinchDist) / PINCH_SENSITIVITY, anchor);
|
|
222
|
+
}
|
|
223
|
+
this.#pinchDist = dist;
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (event.touches.length !== 1) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const pos = this.#touchPos(event.touches[0]);
|
|
232
|
+
if (this.#longPressTimer !== null && Distance(pos, this.#longPressOrigin) > LONG_PRESS_SLOP) {
|
|
233
|
+
this.#clearLongPress();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
this.#moveCallback(pos);
|
|
237
|
+
if (this.#clicked && !this.#longPressFired) {
|
|
119
238
|
this.#dragCallback({
|
|
120
239
|
x: pos.x - this.#lastTouch.x,
|
|
121
240
|
y: pos.y - this.#lastTouch.y,
|
|
122
241
|
});
|
|
123
242
|
}
|
|
124
|
-
|
|
125
243
|
CopyVector2(this.#lastTouch, pos);
|
|
126
244
|
}
|
|
127
245
|
|
|
128
|
-
#
|
|
129
|
-
|
|
130
|
-
|
|
246
|
+
#touchEnd(event: TouchEvent): void {
|
|
247
|
+
this.#lastTouchAt = Date.now();
|
|
248
|
+
this.#clearLongPress();
|
|
249
|
+
|
|
250
|
+
if (event.touches.length >= 1) {
|
|
251
|
+
this.#pinchDist = 0;
|
|
131
252
|
return;
|
|
132
253
|
}
|
|
133
|
-
this.#clicked = true;
|
|
134
|
-
this.#clickStart(this.#mousePosition(event), event.ctrlKey || event.shiftKey);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
#touchDown(event: TouchEvent) {
|
|
138
|
-
this.#clicked = true;
|
|
139
|
-
const rect = this.#ele.getBoundingClientRect();
|
|
140
|
-
this.#lastTouch.x = event.touches[0].clientX - rect.left;
|
|
141
|
-
this.#lastTouch.y = event.touches[0].clientY - rect.top;
|
|
142
|
-
this.#clickStart(this.#lastTouch, false);
|
|
143
|
-
}
|
|
144
254
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
if (
|
|
148
|
-
|
|
255
|
+
this.#pinchDist = 0;
|
|
256
|
+
this.#clicked = false;
|
|
257
|
+
if (!this.#longPressFired) {
|
|
258
|
+
this.#clickStop();
|
|
149
259
|
}
|
|
150
|
-
this.#
|
|
151
|
-
this.#clickStop();
|
|
260
|
+
this.#longPressFired = false;
|
|
152
261
|
}
|
|
153
|
-
}
|
|
262
|
+
}
|