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 { }