diff --git a/README.md b/README.md
index 3568965..acc0e04 100644
--- a/README.md
+++ b/README.md
@@ -16,7 +16,7 @@ Node Flow is a javascript library that enables developers to build node based to
## Install
-Download the latest build [here](https://github.com/EliCDavis/node-flow/blob/gh-pages/dist/web/index.js).
+Download the latest build [here](https://raw.githubusercontent.com/EliCDavis/node-flow/gh-pages/dist/web/NodeFlow.js).
## API
diff --git a/esbuild.dev.ts b/esbuild.dev.ts
index 79d2352..c1a812e 100644
--- a/esbuild.dev.ts
+++ b/esbuild.dev.ts
@@ -5,7 +5,7 @@ import { NodeGlobalsPolyfillPlugin } from '@esbuild-plugins/node-globals-polyfil
const buildOptsNode: BuildOptions = {
entryPoints: ['./src/index.ts'],
- outfile: './dist/node/index.js',
+ outfile: './dist/node/NodeFlow.js',
platform: 'node',
target: ['es2018'],
format: 'cjs',
@@ -20,7 +20,7 @@ const buildOptsNode: BuildOptions = {
const buildOptsWeb: BuildOptions = {
entryPoints: ['./src/index.ts'],
// inject: [],
- outfile: './dist/web/index.js',
+ outfile: './dist/web/NodeFlow.js',
// external: [],
platform: 'browser',
target: ['esNext'],
diff --git a/index.html b/index.html
index 51d4872..a10443e 100644
--- a/index.html
+++ b/index.html
@@ -27,7 +27,7 @@
}
-
+
diff --git a/src/graph.ts b/src/graph.ts
index 65851cc..284dbf8 100644
--- a/src/graph.ts
+++ b/src/graph.ts
@@ -272,7 +272,8 @@ export class NodeFlowGraph {
#render(): void {
if (this.#canvas.parentNode !== null) {
- var rect = this.#canvas.parentNode.getBoundingClientRect();
+ // Stupid as any because typescript doesn't think it exists
+ var rect = (this.#canvas.parentNode as any).getBoundingClientRect();
this.#canvas.width = rect.width;
this.#canvas.height = rect.height;
}
diff --git a/src/node.ts b/src/node.ts
index 0a8374d..d4a04e3 100644
--- a/src/node.ts
+++ b/src/node.ts
@@ -11,6 +11,13 @@ import { Theme } from "./theme";
import { TextAlign } from "./styles/canvasTextAlign";
import { TextBaseline } from "./styles/canvasTextBaseline";
+type AnyPropertyChangeCallback = (propertyName: string, oldValue: any, newValue: any) => void
+type PropertyChangeCallback = (oldValue: any, newValue: any) => void
+
+interface NodeData {
+ [name: string]: any;
+}
+
export interface WidgetConfig {
type?: string,
config?: any
@@ -26,6 +33,7 @@ export interface FlowNodeConfig {
position?: Vector2;
title?: string;
locked?: boolean;
+ data?: NodeData;
// Ports
inputs?: Array;
@@ -113,6 +121,12 @@ export class FlowNode {
#widgetPositions: List;
+ #data: NodeData;
+
+ #registeredAnyPropertyChangeCallbacks: Array;
+
+ #registeredPropertyChangeCallbacks: Map>;
+
constructor(config?: FlowNodeConfig) {
this.#input = new Array();
this.#output = new Array();
@@ -122,6 +136,9 @@ export class FlowNode {
this.#widgetPositions = new List();
this.#elementSpacing = 15;
this.#locked = config?.locked === undefined ? false : config.locked;
+ this.#data = config?.data === undefined ? {} : config?.data;
+ this.#registeredPropertyChangeCallbacks = new Map>();
+ this.#registeredAnyPropertyChangeCallbacks = new Array();
this.#selected = false;
this.#onSelect = config?.onSelect;
@@ -187,7 +204,7 @@ export class FlowNode {
if (widget.type === undefined) {
continue;
}
- this.addWidget(GlobalWidgetFactory.create(widget.type, widget.config))
+ this.addWidget(GlobalWidgetFactory.create(this, widget.type, widget.config))
}
}
}
@@ -206,6 +223,46 @@ export class FlowNode {
}
}
+ public subscribeToAnyPropertyChange(callback: AnyPropertyChangeCallback): void {
+ if (callback === undefined || callback === null) {
+ }
+ this.#registeredAnyPropertyChangeCallbacks.push(callback);
+ }
+
+ public subscribeToProperty(name: string, callback: PropertyChangeCallback): void {
+ if (!this.#registeredPropertyChangeCallbacks.has(name)) {
+ this.#registeredPropertyChangeCallbacks.set(name, []);
+ }
+
+ const callbacks = this.#registeredPropertyChangeCallbacks.get(name);
+ if (callbacks === undefined) {
+ return;
+ }
+ callbacks.push(callback);
+ }
+
+ public setProperty(name: string, value: any): void {
+ const oldValue = this.#data[name];
+ this.#data[name] = value;
+
+ for (let i = 0; i < this.#registeredAnyPropertyChangeCallbacks.length; i++) {
+ this.#registeredAnyPropertyChangeCallbacks[i](name, oldValue, value);
+ }
+
+ const callbacks = this.#registeredPropertyChangeCallbacks.get(name);
+ if (callbacks === undefined) {
+ return;
+ }
+
+ for (let i = 0; i < callbacks.length; i++) {
+ callbacks[i](oldValue, value);
+ }
+ }
+
+ public getProperty(name: string): any {
+ return this.#data[name];
+ }
+
public unselect(): void {
if (!this.#selected) {
return;
@@ -403,10 +460,10 @@ export class FlowNode {
ctx.fillStyle = "#154050"
ctx.beginPath();
ctx.roundRect(
- titleBox.Position.x + (borderSize*scale*0.5),
- titleBox.Position.y + (borderSize*scale*0.5),
- titleBox.Size.x - (borderSize*scale),
- titleBox.Size.y - (borderSize*scale*0.5),
+ titleBox.Position.x + (borderSize * scale * 0.5),
+ titleBox.Position.y + (borderSize * scale * 0.5),
+ titleBox.Size.x - (borderSize * scale),
+ titleBox.Size.y - (borderSize * scale * 0.5),
[nodeStyle.radius() * scale, nodeStyle.radius() * scale, 0, 0]
);
ctx.fill();
diff --git a/src/widgets/button.ts b/src/widgets/button.ts
index d5b5056..d856f41 100644
--- a/src/widgets/button.ts
+++ b/src/widgets/button.ts
@@ -1,6 +1,4 @@
import { Theme } from "../theme";
-import { BoxStyle } from "../styles/box";
-import { TextStyle, TextStyleConfig } from "../styles/text";
import { TextBoxStyle, TextBoxStyleConfig, TextBoxStyleWithFallback } from "../styles/textBox";
import { Box, InBox } from "../types/box";
import { Vector2 } from "../types/vector2";
diff --git a/src/widgets/color.ts b/src/widgets/color.ts
index c8a373d..dd90318 100644
--- a/src/widgets/color.ts
+++ b/src/widgets/color.ts
@@ -6,10 +6,13 @@ import { height, width } from "./widget";
import { Popup } from "../popup";
import { TextBoxStyle } from '../styles/textBox';
import { Theme } from "../theme";
+import { FlowNode } from "../node";
export interface ColorWidgetConfig {
value?: string;
+ property?: string;
+
textStyle?: TextStyleConfig;
callback?: (newColor: string) => void;
@@ -30,6 +33,10 @@ function contrastColor(color: string): string {
export class ColorWidget {
+ #node: FlowNode;
+
+ #nodeProperty: string | undefined;
+
#value: string;
#contrast: string;
@@ -38,9 +45,10 @@ export class ColorWidget {
#callback?: (newColor: string) => void;
- constructor(config?: ColorWidgetConfig) {
- this.#value = config?.value === undefined ? "#000000" : config?.value;
- this.#contrast = contrastColor(this.#value);
+ constructor(node: FlowNode, config?: ColorWidgetConfig) {
+ this.#node = node;
+ this.#nodeProperty = config?.property;
+ this.Set(config?.value === undefined ? "#000000" : config?.value);
this.#textBoxStyle = new TextBoxStyle({
box: {
color: this.#value,
@@ -53,8 +61,11 @@ export class ColorWidget {
});
this.#callback = config?.callback;
-
- this.#textBoxStyle.setTextColor(this.#contrast);
+ if (this.#nodeProperty !== undefined) {
+ this.#node.subscribeToProperty(this.#nodeProperty, (oldVal, newVal) => {
+ this.Set(newVal);
+ });
+ }
}
Size(): Vector2 {
@@ -62,16 +73,25 @@ export class ColorWidget {
}
Set(value: string): void {
- this.#value = value;
- this.#contrast = contrastColor(this.#value);
+ if (this.#value === value) {
+ return;
+ }
- this.#textBoxStyle.setBoxColor(this.#value);
- this.#textBoxStyle.setBorderColor(this.#contrast);
- this.#textBoxStyle.setTextColor(this.#contrast);
+ this.#value = value;
if (this.#callback !== undefined) {
this.#callback(this.#value);
}
+
+ if (this.#nodeProperty !== undefined) {
+ this.#node.setProperty(this.#nodeProperty, this.#value);
+ }
+
+ // Update Styling
+ this.#contrast = contrastColor(this.#value);
+ this.#textBoxStyle.setBoxColor(this.#value);
+ this.#textBoxStyle.setBorderColor(this.#contrast);
+ this.#textBoxStyle.setTextColor(this.#contrast);
}
ClickStart(): void {
diff --git a/src/widgets/factory.ts b/src/widgets/factory.ts
index 63a0a7f..3f864b4 100644
--- a/src/widgets/factory.ts
+++ b/src/widgets/factory.ts
@@ -1,12 +1,13 @@
import { Widget } from "./widget";
-import { NumberWidget } from './number';
-import { ButtonWidget } from './button';
-import { ColorWidget } from './color';
-import { SliderWidget } from './slider';
-import { StringWidget } from './string';
-import { ToggleWidget } from './toggle';
+import { NumberWidget, NumberWidgetConfig } from './number';
+import { ButtonWidget, ButtonWidgetConfig } from './button';
+import { ColorWidget, ColorWidgetConfig } from './color';
+import { SliderWidget, SliderWidgetConfig } from './slider';
+import { StringWidget, StringWidgetConfig } from './string';
+import { ToggleWidget, ToggleWidgetConfig } from './toggle';
+import { FlowNode } from "../node";
-export type WidgetBuilder = (confg?: any) => Widget;
+export type WidgetBuilder = (node: FlowNode, confg?: any) => Widget;
class WidgetFactory {
@@ -20,22 +21,22 @@ class WidgetFactory {
this.#registeredWidgets.set(widgetType, builder);
}
- create(widgetType: string, config: any): Widget {
+ create(node: FlowNode, widgetType: string, config: any): Widget {
const builder = this.#registeredWidgets.get(widgetType)
if (builder === undefined) {
throw new Error("no builder registered for widget: " + widgetType);
}
- return builder(config);
+ return builder(node, config);
}
}
const GlobalWidgetFactory = new WidgetFactory();
-GlobalWidgetFactory.register("button", (config) => new ButtonWidget(config));
-GlobalWidgetFactory.register("number", (config) => new NumberWidget(config));
-GlobalWidgetFactory.register("color", (config) => new ColorWidget(config));
-GlobalWidgetFactory.register("slider", (config) => new SliderWidget(config));
-GlobalWidgetFactory.register("string", (config) => new StringWidget(config));
-GlobalWidgetFactory.register("toggle", (config) => new ToggleWidget(config));
+GlobalWidgetFactory.register("button", (node: FlowNode, config?: ButtonWidgetConfig) => new ButtonWidget(config));
+GlobalWidgetFactory.register("number", (node: FlowNode, config?: NumberWidgetConfig) => new NumberWidget(node, config));
+GlobalWidgetFactory.register("color", (node: FlowNode, config?: ColorWidgetConfig) => new ColorWidget(node, config));
+GlobalWidgetFactory.register("slider", (node: FlowNode, config?: SliderWidgetConfig) => new SliderWidget(node, config));
+GlobalWidgetFactory.register("string", (node: FlowNode, config?: StringWidgetConfig) => new StringWidget(node, config));
+GlobalWidgetFactory.register("toggle", (node: FlowNode, config?: ToggleWidgetConfig) => new ToggleWidget(node, config));
export { GlobalWidgetFactory };
\ No newline at end of file
diff --git a/src/widgets/number.ts b/src/widgets/number.ts
index 536ad87..6be81f8 100644
--- a/src/widgets/number.ts
+++ b/src/widgets/number.ts
@@ -4,10 +4,13 @@ import { TextBoxStyle, TextBoxStyleConfig, TextBoxStyleWithFallback } from "../s
import { Box, InBox } from '../types/box';
import { Vector2 } from "../types/vector2";
import { height, width } from "./widget";
+import { FlowNode } from "../node";
export interface NumberWidgetConfig {
value?: number;
+ property?: string;
+
idleBoxStyle?: TextBoxStyleConfig;
highlightBoxStyle?: TextBoxStyleConfig;
@@ -16,7 +19,11 @@ export interface NumberWidgetConfig {
}
export class NumberWidget {
-
+
+ #node: FlowNode;
+
+ #nodeProperty: string | undefined;
+
#value: number;
#idleBoxStyle: TextBoxStyle;
@@ -27,8 +34,10 @@ export class NumberWidget {
#callback?: (newNumber: number) => void;
- constructor(config?: NumberWidgetConfig) {
- this.#value = config?.value === undefined ? 0 : config?.value;
+ constructor(node: FlowNode, config?: NumberWidgetConfig) {
+ this.#node = node;
+ this.#nodeProperty = config?.property;
+ this.Set(config?.value === undefined ? 0 : config?.value);
this.#idleBoxStyle = new TextBoxStyle(TextBoxStyleWithFallback(config?.idleBoxStyle, {
box: {
color: Theme.Widget.BackgroundColor,
@@ -53,8 +62,12 @@ export class NumberWidget {
}));
this.#callback = config?.callback;
- // https://stackoverflow.com/questions/5765398/whats-the-best-way-to-convert-a-number-to-a-string-in-javascript
- this.#text = '' + this.#value;
+ if (this.#nodeProperty !== undefined) {
+ this.#node.subscribeToProperty(this.#nodeProperty, (oldVal, newVal) => {
+ this.Set(newVal);
+ });
+ }
+
}
Size(): Vector2 {
@@ -62,11 +75,22 @@ export class NumberWidget {
}
Set(newNumber: number): void {
+ if (this.#value === newNumber) {
+ return;
+ }
+
this.#value = newNumber;
- this.#text = '' + this.#value;
+
+ if (this.#nodeProperty !== undefined) {
+ this.#node.setProperty(this.#nodeProperty, this.#value);
+ }
+
if (this.#callback !== undefined) {
this.#callback(this.#value);
}
+
+ // https://stackoverflow.com/questions/5765398/whats-the-best-way-to-convert-a-number-to-a-string-in-javascript
+ this.#text = '' + this.#value;
}
ClickStart(): void {
diff --git a/src/widgets/slider.ts b/src/widgets/slider.ts
index 88a3b64..78d9170 100644
--- a/src/widgets/slider.ts
+++ b/src/widgets/slider.ts
@@ -6,8 +6,12 @@ import { CopyVector2, Vector2 } from "../types/vector2";
import { Clamp, Clamp01 } from "../utils/math";
import { height, width } from "./widget";
import { TextBaseline } from "../styles/canvasTextBaseline";
+import { FlowNode } from "../node";
export interface SliderWidgetConfig {
+
+ property?: string;
+
min?: number;
max?: number;
@@ -27,6 +31,7 @@ export interface SliderWidgetConfig {
release?: (newValue: number) => void;
snapIncrement?: number;
+
}
export class SliderWidget {
@@ -43,6 +48,10 @@ export class SliderWidget {
#snapIncrement?: number;
+ #node: FlowNode;
+
+ #nodeProperty: string | undefined;
+
// Callbacks ==============================================================
#change?: (newValue: number) => void;
@@ -67,14 +76,13 @@ export class SliderWidget {
#clicking: boolean;
- constructor(config?: SliderWidgetConfig) {
+ constructor(node: FlowNode, config?: SliderWidgetConfig) {
this.#min = config?.min === undefined ? 0 : config?.min;
this.#max = config?.max === undefined ? 1 : config?.max;
- this.#value = config?.value === undefined ? 0 : config?.value;
- this.#value = Clamp(this.#value, this.#min, this.#max);
this.#snapIncrement = config?.snapIncrement;
-
- this.#change = config?.change;
+ this.#nodeProperty = config?.property;
+ this.#text = "";
+
this.#release = config?.release;
this.#backgroundColor = config?.backgroundColor === undefined ? Theme.Widget.BackgroundColor : config?.backgroundColor;
@@ -87,15 +95,30 @@ export class SliderWidget {
this.#clickStartMousePosition = { x: 0, y: 0 };
this.#clicking = false;
- this.#text = this.#value.toFixed(3);
+
+ if (config?.property !== undefined && config?.property !== null) {
+ node.subscribeToProperty(config.property, (oldVal, newVal) => {
+ this.SetValue(newVal);
+ });
+ }
+
+ this.SetValue(config?.value === undefined ? 0 : config?.value);
+
+ // Setup change callback after we set the initial value to prevent the callback from being
+ this.#change = config?.change;
}
SetValue(newValue: number): void {
- if (this.#value === newValue) {
+ const cleanedValue = Clamp(newValue, this.#min, this.#max);
+ if (this.#value === cleanedValue) {
return;
}
- this.#value = Clamp(newValue, this.#min, this.#max);
+ this.#value = cleanedValue;
+
+ if (this.#nodeProperty) {
+ this.#node.setProperty(this.#nodeProperty, this.#value);
+ }
this.#text = this.#value.toFixed(3);
if (this.#change !== undefined) {
diff --git a/src/widgets/string.ts b/src/widgets/string.ts
index 00794b0..33ffb9e 100644
--- a/src/widgets/string.ts
+++ b/src/widgets/string.ts
@@ -5,8 +5,11 @@ import { Box, InBox } from "../types/box";
import { Vector2 } from "../types/vector2";
import { fitString } from "../utils/string";
import { height, width } from "./widget";
+import { FlowNode } from '../node';
export interface StringWidgetConfig {
+ property?: string;
+
value?: string;
textBoxStyle?: TextBoxStyleConfig;
@@ -24,8 +27,13 @@ export class StringWidget {
#callback?: (newString: string) => void;
- constructor(config?: StringWidgetConfig) {
- this.#value = config?.value === undefined ? "" : config?.value;
+ #node: FlowNode
+
+ #nodeProperty: string | undefined;
+
+ constructor(node: FlowNode, config?: StringWidgetConfig) {
+ this.#node = node;
+ this.#nodeProperty = config?.property;
this.#idleStyle = new TextBoxStyle(TextBoxStyleWithFallback(config?.textBoxStyle, {
box: {
color: Theme.Widget.BackgroundColor,
@@ -49,7 +57,14 @@ export class StringWidget {
},
text: { color: Theme.Widget.FontColor },
}));
+
+ this.Set(config?.value === undefined ? "" : config?.value);
this.#callback = config?.callback;
+ if (this.#nodeProperty !== undefined) {
+ this.#node.subscribeToProperty(this.#nodeProperty, (oldVal, newVal) => {
+ this.Set(newVal);
+ });
+ }
}
Size(): Vector2 {
@@ -57,7 +72,15 @@ export class StringWidget {
}
Set(value: string): void {
+ if (this.#value === value) {
+ return;
+ }
this.#value = value;
+
+ if (this.#nodeProperty !== undefined) {
+ this.#node.setProperty(this.#nodeProperty, this.#value)
+ }
+
if (this.#callback !== undefined) {
this.#callback(this.#value);
}
diff --git a/src/widgets/toggle.ts b/src/widgets/toggle.ts
index 5a62bac..bf747f5 100644
--- a/src/widgets/toggle.ts
+++ b/src/widgets/toggle.ts
@@ -3,6 +3,7 @@ import { Box, InBox } from "../types/box";
import { Vector2 } from "../types/vector2";
import { height, width } from "./widget";
import { TextBoxStyle, TextBoxStyleConfig, TextBoxStyleWithFallback } from "../styles/textBox";
+import { FlowNode } from "../node";
export interface ToggleStyleConfig {
idle?: TextBoxStyleConfig,
@@ -13,6 +14,7 @@ export interface ToggleStyleConfig {
}
export interface ToggleWidgetConfig {
+ property?: string;
value?: boolean;
text?: string;
callback?: () => void;
@@ -106,7 +108,11 @@ class ToggleStyle {
export class ToggleWidget {
- #enabled: boolean;
+ #node: FlowNode;
+
+ #nodeProperty: string | undefined;
+
+ #value: boolean;
#text: string;
@@ -116,10 +122,13 @@ export class ToggleWidget {
#callback?: (value: boolean) => void;
- constructor(config?: ToggleWidgetConfig) {
+ constructor(node: FlowNode, config?: ToggleWidgetConfig) {
+ this.#node = node;
+ this.#nodeProperty = config?.property;
this.#text = config?.text === undefined ? "Toggle" : config?.text;
- this.#enabled = config?.value === undefined ? false : config?.value;
- this.#callback = config?.callback
+ this.Set(config?.value === undefined ? false : config?.value);
+ this.#callback = config?.callback;
+
this.#enabledStyle = new ToggleStyle({
idle: TextBoxStyleWithFallback(config?.enabledStyle?.idle, {
box: {
@@ -149,6 +158,12 @@ export class ToggleWidget {
lightBorderColor: config?.disabledStyle?.lightBorderColor,
lightColor: config?.disabledStyle?.lightColor === undefined ? "#004400" : config?.enabledStyle?.lightColor,
});
+
+ if (this.#nodeProperty !== undefined) {
+ this.#node.subscribeToProperty(this.#nodeProperty, (oldVal, newVal) => {
+ this.Set(newVal);
+ });
+ }
}
Size(): Vector2 {
@@ -156,17 +171,33 @@ export class ToggleWidget {
}
Draw(ctx: CanvasRenderingContext2D, position: Vector2, scale: number, mousePosition: Vector2 | undefined): Box {
- let style = this.#enabled ? this.#enabledStyle : this.#disabledStyle;
+ let style = this.#value ? this.#enabledStyle : this.#disabledStyle;
return style.Draw(ctx, position, scale, this.#text, mousePosition);
}
- ClickStart(): void {
- this.#enabled = !this.#enabled;
+ Toggle(): void {
+ this.Set(!this.#value);
+ }
+
+ Set(value: boolean): void {
+ if (this.#value === value) {
+ return;
+ }
+ this.#value = value;
+
+ if (this.#nodeProperty !== undefined) {
+ this.#node.setProperty(this.#nodeProperty, this.#value);
+ }
+
if (this.#callback !== undefined) {
- this.#callback(this.#enabled);
+ this.#callback(this.#value);
}
}
+ ClickStart(): void {
+ this.Toggle();
+ }
+
ClickEnd(): void {
}