@elicdavis/node-flow 0.1.20 → 0.2.1
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 +393 -226
- package/dist/index.d.ts +393 -226
- package/dist/index.js +1905 -1058
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1888 -1057
- 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 +19 -14
- package/src/index.ts +26 -12
- package/src/markdown/entry.ts +11 -15
- package/src/markdown/syntaxParser.ts +2 -1
- package/src/node.ts +160 -101
- package/src/nodes/subsystem.ts +113 -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 +136 -0
- package/src/widgets/number.ts +115 -154
- package/src/widgets/row.ts +138 -0
- package/src/widgets/slider.ts +101 -216
- package/src/widgets/string.ts +32 -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 +173 -6
- 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/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/support.ts +188 -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 +386 -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);
|
|
@@ -955,9 +984,11 @@ export class NodeSubsystem {
|
|
|
955
984
|
}
|
|
956
985
|
|
|
957
986
|
if (intersection.Widget !== undefined) {
|
|
958
|
-
// console.log(i, this.#nodes[i].title(), "widget")
|
|
959
987
|
this.#widgetHovering = intersection.Widget
|
|
960
|
-
this.#
|
|
988
|
+
this.#widgetHoveringBox = intersection.WidgetBox ?? null;
|
|
989
|
+
this.#cursor = intersection.Widget.Disabled?.()
|
|
990
|
+
? CursorStyle.Default
|
|
991
|
+
: intersection.Widget.Cursor?.() ?? CursorStyle.Pointer;
|
|
961
992
|
}
|
|
962
993
|
|
|
963
994
|
if (intersection.Port !== undefined && intersection.Node !== undefined && intersection.PortIndex !== undefined && intersection.PortIsInput !== undefined) {
|
|
@@ -988,6 +1019,10 @@ export class NodeSubsystem {
|
|
|
988
1019
|
|
|
989
1020
|
this.#nodes[i].render(ctx, camera, state, mousePosition, this.#postProcessPass);
|
|
990
1021
|
}
|
|
1022
|
+
|
|
1023
|
+
if (this.#widgetCurrentlyClicking !== null) {
|
|
1024
|
+
this.#cursor = this.#widgetCurrentlyClicking.Cursor?.() ?? CursorStyle.Pointer;
|
|
1025
|
+
}
|
|
991
1026
|
}
|
|
992
1027
|
|
|
993
1028
|
#boxSelectionScreenspaceBox(camera: Camera): Box {
|
|
@@ -1001,20 +1036,65 @@ export class NodeSubsystem {
|
|
|
1001
1036
|
return box;
|
|
1002
1037
|
}
|
|
1003
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
|
+
|
|
1004
1076
|
render(ctx: CanvasRenderingContext2D, camera: Camera, mousePosition: Vector2 | undefined): RenderResults | undefined {
|
|
1005
1077
|
this.#lastCtx = ctx;
|
|
1078
|
+
this.#lastMousePosition = mousePosition;
|
|
1006
1079
|
this.#cursor = CursorStyle.Default;
|
|
1007
1080
|
|
|
1008
1081
|
// Connections draw first but need port positions, which only exist
|
|
1009
1082
|
// once a node has been drawn so a new node costs one extra frame.
|
|
1010
1083
|
const awaitingLayout = this.#nodes.some((node) => !node.hasBeenLaidOut());
|
|
1011
1084
|
|
|
1085
|
+
for (const node of this.#nodes) {
|
|
1086
|
+
if (node.hasBeenLaidOut()) {
|
|
1087
|
+
node.placePorts(node.calculateBounds(ctx, camera), camera.zoom);
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1012
1091
|
TimeExecution("Render_Connections", () => {
|
|
1013
1092
|
this.#renderConnections(ctx, camera, mousePosition);
|
|
1014
1093
|
})
|
|
1015
1094
|
TimeExecution("Render_Nodes", () => {
|
|
1016
1095
|
this.#renderNodes(ctx, camera, mousePosition);
|
|
1017
1096
|
})
|
|
1097
|
+
this.#renderInsertMarker(ctx, camera);
|
|
1018
1098
|
|
|
1019
1099
|
if (awaitingLayout) {
|
|
1020
1100
|
requestRender();
|
|
@@ -1023,7 +1103,11 @@ export class NodeSubsystem {
|
|
|
1023
1103
|
if (this.#boxSelect) {
|
|
1024
1104
|
const box = this.#boxSelectionScreenspaceBox(camera);
|
|
1025
1105
|
ctx.setLineDash([Theme.BoxSelect.LineDashLength]);
|
|
1026
|
-
|
|
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);
|
|
1027
1111
|
ctx.setLineDash([]);
|
|
1028
1112
|
}
|
|
1029
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
|
|