@elicdavis/node-flow 0.1.20 → 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.
Files changed (53) hide show
  1. package/dist/index.d.mts +386 -227
  2. package/dist/index.d.ts +386 -227
  3. package/dist/index.js +1866 -1059
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.mjs +1849 -1058
  6. package/dist/index.mjs.map +1 -1
  7. package/index.html +1134 -329
  8. package/package.json +1 -1
  9. package/src/connection.ts +30 -5
  10. package/src/contextMenu.ts +66 -34
  11. package/src/graph.ts +12 -10
  12. package/src/index.ts +26 -12
  13. package/src/markdown/entry.ts +11 -15
  14. package/src/markdown/syntaxParser.ts +2 -1
  15. package/src/node.ts +160 -101
  16. package/src/nodes/subsystem.ts +113 -29
  17. package/src/notes/note.ts +9 -1
  18. package/src/organize.ts +540 -111
  19. package/src/popup.ts +4 -2
  20. package/src/port.ts +83 -6
  21. package/src/styles/bevel.ts +31 -0
  22. package/src/styles/cursor.ts +1 -0
  23. package/src/styles/text.ts +4 -4
  24. package/src/theme.ts +179 -92
  25. package/src/types/text.ts +5 -1
  26. package/src/utils/color.ts +1 -1
  27. package/src/widgets/button.ts +52 -107
  28. package/src/widgets/color.ts +91 -150
  29. package/src/widgets/factory.ts +89 -46
  30. package/src/widgets/field.ts +99 -0
  31. package/src/widgets/image.ts +107 -165
  32. package/src/widgets/inplace.ts +84 -0
  33. package/src/widgets/number.ts +122 -154
  34. package/src/widgets/row.ts +138 -0
  35. package/src/widgets/slider.ts +101 -216
  36. package/src/widgets/string.ts +39 -130
  37. package/src/widgets/text.ts +81 -92
  38. package/src/widgets/toggle.ts +62 -207
  39. package/src/widgets/value.ts +127 -0
  40. package/src/widgets/widget.ts +180 -34
  41. package/test/context_menu.test.ts +82 -0
  42. package/test/cursor.test.ts +17 -0
  43. package/test/hidden_port.test.ts +125 -0
  44. package/test/markdown.test.ts +2 -2
  45. package/test/markdown_code_block.test.ts +58 -0
  46. package/test/multi_input.test.ts +124 -0
  47. package/test/organize.test.ts +117 -1
  48. package/test/support.ts +175 -0
  49. package/test/theme.test.ts +86 -0
  50. package/test/widget_click.test.ts +111 -97
  51. package/test/widget_construction.test.ts +5 -4
  52. package/test/widgets.test.ts +333 -0
  53. package/test/wire_follows_camera.test.ts +57 -0
