@elicdavis/node-flow 0.1.12 → 0.1.14
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/dist/web/NodeFlow.js +4 -4
- package/dist/web/NodeFlow.js.map +3 -3
- package/package.json +1 -1
- package/src/camera.ts +23 -1
- package/src/connection.ts +33 -4
- package/src/contextMenu.ts +35 -3
- package/src/graph.ts +97 -38
- 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/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
|
+
}
|
package/src/node.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Port, PortConfig, PortType } from "./port";
|
|
2
2
|
import { FontWeight, TextStyle, TextStyleConfig, TextStyleFallback } from "./styles/text";
|
|
3
3
|
import { Box, InBox } from "./types/box";
|
|
4
|
-
import { CopyVector2, Distance,
|
|
4
|
+
import { CopyVector2, Distance, Vector2, Zero } from "./types/vector2";
|
|
5
5
|
import { Widget } from './widgets/widget';
|
|
6
6
|
import { List } from './types/list';
|
|
7
7
|
import { BoxStyle, BoxStyleConfig, BoxStyleWithFallback } from "./styles/box";
|
|
@@ -784,55 +784,56 @@ export class FlowNode {
|
|
|
784
784
|
|
|
785
785
|
public calculateBounds(ctx: CanvasRenderingContext2D, camera: Camera): Box {
|
|
786
786
|
const tempMeasurement = Zero();
|
|
787
|
-
|
|
788
|
-
const
|
|
787
|
+
const zoom = camera.zoom;
|
|
788
|
+
const scaledPadding = this.#padding * zoom;
|
|
789
|
+
const doubleScaledPadding = scaledPadding * 2;
|
|
790
|
+
const scaledElementSpacing = this.#elementSpacing * zoom;
|
|
789
791
|
|
|
790
792
|
const screenSpacePosition = Zero();
|
|
791
793
|
camera.graphSpaceToScreenSpace(this.#position, screenSpacePosition);
|
|
792
794
|
|
|
793
795
|
const size = Zero();
|
|
794
|
-
this.#title.size(ctx,
|
|
796
|
+
this.#title.size(ctx, zoom, size);
|
|
795
797
|
|
|
796
798
|
const subtitleSize = Zero();
|
|
797
|
-
this.#subTitle.size(ctx,
|
|
799
|
+
this.#subTitle.size(ctx, zoom, subtitleSize);
|
|
798
800
|
size.x = Math.max(size.x, subtitleSize.x);
|
|
799
801
|
|
|
800
802
|
if (this.#infoText !== "") {
|
|
801
803
|
size.x += (size.y * 4);
|
|
802
804
|
}
|
|
803
805
|
|
|
804
|
-
size.x +=
|
|
805
|
-
size.y +=
|
|
806
|
+
size.x += doubleScaledPadding;
|
|
807
|
+
size.y += doubleScaledPadding + (scaledElementSpacing * this.#input.length);
|
|
806
808
|
|
|
807
809
|
for (let i = 0; i < this.#input.length; i++) {
|
|
808
810
|
const port = this.#input[i];
|
|
809
|
-
this.#portTextStyle.measure(ctx,
|
|
811
|
+
this.#portTextStyle.measure(ctx, zoom, port.getDisplayName(), tempMeasurement);
|
|
810
812
|
size.y += tempMeasurement.y;
|
|
811
|
-
size.x = Math.max(size.x, tempMeasurement.x +
|
|
813
|
+
size.x = Math.max(size.x, tempMeasurement.x + doubleScaledPadding)
|
|
812
814
|
}
|
|
813
815
|
|
|
814
|
-
size.y += (
|
|
816
|
+
size.y += (scaledElementSpacing * this.#output.length);
|
|
815
817
|
|
|
816
818
|
for (let i = 0; i < this.#output.length; i++) {
|
|
817
819
|
const port = this.#output[i];
|
|
818
|
-
this.#portTextStyle.measure(ctx,
|
|
820
|
+
this.#portTextStyle.measure(ctx, zoom, port.getDisplayName(), tempMeasurement);
|
|
819
821
|
size.y += tempMeasurement.y;
|
|
820
|
-
size.x = Math.max(size.x, tempMeasurement.x +
|
|
822
|
+
size.x = Math.max(size.x, tempMeasurement.x + doubleScaledPadding)
|
|
821
823
|
}
|
|
822
824
|
|
|
823
|
-
size.y += (
|
|
825
|
+
size.y += (scaledElementSpacing * this.#widgets.length);
|
|
824
826
|
for (let i = 0; i < this.#widgets.length; i++) {
|
|
825
827
|
const element = this.#widgets[i];
|
|
826
828
|
const eleSize = element.Size();
|
|
827
|
-
size.y += eleSize.y
|
|
828
|
-
size.x = Math.max(size.x, (eleSize.x) +
|
|
829
|
+
size.y += eleSize.y * zoom;
|
|
830
|
+
size.x = Math.max(size.x, (eleSize.x * zoom) + doubleScaledPadding)
|
|
829
831
|
}
|
|
830
832
|
|
|
831
833
|
// Add some padding at the end!
|
|
832
|
-
size.y +=
|
|
834
|
+
size.y += scaledElementSpacing
|
|
833
835
|
|
|
834
|
-
size.x = Math.max(size.x, MINIMUM_NODE_WIDTH)
|
|
835
|
-
ScaleVector(size, camera.zoom);
|
|
836
|
+
size.x = Math.max(size.x, MINIMUM_NODE_WIDTH * zoom)
|
|
836
837
|
|
|
837
838
|
return {
|
|
838
839
|
Position: screenSpacePosition,
|
package/src/nodes/subsystem.ts
CHANGED
|
@@ -94,6 +94,8 @@ export class NodeSubsystem {
|
|
|
94
94
|
|
|
95
95
|
#boxSelectStyle: BoxStyle;
|
|
96
96
|
|
|
97
|
+
#lastCtx: CanvasRenderingContext2D | null;
|
|
98
|
+
|
|
97
99
|
// LISTENERS ==============================================================
|
|
98
100
|
|
|
99
101
|
#nodeRemovedCallbacks: Array<NodeRemovedCallback>;
|
|
@@ -122,6 +124,7 @@ export class NodeSubsystem {
|
|
|
122
124
|
this.#boxSelectStart_graphSpace = Zero();
|
|
123
125
|
this.#boxSelectEnd_graphSpace = Zero();
|
|
124
126
|
this.#boxSelectionNodes = new List<number>();
|
|
127
|
+
this.#lastCtx = null;
|
|
125
128
|
this.#boxSelectStyle = new BoxStyle({
|
|
126
129
|
border: {
|
|
127
130
|
color: Theme.BoxSelect.Color,
|
|
@@ -160,6 +163,31 @@ export class NodeSubsystem {
|
|
|
160
163
|
|
|
161
164
|
clickStart(mousePosition: Vector2, camera: Camera, ctrlKey: boolean): boolean {
|
|
162
165
|
this.#boxSelect = false;
|
|
166
|
+
this.#nodesGrabbed.Clear();
|
|
167
|
+
|
|
168
|
+
// Touch has no continuous hover — hit-test at the click position.
|
|
169
|
+
if (this.#lastCtx !== null) {
|
|
170
|
+
this.#portHovering = null;
|
|
171
|
+
this.#widgetHovering = null;
|
|
172
|
+
this.#nodeHovering = -1;
|
|
173
|
+
for (let i = 0; i < this.#nodes.length; i++) {
|
|
174
|
+
const intersection = this.#nodes[i].inBounds(this.#lastCtx, camera, mousePosition);
|
|
175
|
+
if (intersection.Node !== undefined && intersection.PortIndex === undefined && intersection.Widget === undefined) {
|
|
176
|
+
this.#nodeHovering = i;
|
|
177
|
+
}
|
|
178
|
+
if (intersection.Widget !== undefined) {
|
|
179
|
+
this.#widgetHovering = intersection.Widget;
|
|
180
|
+
}
|
|
181
|
+
if (intersection.Port !== undefined && intersection.Node !== undefined && intersection.PortIndex !== undefined && intersection.PortIsInput !== undefined) {
|
|
182
|
+
this.#portHovering = {
|
|
183
|
+
Index: intersection.PortIndex,
|
|
184
|
+
Node: intersection.Node,
|
|
185
|
+
Port: intersection.Port,
|
|
186
|
+
InputPort: intersection.PortIsInput
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
163
191
|
|
|
164
192
|
let hoveringSomething = false;
|
|
165
193
|
if (this.#nodeHovering > -1) {
|
|
@@ -387,6 +415,20 @@ export class NodeSubsystem {
|
|
|
387
415
|
return this.#nodes;
|
|
388
416
|
}
|
|
389
417
|
|
|
418
|
+
/**
|
|
419
|
+
* Returns currently selected nodes in stable graph order (same selection
|
|
420
|
+
* state used by Ctrl+click and box-select styling via FlowNode.selected()).
|
|
421
|
+
*/
|
|
422
|
+
getSelectedNodes(): Array<FlowNode> {
|
|
423
|
+
const selected = new Array<FlowNode>();
|
|
424
|
+
for (let i = 0; i < this.#nodes.length; i++) {
|
|
425
|
+
if (this.#nodes[i].selected()) {
|
|
426
|
+
selected.push(this.#nodes[i]);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
return selected;
|
|
430
|
+
}
|
|
431
|
+
|
|
390
432
|
/**
|
|
391
433
|
* Returns all nodes who are connected to the inputs of the
|
|
392
434
|
* node in question
|
|
@@ -747,6 +789,7 @@ export class NodeSubsystem {
|
|
|
747
789
|
}
|
|
748
790
|
|
|
749
791
|
render(ctx: CanvasRenderingContext2D, camera: Camera, mousePosition: Vector2 | undefined): RenderResults | undefined {
|
|
792
|
+
this.#lastCtx = ctx;
|
|
750
793
|
this.#cursor = CursorStyle.Default;
|
|
751
794
|
TimeExecution("Render_Connections", () => {
|
|
752
795
|
this.#renderConnections(ctx, camera, mousePosition);
|