@elicdavis/node-flow 0.1.19 → 0.2.0
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 +386 -227
- package/dist/index.d.ts +386 -227
- package/dist/index.js +1884 -1076
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1867 -1075
- package/dist/index.mjs.map +1 -1
- package/index.html +1134 -329
- package/package.json +1 -1
- package/src/connection.ts +30 -5
- package/src/contextMenu.ts +66 -34
- package/src/graph.ts +12 -10
- package/src/index.ts +26 -12
- package/src/markdown/entry.ts +11 -15
- package/src/markdown/syntaxParser.ts +2 -1
- package/src/node.ts +162 -111
- package/src/nodes/subsystem.ts +123 -29
- package/src/notes/note.ts +9 -1
- package/src/organize.ts +540 -111
- package/src/popup.ts +4 -2
- package/src/port.ts +83 -6
- package/src/styles/bevel.ts +31 -0
- package/src/styles/cursor.ts +1 -0
- package/src/styles/text.ts +4 -4
- package/src/theme.ts +179 -92
- package/src/types/text.ts +5 -1
- package/src/utils/color.ts +1 -1
- package/src/widgets/button.ts +52 -107
- package/src/widgets/color.ts +91 -150
- package/src/widgets/factory.ts +89 -46
- package/src/widgets/field.ts +99 -0
- package/src/widgets/image.ts +107 -165
- package/src/widgets/inplace.ts +84 -0
- package/src/widgets/number.ts +122 -154
- package/src/widgets/row.ts +138 -0
- package/src/widgets/slider.ts +101 -216
- package/src/widgets/string.ts +39 -130
- package/src/widgets/text.ts +81 -92
- package/src/widgets/toggle.ts +62 -207
- package/src/widgets/value.ts +127 -0
- package/src/widgets/widget.ts +180 -34
- package/test/context_menu.test.ts +82 -0
- package/test/cursor.test.ts +17 -0
- package/test/hidden_port.test.ts +125 -0
- package/test/hover_after_removal.test.ts +72 -0
- package/test/markdown.test.ts +2 -2
- package/test/markdown_code_block.test.ts +58 -0
- package/test/multi_input.test.ts +124 -0
- package/test/organize.test.ts +117 -1
- package/test/property_binding.test.ts +49 -0
- package/test/support.ts +175 -0
- package/test/theme.test.ts +86 -0
- package/test/widget_click.test.ts +111 -97
- package/test/widget_construction.test.ts +5 -4
- package/test/widgets.test.ts +333 -0
- package/test/wire_follows_camera.test.ts +57 -0
package/src/nodes/subsystem.ts
CHANGED
|
@@ -6,16 +6,16 @@ import { FlowNode, NodeState } from "../node";
|
|
|
6
6
|
import { Organize } from "../organize";
|
|
7
7
|
import { PassSubsystem } from "../pass/subsystem";
|
|
8
8
|
import { TimeExecution } from "../performance";
|
|
9
|
-
import { Port, PortType, portsCompatible } from "../port";
|
|
9
|
+
import { arrayInsertIndex, Port, PortType, portsCompatible } from "../port";
|
|
10
10
|
import { requestRender } from "../render_scheduler";
|
|
11
11
|
import { BoxStyle } from "../styles/box";
|
|
12
12
|
import { CursorStyle } from "../styles/cursor";
|
|
13
13
|
import { Theme } from "../theme";
|
|
14
|
-
import { Box, BoxIntersection } from "../types/box";
|
|
14
|
+
import { Box, BoxIntersection, InBox } from "../types/box";
|
|
15
15
|
import { List } from "../types/list";
|
|
16
16
|
import { Pool, VectorPool } from "../types/pool";
|
|
17
17
|
import { AddVector2, CopyVector2, SubVector2, Vector2, Zero } from "../types/vector2";
|
|
18
|
-
import { Widget } from "../widgets/widget";
|
|
18
|
+
import { Widget, WidgetPointer } from "../widgets/widget";
|
|
19
19
|
import { NodeCreatedCallback, NodeFactory, NodeFactoryConfig } from "./factory";
|
|
20
20
|
import { Publisher } from "./publisher";
|
|
21
21
|
|
|
@@ -108,6 +108,12 @@ export class NodeSubsystem {
|
|
|
108
108
|
|
|
109
109
|
#widgetCurrentlyClicking: Widget | null;
|
|
110
110
|
|
|
111
|
+
#widgetHoveringBox: Box | null = null;
|
|
112
|
+
|
|
113
|
+
#capturedWidgetBox: Box | null = null;
|
|
114
|
+
|
|
115
|
+
#capturedPointer: Vector2 = Zero();
|
|
116
|
+
|
|
111
117
|
#cursor: CursorStyle;
|
|
112
118
|
|
|
113
119
|
#nodeFactory: NodeFactory;
|
|
@@ -120,10 +126,11 @@ export class NodeSubsystem {
|
|
|
120
126
|
|
|
121
127
|
#boxSelectionNodes: List<number>;
|
|
122
128
|
|
|
123
|
-
#boxSelectStyle: BoxStyle;
|
|
124
129
|
|
|
125
130
|
#lastCtx: CanvasRenderingContext2D | null;
|
|
126
131
|
|
|
132
|
+
#lastMousePosition: Vector2 | undefined;
|
|
133
|
+
|
|
127
134
|
// LISTENERS ==============================================================
|
|
128
135
|
|
|
129
136
|
#nodeRemovedCallbacks: Array<NodeRemovedCallback>;
|
|
@@ -153,14 +160,6 @@ export class NodeSubsystem {
|
|
|
153
160
|
this.#boxSelectEnd_graphSpace = Zero();
|
|
154
161
|
this.#boxSelectionNodes = new List<number>();
|
|
155
162
|
this.#lastCtx = null;
|
|
156
|
-
this.#boxSelectStyle = new BoxStyle({
|
|
157
|
-
border: {
|
|
158
|
-
color: Theme.BoxSelect.Color,
|
|
159
|
-
size: Theme.BoxSelect.Size,
|
|
160
|
-
},
|
|
161
|
-
color: "rgba(0,0,0,0)",
|
|
162
|
-
radius: Theme.BoxSelect.Radius
|
|
163
|
-
});
|
|
164
163
|
}
|
|
165
164
|
|
|
166
165
|
public addPublisher(identifier: string, publisher: Publisher): void {
|
|
@@ -205,6 +204,7 @@ export class NodeSubsystem {
|
|
|
205
204
|
}
|
|
206
205
|
if (intersection.Widget !== undefined) {
|
|
207
206
|
this.#widgetHovering = intersection.Widget;
|
|
207
|
+
this.#widgetHoveringBox = intersection.WidgetBox ?? null;
|
|
208
208
|
}
|
|
209
209
|
if (intersection.Port !== undefined && intersection.Node !== undefined && intersection.PortIndex !== undefined && intersection.PortIsInput !== undefined) {
|
|
210
210
|
this.#portHovering = {
|
|
@@ -231,8 +231,15 @@ export class NodeSubsystem {
|
|
|
231
231
|
}
|
|
232
232
|
|
|
233
233
|
if (this.#widgetHovering !== null) {
|
|
234
|
-
this.#widgetHovering.
|
|
235
|
-
|
|
234
|
+
if (!this.#widgetHovering.Disabled?.()) {
|
|
235
|
+
this.#widgetCurrentlyClicking = this.#widgetHovering;
|
|
236
|
+
this.#capturedWidgetBox = this.#widgetHoveringBox === null ? null : {
|
|
237
|
+
Position: { x: this.#widgetHoveringBox.Position.x, y: this.#widgetHoveringBox.Position.y },
|
|
238
|
+
Size: { x: this.#widgetHoveringBox.Size.x, y: this.#widgetHoveringBox.Size.y },
|
|
239
|
+
};
|
|
240
|
+
CopyVector2(this.#capturedPointer, mousePosition);
|
|
241
|
+
this.#widgetCurrentlyClicking.PointerDown?.(this.#widgetPointer());
|
|
242
|
+
}
|
|
236
243
|
hoveringSomething = true;
|
|
237
244
|
}
|
|
238
245
|
|
|
@@ -252,12 +259,12 @@ export class NodeSubsystem {
|
|
|
252
259
|
}
|
|
253
260
|
|
|
254
261
|
if (this.#portHovering.InputPort) {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
262
|
+
const port = this.#portHovering.Port;
|
|
263
|
+
const wires = port.connections();
|
|
264
|
+
const picked = wires.find((wire) => wire.mouseOverPort(mousePosition) === port) ?? wires[0];
|
|
265
|
+
if (picked !== undefined) {
|
|
266
|
+
picked.clearInput();
|
|
267
|
+
this.#connectionSelected = picked;
|
|
261
268
|
}
|
|
262
269
|
} else {
|
|
263
270
|
let inNode: FlowNode | null = this.#portHovering.Node
|
|
@@ -281,7 +288,21 @@ export class NodeSubsystem {
|
|
|
281
288
|
return true;
|
|
282
289
|
}
|
|
283
290
|
|
|
291
|
+
#widgetPointer(): WidgetPointer {
|
|
292
|
+
return {
|
|
293
|
+
position: { x: this.#capturedPointer.x, y: this.#capturedPointer.y },
|
|
294
|
+
inside: this.#capturedWidgetBox !== null && InBox(this.#capturedWidgetBox, this.#capturedPointer),
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
284
298
|
mouseDragEvent(delta: Vector2, scale: number): boolean {
|
|
299
|
+
if (this.#widgetCurrentlyClicking !== null) {
|
|
300
|
+
this.#capturedPointer.x += delta.x;
|
|
301
|
+
this.#capturedPointer.y += delta.y;
|
|
302
|
+
this.#widgetCurrentlyClicking.PointerMove?.(this.#widgetPointer());
|
|
303
|
+
return true;
|
|
304
|
+
}
|
|
305
|
+
|
|
285
306
|
let nodeMoved = false;
|
|
286
307
|
|
|
287
308
|
VectorPool.run(() => {
|
|
@@ -421,13 +442,20 @@ export class NodeSubsystem {
|
|
|
421
442
|
|
|
422
443
|
#firstCompatiblePort(node: FlowNode, side: PortType, against: Port): number {
|
|
423
444
|
const count = side === PortType.Input ? node.inputs() : node.outputs();
|
|
445
|
+
let firstHidden = -1;
|
|
424
446
|
for (let i = 0; i < count; i++) {
|
|
425
447
|
const port = side === PortType.Input ? node.inputPort(i) : node.outputPort(i);
|
|
426
|
-
if (portsCompatible(port, against)) {
|
|
448
|
+
if (!portsCompatible(port, against)) {
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
if (!port.hidden()) {
|
|
427
452
|
return i;
|
|
428
453
|
}
|
|
454
|
+
if (firstHidden === -1) {
|
|
455
|
+
firstHidden = i;
|
|
456
|
+
}
|
|
429
457
|
}
|
|
430
|
-
return
|
|
458
|
+
return firstHidden;
|
|
431
459
|
}
|
|
432
460
|
|
|
433
461
|
clickEnd(): void {
|
|
@@ -448,10 +476,11 @@ export class NodeSubsystem {
|
|
|
448
476
|
}
|
|
449
477
|
|
|
450
478
|
if (this.#widgetCurrentlyClicking !== null) {
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
}
|
|
479
|
+
const widget = this.#widgetCurrentlyClicking;
|
|
480
|
+
const pointer = this.#widgetPointer();
|
|
454
481
|
this.#widgetCurrentlyClicking = null;
|
|
482
|
+
this.#capturedWidgetBox = null;
|
|
483
|
+
widget.PointerUp?.(pointer);
|
|
455
484
|
}
|
|
456
485
|
|
|
457
486
|
if (this.#connectionSelected === null) {
|
|
@@ -518,7 +547,7 @@ export class NodeSubsystem {
|
|
|
518
547
|
// Remove connection from this subsystem
|
|
519
548
|
this.#removeConnection(inputPort.connections()[0], false);
|
|
520
549
|
}
|
|
521
|
-
conn.setInput(this.#portHovering.Node, this.#portHovering.Index, replace);
|
|
550
|
+
conn.setInput(this.#portHovering.Node, this.#portHovering.Index, replace, this.#insertIndex());
|
|
522
551
|
|
|
523
552
|
} else {
|
|
524
553
|
conn.setOutput(this.#portHovering.Node, this.#portHovering.Index);
|
|
@@ -684,6 +713,16 @@ export class NodeSubsystem {
|
|
|
684
713
|
this.#removeNodeConnections(nodeIndex);
|
|
685
714
|
const node = this.#nodes[nodeIndex];
|
|
686
715
|
this.#nodes.splice(nodeIndex, 1);
|
|
716
|
+
|
|
717
|
+
// Hover is only recomputed on render, which is lazy, so it must follow the splice.
|
|
718
|
+
if (this.#nodeHovering === nodeIndex) {
|
|
719
|
+
this.#nodeHovering = -1;
|
|
720
|
+
} else if (this.#nodeHovering > nodeIndex) {
|
|
721
|
+
this.#nodeHovering--;
|
|
722
|
+
}
|
|
723
|
+
if (this.#portHovering?.Node === node) {
|
|
724
|
+
this.#portHovering = null;
|
|
725
|
+
}
|
|
687
726
|
requestRender();
|
|
688
727
|
for (let i = 0; i < this.#nodeRemovedCallbacks.length; i++) {
|
|
689
728
|
this.#nodeRemovedCallbacks[i](node);
|
|
@@ -945,9 +984,11 @@ export class NodeSubsystem {
|
|
|
945
984
|
}
|
|
946
985
|
|
|
947
986
|
if (intersection.Widget !== undefined) {
|
|
948
|
-
// console.log(i, this.#nodes[i].title(), "widget")
|
|
949
987
|
this.#widgetHovering = intersection.Widget
|
|
950
|
-
this.#
|
|
988
|
+
this.#widgetHoveringBox = intersection.WidgetBox ?? null;
|
|
989
|
+
this.#cursor = intersection.Widget.Disabled?.()
|
|
990
|
+
? CursorStyle.Default
|
|
991
|
+
: intersection.Widget.Cursor?.() ?? CursorStyle.Pointer;
|
|
951
992
|
}
|
|
952
993
|
|
|
953
994
|
if (intersection.Port !== undefined && intersection.Node !== undefined && intersection.PortIndex !== undefined && intersection.PortIsInput !== undefined) {
|
|
@@ -978,6 +1019,10 @@ export class NodeSubsystem {
|
|
|
978
1019
|
|
|
979
1020
|
this.#nodes[i].render(ctx, camera, state, mousePosition, this.#postProcessPass);
|
|
980
1021
|
}
|
|
1022
|
+
|
|
1023
|
+
if (this.#widgetCurrentlyClicking !== null) {
|
|
1024
|
+
this.#cursor = this.#widgetCurrentlyClicking.Cursor?.() ?? CursorStyle.Pointer;
|
|
1025
|
+
}
|
|
981
1026
|
}
|
|
982
1027
|
|
|
983
1028
|
#boxSelectionScreenspaceBox(camera: Camera): Box {
|
|
@@ -991,20 +1036,65 @@ export class NodeSubsystem {
|
|
|
991
1036
|
return box;
|
|
992
1037
|
}
|
|
993
1038
|
|
|
1039
|
+
/**
|
|
1040
|
+
* Where a wire being dropped on the hovered array input would go, or
|
|
1041
|
+
* undefined when it is not over one.
|
|
1042
|
+
*/
|
|
1043
|
+
#insertIndex(): number | undefined {
|
|
1044
|
+
const hovering = this.#portHovering;
|
|
1045
|
+
if (hovering === null || !hovering.InputPort || this.#lastMousePosition === undefined
|
|
1046
|
+
|| hovering.Port.getPortType() !== PortType.InputArray) {
|
|
1047
|
+
return undefined;
|
|
1048
|
+
}
|
|
1049
|
+
const box = hovering.Node.inputPortPosition(hovering.Index);
|
|
1050
|
+
return arrayInsertIndex(box, hovering.Port.connections().length, this.#lastMousePosition.y);
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
#renderInsertMarker(ctx: CanvasRenderingContext2D, camera: Camera): void {
|
|
1054
|
+
const hovering = this.#portHovering;
|
|
1055
|
+
if (this.#connectionSelected === null || this.#connectionSelected.inPort() !== null || hovering === null) {
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
const index = this.#insertIndex();
|
|
1059
|
+
const count = hovering.Port.connections().length;
|
|
1060
|
+
if (index === undefined || count === 0) {
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
const box = hovering.Node.inputPortPosition(hovering.Index);
|
|
1064
|
+
const y = box.Position.y + (index * box.Size.y / count);
|
|
1065
|
+
const overhang = 6 * camera.zoom;
|
|
1066
|
+
ctx.save();
|
|
1067
|
+
ctx.strokeStyle = "#ffffff";
|
|
1068
|
+
ctx.lineWidth = Math.max(1, 2 * camera.zoom);
|
|
1069
|
+
ctx.beginPath();
|
|
1070
|
+
ctx.moveTo(box.Position.x - overhang, y);
|
|
1071
|
+
ctx.lineTo(box.Position.x + box.Size.x + overhang, y);
|
|
1072
|
+
ctx.stroke();
|
|
1073
|
+
ctx.restore();
|
|
1074
|
+
}
|
|
1075
|
+
|
|
994
1076
|
render(ctx: CanvasRenderingContext2D, camera: Camera, mousePosition: Vector2 | undefined): RenderResults | undefined {
|
|
995
1077
|
this.#lastCtx = ctx;
|
|
1078
|
+
this.#lastMousePosition = mousePosition;
|
|
996
1079
|
this.#cursor = CursorStyle.Default;
|
|
997
1080
|
|
|
998
1081
|
// Connections draw first but need port positions, which only exist
|
|
999
1082
|
// once a node has been drawn so a new node costs one extra frame.
|
|
1000
1083
|
const awaitingLayout = this.#nodes.some((node) => !node.hasBeenLaidOut());
|
|
1001
1084
|
|
|
1085
|
+
for (const node of this.#nodes) {
|
|
1086
|
+
if (node.hasBeenLaidOut()) {
|
|
1087
|
+
node.placePorts(node.calculateBounds(ctx, camera), camera.zoom);
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1002
1091
|
TimeExecution("Render_Connections", () => {
|
|
1003
1092
|
this.#renderConnections(ctx, camera, mousePosition);
|
|
1004
1093
|
})
|
|
1005
1094
|
TimeExecution("Render_Nodes", () => {
|
|
1006
1095
|
this.#renderNodes(ctx, camera, mousePosition);
|
|
1007
1096
|
})
|
|
1097
|
+
this.#renderInsertMarker(ctx, camera);
|
|
1008
1098
|
|
|
1009
1099
|
if (awaitingLayout) {
|
|
1010
1100
|
requestRender();
|
|
@@ -1013,7 +1103,11 @@ export class NodeSubsystem {
|
|
|
1013
1103
|
if (this.#boxSelect) {
|
|
1014
1104
|
const box = this.#boxSelectionScreenspaceBox(camera);
|
|
1015
1105
|
ctx.setLineDash([Theme.BoxSelect.LineDashLength]);
|
|
1016
|
-
|
|
1106
|
+
new BoxStyle({
|
|
1107
|
+
border: { color: Theme.BoxSelect.Color, size: Theme.BoxSelect.Size },
|
|
1108
|
+
color: "rgba(0,0,0,0)",
|
|
1109
|
+
radius: Theme.BoxSelect.Radius,
|
|
1110
|
+
}).Draw(ctx, box, 1);
|
|
1017
1111
|
ctx.setLineDash([]);
|
|
1018
1112
|
}
|
|
1019
1113
|
|
package/src/notes/note.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { BoxStyle } from "../styles/box";
|
|
|
5
5
|
import { TextAlign } from "../styles/canvasTextAlign";
|
|
6
6
|
import { TextBaseline } from "../styles/canvasTextBaseline";
|
|
7
7
|
import { TextStyleConfig } from "../styles/text";
|
|
8
|
-
import { Theme } from "../theme";
|
|
8
|
+
import { Theme, themeVersion } from "../theme";
|
|
9
9
|
import { Box } from "../types/box";
|
|
10
10
|
import { CopyVector2, Vector2, Zero } from "../types/vector2";
|
|
11
11
|
import { Camera } from "../camera";
|
|
@@ -40,6 +40,8 @@ export class FlowNote {
|
|
|
40
40
|
|
|
41
41
|
#document: Array<MarkdownEntry>;
|
|
42
42
|
|
|
43
|
+
#parsedForTheme = -1;
|
|
44
|
+
|
|
43
45
|
#width: number;
|
|
44
46
|
|
|
45
47
|
#edittingStyle: BoxStyle;
|
|
@@ -99,6 +101,7 @@ export class FlowNote {
|
|
|
99
101
|
setText(text: string): void {
|
|
100
102
|
this.#originalText = text;
|
|
101
103
|
this.#document = BuildMarkdown(this.#originalText);
|
|
104
|
+
this.#parsedForTheme = themeVersion();
|
|
102
105
|
for (let i = 0; i < this.#contentChangeCallbacks.length; i++) {
|
|
103
106
|
this.#contentChangeCallbacks[i](this, text);
|
|
104
107
|
}
|
|
@@ -210,6 +213,11 @@ export class FlowNote {
|
|
|
210
213
|
camera.graphSpaceToScreenSpace(this.#position, this.#tempPosition);
|
|
211
214
|
CopyVector2(this.#lastRenderedBox.Position, this.#tempPosition)
|
|
212
215
|
|
|
216
|
+
if (this.#parsedForTheme !== themeVersion()) {
|
|
217
|
+
this.#document = BuildMarkdown(this.#originalText);
|
|
218
|
+
this.#parsedForTheme = themeVersion();
|
|
219
|
+
}
|
|
220
|
+
|
|
213
221
|
const startY = this.#tempPosition.y;
|
|
214
222
|
const lineSpacing = Theme.Note.EntrySpacing * camera.zoom;
|
|
215
223
|
|