@@ -1,165 +1,107 @@
1
- import { requestRender } from "../render_scheduler";
2
- import { Box } from "../types/box";
3
- import { CopyVector2, Vector2 } from "../types/vector2";
4
-
5
- const margin = 15;
6
-
7
- export interface ImageWidgetConfig {
8
- image?: string;
9
- maxWidth?: number
10
- maxHeight?: number
11
- blob?: Blob;
12
- }
13
-
14
- /**
15
- * constructor(lightNode, nodeManager, id, parameterData, app) {
16
- this.lightNode = lightNode;
17
- this.id = id;
18
- this.app = app;
19
- this.lightNode.title = parameterData.name;
20
-
21
- this.lightNode.onDropFile = (file) => {
22
- // console.log(file)
23
- var reader = new FileReader();
24
- reader.onload = (evt) => {
25
- console.log(evt.target.result)
26
- nodeManager.nodeParameterChanged({
27
- id: id,
28
- data: evt.target.result,
29
- binary: true
30
- });
31
- }
32
- reader.readAsArrayBuffer(file);
33
-
34
- const url = URL.createObjectURL(file);
35
- // this.loadImage(this._url, function (img) {
36
- // that.size[1] = (img.height / img.width) * that.size[0];
37
- // });
38
- this.loadImgFromURL(url);
39
- }
40
- }
41
-
42
- loadImgFromURL(url) {
43
- const img = document.createElement("img");
44
- img.src = url;
45
- img.onload = () => {
46
- // if (callback) {
47
- // callback(this);
48
- // }
49
- // console.log("Image loaded, size: " + img.width + "x" + img.height);
50
- // this.dirty = true;
51
- // that.boxcolor = "#9F9";f
52
- // that.setDirtyCanvas(true);
53
- this.lightNode.widgets[0].image = img
54
- this.lightNode.setSize(this.lightNode.computeSize());
55
- };
56
- img.onerror = () => {
57
- console.log("error loading the image:" + url);
58
- }
59
- }
60
-
61
- update(parameterData) {
62
- console.log("image parameter", parameterData)
63
- const curVal = parameterData.currentValue;
64
- this.app.RequestManager.getParameterValue(this.id, (response) => {
65
- const url = URL.createObjectURL(response);
66
- this.loadImgFromURL(url)
67
- })
68
- }
69
- */
70
-
71
- export class ImageWidget {
72
-
73
- #url: string | undefined;
74
-
75
- #image: HTMLImageElement | undefined;
76
-
77
- #maxWidth: number;
78
-
79
- #maxHeight: number;
80
-
81
- constructor(config?: ImageWidgetConfig) {
82
- this.#maxWidth = config?.maxWidth === undefined ? 150 : config?.maxWidth;
83
- this.#maxHeight = config?.maxHeight === undefined ? 150 : config?.maxHeight;
84
- if (config?.image) {
85
- this.SetUrl(config?.image);
86
- } else if (config?.blob) {
87
- this.SetBlob(config?.blob);
88
- }
89
- }
90
-
91
- SetBlob(blob: Blob) {
92
- const urlCreator = window.URL || window.webkitURL;
93
- const imageUrl = urlCreator.createObjectURL(blob);
94
- this.SetUrl(imageUrl);
95
- }
96
-
97
- SetUrl(url: string) {
98
- this.#image = undefined;
99
- this.#url = url;
100
-
101
- const img = document.createElement("img");
102
- img.src = url;
103
- img.onload = () => {
104
- this.#image = img;
105
- requestRender();
106
- };
107
- img.onerror = (event) => {
108
- console.log("error loading image:", url, event);
109
- }
110
- }
111
-
112
- Size(): Vector2 {
113
- if (this.#image === undefined) {
114
- return { "x": 0, "y": 0 }
115
- }
116
-
117
- let adjust = 1;
118
- if (this.#image.width > this.#maxWidth) {
119
- adjust = this.#maxWidth / this.#image.width
120
- }
121
-
122
- if (this.#image.height > this.#maxHeight) {
123
- let heightAdjust = this.#maxHeight / this.#image.height
124
- if (heightAdjust < adjust) {
125
- adjust = heightAdjust;
126
- }
127
- }
128
-
129
- return {
130
- "x": adjust * this.#image.width,
131
- "y": adjust * this.#image.height
132
- }
133
- }
134
-
135
- ClickStart(): void {
136
- }
137
-
138
- ClickEnd(): void {
139
- }
140
-
141
- Draw(ctx: CanvasRenderingContext2D, position: Vector2, scale: number, mousePosition: Vector2 | undefined): Box {
142
- const size = this.Size();
143
- const box: Box = {
144
- Position: { x: 0, y: 0 },
145
- Size: {
146
- x: size.x * scale,
147
- y: size.y * scale,
148
- }
149
- }
150
- CopyVector2(box.Position, position);
151
-
152
- if (!this.#image) {
153
- return box;
154
- }
155
-
156
- ctx.drawImage(
157
- this.#image,
158
- position.x,
159
- position.y,
160
- box.Size.x,
161
- box.Size.y
162
- );
163
- return box;
164
- }
165
- }
1
+ import { requestRender } from "../render_scheduler";
2
+ import { CursorStyle } from "../styles/cursor";
3
+ import { Box } from "../types/box";
4
+ import { Vector2 } from "../types/vector2";
5
+ import { WidgetBase } from "./widget";
6
+
7
+ export interface ImageWidgetConfig {
8
+ image?: string;
9
+ maxWidth?: number
10
+ maxHeight?: number
11
+ blob?: Blob;
12
+ }
13
+
14
+ export class ImageWidget extends WidgetBase {
15
+
16
+ #url: string | undefined;
17
+
18
+ #ownedUrl: string | undefined;
19
+
20
+ #image: HTMLImageElement | undefined;
21
+
22
+ #maxWidth: number;
23
+
24
+ #maxHeight: number;
25
+
26
+ constructor(config?: ImageWidgetConfig) {
27
+ super();
28
+ this.#maxWidth = config?.maxWidth === undefined ? 150 : config?.maxWidth;
29
+ this.#maxHeight = config?.maxHeight === undefined ? 150 : config?.maxHeight;
30
+ if (config?.image) {
31
+ this.SetUrl(config?.image);
32
+ } else if (config?.blob) {
33
+ this.SetBlob(config?.blob);
34
+ }
35
+ }
36
+
37
+ SetBlob(blob: Blob) {
38
+ const url = URL.createObjectURL(blob);
39
+ this.SetUrl(url);
40
+ this.#ownedUrl = url;
41
+ }
42
+
43
+ SetUrl(url: string) {
44
+ if (this.#ownedUrl !== undefined) {
45
+ URL.revokeObjectURL(this.#ownedUrl);
46
+ this.#ownedUrl = undefined;
47
+ }
48
+ this.#image = undefined;
49
+ this.#url = url;
50
+
51
+ const img = document.createElement("img");
52
+ img.src = url;
53
+ img.onload = () => {
54
+ if (this.#url !== url) {
55
+ return;
56
+ }
57
+ this.#image = img;
58
+ requestRender();
59
+ };
60
+ img.onerror = (event) => {
61
+ console.log("error loading image:", url, event);
62
+ }
63
+ }
64
+
65
+ Cursor(): CursorStyle {
66
+ return CursorStyle.Default;
67
+ }
68
+
69
+ Size(): Vector2 {
70
+ if (this.#image === undefined) {
71
+ return { "x": 0, "y": 0 }
72
+ }
73
+
74
+ let adjust = 1;
75
+ if (this.#image.width > this.#maxWidth) {
76
+ adjust = this.#maxWidth / this.#image.width
77
+ }
78
+
79
+ if (this.#image.height > this.#maxHeight) {
80
+ let heightAdjust = this.#maxHeight / this.#image.height
81
+ if (heightAdjust < adjust) {
82
+ adjust = heightAdjust;
83
+ }
84
+ }
85
+
86
+ return {
87
+ "x": adjust * this.#image.width,
88
+ "y": adjust * this.#image.height
89
+ }
90
+ }
91
+
92
+ protected render(ctx: CanvasRenderingContext2D, box: Box, scale: number): void {
93
+ if (!this.#image) {
94
+ return;
95
+ }
96
+ const size = this.Size();
97
+ const w = size.x * scale;
98
+ const h = size.y * scale;
99
+ ctx.drawImage(
100
+ this.#image,
101
+ box.Position.x + ((box.Size.x - w) / 2),
102
+ box.Position.y,
103
+ w,
104
+ h
105
+ );
106
+ }
107
+ }
@@ -0,0 +1,84 @@
1
+ import { requestRender } from "../render_scheduler";
2
+ import { Theme } from "../theme";
3
+ import { Box } from "../types/box";
4
+
5
+ export interface InPlaceEditConfig {
6
+ canvas: HTMLCanvasElement | null;
7
+
8
+ /**
9
+ * Where to sit, in canvas pixels.
10
+ */
11
+ box: Box;
12
+
13
+ scale: number;
14
+
15
+ value: string;
16
+
17
+ onCommit: (text: string) => void;
18
+ }
19
+
20
+ /**
21
+ * Lays a text input over a widget. Enter or clicking away keeps what was
22
+ * typed; Escape throws it away.
23
+ */
24
+ export function editInPlace(config: InPlaceEditConfig): HTMLInputElement | null {
25
+ const canvas = config.canvas;
26
+ if (canvas === null) {
27
+ return null;
28
+ }
29
+
30
+ const rect = canvas.getBoundingClientRect();
31
+ const sx = canvas.width > 0 ? rect.width / canvas.width : 1;
32
+ const sy = canvas.height > 0 ? rect.height / canvas.height : 1;
33
+
34
+ const input = document.createElement("input");
35
+ input.type = "text";
36
+ input.value = config.value;
37
+ input.style.cssText = [
38
+ "position: fixed",
39
+ `left: ${rect.left + (config.box.Position.x * sx)}px`,
40
+ `top: ${rect.top + (config.box.Position.y * sy)}px`,
41
+ `width: ${config.box.Size.x * sx}px`,
42
+ `height: ${config.box.Size.y * sy}px`,
43
+ "box-sizing: border-box",
44
+ "margin: 0",
45
+ `padding: 0 ${Theme.Widget.Padding * config.scale}px`,
46
+ `font: ${Theme.Widget.FontSize * config.scale * sy}px ${Theme.FontFamily}`,
47
+ `color: ${Theme.Widget.FontColor}`,
48
+ `background: ${Theme.Widget.Hover.BackgroundColor}`,
49
+ `border: ${Theme.Widget.Border.Size * config.scale}px solid ${Theme.Widget.Border.Color}`,
50
+ `border-radius: ${Theme.Widget.Border.Radius * config.scale}px`,
51
+ "outline: none",
52
+ "z-index: 1000",
53
+ ].join(";");
54
+
55
+ let done = false;
56
+ const finish = (keep: boolean) => {
57
+ if (done) {
58
+ return;
59
+ }
60
+ done = true;
61
+ input.remove();
62
+ if (keep) {
63
+ config.onCommit(input.value);
64
+ }
65
+ requestRender();
66
+ };
67
+
68
+ input.addEventListener("keydown", (event: KeyboardEvent) => {
69
+ event.stopPropagation();
70
+ if (event.key === "Enter") {
71
+ event.preventDefault();
72
+ finish(true);
73
+ } else if (event.key === "Escape") {
74
+ event.preventDefault();
75
+ finish(false);
76
+ }
77
+ });
78
+ input.addEventListener("blur", () => finish(true));
79
+
80
+ document.body.appendChild(input);
81
+ input.focus();
82
+ input.select();
83
+ return input;
84
+ }
@@ -1,154 +1,122 @@
1
- import { Theme } from "../theme";
2
- import { Popup } from "../popup";
3
- import { TextBoxStyle, TextBoxStyleConfig, TextBoxStyleWithFallback } from "../styles/textBox";
4
- import { Box, InBox } from '../types/box';
5
- import { CopyVector2, Vector2 } from "../types/vector2";
6
- import { height, startingWidgetValue, width } from "./widget";
7
- import { FlowNode } from "../node";
8
-
9
- export interface NumberWidgetConfig {
10
- value?: number;
11
-
12
- property?: string;
13
-
14
- idleBoxStyle?: TextBoxStyleConfig;
15
-
16
- highlightBoxStyle?: TextBoxStyleConfig;
17
-
18
- callback?: (newNumber: number) => void;
19
- }
20
-
21
- export class NumberWidget {
22
-
23
- #node: FlowNode;
24
-
25
- #nodeProperty: string | undefined;
26
-
27
- #value: number;
28
-
29
- #idleBoxStyle: TextBoxStyle;
30
-
31
- #highlightBoxStyle: TextBoxStyle;
32
-
33
- #text: string;
34
-
35
- #callback?: (newNumber: number) => void;
36
-
37
- constructor(node: FlowNode, config?: NumberWidgetConfig) {
38
- this.#value = 0;
39
- this.#text = "0";
40
-
41
- this.#node = node;
42
- this.#nodeProperty = config?.property;
43
- this.#idleBoxStyle = new TextBoxStyle(TextBoxStyleWithFallback(config?.idleBoxStyle, {
44
- box: {
45
- color: Theme.Widget.BackgroundColor,
46
- border: {
47
- size: Theme.Widget.Border.Size,
48
- color: Theme.Widget.Border.Color,
49
- },
50
- radius: Theme.Widget.Border.Radius
51
- },
52
- text: { color: Theme.Widget.FontColor },
53
- }));
54
- this.#highlightBoxStyle = new TextBoxStyle(TextBoxStyleWithFallback(config?.highlightBoxStyle, {
55
- box: {
56
- color: Theme.Widget.Hover.BackgroundColor,
57
- border: {
58
- size: Theme.Widget.Border.Size,
59
- color: Theme.Widget.Border.Color,
60
- },
61
- radius: Theme.Widget.Border.Radius
62
- },
63
- text: { color: Theme.Widget.FontColor },
64
- }));
65
- this.Set(startingWidgetValue(node, this.#nodeProperty, config?.value, 0));
66
-
67
- this.#callback = config?.callback;
68
-
69
- if (this.#nodeProperty !== undefined) {
70
- this.#node.addPropertyChangeListener(this.#nodeProperty, (oldVal, newVal) => {
71
- this.Set(newVal);
72
- });
73
- }
74
-
75
- }
76
-
77
- Size(): Vector2 {
78
- return { "x": width, "y": height }
79
- }
80
-
81
- Set(newNumber: number): void {
82
- if (Object.is(this.#value, newNumber)) {
83
- return;
84
- }
85
-
86
- this.#value = newNumber;
87
-
88
- if (this.#nodeProperty !== undefined) {
89
- this.#node.setProperty(this.#nodeProperty, this.#value);
90
- }
91
-
92
- if (this.#callback !== undefined) {
93
- this.#callback(this.#value);
94
- }
95
-
96
- // https://stackoverflow.com/questions/5765398/whats-the-best-way-to-convert-a-number-to-a-string-in-javascript
97
- this.#text = '' + parseFloat(this.#value.toPrecision(6));
98
- }
99
-
100
- ClickStart(): void {
101
- }
102
-
103
- ClickEnd(): void {
104
- let input: HTMLInputElement | null = null;
105
-
106
- const setOption = "Set";
107
- const cancelOption = "Cancel";
108
- const popup = new Popup({
109
- title: "Set Number",
110
- options: [setOption, cancelOption],
111
- content: () => {
112
- const container = document.createElement('div');
113
- input = document.createElement('input')
114
- input.type = "number";
115
- input.valueAsNumber = this.#value;
116
- container.append(input);
117
- return container;
118
- },
119
- onClose: (button: string | null): void => {
120
- if (button !== setOption || input === null) {
121
- return;
122
- }
123
-
124
- this.Set(input.valueAsNumber);
125
- },
126
- });
127
-
128
- popup.Show();
129
- }
130
-
131
-
132
- Draw(ctx: CanvasRenderingContext2D, position: Vector2, scale: number, mousePosition: Vector2 | undefined): Box {
133
- const box = {
134
- Position: { x: 0, y: 0 },
135
- Size: {
136
- x: width * scale,
137
- y: height * scale
138
- }
139
- };
140
- CopyVector2(box.Position, position);
141
-
142
- let style: TextBoxStyle = this.#idleBoxStyle;
143
-
144
- if (mousePosition !== undefined) {
145
- if (InBox(box, mousePosition)) {
146
- style = this.#highlightBoxStyle;
147
- }
148
- }
149
-
150
- style.DrawUnderline(ctx, box, scale, this.#text)
151
-
152
- return box;
153
- }
154
- }
1
+ import { FlowNode } from "../node";
2
+ import { CursorStyle } from "../styles/cursor";
3
+ import { Box } from "../types/box";
4
+ import { drawField } from "./field";
5
+ import { editInPlace } from "./inplace";
6
+ import { ValueWidget, ValueWidgetConfig } from "./value";
7
+ import { WidgetPointer } from "./widget";
8
+
9
+ export interface NumberWidgetConfig extends ValueWidgetConfig<number> {
10
+ /**
11
+ * How much the value moves per pixel dragged. Left out, it scales with
12
+ * the value's size.
13
+ */
14
+ step?: number;
15
+
16
+ integer?: boolean;
17
+
18
+ min?: number;
19
+
20
+ max?: number;
21
+ }
22
+
23
+ const DRAG_THRESHOLD = 3;
24
+
25
+ export class NumberWidget extends ValueWidget<number> {
26
+
27
+ #step: number | undefined;
28
+
29
+ #integer: boolean;
30
+
31
+ #min: number;
32
+
33
+ #max: number;
34
+
35
+ #startX = 0;
36
+
37
+ #startValue = 0;
38
+
39
+ #dragging = false;
40
+
41
+ #dragged = false;
42
+
43
+ constructor(node: FlowNode, config?: NumberWidgetConfig) {
44
+ super(node, config, 0);
45
+ this.#step = config?.step;
46
+ this.#integer = config?.integer === true;
47
+ this.#min = config?.min ?? -Infinity;
48
+ this.#max = config?.max ?? Infinity;
49
+ }
50
+
51
+ Cursor(): CursorStyle {
52
+ return CursorStyle.EWResize;
53
+ }
54
+
55
+ protected render(ctx: CanvasRenderingContext2D, box: Box, scale: number): void {
56
+ drawField(ctx, box, scale, {
57
+ input: true,
58
+ hovered: this.hovered() || this.pressed(),
59
+ label: this.label(),
60
+ value: formatNumber(this.value()),
61
+ });
62
+ }
63
+
64
+ protected onPointerDown(pointer: WidgetPointer): void {
65
+ this.#startX = pointer.position.x;
66
+ this.#startValue = this.value();
67
+ this.#dragging = false;
68
+ this.#dragged = false;
69
+ }
70
+
71
+ protected onPointerMove(pointer: WidgetPointer): void {
72
+ const dx = pointer.position.x - this.#startX;
73
+ if (!this.#dragging && Math.abs(dx) < DRAG_THRESHOLD) {
74
+ return;
75
+ }
76
+ this.#dragging = true;
77
+ this.input(this.#clean(this.#startValue + (dx * this.#stepFor(this.#startValue))));
78
+ }
79
+
80
+ protected onPointerUp(): void {
81
+ this.#dragged = this.#dragging;
82
+ if (this.#dragging) {
83
+ this.#dragging = false;
84
+ this.commit(this.value(), this.#startValue);
85
+ }
86
+ }
87
+
88
+ protected onClick(): void {
89
+ if (this.#dragged) {
90
+ return;
91
+ }
92
+ const before = this.value();
93
+ editInPlace({
94
+ canvas: this.canvas(),
95
+ box: this.box(),
96
+ scale: this.scale(),
97
+ value: String(before),
98
+ onCommit: (text) => {
99
+ const parsed = Number.parseFloat(text);
100
+ if (Number.isFinite(parsed)) {
101
+ this.commit(this.#clean(parsed), before);
102
+ }
103
+ },
104
+ });
105
+ }
106
+
107
+ #stepFor(value: number): number {
108
+ if (this.#step !== undefined) {
109
+ return this.#step;
110
+ }
111
+ return Math.max(Math.abs(value) * 0.01, this.#integer ? 0.1 : 0.01);
112
+ }
113
+
114
+ #clean(value: number): number {
115
+ const rounded = this.#integer ? Math.round(value) : value;
116
+ return Math.min(this.#max, Math.max(this.#min, rounded));
117
+ }
118
+ }
119
+
120
+ export function formatNumber(value: number): string {
121
+ return '' + parseFloat(value.toPrecision(6));
122
+ }