From 29fe653b84fb197074dc26fd45fd64c9dca03f93 Mon Sep 17 00:00:00 2001 From: Ben Houston Date: Thu, 5 Jan 2023 07:54:07 -0500 Subject: [PATCH 1/3] splitting out scene package. WIP --- packages/scene/src/Nodes/Logic/ColorNodes.ts | 121 +++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 packages/scene/src/Nodes/Logic/ColorNodes.ts diff --git a/packages/scene/src/Nodes/Logic/ColorNodes.ts b/packages/scene/src/Nodes/Logic/ColorNodes.ts new file mode 100644 index 00000000..ba051a8a --- /dev/null +++ b/packages/scene/src/Nodes/Logic/ColorNodes.ts @@ -0,0 +1,121 @@ +import { makeInNOutFunctionDesc } from '@behave-graph/core'; + +import { + hexToRGB, + hslToRGB, + rgbToHex, + rgbToHSL, + Vec3, + vec3Add, + vec3Equals, + vec3Mix, + vec3MultiplyByScalar, + vec3Negate, + vec3Subtract +} from '../../Values/Internal/Vec3'; + +export const Constant = makeInNOutFunctionDesc({ + name: 'math/color', + label: 'Color', + in: ['color'], + out: 'color', + exec: (a: Vec3) => a +}); + +export const Create = makeInNOutFunctionDesc({ + name: 'math/toColor/rgb', + label: 'RGB To Color', + in: [{ r: 'float' }, { g: 'float' }, { b: 'float' }], + out: 'color', + exec: (r: number, g: number, b: number) => new Vec3(r, g, b) +}); + +export const Elements = makeInNOutFunctionDesc({ + name: 'math/toRgb/color', + label: 'Color to RGB', + in: ['color'], + out: [{ r: 'float' }, { g: 'float' }, { b: 'float' }], + exec: (a: Vec3) => { + return { r: a.x, g: a.y, b: a.z }; + } +}); + +export const Add = makeInNOutFunctionDesc({ + name: 'math/add/color', + label: '+', + in: ['color', 'color'], + out: 'color', + exec: vec3Add +}); + +export const Subtract = makeInNOutFunctionDesc({ + name: 'math/subtract/color', + label: '-', + in: ['color', 'color'], + out: 'color', + exec: vec3Subtract +}); + +export const Negate = makeInNOutFunctionDesc({ + name: 'math/negate/color', + label: '-', + in: ['color'], + out: 'color', + exec: vec3Negate +}); + +export const Scale = makeInNOutFunctionDesc({ + name: 'math/scale/color', + label: '×', + in: ['color', 'float'], + out: 'color', + exec: vec3MultiplyByScalar +}); + +export const Mix = makeInNOutFunctionDesc({ + name: 'math/mix/color', + label: '÷', + in: [{ a: 'color' }, { b: 'color' }, { t: 'float' }], + out: 'color', + exec: vec3Mix +}); + +export const HslToColor = makeInNOutFunctionDesc({ + name: 'math/ToColor/hsl', + label: 'HSL to Color', + in: ['vec3'], + out: 'color', + exec: hslToRGB +}); + +export const ColorToHsl = makeInNOutFunctionDesc({ + name: 'math/toHsl/color', + label: 'Color to HSL', + in: ['color'], + out: 'vec3', + exec: rgbToHSL +}); + +export const HexToColor = makeInNOutFunctionDesc({ + name: 'math/toColor/hex', + label: 'HEX to Color', + in: ['float'], + out: 'color', + exec: hexToRGB +}); + +export const ColorToHex = makeInNOutFunctionDesc({ + name: 'math/toHex/color', + label: 'Color to HEX', + in: ['color'], + out: 'float', + exec: rgbToHex +}); + +export const Equal = makeInNOutFunctionDesc({ + name: 'math/equal/color', + label: '=', + in: [{ a: 'color' }, { b: 'color' }, { tolerance: 'float' }], + out: 'boolean', + exec: vec3Equals +}); From c2e688267cbd26fcf0f7bd62360e0a3c796e7d19 Mon Sep 17 00:00:00 2001 From: Ben Houston Date: Thu, 5 Jan 2023 07:55:24 -0500 Subject: [PATCH 2/3] more WIP for scene-package --- behave-graph.code-workspace | 5 + package.json | 2 + .../src/Profiles/Core/Values/FloatNodes.ts | 4 +- .../src/Profiles/Core/registerCoreProfile.ts | 2 +- .../src/Profiles/Scene/Values/ColorNodes.ts | 120 ---------- .../registerSerializersForValueType.ts | 6 +- packages/core/src/index.ts | 35 +-- .../Internal/Common.ts => mathUtilities.ts} | 0 packages/flow/package.json | 1 - packages/scene/package.json | 46 ++++ .../src}/Abstractions/Drivers/DummyScene.ts | 0 .../src}/Abstractions/IScene.ts | 0 .../src/Nodes}/Actions/EaseSceneProperty.ts | 23 +- .../src/Nodes}/Actions/SetSceneProperty.ts | 17 +- .../src/Nodes}/Events/OnSceneNodeClick.ts | 10 +- .../src/Nodes/Logic}/EulerNodes.ts | 5 +- .../src/Nodes/Logic}/Mat3Nodes.ts | 5 +- .../src/Nodes/Logic}/Mat4Nodes.ts | 5 +- .../src/Nodes/Logic}/QuatNodes.ts | 5 +- .../src/Nodes/Logic}/Vec2Nodes.ts | 5 +- .../src/Nodes/Logic}/Vec3Nodes.ts | 5 +- .../src/Nodes/Logic}/Vec4Nodes.ts | 5 +- .../src/Nodes}/Logic/VecElements.ts | 0 .../src/Nodes}/Queries/GetSceneProperty.ts | 15 +- .../Scene => scene/src}/Values/ColorValue.ts | 3 +- .../Scene => scene/src}/Values/EulerValue.ts | 3 +- packages/scene/src/Values/Internal/Mat2.ts | 211 ++++++++++++++++++ .../src}/Values/Internal/Mat3.ts | 9 +- .../src}/Values/Internal/Mat4.ts | 9 +- .../src}/Values/Internal/Vec2.test.ts | 0 .../src}/Values/Internal/Vec2.ts | 8 +- .../src}/Values/Internal/Vec3.test.ts | 0 .../src}/Values/Internal/Vec3.ts | 10 +- .../src}/Values/Internal/Vec4.test.ts | 0 .../src}/Values/Internal/Vec4.ts | 9 +- .../Scene => scene/src}/Values/Mat3Value.ts | 3 +- .../Scene => scene/src}/Values/Mat4Value.ts | 3 +- .../Scene => scene/src}/Values/QuatValue.ts | 3 +- .../Scene => scene/src}/Values/Vec2Value.ts | 3 +- .../Scene => scene/src}/Values/Vec3Value.ts | 3 +- .../Scene => scene/src}/Values/Vec4Value.ts | 3 +- packages/scene/src/index.ts | 37 +++ .../src}/readSceneGraphs.test.ts | 0 .../src}/registerSceneProfile.test.ts | 0 .../src}/registerSceneProfile.ts | 31 +-- 45 files changed, 442 insertions(+), 227 deletions(-) delete mode 100644 packages/core/src/Profiles/Scene/Values/ColorNodes.ts rename packages/core/src/Profiles/{Core => }/registerSerializersForValueType.ts (81%) rename packages/core/src/{Profiles/Core/Values/Internal/Common.ts => mathUtilities.ts} (100%) create mode 100644 packages/scene/package.json rename packages/{core/src/Profiles/Scene => scene/src}/Abstractions/Drivers/DummyScene.ts (100%) rename packages/{core/src/Profiles/Scene => scene/src}/Abstractions/IScene.ts (100%) rename packages/{core/src/Profiles/Scene => scene/src/Nodes}/Actions/EaseSceneProperty.ts (86%) rename packages/{core/src/Profiles/Scene => scene/src/Nodes}/Actions/SetSceneProperty.ts (74%) rename packages/{core/src/Profiles/Scene => scene/src/Nodes}/Events/OnSceneNodeClick.ts (66%) rename packages/{core/src/Profiles/Scene/Values => scene/src/Nodes/Logic}/EulerNodes.ts (96%) rename packages/{core/src/Profiles/Scene/Values => scene/src/Nodes/Logic}/Mat3Nodes.ts (97%) rename packages/{core/src/Profiles/Scene/Values => scene/src/Nodes/Logic}/Mat4Nodes.ts (98%) rename packages/{core/src/Profiles/Scene/Values => scene/src/Nodes/Logic}/QuatNodes.ts (97%) rename packages/{core/src/Profiles/Scene/Values => scene/src/Nodes/Logic}/Vec2Nodes.ts (95%) rename packages/{core/src/Profiles/Scene/Values => scene/src/Nodes/Logic}/Vec3Nodes.ts (95%) rename packages/{core/src/Profiles/Scene/Values => scene/src/Nodes/Logic}/Vec4Nodes.ts (95%) rename packages/{core/src/Profiles/Scene => scene/src/Nodes}/Logic/VecElements.ts (100%) rename packages/{core/src/Profiles/Scene => scene/src/Nodes}/Queries/GetSceneProperty.ts (72%) rename packages/{core/src/Profiles/Scene => scene/src}/Values/ColorValue.ts (87%) rename packages/{core/src/Profiles/Scene => scene/src}/Values/EulerValue.ts (87%) create mode 100644 packages/scene/src/Values/Internal/Mat2.ts rename packages/{core/src/Profiles/Scene => scene/src}/Values/Internal/Mat3.ts (98%) rename packages/{core/src/Profiles/Scene => scene/src}/Values/Internal/Mat4.ts (99%) rename packages/{core/src/Profiles/Scene => scene/src}/Values/Internal/Vec2.test.ts (100%) rename packages/{core/src/Profiles/Scene => scene/src}/Values/Internal/Vec2.ts (92%) rename packages/{core/src/Profiles/Scene => scene/src}/Values/Internal/Vec3.test.ts (100%) rename packages/{core/src/Profiles/Scene => scene/src}/Values/Internal/Vec3.ts (97%) rename packages/{core/src/Profiles/Scene => scene/src}/Values/Internal/Vec4.test.ts (100%) rename packages/{core/src/Profiles/Scene => scene/src}/Values/Internal/Vec4.ts (98%) rename packages/{core/src/Profiles/Scene => scene/src}/Values/Mat3Value.ts (86%) rename packages/{core/src/Profiles/Scene => scene/src}/Values/Mat4Value.ts (86%) rename packages/{core/src/Profiles/Scene => scene/src}/Values/QuatValue.ts (88%) rename packages/{core/src/Profiles/Scene => scene/src}/Values/Vec2Value.ts (86%) rename packages/{core/src/Profiles/Scene => scene/src}/Values/Vec3Value.ts (87%) rename packages/{core/src/Profiles/Scene => scene/src}/Values/Vec4Value.ts (88%) create mode 100644 packages/scene/src/index.ts rename packages/{core/src/Profiles/Scene => scene/src}/readSceneGraphs.test.ts (100%) rename packages/{core/src/Profiles/Scene => scene/src}/registerSceneProfile.test.ts (100%) rename packages/{core/src/Profiles/Scene => scene/src}/registerSceneProfile.ts (71%) diff --git a/behave-graph.code-workspace b/behave-graph.code-workspace index e6b017fa..db694c0f 100644 --- a/behave-graph.code-workspace +++ b/behave-graph.code-workspace @@ -14,6 +14,11 @@ "cSpell.words": [ "fortawesome", "reactflow" + ], + "jest.disabledWorkspaceFolders": [ + "gl-matrix", + "behave-graph", + "threeify" ] }, "extensions": {} diff --git a/package.json b/package.json index ad6e2a88..716e0215 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ }, "workspaces": [ "packages/core", + "packages/scene", "packages/flow", "examples/exec-graph", "examples/export-node-spec", @@ -71,6 +72,7 @@ "preconstruct": { "packages": [ "packages/core", + "packages/scene", "packages/flow" ] }, diff --git a/packages/core/src/Profiles/Core/Values/FloatNodes.ts b/packages/core/src/Profiles/Core/Values/FloatNodes.ts index 2318d4a2..3a2062fb 100644 --- a/packages/core/src/Profiles/Core/Values/FloatNodes.ts +++ b/packages/core/src/Profiles/Core/Values/FloatNodes.ts @@ -1,9 +1,9 @@ -import { makeInNOutFunctionDesc } from '../../../Nodes/FunctionNode'; import { degreesToRadians, equalsTolerance, radiansToDegrees -} from './Internal/Common'; +} from '../../../mathUtilities'; +import { makeInNOutFunctionDesc } from '../../../Nodes/FunctionNode'; // Unreal Engine Blueprint Float nodes: https://docs.unrealengine.com/4.27/en-US/BlueprintAPI/Math/Float/ export const Constant = makeInNOutFunctionDesc({ diff --git a/packages/core/src/Profiles/Core/registerCoreProfile.ts b/packages/core/src/Profiles/Core/registerCoreProfile.ts index fb2c08d2..5d61433b 100644 --- a/packages/core/src/Profiles/Core/registerCoreProfile.ts +++ b/packages/core/src/Profiles/Core/registerCoreProfile.ts @@ -28,7 +28,7 @@ import { LifecycleOnEnd } from './Lifecycle/LifecycleOnEnd'; import { LifecycleOnStart } from './Lifecycle/LifecycleOnStart'; import { LifecycleOnTick } from './Lifecycle/LifecycleOnTick'; import { Easing } from './Logic/Easing'; -import { registerSerializersForValueType } from './registerSerializersForValueType'; +import { registerSerializersForValueType } from '../registerSerializersForValueType'; import { Delay } from './Time/Delay'; import * as TimeNodes from './Time/TimeNodes'; import * as BooleanNodes from './Values/BooleanNodes'; diff --git a/packages/core/src/Profiles/Scene/Values/ColorNodes.ts b/packages/core/src/Profiles/Scene/Values/ColorNodes.ts deleted file mode 100644 index 4e231f03..00000000 --- a/packages/core/src/Profiles/Scene/Values/ColorNodes.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { makeInNOutFunctionDesc } from '../../../Nodes/FunctionNode'; -import { - hexToRGB, - hslToRGB, - rgbToHex, - rgbToHSL, - Vec3, - vec3Add, - vec3Equals, - vec3Mix, - vec3MultiplyByScalar, - vec3Negate, - vec3Subtract -} from './Internal/Vec3'; - -export const Constant = makeInNOutFunctionDesc({ - name: 'math/color', - label: 'Color', - in: ['color'], - out: 'color', - exec: (a: Vec3) => a -}); - -export const Create = makeInNOutFunctionDesc({ - name: 'math/toColor/rgb', - label: 'RGB To Color', - in: [{ r: 'float' }, { g: 'float' }, { b: 'float' }], - out: 'color', - exec: (r: number, g: number, b: number) => new Vec3(r, g, b) -}); - -export const Elements = makeInNOutFunctionDesc({ - name: 'math/toRgb/color', - label: 'Color to RGB', - in: ['color'], - out: [{ r: 'float' }, { g: 'float' }, { b: 'float' }], - exec: (a: Vec3) => { - return { r: a.x, g: a.y, b: a.z }; - } -}); - -export const Add = makeInNOutFunctionDesc({ - name: 'math/add/color', - label: '+', - in: ['color', 'color'], - out: 'color', - exec: vec3Add -}); - -export const Subtract = makeInNOutFunctionDesc({ - name: 'math/subtract/color', - label: '-', - in: ['color', 'color'], - out: 'color', - exec: vec3Subtract -}); - -export const Negate = makeInNOutFunctionDesc({ - name: 'math/negate/color', - label: '-', - in: ['color'], - out: 'color', - exec: vec3Negate -}); - -export const Scale = makeInNOutFunctionDesc({ - name: 'math/scale/color', - label: '×', - in: ['color', 'float'], - out: 'color', - exec: vec3MultiplyByScalar -}); - -export const Mix = makeInNOutFunctionDesc({ - name: 'math/mix/color', - label: '÷', - in: [{ a: 'color' }, { b: 'color' }, { t: 'float' }], - out: 'color', - exec: vec3Mix -}); - -export const HslToColor = makeInNOutFunctionDesc({ - name: 'math/ToColor/hsl', - label: 'HSL to Color', - in: ['vec3'], - out: 'color', - exec: hslToRGB -}); - -export const ColorToHsl = makeInNOutFunctionDesc({ - name: 'math/toHsl/color', - label: 'Color to HSL', - in: ['color'], - out: 'vec3', - exec: rgbToHSL -}); - -export const HexToColor = makeInNOutFunctionDesc({ - name: 'math/toColor/hex', - label: 'HEX to Color', - in: ['float'], - out: 'color', - exec: hexToRGB -}); - -export const ColorToHex = makeInNOutFunctionDesc({ - name: 'math/toHex/color', - label: 'Color to HEX', - in: ['color'], - out: 'float', - exec: rgbToHex -}); - -export const Equal = makeInNOutFunctionDesc({ - name: 'math/equal/color', - label: '=', - in: [{ a: 'color' }, { b: 'color' }, { tolerance: 'float' }], - out: 'boolean', - exec: vec3Equals -}); diff --git a/packages/core/src/Profiles/Core/registerSerializersForValueType.ts b/packages/core/src/Profiles/registerSerializersForValueType.ts similarity index 81% rename from packages/core/src/Profiles/Core/registerSerializersForValueType.ts rename to packages/core/src/Profiles/registerSerializersForValueType.ts index d658926b..806c5987 100644 --- a/packages/core/src/Profiles/Core/registerSerializersForValueType.ts +++ b/packages/core/src/Profiles/registerSerializersForValueType.ts @@ -1,6 +1,6 @@ -import { makeInNOutFunctionDesc } from '../../Nodes/FunctionNode'; -import { Registry } from '../../Registry'; -import { toCamelCase } from '../../toCamelCase'; +import { makeInNOutFunctionDesc } from '../Nodes/FunctionNode'; +import { Registry } from '../Registry'; +import { toCamelCase } from '../toCamelCase'; export function registerSerializersForValueType( registry: Registry, diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index f9008fb7..d81d2d67 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,3 +1,10 @@ +export * from './parseFloats'; +export * from './toCamelCase'; +export * from './Easing'; +export * from './sleep'; +export * from './sequence'; +export * from './mathUtilities'; + export * from './Diagnostics/Logger'; export * from './Diagnostics/Assert'; @@ -45,6 +52,8 @@ export * from './Graphs/Validation/validateGraph'; export * from './Graphs/IO/GraphJSON'; export * from './Graphs/IO/NodeSpecJSON'; +export * from './Profiles/registerSerializersForValueType'; + // core profile export * from './Profiles/Core/Abstractions/ILifecycleEventEmitter'; export * from './Profiles/Core/Abstractions/ILogger'; @@ -81,29 +90,3 @@ export * from './Profiles/Core/Values/StringValue'; export * from './Profiles/Core/Variables/VariableSet'; export * from './Profiles/Core/Variables/VariableGet'; export * from './Profiles/Core/registerCoreProfile'; - -// scene profile -export * from './Profiles/Scene/Abstractions/IScene'; -export * from './Profiles/Scene/Abstractions/Drivers/DummyScene'; -export * from './Profiles/Scene/Actions/SetSceneProperty'; -export * from './Profiles/Scene/Events/OnSceneNodeClick'; -export * from './Profiles/Scene/Logic/VecElements'; -export * from './Profiles/Scene/Queries/GetSceneProperty'; -export * from './Profiles/Scene/Values/Internal/Vec2'; -export * from './Profiles/Scene/Values/Internal/Vec3'; -export * from './Profiles/Scene/Values/Internal/Vec4'; -export * as ColorNodes from './Profiles/Scene/Values/ColorNodes'; -export * from './Profiles/Scene/Values/ColorValue'; -export * as EulerNodes from './Profiles/Scene/Values/EulerNodes'; -export * from './Profiles/Scene/Values/EulerValue'; -export * as Vec2Nodes from './Profiles/Scene/Values/Vec2Nodes'; -export * from './Profiles/Scene/Values/Vec2Value'; -export * as Vec3Nodes from './Profiles/Scene/Values/Vec3Nodes'; -export * from './Profiles/Scene/Values/Vec3Value'; -export * as Vec4Nodes from './Profiles/Scene/Values/Vec4Nodes'; -export * from './Profiles/Scene/Values/Vec4Value'; -export * as QuatNodes from './Profiles/Scene/Values/QuatNodes'; -export * from './Profiles/Scene/Values/QuatValue'; -export * from './Profiles/Scene/registerSceneProfile'; - -export * from './parseFloats'; diff --git a/packages/core/src/Profiles/Core/Values/Internal/Common.ts b/packages/core/src/mathUtilities.ts similarity index 100% rename from packages/core/src/Profiles/Core/Values/Internal/Common.ts rename to packages/core/src/mathUtilities.ts diff --git a/packages/flow/package.json b/packages/flow/package.json index fe17f011..c6bb8f59 100644 --- a/packages/flow/package.json +++ b/packages/flow/package.json @@ -51,7 +51,6 @@ "@fortawesome/fontawesome-svg-core": "^6.1.2", "@fortawesome/free-solid-svg-icons": "^6.1.2", "@fortawesome/react-fontawesome": "^0.2.0", - "behave-graph": "^0.9.9", "classnames": "^2.3.1", "downshift": "^6.1.7", "react": "^18.2.0", diff --git a/packages/scene/package.json b/packages/scene/package.json new file mode 100644 index 00000000..6cfa2b2b --- /dev/null +++ b/packages/scene/package.json @@ -0,0 +1,46 @@ +{ + "name": "@behave-graph/scene", + "version": "0.9.12", + "source": "src/index.ts", + "main": "dist/behave-graph-scene.cjs.js", + "module": "dist/behave-graph-scene.esm.js", + "types": "dist/behave-graph-scene.cjs.d.ts", + "sideEffects": false, + "preconstruct": { + "entrypoints": [ + "index.ts" + ] + }, + "scripts": { + "watch": "tsc -w", + "build": "tsc", + "test": "jest" + }, + "devDependencies": { + "@types/glob": "^8.0.0", + "@types/jest": "^29.1.1", + "@types/node": "^18.0.6", + "@types/offscreencanvas": "^2019.7.0", + "@types/three": "^0.144.0", + "@typescript-eslint/eslint-plugin": "^5.38.1", + "eslint": "^8.24.0", + "eslint-config-prettier": "^8.5.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-prettier": "^4.2.1", + "eslint-plugin-promise": "^6.0.1", + "eslint-plugin-simple-import-sort": "^8.0.0", + "eslint-plugin-unicorn": "^44.0.0", + "eslint-plugin-unused-imports": "^2.0.0", + "jest": "^29.1.2", + "prettier": "^2.7.1", + "ts-jest": "^29.0.3", + "ts-jest-resolver": "^2.0.0", + "typescript": "^4.8.4" + }, + "description": "Simple, extensible behavior graph engine", + "author": "", + "license": "ISC", + "dependencies": { + "@behave-graph/core": "*" + } +} diff --git a/packages/core/src/Profiles/Scene/Abstractions/Drivers/DummyScene.ts b/packages/scene/src/Abstractions/Drivers/DummyScene.ts similarity index 100% rename from packages/core/src/Profiles/Scene/Abstractions/Drivers/DummyScene.ts rename to packages/scene/src/Abstractions/Drivers/DummyScene.ts diff --git a/packages/core/src/Profiles/Scene/Abstractions/IScene.ts b/packages/scene/src/Abstractions/IScene.ts similarity index 100% rename from packages/core/src/Profiles/Scene/Abstractions/IScene.ts rename to packages/scene/src/Abstractions/IScene.ts diff --git a/packages/core/src/Profiles/Scene/Actions/EaseSceneProperty.ts b/packages/scene/src/Nodes/Actions/EaseSceneProperty.ts similarity index 86% rename from packages/core/src/Profiles/Scene/Actions/EaseSceneProperty.ts rename to packages/scene/src/Nodes/Actions/EaseSceneProperty.ts index 6395c371..85c652d6 100644 --- a/packages/core/src/Profiles/Scene/Actions/EaseSceneProperty.ts +++ b/packages/scene/src/Nodes/Actions/EaseSceneProperty.ts @@ -1,12 +1,17 @@ -import { Easing, EasingFunctions, EasingModes } from '../../../Easing'; -import { Engine } from '../../../Execution/Engine'; -import { IGraphApi } from '../../../Graphs/Graph'; -import { AsyncNode } from '../../../Nodes/AsyncNode'; -import { NodeDescription } from '../../../Nodes/Registry/NodeDescription'; -import { Socket } from '../../../Sockets/Socket'; -import { toCamelCase } from '../../../toCamelCase'; -import { ILifecycleEventEmitter } from '../../Core/Abstractions/ILifecycleEventEmitter'; -import { IScene } from '../Abstractions/IScene'; +import { + AsyncNode, + Easing, + EasingFunctions, + EasingModes, + Engine, + IGraphApi, + ILifecycleEventEmitter, + NodeDescription, + Socket, + toCamelCase +} from '@behave-graph/core'; + +import { IScene } from '../../Abstractions/IScene'; export class EaseSceneProperty extends AsyncNode { public static GetDescriptions( diff --git a/packages/core/src/Profiles/Scene/Actions/SetSceneProperty.ts b/packages/scene/src/Nodes/Actions/SetSceneProperty.ts similarity index 74% rename from packages/core/src/Profiles/Scene/Actions/SetSceneProperty.ts rename to packages/scene/src/Nodes/Actions/SetSceneProperty.ts index 349d94dd..79a79641 100644 --- a/packages/core/src/Profiles/Scene/Actions/SetSceneProperty.ts +++ b/packages/scene/src/Nodes/Actions/SetSceneProperty.ts @@ -1,10 +1,13 @@ -import { Fiber } from '../../../Execution/Fiber'; -import { IGraphApi } from '../../../Graphs/Graph'; -import { FlowNode } from '../../../Nodes/FlowNode'; -import { NodeDescription } from '../../../Nodes/Registry/NodeDescription'; -import { Socket } from '../../../Sockets/Socket'; -import { toCamelCase } from '../../../toCamelCase'; -import { IScene } from '../Abstractions/IScene'; +import { + Fiber, + FlowNode, + IGraphApi, + NodeDescription, + Socket, + toCamelCase +} from '@behave-graph/core'; + +import { IScene } from '../../Abstractions/IScene'; export class SetSceneProperty extends FlowNode { public static GetDescriptions(scene: IScene, ...valueTypeNames: string[]) { diff --git a/packages/core/src/Profiles/Scene/Events/OnSceneNodeClick.ts b/packages/scene/src/Nodes/Events/OnSceneNodeClick.ts similarity index 66% rename from packages/core/src/Profiles/Scene/Events/OnSceneNodeClick.ts rename to packages/scene/src/Nodes/Events/OnSceneNodeClick.ts index 8df3b625..7da636ff 100644 --- a/packages/core/src/Profiles/Scene/Events/OnSceneNodeClick.ts +++ b/packages/scene/src/Nodes/Events/OnSceneNodeClick.ts @@ -1,7 +1,9 @@ -import { IGraphApi } from '../../../Graphs/Graph'; -import { EventNode } from '../../../Nodes/EventNode'; -import { NodeDescription } from '../../../Nodes/Registry/NodeDescription'; -import { Socket } from '../../../Sockets/Socket'; +import { + EventNode, + IGraphApi, + NodeDescription, + Socket +} from '@behave-graph/core'; // very 3D specific. export class OnSceneNodeClick extends EventNode { diff --git a/packages/core/src/Profiles/Scene/Values/EulerNodes.ts b/packages/scene/src/Nodes/Logic/EulerNodes.ts similarity index 96% rename from packages/core/src/Profiles/Scene/Values/EulerNodes.ts rename to packages/scene/src/Nodes/Logic/EulerNodes.ts index 0678589d..e0ac36c4 100644 --- a/packages/core/src/Profiles/Scene/Values/EulerNodes.ts +++ b/packages/scene/src/Nodes/Logic/EulerNodes.ts @@ -1,4 +1,5 @@ -import { makeInNOutFunctionDesc } from '../../../Nodes/FunctionNode'; +import { makeInNOutFunctionDesc } from '@behave-graph/core'; + import { mat3ToEuler, mat4ToEuler, @@ -10,7 +11,7 @@ import { vec3MultiplyByScalar, vec3Negate, vec3Subtract -} from './Internal/Vec3'; +} from '../../Values/Internal/Vec3'; export const Constant = makeInNOutFunctionDesc({ name: 'math/euler', diff --git a/packages/core/src/Profiles/Scene/Values/Mat3Nodes.ts b/packages/scene/src/Nodes/Logic/Mat3Nodes.ts similarity index 97% rename from packages/core/src/Profiles/Scene/Values/Mat3Nodes.ts rename to packages/scene/src/Nodes/Logic/Mat3Nodes.ts index 0281c01a..2604f07a 100644 --- a/packages/core/src/Profiles/Scene/Values/Mat3Nodes.ts +++ b/packages/scene/src/Nodes/Logic/Mat3Nodes.ts @@ -1,4 +1,5 @@ -import { makeInNOutFunctionDesc } from '../../../Nodes/FunctionNode'; +import { makeInNOutFunctionDesc } from '@behave-graph/core'; + import { column3ToMat3, eulerToMat3, @@ -20,7 +21,7 @@ import { mat4ToMat3, scale2ToMat3, translation2ToMat3 -} from './Internal/Mat3'; +} from '../../Values/Internal/Mat3'; export const Constant = makeInNOutFunctionDesc({ name: 'math/mat3', diff --git a/packages/core/src/Profiles/Scene/Values/Mat4Nodes.ts b/packages/scene/src/Nodes/Logic/Mat4Nodes.ts similarity index 98% rename from packages/core/src/Profiles/Scene/Values/Mat4Nodes.ts rename to packages/scene/src/Nodes/Logic/Mat4Nodes.ts index c532862c..26cbd03f 100644 --- a/packages/core/src/Profiles/Scene/Values/Mat4Nodes.ts +++ b/packages/scene/src/Nodes/Logic/Mat4Nodes.ts @@ -1,4 +1,5 @@ -import { makeInNOutFunctionDesc } from '../../../Nodes/FunctionNode'; +import { makeInNOutFunctionDesc } from '@behave-graph/core'; + import { column4ToMat4, eulerToMat4, @@ -27,7 +28,7 @@ import { quatToMat4, scale3ToMat4, translation3ToMat4 -} from './Internal/Mat4'; +} from '../../Values/Internal/Mat4'; export const Constant = makeInNOutFunctionDesc({ name: 'math/mat4', diff --git a/packages/core/src/Profiles/Scene/Values/QuatNodes.ts b/packages/scene/src/Nodes/Logic/QuatNodes.ts similarity index 97% rename from packages/core/src/Profiles/Scene/Values/QuatNodes.ts rename to packages/scene/src/Nodes/Logic/QuatNodes.ts index 63374eb8..82a63ebe 100644 --- a/packages/core/src/Profiles/Scene/Values/QuatNodes.ts +++ b/packages/scene/src/Nodes/Logic/QuatNodes.ts @@ -1,4 +1,5 @@ -import { makeInNOutFunctionDesc } from '../../../Nodes/FunctionNode'; +import { makeInNOutFunctionDesc } from '@behave-graph/core'; + import { angleAxisToQuat, eulerToQuat, @@ -17,7 +18,7 @@ import { vec4MultiplyByScalar, vec4Normalize, vec4ToArray -} from './Internal/Vec4'; +} from '../../Values/Internal/Vec4'; /* - from Angle Axis diff --git a/packages/core/src/Profiles/Scene/Values/Vec2Nodes.ts b/packages/scene/src/Nodes/Logic/Vec2Nodes.ts similarity index 95% rename from packages/core/src/Profiles/Scene/Values/Vec2Nodes.ts rename to packages/scene/src/Nodes/Logic/Vec2Nodes.ts index 6d015a09..605eb7c6 100644 --- a/packages/core/src/Profiles/Scene/Values/Vec2Nodes.ts +++ b/packages/scene/src/Nodes/Logic/Vec2Nodes.ts @@ -1,4 +1,5 @@ -import { makeInNOutFunctionDesc } from '../../../Nodes/FunctionNode'; +import { makeInNOutFunctionDesc } from '@behave-graph/core'; + import { Vec2, vec2Add, @@ -11,7 +12,7 @@ import { vec2Normalize, vec2Subtract, vec2ToArray -} from './Internal/Vec2'; +} from '../../Values/Internal/Vec2'; export const Constant = makeInNOutFunctionDesc({ name: 'math/vec2', diff --git a/packages/core/src/Profiles/Scene/Values/Vec3Nodes.ts b/packages/scene/src/Nodes/Logic/Vec3Nodes.ts similarity index 95% rename from packages/core/src/Profiles/Scene/Values/Vec3Nodes.ts rename to packages/scene/src/Nodes/Logic/Vec3Nodes.ts index 40173097..72398e2b 100644 --- a/packages/core/src/Profiles/Scene/Values/Vec3Nodes.ts +++ b/packages/scene/src/Nodes/Logic/Vec3Nodes.ts @@ -1,4 +1,5 @@ -import { makeInNOutFunctionDesc } from '../../../Nodes/FunctionNode'; +import { makeInNOutFunctionDesc } from '@behave-graph/core'; + import { Vec3, vec3Add, @@ -11,7 +12,7 @@ import { vec3Negate, vec3Normalize, vec3Subtract -} from './Internal/Vec3'; +} from '../../Values/Internal/Vec3'; export const Constant = makeInNOutFunctionDesc({ name: 'math/vec3', diff --git a/packages/core/src/Profiles/Scene/Values/Vec4Nodes.ts b/packages/scene/src/Nodes/Logic/Vec4Nodes.ts similarity index 95% rename from packages/core/src/Profiles/Scene/Values/Vec4Nodes.ts rename to packages/scene/src/Nodes/Logic/Vec4Nodes.ts index c602fb04..21c79464 100644 --- a/packages/core/src/Profiles/Scene/Values/Vec4Nodes.ts +++ b/packages/scene/src/Nodes/Logic/Vec4Nodes.ts @@ -1,4 +1,5 @@ -import { makeInNOutFunctionDesc } from '../../../Nodes/FunctionNode'; +import { makeInNOutFunctionDesc } from '@behave-graph/core'; + import { Vec4, vec4Add, @@ -10,7 +11,7 @@ import { vec4Negate, vec4Normalize, vec4Subtract -} from './Internal/Vec4'; +} from '../../Values/Internal/Vec4'; export const Constant = makeInNOutFunctionDesc({ name: 'math/vec4', diff --git a/packages/core/src/Profiles/Scene/Logic/VecElements.ts b/packages/scene/src/Nodes/Logic/VecElements.ts similarity index 100% rename from packages/core/src/Profiles/Scene/Logic/VecElements.ts rename to packages/scene/src/Nodes/Logic/VecElements.ts diff --git a/packages/core/src/Profiles/Scene/Queries/GetSceneProperty.ts b/packages/scene/src/Nodes/Queries/GetSceneProperty.ts similarity index 72% rename from packages/core/src/Profiles/Scene/Queries/GetSceneProperty.ts rename to packages/scene/src/Nodes/Queries/GetSceneProperty.ts index 7259a328..66268b9d 100644 --- a/packages/core/src/Profiles/Scene/Queries/GetSceneProperty.ts +++ b/packages/scene/src/Nodes/Queries/GetSceneProperty.ts @@ -1,9 +1,12 @@ -import { IGraphApi } from '../../../Graphs/Graph'; -import { FunctionNode } from '../../../Nodes/FunctionNode'; -import { NodeDescription } from '../../../Nodes/Registry/NodeDescription'; -import { Socket } from '../../../Sockets/Socket'; -import { toCamelCase } from '../../../toCamelCase'; -import { IScene } from '../Abstractions/IScene'; +import { + FunctionNode, + IGraphApi, + NodeDescription, + Socket, + toCamelCase +} from '@behave-graph/core'; + +import { IScene } from '../../Abstractions/IScene'; export class GetSceneProperty extends FunctionNode { public static GetDescriptions(scene: IScene, ...valueTypeNames: string[]) { diff --git a/packages/core/src/Profiles/Scene/Values/ColorValue.ts b/packages/scene/src/Values/ColorValue.ts similarity index 87% rename from packages/core/src/Profiles/Scene/Values/ColorValue.ts rename to packages/scene/src/Values/ColorValue.ts index f25df611..963ff391 100644 --- a/packages/core/src/Profiles/Scene/Values/ColorValue.ts +++ b/packages/scene/src/Values/ColorValue.ts @@ -1,4 +1,5 @@ -import { ValueType } from '../../../Values/ValueType'; +import { ValueType } from '@behave-graph/core'; + import { Vec3, Vec3JSON, vec3Mix, vec3Parse } from './Internal/Vec3'; export const ColorValue = new ValueType( diff --git a/packages/core/src/Profiles/Scene/Values/EulerValue.ts b/packages/scene/src/Values/EulerValue.ts similarity index 87% rename from packages/core/src/Profiles/Scene/Values/EulerValue.ts rename to packages/scene/src/Values/EulerValue.ts index 52aff863..3e26579d 100644 --- a/packages/core/src/Profiles/Scene/Values/EulerValue.ts +++ b/packages/scene/src/Values/EulerValue.ts @@ -1,4 +1,5 @@ -import { ValueType } from '../../../Values/ValueType'; +import { ValueType } from '@behave-graph/core'; + import { Vec3, Vec3JSON, vec3Mix, vec3Parse } from './Internal/Vec3'; export const EulerValue = new ValueType( diff --git a/packages/scene/src/Values/Internal/Mat2.ts b/packages/scene/src/Values/Internal/Mat2.ts new file mode 100644 index 00000000..585eb27e --- /dev/null +++ b/packages/scene/src/Values/Internal/Mat2.ts @@ -0,0 +1,211 @@ +import { + EPSILON, + equalsTolerance, + parseSafeFloats, + toSafeString +} from '@behave-graph/core'; + +import { Mat3 } from './Mat3'; +import { Mat4 } from './Mat4'; +import { Vec2 } from './Vec2'; + +// uses OpenGL matrix layout where each column is specified subsequently in order from left to right. +// ( x, y ) x [ 0 2 ] = ( x', y' ) +// [ 1 3 ] + +const NUM_ROWS = 2; +const NUM_COLUMNS = 2; +const NUM_ELEMENTS = NUM_ROWS * NUM_COLUMNS; + +export type Mat2JSON = number[]; + +export class Mat2 { + constructor(public elements: number[] = [1, 0, 0, 1]) { + if (elements.length !== NUM_ELEMENTS) { + throw new Error( + `elements must have length ${NUM_ELEMENTS}, got ${elements.length}` + ); + } + } + + clone(result = new Mat2()): Mat2 { + return result.set(this.elements); + } + set(elements: number[]): this { + if (elements.length !== NUM_ELEMENTS) { + throw new Error( + `elements must have length ${NUM_ELEMENTS}, got ${elements.length}` + ); + } + for (let i = 0; i < NUM_ELEMENTS; i++) { + this.elements[i] = elements[i]; + } + return this; + } +} + +export function mat2SetColumn3( + m: Mat2, + columnIndex: number, + column: Vec2, + result = new Mat2() +): Mat2 { + const re = result.set(m.elements).elements; + const base = columnIndex * NUM_ROWS; + re[base + 0] = column.x; + re[base + 1] = column.y; + return result; +} + +export function mat2SetRow3( + m: Mat2, + rowIndex: number, + row: Vec2, + result = new Mat2() +): Mat2 { + const re = result.set(m.elements).elements; + re[rowIndex + NUM_COLUMNS * 0] = row.x; + re[rowIndex + NUM_COLUMNS * 1] = row.y; + return result; +} + +export function column3ToMat2( + a: Vec2, + b: Vec2, + c: Vec2, + result = new Mat2() +): Mat2 { + const re = result.elements; + const columns = [a, b, c]; + for (let c = 0; c < columns.length; c++) { + const base = c * NUM_ROWS; + const column = columns[c]; + re[base + 0] = column.x; + re[base + 1] = column.y; + } + return result; +} + +export function mat2Equals(a: Mat2, b: Mat2, tolerance = EPSILON): boolean { + for (let i = 0; i < NUM_ELEMENTS; i++) { + if (!equalsTolerance(a.elements[i], b.elements[i], tolerance)) return false; + } + return true; +} +export function mat2Add(a: Mat2, b: Mat2, result: Mat2 = new Mat2()): Mat2 { + for (let i = 0; i < NUM_ELEMENTS; i++) { + result.elements[i] = a.elements[i] + b.elements[i]; + } + return result; +} +export function mat2Subtract( + a: Mat2, + b: Mat2, + result: Mat2 = new Mat2() +): Mat2 { + for (let i = 0; i < NUM_ELEMENTS; i++) { + result.elements[i] = a.elements[i] - b.elements[i]; + } + return result; +} +export function mat2MultiplyByScalar( + a: Mat2, + b: number, + result: Mat2 = new Mat2() +): Mat2 { + for (let i = 0; i < NUM_ELEMENTS; i++) { + result.elements[i] = a.elements[i] * b; + } + return result; +} +export function mat2Negate(a: Mat2, result: Mat2 = new Mat2()): Mat2 { + for (let i = 0; i < NUM_ELEMENTS; i++) { + result.elements[i] = -a.elements[i]; + } + return result; +} + +export function mat2Multiply(a: Mat2, b: Mat2, result = new Mat2()): Mat2 { + throw new Error('not implemented'); +} + +export function mat2Determinant(m: Mat2): number { + throw new Error('not implemented'); +} + +export function mat2Transpose(m: Mat2, result = new Mat2()): Mat2 { + const me = m.elements; + const te = result.elements; + + te[0] = me[0]; + te[1] = me[2]; + te[2] = me[1]; + te[3] = me[3]; + + return result; +} + +export function mat2Inverse(m: Mat2, result = new Mat2()): Mat2 { + throw new Error('not implemented'); +} + +export function mat2Mix( + a: Mat2, + b: Mat2, + t: number, + result = new Mat2() +): Mat2 { + const s = 1 - t; + for (let i = 0; i < NUM_ELEMENTS; i++) { + result.elements[i] = a.elements[i] * s + b.elements[i] * t; + } + return result; +} +export function mat2FromArray( + array: Float32Array | number[], + offset = 0, + result: Mat2 = new Mat2() +): Mat2 { + for (let i = 0; i < NUM_ELEMENTS; i++) { + result.elements[i] = array[offset + i]; + } + return result; +} +export function mat2ToArray( + a: Mat2, + array: Float32Array | number[], + offset = 0 +): void { + for (let i = 0; i < NUM_ELEMENTS; i++) { + array[offset + i] = a.elements[i]; + } +} + +export function mat2ToString(a: Mat2): string { + return toSafeString(a.elements); +} +export function mat2Parse(text: string, result = new Mat2()): Mat2 { + return mat2FromArray(parseSafeFloats(text), 0, result); +} + +export function scale2ToMat2(s: Vec2, result = new Mat2()): Mat2 { + return result.set([s.x, 0, 0, s.y]); +} +// from gl-matrix +export function mat2ToScale2(m: Mat4, result = new Vec2()): Vec2 { + const mat = m.elements; + const m11 = mat[0]; + const m12 = mat[1]; + const m21 = mat[2]; + const m22 = mat[3]; + + return result.set( + Math.sqrt(m11 * m11 + m12 * m12), + Math.sqrt(m21 * m21 + m22 * m22) + ); +} + +export function mat3ToMat2(a: Mat3, result = new Mat2()): Mat2 { + const ae = a.elements; + return result.set([ae[0], ae[1], ae[3], ae[4]]); +} diff --git a/packages/core/src/Profiles/Scene/Values/Internal/Mat3.ts b/packages/scene/src/Values/Internal/Mat3.ts similarity index 98% rename from packages/core/src/Profiles/Scene/Values/Internal/Mat3.ts rename to packages/scene/src/Values/Internal/Mat3.ts index d6d04662..7bfb9631 100644 --- a/packages/core/src/Profiles/Scene/Values/Internal/Mat3.ts +++ b/packages/scene/src/Values/Internal/Mat3.ts @@ -1,5 +1,10 @@ -import { parseSafeFloats, toSafeString } from '../../../../parseFloats'; -import { EPSILON, equalsTolerance } from '../../../Core/Values/Internal/Common'; +import { + EPSILON, + equalsTolerance, + parseSafeFloats, + toSafeString +} from '@behave-graph/core'; + import { Mat4 } from './Mat4'; import { Vec2 } from './Vec2'; import { Vec3 } from './Vec3'; diff --git a/packages/core/src/Profiles/Scene/Values/Internal/Mat4.ts b/packages/scene/src/Values/Internal/Mat4.ts similarity index 99% rename from packages/core/src/Profiles/Scene/Values/Internal/Mat4.ts rename to packages/scene/src/Values/Internal/Mat4.ts index 655cab95..08268d04 100644 --- a/packages/core/src/Profiles/Scene/Values/Internal/Mat4.ts +++ b/packages/scene/src/Values/Internal/Mat4.ts @@ -1,5 +1,10 @@ -import { parseSafeFloats, toSafeString } from '../../../../parseFloats'; -import { EPSILON, equalsTolerance } from '../../../Core/Values/Internal/Common'; +import { + EPSILON, + equalsTolerance, + parseSafeFloats, + toSafeString +} from '@behave-graph/core'; + import { eulerToMat3, Mat3, quatToMat3 } from './Mat3'; import { Vec2 } from './Vec2'; import { diff --git a/packages/core/src/Profiles/Scene/Values/Internal/Vec2.test.ts b/packages/scene/src/Values/Internal/Vec2.test.ts similarity index 100% rename from packages/core/src/Profiles/Scene/Values/Internal/Vec2.test.ts rename to packages/scene/src/Values/Internal/Vec2.test.ts diff --git a/packages/core/src/Profiles/Scene/Values/Internal/Vec2.ts b/packages/scene/src/Values/Internal/Vec2.ts similarity index 92% rename from packages/core/src/Profiles/Scene/Values/Internal/Vec2.ts rename to packages/scene/src/Values/Internal/Vec2.ts index ed203ae8..a209f0da 100644 --- a/packages/core/src/Profiles/Scene/Values/Internal/Vec2.ts +++ b/packages/scene/src/Values/Internal/Vec2.ts @@ -1,5 +1,9 @@ -import { parseSafeFloats, toSafeString } from '../../../../parseFloats'; -import { EPSILON, equalsTolerance } from '../../../Core/Values/Internal/Common'; +import { + EPSILON, + equalsTolerance, + parseSafeFloats, + toSafeString +} from '@behave-graph/core'; export type Vec2JSON = number[]; diff --git a/packages/core/src/Profiles/Scene/Values/Internal/Vec3.test.ts b/packages/scene/src/Values/Internal/Vec3.test.ts similarity index 100% rename from packages/core/src/Profiles/Scene/Values/Internal/Vec3.test.ts rename to packages/scene/src/Values/Internal/Vec3.test.ts diff --git a/packages/core/src/Profiles/Scene/Values/Internal/Vec3.ts b/packages/scene/src/Values/Internal/Vec3.ts similarity index 97% rename from packages/core/src/Profiles/Scene/Values/Internal/Vec3.ts rename to packages/scene/src/Values/Internal/Vec3.ts index ced175ce..314da414 100644 --- a/packages/core/src/Profiles/Scene/Values/Internal/Vec3.ts +++ b/packages/scene/src/Values/Internal/Vec3.ts @@ -1,9 +1,11 @@ -import { parseSafeFloats, toSafeString } from '../../../../parseFloats'; import { - clamp, EPSILON, - equalsTolerance -} from '../../../Core/Values/Internal/Common'; + equalsTolerance, + parseSafeFloats, + toSafeString +} from '@behave-graph/core'; +import { clamp } from 'three/src/math/MathUtils'; + import { Mat3, mat4ToMat3, quatToMat3 } from './Mat3'; import { Mat4 } from './Mat4'; import { Vec4 } from './Vec4'; diff --git a/packages/core/src/Profiles/Scene/Values/Internal/Vec4.test.ts b/packages/scene/src/Values/Internal/Vec4.test.ts similarity index 100% rename from packages/core/src/Profiles/Scene/Values/Internal/Vec4.test.ts rename to packages/scene/src/Values/Internal/Vec4.test.ts diff --git a/packages/core/src/Profiles/Scene/Values/Internal/Vec4.ts b/packages/scene/src/Values/Internal/Vec4.ts similarity index 98% rename from packages/core/src/Profiles/Scene/Values/Internal/Vec4.ts rename to packages/scene/src/Values/Internal/Vec4.ts index bbf0c431..deb3b86a 100644 --- a/packages/core/src/Profiles/Scene/Values/Internal/Vec4.ts +++ b/packages/scene/src/Values/Internal/Vec4.ts @@ -1,5 +1,10 @@ -import { parseSafeFloats, toSafeString } from '../../../../parseFloats'; -import { EPSILON, equalsTolerance } from '../../../Core/Values/Internal/Common'; +import { + EPSILON, + equalsTolerance, + parseSafeFloats, + toSafeString +} from '@behave-graph/core'; + import { Mat3, mat4ToMat3 } from './Mat3'; import { Mat4 } from './Mat4'; import { Vec3 } from './Vec3'; diff --git a/packages/core/src/Profiles/Scene/Values/Mat3Value.ts b/packages/scene/src/Values/Mat3Value.ts similarity index 86% rename from packages/core/src/Profiles/Scene/Values/Mat3Value.ts rename to packages/scene/src/Values/Mat3Value.ts index 22f30bc2..008e2276 100644 --- a/packages/core/src/Profiles/Scene/Values/Mat3Value.ts +++ b/packages/scene/src/Values/Mat3Value.ts @@ -1,4 +1,5 @@ -import { ValueType } from '../../../Values/ValueType'; +import { ValueType } from '@behave-graph/core'; + import { Mat3, Mat3JSON, mat3Mix, mat3Parse } from './Internal/Mat3'; export const Mat3Value = new ValueType( diff --git a/packages/core/src/Profiles/Scene/Values/Mat4Value.ts b/packages/scene/src/Values/Mat4Value.ts similarity index 86% rename from packages/core/src/Profiles/Scene/Values/Mat4Value.ts rename to packages/scene/src/Values/Mat4Value.ts index efbf441e..de89bea3 100644 --- a/packages/core/src/Profiles/Scene/Values/Mat4Value.ts +++ b/packages/scene/src/Values/Mat4Value.ts @@ -1,4 +1,5 @@ -import { ValueType } from '../../../Values/ValueType'; +import { ValueType } from '@behave-graph/core'; + import { Mat4, Mat4JSON, mat4Mix, mat4Parse } from './Internal/Mat4'; export const Mat4Value = new ValueType( diff --git a/packages/core/src/Profiles/Scene/Values/QuatValue.ts b/packages/scene/src/Values/QuatValue.ts similarity index 88% rename from packages/core/src/Profiles/Scene/Values/QuatValue.ts rename to packages/scene/src/Values/QuatValue.ts index 15f5513e..8ff64664 100644 --- a/packages/core/src/Profiles/Scene/Values/QuatValue.ts +++ b/packages/scene/src/Values/QuatValue.ts @@ -1,4 +1,5 @@ -import { ValueType } from '../../../Values/ValueType'; +import { ValueType } from '@behave-graph/core'; + import { quatSlerp, Vec4, Vec4JSON, vec4Parse } from './Internal/Vec4'; export const QuatValue = new ValueType( diff --git a/packages/core/src/Profiles/Scene/Values/Vec2Value.ts b/packages/scene/src/Values/Vec2Value.ts similarity index 86% rename from packages/core/src/Profiles/Scene/Values/Vec2Value.ts rename to packages/scene/src/Values/Vec2Value.ts index 2ed77d2e..5c847ed1 100644 --- a/packages/core/src/Profiles/Scene/Values/Vec2Value.ts +++ b/packages/scene/src/Values/Vec2Value.ts @@ -1,4 +1,5 @@ -import { ValueType } from '../../../Values/ValueType'; +import { ValueType } from '@behave-graph/core'; + import { Vec2, Vec2JSON, vec2Mix, vec2Parse } from './Internal/Vec2'; export const Vec2Value = new ValueType( diff --git a/packages/core/src/Profiles/Scene/Values/Vec3Value.ts b/packages/scene/src/Values/Vec3Value.ts similarity index 87% rename from packages/core/src/Profiles/Scene/Values/Vec3Value.ts rename to packages/scene/src/Values/Vec3Value.ts index ded27320..837b9bdb 100644 --- a/packages/core/src/Profiles/Scene/Values/Vec3Value.ts +++ b/packages/scene/src/Values/Vec3Value.ts @@ -1,4 +1,5 @@ -import { ValueType } from '../../../Values/ValueType'; +import { ValueType } from '@behave-graph/core'; + import { Vec3, Vec3JSON, vec3Mix, vec3Parse } from './Internal/Vec3'; export const Vec3Value = new ValueType( diff --git a/packages/core/src/Profiles/Scene/Values/Vec4Value.ts b/packages/scene/src/Values/Vec4Value.ts similarity index 88% rename from packages/core/src/Profiles/Scene/Values/Vec4Value.ts rename to packages/scene/src/Values/Vec4Value.ts index 487efcea..a0af5b9c 100644 --- a/packages/core/src/Profiles/Scene/Values/Vec4Value.ts +++ b/packages/scene/src/Values/Vec4Value.ts @@ -1,4 +1,5 @@ -import { ValueType } from '../../../Values/ValueType'; +import { ValueType } from '@behave-graph/core'; + import { Vec4, Vec4JSON, vec4Mix, vec4Parse } from './Internal/Vec4'; export const Vec4Value = new ValueType( diff --git a/packages/scene/src/index.ts b/packages/scene/src/index.ts new file mode 100644 index 00000000..7caa3b59 --- /dev/null +++ b/packages/scene/src/index.ts @@ -0,0 +1,37 @@ +// scene profile +export * from './Abstractions/IScene'; +export * from './Abstractions/Drivers/DummyScene'; + +export * from './Values/Internal/Mat3'; +export * from './Values/Internal/Mat4'; +export * from './Values/Internal/Vec2'; +export * from './Values/Internal/Vec3'; +export * from './Values/Internal/Vec4'; + +export * from './Values/ColorValue'; +export * from './Values/EulerValue'; +export * from './Values/Mat3Value'; +export * from './Values/Mat4Value'; +export * from './Values/Vec2Value'; +export * from './Values/Vec3Value'; +export * from './Values/Vec4Value'; +export * from './Values/QuatValue'; + +export * from './Nodes/Actions/SetSceneProperty'; +export * from './Nodes/Actions/EaseSceneProperty'; + +export * from './Nodes/Events/OnSceneNodeClick'; + +export * as ColorNodes from './Nodes/Logic/ColorNodes'; +export * as EulerNodes from './Nodes/Logic/EulerNodes'; +export * as Mat3Nodes from './Nodes/Logic/Mat3Nodes'; +export * as Mat4Nodes from './Nodes/Logic/Mat4Nodes'; +export * as Vec2Nodes from './Nodes/Logic/Vec2Nodes'; +export * as Vec3Nodes from './Nodes/Logic/Vec3Nodes'; +export * as Vec4Nodes from './Nodes/Logic/Vec4Nodes'; +export * as QuatNodes from './Nodes/Logic/QuatNodes'; +export * from './Nodes/Logic/VecElements'; + +export * from './Nodes/Queries/GetSceneProperty'; + +export * from './registerSceneProfile'; diff --git a/packages/core/src/Profiles/Scene/readSceneGraphs.test.ts b/packages/scene/src/readSceneGraphs.test.ts similarity index 100% rename from packages/core/src/Profiles/Scene/readSceneGraphs.test.ts rename to packages/scene/src/readSceneGraphs.test.ts diff --git a/packages/core/src/Profiles/Scene/registerSceneProfile.test.ts b/packages/scene/src/registerSceneProfile.test.ts similarity index 100% rename from packages/core/src/Profiles/Scene/registerSceneProfile.test.ts rename to packages/scene/src/registerSceneProfile.test.ts diff --git a/packages/core/src/Profiles/Scene/registerSceneProfile.ts b/packages/scene/src/registerSceneProfile.ts similarity index 71% rename from packages/core/src/Profiles/Scene/registerSceneProfile.ts rename to packages/scene/src/registerSceneProfile.ts index 8fbc43d6..e075879f 100644 --- a/packages/core/src/Profiles/Scene/registerSceneProfile.ts +++ b/packages/scene/src/registerSceneProfile.ts @@ -1,27 +1,30 @@ /* eslint-disable max-len */ -import { getNodeDescriptions } from '../../Nodes/Registry/NodeDescription'; -import { Registry } from '../../Registry'; -import { registerSerializersForValueType } from '../Core/registerSerializersForValueType'; +import { + getNodeDescriptions, + registerSerializersForValueType, + Registry +} from '@behave-graph/core'; + import { DummyScene } from './Abstractions/Drivers/DummyScene'; import { IScene } from './Abstractions/IScene'; -import { SetSceneProperty } from './Actions/SetSceneProperty'; -import { OnSceneNodeClick } from './Events/OnSceneNodeClick'; -import { GetSceneProperty } from './Queries/GetSceneProperty'; -import * as ColorNodes from './Values/ColorNodes'; +import { SetSceneProperty } from './Nodes/Actions/SetSceneProperty'; +import { OnSceneNodeClick } from './Nodes/Events/OnSceneNodeClick'; +import * as ColorNodes from './Nodes/Logic/ColorNodes'; +import * as EulerNodes from './Nodes/Logic/EulerNodes'; +import * as Mat3Nodes from './Nodes/Logic/Mat3Nodes'; +import * as Mat4Nodes from './Nodes/Logic/Mat4Nodes'; +import * as QuatNodes from './Nodes/Logic/QuatNodes'; +import * as Vec2Nodes from './Nodes/Logic/Vec2Nodes'; +import * as Vec3Nodes from './Nodes/Logic/Vec3Nodes'; +import * as Vec4Nodes from './Nodes/Logic/Vec4Nodes'; +import { GetSceneProperty } from './Nodes/Queries/GetSceneProperty'; import { ColorValue } from './Values/ColorValue'; -import * as EulerNodes from './Values/EulerNodes'; import { EulerValue } from './Values/EulerValue'; -import * as Mat3Nodes from './Values/Mat3Nodes'; import { Mat3Value } from './Values/Mat3Value'; -import * as Mat4Nodes from './Values/Mat4Nodes'; import { Mat4Value } from './Values/Mat4Value'; -import * as QuatNodes from './Values/QuatNodes'; import { QuatValue } from './Values/QuatValue'; -import * as Vec2Nodes from './Values/Vec2Nodes'; import { Vec2Value } from './Values/Vec2Value'; -import * as Vec3Nodes from './Values/Vec3Nodes'; import { Vec3Value } from './Values/Vec3Value'; -import * as Vec4Nodes from './Values/Vec4Nodes'; import { Vec4Value } from './Values/Vec4Value'; export function registerSceneProfile( From fce27fb06920870d8af6c708200e6600aef05b03 Mon Sep 17 00:00:00 2001 From: Dan Oved Date: Tue, 4 Apr 2023 16:19:42 -0700 Subject: [PATCH 3/3] Imported improvements from @oveddan's repo: * Swap Registry for an interface IRegistry, making that element more portable and testable * sub registries - value type and node types, are now just simple key value pairs that can be more easily setup, updated, and tested against. * Simplify Fiber - no longer pass it the whole Graph, but just the graph nodes it needs * Substitute Graph instance for * Dependency registry is now a simple key value pair, with dependencies mapped by key, and the keys can be a shared constant value. * Sockets now have strongly typed choices. This is used to display specific choices for scene elements to interact with, by extracting those properties from the IScene Example graph editor & flow package: * Apply graph changes in the editor immediately to the live graph, and re-run it * Dont bundle example graph jsons in the flow package, have them be passed in as an argument from the outside. * Input sockets for scene nodes in the editor, where a property is selected from the scene, are now extracted and generated from functiction on the IScene. * A bunch of reusable hooks to make building a custom flow component more easy, including: * useBehaveGraphFlow * useCoreRegistry * useCustomNodeTypes * useMergeDependencies * useFlowHandlers * useGraphRunner --- examples/graph-editor/package.json | 1 + examples/graph-editor/src/index.tsx | 15 +- examples/graph-editor/tsconfig.json | 10 + packages/core/package.json | 8 +- packages/core/src/Execution/Engine.ts | 6 +- packages/core/src/Execution/Fiber.ts | 8 +- .../core/src/Execution/resolveSocketValue.ts | 4 +- packages/core/src/Graphs/Graph.ts | 117 +++-- packages/core/src/Graphs/IO/NodeSpecJSON.ts | 3 + .../src/Graphs/IO/readGraphFromJSON.test.ts | 23 +- .../core/src/Graphs/IO/readGraphFromJSON.ts | 158 ++++-- .../core/src/Graphs/IO/writeGraphToJSON.ts | 20 +- .../src/Graphs/IO/writeNodeSpecsToJSON.ts | 48 +- .../src/Graphs/Validation/validateGraph.ts | 9 +- .../Graphs/Validation/validateGraphAcyclic.ts | 12 +- .../Graphs/Validation/validateGraphLinks.ts | 10 +- packages/core/src/Nodes/NodeDefinitions.ts | 13 +- .../Nodes/Registry/DependenciesRegistry.ts | 29 +- .../core/src/Nodes/Registry/NodeCategory.ts | 3 +- .../src/Nodes/Registry/NodeTypeRegistry.ts | 46 +- .../Nodes/Validation/validateNodeRegistry.ts | 27 +- packages/core/src/Nodes/nodeFactory.ts | 28 +- packages/core/src/Nodes/testUtils.ts | 11 +- .../core/src/Profiles/Core/Debug/DebugLog.ts | 8 +- .../Profiles/Core/Lifecycle/LifecycleOnEnd.ts | 4 +- .../Core/Lifecycle/LifecycleOnStart.ts | 4 +- .../Core/Lifecycle/LifecycleOnTick.ts | 4 +- .../src/Profiles/Core/readCoreGraphs.test.ts | 25 +- .../Profiles/Core/registerCoreProfile.test.ts | 10 +- .../src/Profiles/Core/registerCoreProfile.ts | 158 +++--- .../registerSerializersForValueType.ts | 21 +- packages/core/src/Registry.ts | 16 +- packages/core/src/Sockets/Socket.ts | 4 +- .../Validation/validateValueRegistry.ts | 18 +- packages/core/src/Values/ValueTypeRegistry.ts | 28 +- packages/core/src/index.ts | 2 + packages/core/src/validateRegistry.ts | 15 +- packages/flow/package.json | 6 +- .../flow/src/components/AutoSizeInput.tsx | 6 +- packages/flow/src/components/Controls.tsx | 68 +-- packages/flow/src/components/Flow.tsx | 177 ++----- packages/flow/src/components/InputSocket.tsx | 125 +++-- packages/flow/src/components/Node.tsx | 5 +- packages/flow/src/components/NodePicker.tsx | 13 +- packages/flow/src/components/OutputSocket.tsx | 7 +- .../flow/src/components/modals/LoadModal.tsx | 46 +- .../flow/src/components/modals/SaveModal.tsx | 7 +- packages/flow/src/hooks/useBehaveGraphFlow.ts | 75 +++ packages/flow/src/hooks/useCoreRegistry.ts | 34 ++ .../flow/src/hooks/useCustomNodeTypes.tsx | 35 ++ packages/flow/src/hooks/useDependencies.ts | 50 ++ packages/flow/src/hooks/useFlowHandlers.ts | 169 +++++++ packages/flow/src/hooks/useGraphRunner.ts | 111 ++++ packages/flow/src/hooks/useMergeMap.ts | 14 + packages/flow/src/hooks/useNodeSpecJson.ts | 18 +- .../flow/src/hooks/useQueriableDefinitions.ts | 22 + packages/flow/src/index.ts | 14 +- .../src/transformers/flowToBehave.test.ts | 14 +- .../flow/src/transformers/flowToBehave.ts | 12 +- packages/flow/src/util/calculateNewEdge.ts | 7 +- packages/flow/src/util/colors.ts | 16 +- packages/flow/src/util/getPickerFilters.ts | 19 +- packages/flow/src/util/isValidConnection.ts | 7 +- packages/flow/tsconfig.json | 7 +- packages/scene/package.json | 12 +- .../src/Abstractions/Drivers/DummyScene.ts | 52 +- packages/scene/src/Abstractions/IScene.ts | 6 + packages/scene/src/GLTFJson.ts | 34 ++ .../src/Nodes/Actions/SetSceneProperty.ts | 19 +- .../src/Nodes/Events/OnSceneNodeClick.ts | 17 +- .../src/Nodes/Queries/GetSceneProperty.ts | 19 +- packages/scene/src/buildScene.ts | 477 ++++++++++++++++++ packages/scene/src/dependencies.ts | 9 + packages/scene/src/index.ts | 1 + packages/scene/src/loadScene.ts | 81 +++ packages/scene/src/readSceneGraphs.test.ts | 10 +- packages/scene/src/registerSceneProfile.ts | 112 ++-- packages/scene/tsconfig.json | 14 + 78 files changed, 2078 insertions(+), 795 deletions(-) create mode 100644 examples/graph-editor/tsconfig.json create mode 100644 packages/flow/src/hooks/useBehaveGraphFlow.ts create mode 100644 packages/flow/src/hooks/useCoreRegistry.ts create mode 100644 packages/flow/src/hooks/useCustomNodeTypes.tsx create mode 100644 packages/flow/src/hooks/useDependencies.ts create mode 100644 packages/flow/src/hooks/useFlowHandlers.ts create mode 100644 packages/flow/src/hooks/useGraphRunner.ts create mode 100644 packages/flow/src/hooks/useMergeMap.ts create mode 100644 packages/flow/src/hooks/useQueriableDefinitions.ts create mode 100644 packages/scene/src/GLTFJson.ts create mode 100644 packages/scene/src/buildScene.ts create mode 100644 packages/scene/src/dependencies.ts create mode 100644 packages/scene/src/loadScene.ts create mode 100644 packages/scene/tsconfig.json diff --git a/examples/graph-editor/package.json b/examples/graph-editor/package.json index 61a3a256..50c50ae3 100644 --- a/examples/graph-editor/package.json +++ b/examples/graph-editor/package.json @@ -2,6 +2,7 @@ "name": "@behave-graph/examples-graph-editor", "version": "1.0.0", "description": "", + "private": true, "directories": { "lib": "src" }, diff --git a/examples/graph-editor/src/index.tsx b/examples/graph-editor/src/index.tsx index a75d923f..0a0a41ca 100644 --- a/examples/graph-editor/src/index.tsx +++ b/examples/graph-editor/src/index.tsx @@ -6,19 +6,18 @@ import rawGraph from "./graph.json" import "reactflow/dist/style.css"; import "./index.css"; +import Branch from "../../../graphs/core/flow/Branch.json"; +import Delay from "../../../graphs/core/time/Delay.json"; +import HelloWorld from "../../../graphs/core/HelloWorld.json"; +import Polynomial from "../../../graphs/core/logic/Polynomial.json"; +import SetGet from "../../../graphs/core/variables/SetGet.json"; -const graph = rawGraph as GraphJSON +const graph = rawGraph as unknown as GraphJSON; const root = ReactDOM.createRoot( document.getElementById("root") as HTMLElement ); -import Branch from "../../../graphs/core/flow/Branch.json"; -import Delay from "../../../graphs/core/time/Delay.json"; -import HelloWorld from "../../../graphs/core//HelloWorld.json"; -import Polynomial from "../../../graphs/core/logic/Polynomial.json"; -import SetGet from "../../../graphs/core/variables/SetGet.json"; - // TODO remove when json types fixed in behave-graph const examples: Examples = { branch: Branch as unknown as GraphJSON, @@ -30,6 +29,6 @@ const examples: Examples = { root.render( - + ); diff --git a/examples/graph-editor/tsconfig.json b/examples/graph-editor/tsconfig.json new file mode 100644 index 00000000..f48784a0 --- /dev/null +++ b/examples/graph-editor/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "paths": { + "@/*": ["src/*"] + } + }, + "include": ["./src"] +} diff --git a/packages/core/package.json b/packages/core/package.json index 42c24453..08a4ac07 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -37,5 +37,11 @@ }, "description": "Simple, extensible behavior graph engine", "author": "", - "license": "ISC" + "license": "ISC", + "dependencies": { + "three-stdlib": "^2.21.5" + }, + "publishConfig": { + "access": "public" + } } diff --git a/packages/core/src/Execution/Engine.ts b/packages/core/src/Execution/Engine.ts index 12a84b0c..a380b436 100644 --- a/packages/core/src/Execution/Engine.ts +++ b/packages/core/src/Execution/Engine.ts @@ -2,7 +2,7 @@ import { Assert } from '../Diagnostics/Assert'; import { EventEmitter } from '../Events/EventEmitter'; -import { Graph } from '../Graphs/Graph'; +import { GraphNodes } from '../Graphs/Graph'; import { IAsyncNode, IEventNode, @@ -23,9 +23,9 @@ export class Engine { public readonly onNodeExecutionEnd = new EventEmitter(); public executionSteps = 0; - constructor(public readonly graph: Graph) { + constructor(public readonly nodes: GraphNodes) { // collect all event nodes - Object.values(graph.nodes).forEach((node) => { + Object.values(nodes).forEach((node) => { if (isEventNode(node)) { this.eventNodes.push(node); } diff --git a/packages/core/src/Execution/Fiber.ts b/packages/core/src/Execution/Fiber.ts index 65d371a8..2faa23be 100644 --- a/packages/core/src/Execution/Fiber.ts +++ b/packages/core/src/Execution/Fiber.ts @@ -1,5 +1,5 @@ import { Assert } from '../Diagnostics/Assert'; -import { Graph } from '../Graphs/Graph'; +import { GraphNodes } from '../Graphs/Graph'; import { Link } from '../Nodes/Link'; import { INode, isAsyncNode, isFlowNode } from '../Nodes/NodeInstance'; import { Engine } from './Engine'; @@ -7,7 +7,7 @@ import { resolveSocketValue } from './resolveSocketValue'; export class Fiber { private readonly fiberCompletedListenerStack: (() => void)[] = []; - private readonly graph: Graph; + private readonly nodes: GraphNodes; public executionSteps = 0; constructor( @@ -15,7 +15,7 @@ export class Fiber { public nextEval: Link | null, fiberCompletedListener: (() => void) | undefined = undefined ) { - this.graph = engine.graph; + this.nodes = engine.nodes; if (fiberCompletedListener !== undefined) { this.fiberCompletedListenerStack.push(fiberCompletedListener); } @@ -76,7 +76,7 @@ export class Fiber { return; } - const node = this.graph.nodes[link.nodeId]; + const node = this.nodes[link.nodeId]; node.inputs.forEach((inputSocket) => { if (inputSocket.valueTypeName !== 'flow') { diff --git a/packages/core/src/Execution/resolveSocketValue.ts b/packages/core/src/Execution/resolveSocketValue.ts index 766be332..94e2dcce 100644 --- a/packages/core/src/Execution/resolveSocketValue.ts +++ b/packages/core/src/Execution/resolveSocketValue.ts @@ -12,7 +12,7 @@ export function resolveSocketValue( return 0; } - const graph = engine.graph; + const nodes = engine.nodes; const upstreamLink = inputSocket.links[0]; // caching the target node + socket here increases engine performance by 8% on average. This is a hotspot. @@ -23,7 +23,7 @@ export function resolveSocketValue( Assert.mustBeTrue(inputSocket.links.length === 1); // if upstream node is an eval, we just return its last value. - upstreamLink._targetNode = graph.nodes[upstreamLink.nodeId]; + upstreamLink._targetNode = nodes[upstreamLink.nodeId]; // what is inputSocket connected to? upstreamLink._targetSocket = upstreamLink._targetNode.outputs.find( (socket) => socket.name === upstreamLink.socketName diff --git a/packages/core/src/Graphs/Graph.ts b/packages/core/src/Graphs/Graph.ts index 35540a2d..94a700a1 100644 --- a/packages/core/src/Graphs/Graph.ts +++ b/packages/core/src/Graphs/Graph.ts @@ -1,10 +1,11 @@ import { CustomEvent } from '../Events/CustomEvent'; -import { generateUuid } from '../generateUuid'; import { Metadata } from '../Metadata'; import { NodeConfiguration } from '../Nodes/Node'; import { INode } from '../Nodes/NodeInstance'; -import { NodeDefinition } from '../Nodes/Registry/NodeTypeRegistry'; -import { IRegistry, Registry } from '../Registry'; +import { Dependencies } from '../Nodes/Registry/DependenciesRegistry'; +import { NodeDefinitionsMap } from '../Nodes/Registry/NodeTypeRegistry'; +import { Socket } from '../Sockets/Socket'; +import { ValueTypeMap } from '../Values/ValueTypeRegistry'; import { Variable } from '../Variables/Variable'; // Purpose: // - stores the node graph @@ -12,64 +13,74 @@ import { Variable } from '../Variables/Variable'; export interface IGraphApi { readonly variables: { [id: string]: Variable }; readonly customEvents: { [id: string]: CustomEvent }; - readonly values: Registry['values']; - readonly getDependency: (id: string) => T; + readonly values: ValueTypeMap; + readonly getDependency: (id: string) => T | undefined; } -export class Graph { - public name = ''; - // TODO: think about whether I can replace this with an immutable strategy? Rather than having this mutable? - public readonly nodes: { [id: string]: INode } = {}; - // TODO: think about whether I can replace this with an immutable strategy? Rather than having this mutable? - public readonly variables: { [id: string]: Variable } = {}; - // TODO: think about whether I can replace this with an immutable strategy? Rather than having this mutable? - public readonly customEvents: { [id: string]: CustomEvent } = {}; - public metadata: Metadata = {}; - public version = 0; +export type GraphNodes = { [id: string]: INode }; +export type GraphVariables = { [id: string]: Variable }; +export type GraphCustomEvents = { [id: string]: CustomEvent }; - constructor(public readonly registry: IRegistry) {} +export type GraphInstance = { + name: string; + metadata: Metadata; + nodes: GraphNodes; + customEvents: GraphCustomEvents; + variables: GraphVariables; +}; - makeApi(): IGraphApi { - return { - variables: this.variables, - customEvents: this.customEvents, - values: this.registry.values, - getDependency: (id: string) => this.registry.dependencies.get(id) - }; +export const createNode = ({ + graph, + nodes, + values, + nodeTypeName, + nodeConfiguration = {} +}: { + graph: IGraphApi; + nodes: NodeDefinitionsMap; + values: ValueTypeMap; + nodeTypeName: string; + nodeConfiguration?: NodeConfiguration; +}) => { + let nodeDefinition = undefined; + if (nodes[nodeTypeName]) { + nodeDefinition = nodes[nodeTypeName]; + } + if (nodeDefinition === undefined) { + throw new Error( + `no registered node descriptions with the typeName ${nodeTypeName}` + ); } - createNode( - nodeTypeName: string, - nodeId: string = generateUuid(), - nodeConfiguration: NodeConfiguration = {} - ): INode { - if (nodeId in this.nodes) { - throw new Error( - `can not create new node of type ${nodeTypeName} with id ${nodeId} as one with that id already exists.` - ); - } + const node = nodeDefinition.nodeFactory(graph, nodeConfiguration); - let nodeDefinition: NodeDefinition | undefined = undefined; - if (this.registry.nodes.contains(nodeTypeName)) { - nodeDefinition = this.registry.nodes.get(nodeTypeName); - } - if (nodeDefinition === undefined) { - throw new Error( - `no registered node descriptions with the typeName ${nodeTypeName}` - ); + node.inputs.forEach((socket: Socket) => { + if (socket.valueTypeName !== 'flow' && socket.value === undefined) { + socket.value = values[socket.valueTypeName]?.creator(); } + }); - const graph = this.makeApi(); - const node = nodeDefinition.nodeFactory(graph, nodeConfiguration); + return node; +}; - this.nodes[nodeId] = node; - - node.inputs.forEach((socket) => { - if (socket.valueTypeName !== 'flow' && socket.value === undefined) { - socket.value = this.registry.values.get(socket.valueTypeName).creator(); - } - }); - - return node; +export const makeGraphApi = ({ + variables = {}, + customEvents = {}, + valuesTypeRegistry, + dependencies = {} +}: { + customEvents?: GraphCustomEvents; + variables?: GraphVariables; + valuesTypeRegistry: ValueTypeMap; + dependencies: Dependencies; +}): IGraphApi => ({ + variables, + customEvents, + values: valuesTypeRegistry, + getDependency: (id: string) => { + const result = dependencies[id]; + if (!result) + console.error(`Dependency not found ${id}. Did you register it?`); + return result; } -} +}); diff --git a/packages/core/src/Graphs/IO/NodeSpecJSON.ts b/packages/core/src/Graphs/IO/NodeSpecJSON.ts index 3f3f2761..8121571e 100644 --- a/packages/core/src/Graphs/IO/NodeSpecJSON.ts +++ b/packages/core/src/Graphs/IO/NodeSpecJSON.ts @@ -1,10 +1,13 @@ import { NodeCategory } from '../../Nodes/Registry/NodeCategory'; import { ValueJSON } from './GraphJSON'; +export type ChoiceJSON = { text: string; value: any }[]; + export type InputSocketSpecJSON = { name: string; valueType: string; defaultValue?: ValueJSON; + choices?: ChoiceJSON; }; export type OutputSocketSpecJSON = { diff --git a/packages/core/src/Graphs/IO/readGraphFromJSON.test.ts b/packages/core/src/Graphs/IO/readGraphFromJSON.test.ts index 1f860139..2b5403cb 100644 --- a/packages/core/src/Graphs/IO/readGraphFromJSON.test.ts +++ b/packages/core/src/Graphs/IO/readGraphFromJSON.test.ts @@ -1,14 +1,11 @@ import { Logger } from '../../Diagnostics/Logger'; -import { registerCoreProfile } from '../../Profiles/Core/registerCoreProfile'; -import { Registry } from '../../Registry'; +import { getCoreRegistry } from '../../Profiles/Core/registerCoreProfile'; import { readGraphFromJSON } from './readGraphFromJSON'; -const registry = new Registry(); -registerCoreProfile(registry); - Logger.onWarn.clear(); describe('readGraphFromJSON', () => { + const registry = getCoreRegistry(); it('throws if node ids are not unique', () => { const json = { variables: [], @@ -24,7 +21,9 @@ describe('readGraphFromJSON', () => { } ] }; - expect(() => readGraphFromJSON(json, registry)).toThrow(); + expect(() => + readGraphFromJSON({ graphJson: json, ...registry, dependencies: {} }) + ).toThrow(); }); it("throws if input keys don't match known sockets", () => { @@ -41,7 +40,9 @@ describe('readGraphFromJSON', () => { } ] }; - expect(() => readGraphFromJSON(json, registry)).toThrow(); + expect(() => + readGraphFromJSON({ graphJson: json, ...registry, dependencies: {} }) + ).toThrow(); }); it('throws if input points to non-existent node', () => { @@ -65,7 +66,9 @@ describe('readGraphFromJSON', () => { } ] }; - expect(() => readGraphFromJSON(json, registry)).toThrow(); + expect(() => + readGraphFromJSON({ graphJson: json, ...registry, dependencies: {} }) + ).toThrow(); }); it('throws if input points to non-existent socket', () => { @@ -89,6 +92,8 @@ describe('readGraphFromJSON', () => { } ] }; - expect(() => readGraphFromJSON(json, registry)).toThrow(); + expect(() => + readGraphFromJSON({ graphJson: json, ...registry, dependencies: {} }) + ).toThrow(); }); }); diff --git a/packages/core/src/Graphs/IO/readGraphFromJSON.ts b/packages/core/src/Graphs/IO/readGraphFromJSON.ts index cdc71e85..11d1085f 100644 --- a/packages/core/src/Graphs/IO/readGraphFromJSON.ts +++ b/packages/core/src/Graphs/IO/readGraphFromJSON.ts @@ -3,10 +3,20 @@ import { CustomEvent } from '../../Events/CustomEvent'; import { Link } from '../../Nodes/Link'; import { NodeConfiguration } from '../../Nodes/Node'; import { INode } from '../../Nodes/NodeInstance'; -import { Registry } from '../../Registry'; +import { Dependencies } from '../../Nodes/Registry/DependenciesRegistry'; +import { NodeDefinitionsMap } from '../../Nodes/Registry/NodeTypeRegistry'; import { Socket } from '../../Sockets/Socket'; +import { ValueTypeMap } from '../../Values/ValueTypeRegistry'; import { Variable } from '../../Variables/Variable'; -import { Graph } from '../Graph'; +import { + createNode, + GraphCustomEvents, + GraphInstance, + GraphNodes, + GraphVariables, + IGraphApi, + makeGraphApi +} from '../Graph'; import { CustomEventJSON, FlowsJSON, @@ -18,20 +28,34 @@ import { // Purpose: // - loads a node graph -export function readGraphFromJSON( - graphJson: GraphJSON, - registry: Registry -): Graph { - const graph = new Graph(registry); +export function readGraphFromJSON({ + graphJson, + nodes: nodesTypeRegistry, + values: valuesTypeRegistry, + dependencies +}: { + graphJson: GraphJSON; + nodes: NodeDefinitionsMap; + values: ValueTypeMap; + dependencies: Dependencies; +}): GraphInstance { + const graphName = graphJson?.name || ''; + const graphMetadata = graphJson?.metadata || {}; - graph.name = graphJson?.name ?? graph.name; - graph.metadata = graphJson?.metadata ?? graph.metadata; + let variables: GraphVariables = {}; + let customEvents: GraphCustomEvents = {}; if ('variables' in graphJson) { - readVariablesJSON(graph, graphJson.variables ?? []); + variables = readVariablesJSON( + valuesTypeRegistry, + graphJson.variables ?? [] + ); } if ('customEvents' in graphJson) { - readCustomEventsJSON(graph, graphJson.customEvents ?? []); + customEvents = readCustomEventsJSON( + valuesTypeRegistry, + graphJson.customEvents ?? [] + ); } const nodesJson = graphJson?.nodes ?? []; @@ -40,24 +64,46 @@ export function readGraphFromJSON( Logger.warn('readGraphFromJSON: no nodes specified'); } + const graphApi = makeGraphApi({ + valuesTypeRegistry, + variables, + customEvents, + dependencies + }); + + const nodes: GraphNodes = {}; // create new BehaviorNode instances for each node in the json. for (let i = 0; i < nodesJson.length; i += 1) { const nodeJson = nodesJson[i]; - readNodeJSON(graph, nodeJson); + const node = readNodeJSON({ + graph: graphApi, + nodes: nodesTypeRegistry, + values: valuesTypeRegistry, + nodeJson + }); + const id = nodeJson.id; + + if (id in nodes) { + throw new Error( + `can not create new node with id ${id} as one with that id already exists.` + ); + } + + nodes[id] = node; } // connect up the graph edges from BehaviorNode inputs to outputs. This is required to follow execution - Object.entries(graph.nodes).forEach(([nodeId, node]) => { + Object.entries(nodes).forEach(([nodeId, node]) => { // initialize the inputs by resolving to the reference nodes. node.inputs.forEach((inputSocket) => { inputSocket.links.forEach((link) => { - if (!(link.nodeId in graph.nodes)) { + if (!(link.nodeId in nodes)) { throw new Error( `node '${node.description.typeName}' specifies an input '${inputSocket.name}' whose link goes to ` + `a nonexistent upstream node id: ${link.nodeId}` ); } - const upstreamNode = graph.nodes[link.nodeId]; + const upstreamNode = nodes[link.nodeId]; const upstreamOutputSocket = upstreamNode.outputs.find( (socket) => socket.name === link.socketName ); @@ -84,14 +130,14 @@ export function readGraphFromJSON( node.outputs.forEach((outputSocket) => { outputSocket.links.forEach((link) => { - if (!(link.nodeId in graph.nodes)) { + if (!(link.nodeId in nodes)) { throw new Error( `node '${node.description.typeName}' specifies an output '${outputSocket.name}' whose link goes to ` + `a nonexistent downstream node id ${link.nodeId}` ); } - const downstreamNode = graph.nodes[link.nodeId]; + const downstreamNode = nodes[link.nodeId]; const downstreamInputSocket = downstreamNode.inputs.find( (socket) => socket.name === link.socketName ); @@ -117,10 +163,26 @@ export function readGraphFromJSON( }); }); - return graph; + return { + name: graphName, + metadata: graphMetadata, + nodes: nodes, + customEvents, + variables + }; } -function readNodeJSON(graph: Graph, nodeJson: NodeJSON) { +function readNodeJSON({ + graph, + nodes, + values, + nodeJson +}: { + graph: IGraphApi; + nodes: NodeDefinitionsMap; + values: ValueTypeMap; + nodeJson: NodeJSON; +}) { if (nodeJson.type === undefined) { throw new Error('readGraphFromJSON: no type for node'); } @@ -133,21 +195,29 @@ function readNodeJSON(graph: Graph, nodeJson: NodeJSON) { }); } - const node = graph.createNode(nodeName, nodeJson.id, nodeConfiguration); + const node = createNode({ + graph, + nodes, + values, + nodeTypeName: nodeName, + nodeConfiguration + }); node.label = nodeJson?.label ?? node.label; node.metadata = nodeJson?.metadata ?? node.metadata; if (nodeJson.parameters !== undefined) { - readNodeParameterJSON(graph, node, nodeJson.parameters); + readNodeParameterJSON(values, node, nodeJson.parameters); } if (nodeJson.flows !== undefined) { - readNodeFlowsJSON(graph, node, nodeJson.flows); + readNodeFlowsJSON(node, nodeJson.flows); } + + return node; } function readNodeParameterJSON( - graph: Graph, + valuesRegistry: ValueTypeMap, node: INode, parametersJson: NodeParametersJSON ) { @@ -159,9 +229,9 @@ function readNodeParameterJSON( const inputJson = parametersJson[socket.name]; if ('value' in inputJson) { // eslint-disable-next-line no-param-reassign - socket.value = graph.registry.values - .get(socket.valueTypeName) - .deserialize(inputJson.value); + socket.value = valuesRegistry[socket.valueTypeName]?.deserialize( + inputJson.value + ); } if ('link' in inputJson) { @@ -185,7 +255,7 @@ function readNodeParameterJSON( } } -function readNodeFlowsJSON(graph: Graph, node: INode, flowsJson: FlowsJSON) { +function readNodeFlowsJSON(node: INode, flowsJson: FlowsJSON) { node.outputs.forEach((socket) => { if (socket.name in flowsJson) { const outputLinkJson = flowsJson[socket.name]; @@ -210,7 +280,11 @@ function readNodeFlowsJSON(graph: Graph, node: INode, flowsJson: FlowsJSON) { } } -function readVariablesJSON(graph: Graph, variablesJson: VariableJSON[]) { +function readVariablesJSON( + valuesRegistry: ValueTypeMap, + variablesJson: VariableJSON[] +) { + const variables: GraphVariables = {}; for (let i = 0; i < variablesJson.length; i += 1) { const variableJson = variablesJson[i]; @@ -218,24 +292,28 @@ function readVariablesJSON(graph: Graph, variablesJson: VariableJSON[]) { variableJson.id, variableJson.name, variableJson.valueTypeName, - graph.registry.values - .get(variableJson.valueTypeName) - .deserialize(variableJson.initialValue) + valuesRegistry[variableJson.valueTypeName]?.deserialize( + variableJson.initialValue + ) ); variable.label = variableJson?.label ?? variable.label; variable.metadata = variableJson?.metadata ?? variable.metadata; - if (variableJson.id in graph.variables) { + if (variableJson.id in variables) { throw new Error(`duplicate variable id ${variable.id}`); } - graph.variables[variableJson.id] = variable; + variables[variableJson.id] = variable; } + + return variables; } function readCustomEventsJSON( - graph: Graph, + valuesRegistry: ValueTypeMap, customEventsJson: CustomEventJSON[] ) { + const customEvents: GraphCustomEvents = {}; + for (let i = 0; i < customEventsJson.length; i += 1) { const customEventJson = customEventsJson[i]; @@ -245,9 +323,9 @@ function readCustomEventsJSON( new Socket( parameterJson.valueTypeName, parameterJson.name, - graph.registry.values - .get(parameterJson.valueTypeName) - .deserialize(parameterJson.defaultValue) + valuesRegistry[parameterJson.valueTypeName]?.deserialize( + parameterJson.defaultValue + ) ) ); }); @@ -260,9 +338,11 @@ function readCustomEventsJSON( customEvent.label = customEventJson?.label ?? customEvent.label; customEvent.metadata = customEventJson?.metadata ?? customEvent.metadata; - if (customEvent.id in graph.customEvents) { + if (customEvent.id in customEvents) { throw new Error(`duplicate variable id ${customEvent.id}`); } - graph.customEvents[customEvent.id] = customEvent; + customEvents[customEvent.id] = customEvent; } + + return customEvents; } diff --git a/packages/core/src/Graphs/IO/writeGraphToJSON.ts b/packages/core/src/Graphs/IO/writeGraphToJSON.ts index f1d29c85..08c6cab0 100644 --- a/packages/core/src/Graphs/IO/writeGraphToJSON.ts +++ b/packages/core/src/Graphs/IO/writeGraphToJSON.ts @@ -1,4 +1,5 @@ -import { Graph } from '../Graph'; +import { ValueTypeMap } from '../../Values/ValueTypeRegistry'; +import { GraphInstance } from '../Graph'; import { CustomEventJSON, CustomEventParameterJSON, @@ -10,7 +11,10 @@ import { VariableJSON } from './GraphJSON'; -export function writeGraphToJSON(graph: Graph): GraphJSON { +export function writeGraphToJSON( + graph: GraphInstance, + valuesRegistry: ValueTypeMap +): GraphJSON { const graphJson: GraphJSON = {}; if (Object.keys(graph.metadata).length > 0) { @@ -52,9 +56,9 @@ export function writeGraphToJSON(graph: Graph): GraphJSON { valueTypeName: variable.valueTypeName, name: variable.name, id: variable.id, - initialValue: graph.registry.values - .get(variable.valueTypeName) - .serialize(variable.initialValue) + initialValue: valuesRegistry[variable.valueTypeName]?.serialize( + variable.initialValue + ) }; if (variable.label.length > 0) { variableJson.label = variable.label; @@ -96,9 +100,9 @@ export function writeGraphToJSON(graph: Graph): GraphJSON { if (inputSocket.links.length === 0) { parameterJson = { - value: graph.registry.values - .get(inputSocket.valueTypeName) - .serialize(inputSocket.value) + value: valuesRegistry[inputSocket.valueTypeName]?.serialize( + inputSocket.value + ) }; } else if (inputSocket.links.length === 1) { const link = inputSocket.links[0]; diff --git a/packages/core/src/Graphs/IO/writeNodeSpecsToJSON.ts b/packages/core/src/Graphs/IO/writeNodeSpecsToJSON.ts index 1b48f237..78b4a3fc 100644 --- a/packages/core/src/Graphs/IO/writeNodeSpecsToJSON.ts +++ b/packages/core/src/Graphs/IO/writeNodeSpecsToJSON.ts @@ -1,19 +1,50 @@ import { NodeCategory } from '../../Nodes/NodeDefinitions'; -import { Registry } from '../../Registry'; -import { Graph } from '../Graph'; +import { Dependencies } from '../../Nodes/Registry/DependenciesRegistry'; +import { NodeDefinitionsMap } from '../../Nodes/Registry/NodeTypeRegistry'; +import { Choices } from '../../Sockets/Socket'; +import { ValueTypeMap } from '../../Values/ValueTypeRegistry'; +import { createNode, IGraphApi } from '../Graph'; import { + ChoiceJSON, InputSocketSpecJSON, NodeSpecJSON, OutputSocketSpecJSON } from './NodeSpecJSON'; -export function writeNodeSpecsToJSON(registry: Registry): NodeSpecJSON[] { +function toChoices(valueChoices: Choices | undefined): ChoiceJSON | undefined { + return valueChoices?.map((choice) => { + if (typeof choice === 'string') return { text: choice, value: choice }; + return choice; + }); +} + +export function writeNodeSpecsToJSON({ + values, + nodes, + dependencies +}: { + values: ValueTypeMap; + nodes: NodeDefinitionsMap; + dependencies: Dependencies; +}): NodeSpecJSON[] { const nodeSpecsJSON: NodeSpecJSON[] = []; - const graph = new Graph(registry); + // const graph = new Graph(registry); - registry.nodes.getAllNames().forEach((nodeTypeName) => { - const node = graph.createNode(nodeTypeName); + const graph: IGraphApi = { + values: values, + customEvents: {}, + getDependency: (id: string) => dependencies[id], + variables: {} + }; + + Object.keys(nodes).forEach((nodeTypeName) => { + const node = createNode({ + graph, + nodes, + values, + nodeTypeName + }); const nodeSpecJSON: NodeSpecJSON = { type: nodeTypeName, @@ -28,7 +59,7 @@ export function writeNodeSpecsToJSON(registry: Registry): NodeSpecJSON[] { const valueType = inputSocket.valueTypeName === 'flow' ? undefined - : registry.values.get(inputSocket.valueTypeName); + : values[inputSocket.valueTypeName]; let defaultValue = inputSocket.value; if (valueType !== undefined) { @@ -40,7 +71,8 @@ export function writeNodeSpecsToJSON(registry: Registry): NodeSpecJSON[] { const socketSpecJSON: InputSocketSpecJSON = { name: inputSocket.name, valueType: inputSocket.valueTypeName, - defaultValue + defaultValue, + choices: toChoices(inputSocket.valueChoices) }; nodeSpecJSON.inputs.push(socketSpecJSON); }); diff --git a/packages/core/src/Graphs/Validation/validateGraph.ts b/packages/core/src/Graphs/Validation/validateGraph.ts index de491874..1e3225b5 100644 --- a/packages/core/src/Graphs/Validation/validateGraph.ts +++ b/packages/core/src/Graphs/Validation/validateGraph.ts @@ -1,9 +1,12 @@ -import { Graph } from '../Graph'; +import { GraphInstance } from '../Graph'; import { validateGraphAcyclic } from './validateGraphAcyclic'; import { validateGraphLinks } from './validateGraphLinks'; -export function validateGraph(graph: Graph): string[] { +export function validateGraph(graph: GraphInstance): string[] { const errorList: string[] = []; - errorList.push(...validateGraphAcyclic(graph), ...validateGraphLinks(graph)); + errorList.push( + ...validateGraphAcyclic(graph.nodes), + ...validateGraphLinks(graph.nodes) + ); return errorList; } diff --git a/packages/core/src/Graphs/Validation/validateGraphAcyclic.ts b/packages/core/src/Graphs/Validation/validateGraphAcyclic.ts index eed40a17..98b31835 100644 --- a/packages/core/src/Graphs/Validation/validateGraphAcyclic.ts +++ b/packages/core/src/Graphs/Validation/validateGraphAcyclic.ts @@ -1,11 +1,11 @@ import { INode } from '../../Nodes/NodeInstance'; -import { Graph } from '../Graph'; +import { GraphNodes } from '../Graph'; -export function validateGraphAcyclic(graph: Graph): string[] { +export function validateGraphAcyclic(nodes: GraphNodes): string[] { // apparently if you can topological sort, it is a DAG according to: https://stackoverflow.com/questions/4168/graph-serialization/4577#4577 // instead of modifying the graph, I will use metadata to mark it in place. - Object.values(graph.nodes).forEach((node) => { + Object.values(nodes).forEach((node) => { // eslint-disable-next-line no-param-reassign node.metadata['dag.marked'] = 'false'; }); @@ -19,7 +19,7 @@ export function validateGraphAcyclic(graph: Graph): string[] { // clear array: https://stackoverflow.com/a/1232046 nodesToMark.length = 0; - Object.values(graph.nodes).forEach((node) => { + Object.values(nodes).forEach((node) => { // ignore existing marked nodes. if (node.metadata['dag.marked'] === 'true') { return; @@ -29,7 +29,7 @@ export function validateGraphAcyclic(graph: Graph): string[] { node.inputs.forEach((inputSocket) => { inputSocket.links.forEach((link) => { // is the other end marked? If not, then it is still connected. - if (graph.nodes[link.nodeId].metadata['dag.marked'] === 'false') { + if (nodes[link.nodeId].metadata['dag.marked'] === 'false') { inputsConnected = true; } }); @@ -48,7 +48,7 @@ export function validateGraphAcyclic(graph: Graph): string[] { // output errors for each unmarked node // also remove the metadata related to DAG marking - Object.values(graph.nodes).forEach((node) => { + Object.values(nodes).forEach((node) => { if (node.metadata['dag.marked'] === 'false') { errorList.push( `node ${node.description.typeName} is part of a cycle, not a directed acyclic graph` diff --git a/packages/core/src/Graphs/Validation/validateGraphLinks.ts b/packages/core/src/Graphs/Validation/validateGraphLinks.ts index 83ddd707..b9d4e21c 100644 --- a/packages/core/src/Graphs/Validation/validateGraphLinks.ts +++ b/packages/core/src/Graphs/Validation/validateGraphLinks.ts @@ -1,15 +1,15 @@ -import { Graph } from '../Graph'; +import { GraphNodes } from '../Graph'; -export function validateGraphLinks(graph: Graph): string[] { +export function validateGraphLinks(nodes: GraphNodes): string[] { const errorList: string[] = []; // for each node - Object.values(graph.nodes).forEach((node) => { + Object.values(nodes).forEach((node) => { // for each input socket node.inputs.forEach((inputSocket) => { // ensure that connected output sockets are the same type inputSocket.links.forEach((link) => { // check if the node id is correct - if (!(link.nodeId in graph.nodes)) { + if (!(link.nodeId in nodes)) { errorList.push( `node ${node.description.typeName}.${inputSocket.name} has link using invalid nodeId: ${link.nodeId}` ); @@ -17,7 +17,7 @@ export function validateGraphLinks(graph: Graph): string[] { } // check if the socketName is correct - const upstreamNode = graph.nodes[link.nodeId]; + const upstreamNode = nodes[link.nodeId]; const outputSocket = upstreamNode.outputs.find( (socket) => socket.name === link.socketName ); diff --git a/packages/core/src/Nodes/NodeDefinitions.ts b/packages/core/src/Nodes/NodeDefinitions.ts index c589df58..11541c58 100644 --- a/packages/core/src/Nodes/NodeDefinitions.ts +++ b/packages/core/src/Nodes/NodeDefinitions.ts @@ -1,4 +1,5 @@ import { IGraphApi } from '../Graphs/Graph'; +import { Choices } from '../Sockets/Socket'; import { AsyncNodeInstance } from './AsyncNode'; import { EventNodeInstance } from './EventNode'; import { FlowNodeInstance } from './FlowNode'; @@ -12,10 +13,16 @@ import { NodeConfigurationDescription } from './Registry/NodeDescription'; export interface SocketDefinition { valueType: string; defaultValue?: any; - choices?: string[]; + choices?: Choices; label?: string; } -export type SocketsMap = Record; + +export type SocketsMap = Record< + string, + | SocketDefinition + | string + | ((nodeConfig: NodeConfiguration, graph: IGraphApi) => SocketDefinition) +>; export type SocketListDefinition = SocketDefinition & { key: string }; export type SocketsList = SocketListDefinition[]; @@ -54,7 +61,7 @@ export interface INodeDefinition< export type SocketNames = TSockets extends SocketsMap ? keyof TSockets : any; -export type Dependencies = any | undefined; +export type Dependencies = Record; export type TriggeredFn< TInput extends SocketsDefinition = SocketsDefinition, diff --git a/packages/core/src/Nodes/Registry/DependenciesRegistry.ts b/packages/core/src/Nodes/Registry/DependenciesRegistry.ts index ae8029b8..e7e32850 100644 --- a/packages/core/src/Nodes/Registry/DependenciesRegistry.ts +++ b/packages/core/src/Nodes/Registry/DependenciesRegistry.ts @@ -1,21 +1,10 @@ -export class DependenciesRegistry { - private readonly registryKeyToDependency: { [key: string]: any } = {}; +export type Dependencies = Record; - register(key: string, dependency: any) { - if (key in this.registryKeyToDependency) { - throw new Error(`already registered dependency with name '${key}`); - } - this.registryKeyToDependency[key] = dependency; - } - - get(key: string): T { - if (!(key in this.registryKeyToDependency)) { - throw new Error(`can not find dependency with name '${key}`); - } - return this.registryKeyToDependency[key]; - } - - getAllNames(): string[] { - return Object.keys(this.registryKeyToDependency); - } -} +export const registerDependency = ( + dependencies: Dependencies, + key: string, + dependency: any +) => ({ + ...dependencies, + [key]: dependency +}); diff --git a/packages/core/src/Nodes/Registry/NodeCategory.ts b/packages/core/src/Nodes/Registry/NodeCategory.ts index 9c4bf6bb..e9a6a73e 100644 --- a/packages/core/src/Nodes/Registry/NodeCategory.ts +++ b/packages/core/src/Nodes/Registry/NodeCategory.ts @@ -6,5 +6,6 @@ export enum NodeCategory { Variable = 'Variable', Flow = 'Flow', Time = 'Time', - None = 'None' + None = 'None', + Effect = 'Effect' } diff --git a/packages/core/src/Nodes/Registry/NodeTypeRegistry.ts b/packages/core/src/Nodes/Registry/NodeTypeRegistry.ts index 771f2f3d..5ac66b95 100644 --- a/packages/core/src/Nodes/Registry/NodeTypeRegistry.ts +++ b/packages/core/src/Nodes/Registry/NodeTypeRegistry.ts @@ -3,46 +3,6 @@ import { IHasNodeFactory, INodeDefinition } from '../NodeDefinitions'; export type NodeDefinition = IHasNodeFactory & Pick; -export class NodeTypeRegistry { - private readonly typeNameToNodeDescriptions: { - [type: string]: NodeDefinition; - } = {}; - - clear() { - for (const nodeTypeName in this.typeNameToNodeDescriptions) { - delete this.typeNameToNodeDescriptions[nodeTypeName]; - } - } - register(...descriptions: Array) { - descriptions.forEach((description) => { - const allTypeNames = (description.otherTypeNames || []).concat([ - description.typeName - ]); - - allTypeNames.forEach((typeName) => { - if (typeName in this.typeNameToNodeDescriptions) { - throw new Error(`already registered node type ${typeName} (string)`); - } - this.typeNameToNodeDescriptions[typeName] = description; - }); - }); - } - - contains(typeName: string): boolean { - return typeName in this.typeNameToNodeDescriptions; - } - get(typeName: string): NodeDefinition { - if (!(typeName in this.typeNameToNodeDescriptions)) { - throw new Error(`no registered node with type name ${typeName}`); - } - return this.typeNameToNodeDescriptions[typeName]; - } - - getAllNames(): string[] { - return Object.keys(this.typeNameToNodeDescriptions); - } - - getAllDescriptions(): NodeDefinition[] { - return Object.values(this.typeNameToNodeDescriptions); - } -} +export type NodeDefinitionsMap = { + readonly [type: string]: NodeDefinition; +}; diff --git a/packages/core/src/Nodes/Validation/validateNodeRegistry.ts b/packages/core/src/Nodes/Validation/validateNodeRegistry.ts index e9124ff2..ce25ca2e 100644 --- a/packages/core/src/Nodes/Validation/validateNodeRegistry.ts +++ b/packages/core/src/Nodes/Validation/validateNodeRegistry.ts @@ -1,14 +1,25 @@ -import { Graph } from '../../Graphs/Graph'; -import { Registry } from '../../Registry'; +import { createNode, makeGraphApi } from '../../Graphs/Graph'; +import { ValueTypeMap } from '../../Values/ValueTypeRegistry'; +import { NodeDefinitionsMap } from '../Registry/NodeTypeRegistry'; const nodeTypeNameRegex = /^\w+(\/\w+)*$/; const socketNameRegex = /^\w+$/; -export function validateNodeRegistry(registry: Registry): string[] { +export function validateNodeRegistry({ + nodes, + values +}: { + nodes: NodeDefinitionsMap; + values: ValueTypeMap; +}): string[] { const errorList: string[] = []; - const graph = new Graph(registry); - registry.nodes.getAllNames().forEach((nodeTypeName) => { - const node = graph.createNode(nodeTypeName); + // const graph = new Graph(registry); + const graph = makeGraphApi({ + valuesTypeRegistry: values, + dependencies: {} + }); + Object.keys(nodes).forEach((nodeTypeName) => { + const node = createNode({ graph, nodes, values, nodeTypeName }); // ensure node is registered correctly. if (node.description.typeName !== nodeTypeName) { @@ -35,7 +46,7 @@ export function validateNodeRegistry(registry: Registry): string[] { if (socket.valueTypeName === 'flow') { return; } - const valueType = registry.values.get(socket.valueTypeName); + const valueType = values[socket.valueTypeName]; // check to ensure all value types are supported. if (valueType === undefined) { errorList.push( @@ -53,7 +64,7 @@ export function validateNodeRegistry(registry: Registry): string[] { if (socket.valueTypeName === 'flow') { return; } - const valueType = registry.values.get(socket.valueTypeName); + const valueType = values[socket.valueTypeName]; // check to ensure all value types are supported. if (valueType === undefined) { errorList.push( diff --git a/packages/core/src/Nodes/nodeFactory.ts b/packages/core/src/Nodes/nodeFactory.ts index 3f45813c..b1acb3d1 100644 --- a/packages/core/src/Nodes/nodeFactory.ts +++ b/packages/core/src/Nodes/nodeFactory.ts @@ -4,29 +4,35 @@ import { NodeConfiguration } from './Node'; import { INodeDefinition, NodeCategory, + SocketDefinition, SocketsDefinition, SocketsList, SocketsMap } from './NodeDefinitions'; import { INode, NodeType } from './NodeInstance'; +const makeSocketFromDefinition = ( + key: string, + { valueType, defaultValue, choices }: SocketDefinition +) => new Socket(valueType, key as string, defaultValue, undefined, choices); + const makeSocketsFromMap = ( socketConfig: TSockets, - keys: (keyof TSockets)[] + keys: (keyof TSockets)[], + configuration: NodeConfiguration, + graphApi: IGraphApi ): Socket[] => { return keys.map((key) => { const definition = socketConfig[key]; if (typeof definition === 'string') { return new Socket(definition, key as string); } - const { valueType, defaultValue, choices } = definition; - return new Socket( - valueType, - key as string, - defaultValue, - undefined, - choices - ); + if (typeof definition === 'function') { + const socketDef = definition(configuration, graphApi); + + return makeSocketFromDefinition(key as string, socketDef); + } + return makeSocketFromDefinition(key as string, definition); }); }; @@ -56,7 +62,9 @@ export function makeOrGenerateSockets( return makeSocketsFromMap( socketConfigOrFactory, - Object.keys(socketConfigOrFactory) + Object.keys(socketConfigOrFactory), + nodeConfig, + graph ); } diff --git a/packages/core/src/Nodes/testUtils.ts b/packages/core/src/Nodes/testUtils.ts index a5192086..524fba55 100644 --- a/packages/core/src/Nodes/testUtils.ts +++ b/packages/core/src/Nodes/testUtils.ts @@ -1,6 +1,4 @@ -import { Graph, IGraphApi } from '../Graphs/Graph'; -import { registerCoreValueTypes } from '../Profiles/Core/registerCoreProfile'; -import { Registry } from '../Registry'; +import { IGraphApi, makeGraphApi } from '../Graphs/Graph'; import { NodeConfiguration } from './Node'; import { IFunctionNodeDefinition, @@ -12,9 +10,10 @@ import { makeOrGenerateSockets } from './nodeFactory'; import { NodeConfigurationDescription } from './Registry/NodeDescription'; const makeEmptyGraph = (): IGraphApi => { - const registry = new Registry(); - registerCoreValueTypes(registry.values); - return new Graph(registry).makeApi(); + return makeGraphApi({ + dependencies: {}, + valuesTypeRegistry: {} + }); }; export type SocketValues = { diff --git a/packages/core/src/Profiles/Core/Debug/DebugLog.ts b/packages/core/src/Profiles/Core/Debug/DebugLog.ts index 4b1436c7..0b8040ab 100644 --- a/packages/core/src/Profiles/Core/Debug/DebugLog.ts +++ b/packages/core/src/Profiles/Core/Debug/DebugLog.ts @@ -28,16 +28,16 @@ export const Log = makeFlowNodeDefinition({ const text = read('text'); switch (read('severity')) { case 'verbose': - logger.verbose(text); + logger?.verbose(text); break; case 'info': - logger.info(text); + logger?.info(text); break; case 'warning': - logger.warn(text); + logger?.warn(text); break; case 'error': - logger.error(text); + logger?.error(text); break; } diff --git a/packages/core/src/Profiles/Core/Lifecycle/LifecycleOnEnd.ts b/packages/core/src/Profiles/Core/Lifecycle/LifecycleOnEnd.ts index 76ed8225..ffc69121 100644 --- a/packages/core/src/Profiles/Core/Lifecycle/LifecycleOnEnd.ts +++ b/packages/core/src/Profiles/Core/Lifecycle/LifecycleOnEnd.ts @@ -35,7 +35,7 @@ export const LifecycleOnEnd = makeEventNodeDefinition({ lifecycleEventEmitterDependencyKey ); - lifecycleEventEmitter.endEvent.addListener(onEndEvent); + lifecycleEventEmitter?.endEvent.addListener(onEndEvent); return { onEndEvent @@ -48,7 +48,7 @@ export const LifecycleOnEnd = makeEventNodeDefinition({ lifecycleEventEmitterDependencyKey ); - if (onEndEvent) lifecycleEventEmitter.endEvent.removeListener(onEndEvent); + if (onEndEvent) lifecycleEventEmitter?.endEvent.removeListener(onEndEvent); return {}; } diff --git a/packages/core/src/Profiles/Core/Lifecycle/LifecycleOnStart.ts b/packages/core/src/Profiles/Core/Lifecycle/LifecycleOnStart.ts index 395bc64e..e3aef45e 100644 --- a/packages/core/src/Profiles/Core/Lifecycle/LifecycleOnStart.ts +++ b/packages/core/src/Profiles/Core/Lifecycle/LifecycleOnStart.ts @@ -34,7 +34,7 @@ export const LifecycleOnStart = makeEventNodeDefinition({ lifecycleEventEmitterDependencyKey ); - lifecycleEventEmitter.startEvent.addListener(onStartEvent); + lifecycleEventEmitter?.startEvent.addListener(onStartEvent); return { onStartEvent @@ -48,7 +48,7 @@ export const LifecycleOnStart = makeEventNodeDefinition({ ); if (onStartEvent) - lifecycleEventEmitter.startEvent.removeListener(onStartEvent); + lifecycleEventEmitter?.startEvent.removeListener(onStartEvent); return {}; } diff --git a/packages/core/src/Profiles/Core/Lifecycle/LifecycleOnTick.ts b/packages/core/src/Profiles/Core/Lifecycle/LifecycleOnTick.ts index 4ca9d54b..71bc00c3 100644 --- a/packages/core/src/Profiles/Core/Lifecycle/LifecycleOnTick.ts +++ b/packages/core/src/Profiles/Core/Lifecycle/LifecycleOnTick.ts @@ -39,7 +39,7 @@ export const LifecycleOnTick = makeEventNodeDefinition({ lifecycleEventEmitterDependencyKey ); - lifecycleEventEmitter.tickEvent.addListener(onTickEvent); + lifecycleEventEmitter?.tickEvent.addListener(onTickEvent); return { onTickEvent @@ -53,7 +53,7 @@ export const LifecycleOnTick = makeEventNodeDefinition({ ); if (onTickEvent) - lifecycleEventEmitter.tickEvent.removeListener(onTickEvent); + lifecycleEventEmitter?.tickEvent.removeListener(onTickEvent); return {}; } diff --git a/packages/core/src/Profiles/Core/readCoreGraphs.test.ts b/packages/core/src/Profiles/Core/readCoreGraphs.test.ts index b33cf8c1..d718b9ae 100644 --- a/packages/core/src/Profiles/Core/readCoreGraphs.test.ts +++ b/packages/core/src/Profiles/Core/readCoreGraphs.test.ts @@ -12,16 +12,18 @@ import * as frameCounterJson from '../../../../../graphs/core/variables/FrameCou import * as initialValueJson from '../../../../../graphs/core/variables/InitialValue.json'; import * as setGetJson from '../../../../../graphs/core/variables/SetGet.json'; import { Logger } from '../../Diagnostics/Logger'; -import { Graph } from '../../Graphs/Graph'; +import { GraphInstance } from '../../Graphs/Graph'; import { GraphJSON } from '../../Graphs/IO/GraphJSON'; import { readGraphFromJSON } from '../../Graphs/IO/readGraphFromJSON'; import { validateGraphAcyclic } from '../../Graphs/Validation/validateGraphAcyclic'; import { validateGraphLinks } from '../../Graphs/Validation/validateGraphLinks'; -import { Registry } from '../../Registry'; -import { registerCoreProfile } from './registerCoreProfile'; +import { + getCoreNodeDefinitions, + getCoreValueTypes +} from './registerCoreProfile'; -const registry = new Registry(); -registerCoreProfile(registry); +const valueTypes = getCoreValueTypes(); +const nodeDefinitions = getCoreNodeDefinitions(valueTypes); Logger.onWarn.clear(); @@ -45,15 +47,20 @@ for (const key in exampleMap) { describe(`${key}`, () => { const exampleJson = exampleMap[key] as GraphJSON; - let parsedGraphJson: Graph | undefined; + let parsedGraphJson: GraphInstance | undefined; test('parse json to graph', () => { expect(() => { - parsedGraphJson = readGraphFromJSON(exampleJson, registry); + parsedGraphJson = readGraphFromJSON({ + graphJson: exampleJson, + nodes: nodeDefinitions, + values: valueTypes, + dependencies: {} + }); }).not.toThrow(); // await fs.writeFile('./examples/test.json', JSON.stringify(writeGraphToJSON(graph), null, ' '), { encoding: 'utf-8' }); if (parsedGraphJson !== undefined) { - expect(validateGraphLinks(parsedGraphJson)).toHaveLength(0); - expect(validateGraphAcyclic(parsedGraphJson)).toHaveLength(0); + expect(validateGraphLinks(parsedGraphJson.nodes)).toHaveLength(0); + expect(validateGraphAcyclic(parsedGraphJson.nodes)).toHaveLength(0); } else { expect(parsedGraphJson).toBeDefined(); } diff --git a/packages/core/src/Profiles/Core/registerCoreProfile.test.ts b/packages/core/src/Profiles/Core/registerCoreProfile.test.ts index 4ee2326f..9a296e0b 100644 --- a/packages/core/src/Profiles/Core/registerCoreProfile.test.ts +++ b/packages/core/src/Profiles/Core/registerCoreProfile.test.ts @@ -1,17 +1,15 @@ import { validateNodeRegistry } from '../../Nodes/Validation/validateNodeRegistry'; -import { Registry } from '../../Registry'; import { validateValueRegistry } from '../../Values/Validation/validateValueRegistry'; -import { registerCoreProfile } from './registerCoreProfile'; +import { getCoreRegistry } from './registerCoreProfile'; describe('core profile', () => { - const registry = new Registry(); - registerCoreProfile(registry); + const registry = getCoreRegistry(); test('validate node registry', () => { expect(validateNodeRegistry(registry)).toHaveLength(0); }); test('validate value registry', () => { - expect(validateValueRegistry(registry)).toHaveLength(0); + expect(validateValueRegistry(registry.values)).toHaveLength(0); }); const valueTypeNameToExampleValues: { [key: string]: any[] } = { @@ -23,7 +21,7 @@ describe('core profile', () => { for (const valueTypeName in valueTypeNameToExampleValues) { test(`${valueTypeName} serialization/deserialization`, () => { - const valueType = registry.values.get(valueTypeName); + const valueType = registry.values[valueTypeName]; const exampleValues: any[] = valueTypeNameToExampleValues[valueTypeName]; exampleValues.forEach((exampleValue: any) => { const deserializedValue = valueType.deserialize(exampleValue); diff --git a/packages/core/src/Profiles/Core/registerCoreProfile.ts b/packages/core/src/Profiles/Core/registerCoreProfile.ts index c9f8297f..be6b4d9c 100644 --- a/packages/core/src/Profiles/Core/registerCoreProfile.ts +++ b/packages/core/src/Profiles/Core/registerCoreProfile.ts @@ -1,8 +1,12 @@ /* eslint-disable max-len */ -import { DependenciesRegistry } from '../../Nodes/Registry/DependenciesRegistry'; + import { getNodeDescriptions } from '../../Nodes/Registry/NodeDescription'; -import { IRegistry } from '../../Registry'; -import { ValueTypeRegistry } from '../../Values/ValueTypeRegistry'; +import { + NodeDefinition, + NodeDefinitionsMap +} from '../../Nodes/Registry/NodeTypeRegistry'; +import { ValueTypeMap } from '../../Values/ValueTypeRegistry'; +import { getStringConversionsForValueType } from '../registerSerializersForValueType'; import { ILifecycleEventEmitter } from './Abstractions/ILifecycleEventEmitter'; import { ILogger } from './Abstractions/ILogger'; import { OnCustomEvent } from './CustomEvents/OnCustomEvent'; @@ -30,7 +34,6 @@ import { } from './Lifecycle/LifecycleOnStart'; import { LifecycleOnTick } from './Lifecycle/LifecycleOnTick'; import { Easing } from './Logic/Easing'; -import { registerSerializersForValueType } from '../registerSerializersForValueType'; import { Delay } from './Time/Delay'; import * as TimeNodes from './Time/TimeNodes'; import * as BooleanNodes from './Values/BooleanNodes'; @@ -44,93 +47,106 @@ import { StringValue } from './Values/StringValue'; import { VariableGet } from './Variables/VariableGet'; import { VariableSet } from './Variables/VariableSet'; -export function registerLogger( - registry: DependenciesRegistry, - logger: ILogger -) { - registry.register(loggerDependencyKey, logger); +export const makeCoreDependencies = ({ + lifecyleEmitter, + logger +}: { + lifecyleEmitter: ILifecycleEventEmitter; + logger: ILogger; +}) => ({ + [lifecycleEventEmitterDependencyKey]: lifecyleEmitter, + [loggerDependencyKey]: logger +}); + +export function getCoreValueTypes(): ValueTypeMap { + return toMap( + [BooleanValue, StringValue, IntegerValue, FloatValue], + (v) => v.name + ); } -export function registerLifecycleEventEmitter( - registry: DependenciesRegistry, - emitter: ILifecycleEventEmitter -) { - registry.register(lifecycleEventEmitterDependencyKey, emitter); +export function toMap( + elements: T[], + getName: (element: T) => string +): Record { + return Object.fromEntries( + elements.map((element) => [getName(element), element]) + ); } -export function registerCoreValueTypes(values: ValueTypeRegistry) { - // pull in value type nodes - values.register(BooleanValue); - values.register(StringValue); - values.register(IntegerValue); - values.register(FloatValue); +function getStringConversions(values: ValueTypeMap): NodeDefinition[] { + return ['boolean', 'float', 'integer'].flatMap((valueTypeName) => + getStringConversionsForValueType({ values, valueTypeName }) + ); } -export function registerCoreProfile( - registry: Pick -) { - const { nodes, values } = registry; - - registerCoreValueTypes(values); +export function getCoreNodeDefinitions( + values: ValueTypeMap +): NodeDefinitionsMap { + const allNodeDefinitions: NodeDefinition[] = [ + ...getNodeDescriptions(StringNodes), + ...getNodeDescriptions(BooleanNodes), + ...getNodeDescriptions(IntegerNodes), + ...getNodeDescriptions(FloatNodes), - // pull in value type nodes - nodes.register(...getNodeDescriptions(StringNodes)); - nodes.register(...getNodeDescriptions(BooleanNodes)); - nodes.register(...getNodeDescriptions(IntegerNodes)); - nodes.register(...getNodeDescriptions(FloatNodes)); + // custom events - // custom events + OnCustomEvent.Description, + TriggerCustomEvent.Description, - nodes.register(OnCustomEvent.Description); - nodes.register(TriggerCustomEvent.Description); + // variables - // variables + VariableGet, + VariableSet, - nodes.register(VariableGet); - nodes.register(VariableSet); + // complex logic - // complex logic + Easing, - nodes.register(Easing); + // actions - // actions + DebugLog, + AssertExpectTrue.Description, - nodes.register(DebugLog); - nodes.register(AssertExpectTrue.Description); + // events - // events + LifecycleOnStart, + LifecycleOnEnd, + LifecycleOnTick, - nodes.register(LifecycleOnStart); - nodes.register(LifecycleOnEnd); - nodes.register(LifecycleOnTick); + // time - // time + Delay.Description, + ...getNodeDescriptions(TimeNodes), - nodes.register(Delay.Description); - nodes.register(...getNodeDescriptions(TimeNodes)); + // flow control - // flow control + Branch, + FlipFlop, + ForLoop, + Sequence, + SwitchOnInteger, + SwitchOnString, + Debounce.Description, + Throttle.Description, + DoN, + DoOnce, + Gate, + MultiGate, + WaitAll.Description, + Counter, - nodes.register(Branch); - nodes.register(FlipFlop); - nodes.register(ForLoop); - nodes.register(Sequence); - nodes.register(SwitchOnInteger); - nodes.register(SwitchOnString); - nodes.register(Debounce.Description); - nodes.register(Throttle.Description); - nodes.register(DoN); - nodes.register(DoOnce); - nodes.register(Gate); - nodes.register(MultiGate); - nodes.register(WaitAll.Description); - nodes.register(Counter); + ...getStringConversions(values) + ]; - // string converters - - ['boolean', 'float', 'integer'].forEach((valueTypeName) => { - registerSerializersForValueType(registry, valueTypeName); - }); - - return registry; + // convert array to map + return toMap(allNodeDefinitions, (node) => node.typeName); } + +export const getCoreRegistry = () => { + const values = getCoreValueTypes(); + return { + values, + nodes: getCoreNodeDefinitions(getCoreValueTypes()) + }; +}; diff --git a/packages/core/src/Profiles/registerSerializersForValueType.ts b/packages/core/src/Profiles/registerSerializersForValueType.ts index 9094a20d..c652adf0 100644 --- a/packages/core/src/Profiles/registerSerializersForValueType.ts +++ b/packages/core/src/Profiles/registerSerializersForValueType.ts @@ -1,26 +1,29 @@ import { makeInNOutFunctionDesc } from '../Nodes/FunctionNode'; -import { IRegistry } from '../Registry'; import { toCamelCase } from '../toCamelCase'; +import { ValueTypeMap } from '../Values/ValueTypeRegistry'; -export function registerSerializersForValueType( - registry: Pick, - valueTypeName: string -) { +export function getStringConversionsForValueType({ + values, + valueTypeName +}: { + values: ValueTypeMap; + valueTypeName: string; +}) { const camelCaseValueTypeName = toCamelCase(valueTypeName); - registry.nodes.register( + return [ makeInNOutFunctionDesc({ name: `math/to${camelCaseValueTypeName}/string`, label: `To ${camelCaseValueTypeName}`, in: ['string'], out: valueTypeName, - exec: (a: string) => registry.values.get(valueTypeName).deserialize(a) + exec: (a: string) => values[valueTypeName]?.deserialize(a) }), makeInNOutFunctionDesc({ name: `math/toString/${valueTypeName}`, label: 'To String', in: [valueTypeName], out: 'string', - exec: (a: any) => registry.values.get(valueTypeName).serialize(a) + exec: (a: any) => values[valueTypeName]?.serialize(a) }) - ); + ]; } diff --git a/packages/core/src/Registry.ts b/packages/core/src/Registry.ts index b301b7b8..4130ab33 100644 --- a/packages/core/src/Registry.ts +++ b/packages/core/src/Registry.ts @@ -1,15 +1,7 @@ -import { DependenciesRegistry } from './Nodes/Registry/DependenciesRegistry'; -import { NodeTypeRegistry } from './Nodes/Registry/NodeTypeRegistry'; -import { ValueTypeRegistry } from './Values/ValueTypeRegistry'; +import { NodeDefinitionsMap } from '../dist/behave-graph-core.cjs'; +import { ValueTypeMap } from './Values/ValueTypeRegistry'; export interface IRegistry { - readonly values: ValueTypeRegistry; - readonly nodes: NodeTypeRegistry; - readonly dependencies: DependenciesRegistry; -} - -export class Registry implements IRegistry { - public readonly values = new ValueTypeRegistry(); - public readonly nodes = new NodeTypeRegistry(); - public readonly dependencies = new DependenciesRegistry(); + readonly values: ValueTypeMap; + readonly nodes: NodeDefinitionsMap; } diff --git a/packages/core/src/Sockets/Socket.ts b/packages/core/src/Sockets/Socket.ts index 571d1bf4..052a54a3 100644 --- a/packages/core/src/Sockets/Socket.ts +++ b/packages/core/src/Sockets/Socket.ts @@ -1,5 +1,7 @@ import { Link } from '../Nodes/Link'; +export type Choices = string[] | { text: string; value: any }[]; + export class Socket { public readonly links: Link[] = []; @@ -8,6 +10,6 @@ export class Socket { public readonly name: string, public value: any | undefined = undefined, public readonly label: string | undefined = undefined, - public readonly valueChoices: any[] = [] // if not empty, value must be one of these. + public readonly valueChoices?: Choices // if not empty, value must be one of these. ) {} } diff --git a/packages/core/src/Values/Validation/validateValueRegistry.ts b/packages/core/src/Values/Validation/validateValueRegistry.ts index e60a4e98..efff0df7 100644 --- a/packages/core/src/Values/Validation/validateValueRegistry.ts +++ b/packages/core/src/Values/Validation/validateValueRegistry.ts @@ -1,22 +1,22 @@ -import { Registry } from '../../Registry'; +import { ValueTypeMap } from '../ValueTypeRegistry'; const valueTypeNameRegex = /^\w+$/; -export function validateValueRegistry(graphRegistry: Registry): string[] { +export function validateValueRegistry(values: ValueTypeMap): string[] { const errorList: string[] = []; - graphRegistry.values.getAllNames().forEach((valueTypeName) => { + Object.keys(values).forEach((valueTypeName) => { if (!valueTypeNameRegex.test(valueTypeName)) { errorList.push(`invalid value type name ${valueTypeName}`); } - const valueType = graphRegistry.values.get(valueTypeName); + const valueType = values[valueTypeName]; - const value = valueType.creator(); - const serializedValue = valueType.serialize(value); - const deserializedValue = valueType.deserialize(serializedValue); - const reserializedValue = valueType.serialize(deserializedValue); - const redeserializedValue = valueType.deserialize(reserializedValue); + const value = valueType?.creator(); + const serializedValue = valueType?.serialize(value); + const deserializedValue = valueType?.deserialize(serializedValue); + const reserializedValue = valueType?.serialize(deserializedValue); + const redeserializedValue = valueType?.deserialize(reserializedValue); if (JSON.stringify(serializedValue) !== JSON.stringify(reserializedValue)) { errorList.push( diff --git a/packages/core/src/Values/ValueTypeRegistry.ts b/packages/core/src/Values/ValueTypeRegistry.ts index ef2d2818..04540b8b 100644 --- a/packages/core/src/Values/ValueTypeRegistry.ts +++ b/packages/core/src/Values/ValueTypeRegistry.ts @@ -1,29 +1,3 @@ import { ValueType } from './ValueType'; -export class ValueTypeRegistry { - private readonly valueTypeNameToValueType: { [key: string]: ValueType } = {}; - - register(...valueTypes: Array) { - valueTypes.forEach((valueType) => { - if (valueType.name in this.valueTypeNameToValueType) { - throw new Error(`already registered value type ${valueType.name}`); - } - this.valueTypeNameToValueType[valueType.name] = valueType; - }); - } - - get(valueTypeName: string): ValueType { - if (!(valueTypeName in this.valueTypeNameToValueType)) { - throw new Error(`can not find value type with name '${valueTypeName}`); - } - return this.valueTypeNameToValueType[valueTypeName]; - } - - getAllNames(): string[] { - return Object.keys(this.valueTypeNameToValueType); - } - - getAll(): ValueType[] { - return Object.values(this.valueTypeNameToValueType); - } -} +export type ValueTypeMap = { readonly [key: string]: ValueType }; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index d81d2d67..189f917a 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -90,3 +90,5 @@ export * from './Profiles/Core/Values/StringValue'; export * from './Profiles/Core/Variables/VariableSet'; export * from './Profiles/Core/Variables/VariableGet'; export * from './Profiles/Core/registerCoreProfile'; + +export * from './parseFloats'; diff --git a/packages/core/src/validateRegistry.ts b/packages/core/src/validateRegistry.ts index 41b5f123..741b5e64 100644 --- a/packages/core/src/validateRegistry.ts +++ b/packages/core/src/validateRegistry.ts @@ -1,12 +1,19 @@ +import { NodeDefinitionsMap, ValueTypeMap } from '@behave-graph/core'; + import { validateNodeRegistry } from './Nodes/Validation/validateNodeRegistry'; -import { Registry } from './Registry'; import { validateValueRegistry } from './Values/Validation/validateValueRegistry'; -export function validateRegistry(registry: Registry): string[] { +export function validateRegistry({ + nodes, + values +}: { + nodes: NodeDefinitionsMap; + values: ValueTypeMap; +}): string[] { const errorList: string[] = []; errorList.push( - ...validateValueRegistry(registry), - ...validateNodeRegistry(registry) + ...validateValueRegistry(values), + ...validateNodeRegistry({ nodes, values }) ); return errorList; } diff --git a/packages/flow/package.json b/packages/flow/package.json index ee96c4af..71cd64ff 100644 --- a/packages/flow/package.json +++ b/packages/flow/package.json @@ -57,15 +57,15 @@ "typescript": "^4.7.4", "uuid": "^8.3.2" }, - "publishConfig": { - "access": "public" - }, "peerDependencies": { "@behave-graph/core": "*", "react": "^18.2.0", "react-dom": "^18.2.0", "reactflow": "^11.1.1" }, + "publishConfig": { + "access": "public" + }, "description": "Simple, extensible behavior graph engine", "author": "", "license": "ISC" diff --git a/packages/flow/src/components/AutoSizeInput.tsx b/packages/flow/src/components/AutoSizeInput.tsx index 831b8917..67bc60d2 100644 --- a/packages/flow/src/components/AutoSizeInput.tsx +++ b/packages/flow/src/components/AutoSizeInput.tsx @@ -48,9 +48,11 @@ export const AutoSizeInput: FC = ({ if (measureRef.current === null) return; if (inputRef.current === null) return; - const width = measureRef.current.clientWidth; + const padding = props.type === 'number' || props.type === 'float' ? 20 : 0 + + const width = measureRef.current.clientWidth + padding; inputRef.current.style.width = Math.max(minWidth, width) + "px"; - }, [props.value, minWidth, styles]); + }, [props.value, minWidth, styles, props.type]); return ( <> diff --git a/packages/flow/src/components/Controls.tsx b/packages/flow/src/components/Controls.tsx index 0b2771d5..76886bf8 100644 --- a/packages/flow/src/components/Controls.tsx +++ b/packages/flow/src/components/Controls.tsx @@ -1,15 +1,10 @@ -import { - Engine, - ManualLifecycleEventEmitter, - readGraphFromJSON, - Registry, -} from "@behave-graph/core"; import { useState } from "react"; import { ClearModal } from "./modals/ClearModal"; import { HelpModal } from "./modals/HelpModal"; import { faDownload, faPlay, + faPause, faQuestion, faTrash, faUpload, @@ -18,49 +13,26 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Examples, LoadModal } from './modals/LoadModal'; import { SaveModal } from './modals/SaveModal'; -import { flowToBehave } from "../transformers/flowToBehave"; -import { useReactFlow, Controls, ControlButton } from "reactflow"; -import { sleep } from "../util/sleep"; - -export type CustomControlsProps = {examples: Examples, registry: Registry, manualLifecycleEventEmitter: ManualLifecycleEventEmitter}; +import { Controls, ControlButton } from "reactflow"; +import { GraphJSON, NodeSpecJSON } from "@behave-graph/core"; -const CustomControls = ({examples, registry, manualLifecycleEventEmitter}: CustomControlsProps) => { +export const CustomControls = ({ + playing, + togglePlay, + setBehaviorGraph, + examples, + specJson +}: { + playing: boolean; + togglePlay: () => void; + setBehaviorGraph: (value: GraphJSON) => void; + examples: Examples; + specJson: NodeSpecJSON[] | undefined; +}) => { const [loadModalOpen, setLoadModalOpen] = useState(false); const [saveModalOpen, setSaveModalOpen] = useState(false); const [helpModalOpen, setHelpModalOpen] = useState(false); const [clearModalOpen, setClearModalOpen] = useState(false); - const instance = useReactFlow(); - - const handleRun = async () => { - - const nodes = instance.getNodes(); - const edges = instance.getEdges(); - const graphJson = flowToBehave(nodes, edges); - const graph = readGraphFromJSON(graphJson, registry); - - const engine = new Engine(graph); - - - if (manualLifecycleEventEmitter.startEvent.listenerCount > 0) { - manualLifecycleEventEmitter.startEvent.emit(); - await engine.executeAllAsync(5); - } - - if (manualLifecycleEventEmitter.tickEvent.listenerCount > 0) { - const iterations = 20; - const tickDuration = 0.01; - for (let tick = 0; tick < iterations; tick++) { - manualLifecycleEventEmitter.tickEvent.emit(); - engine.executeAllSync(tickDuration); - await sleep( tickDuration ); - } - } - - if (manualLifecycleEventEmitter.endEvent.listenerCount > 0) { - manualLifecycleEventEmitter.endEvent.emit(); - await engine.executeAllAsync(5); - } - }; return ( <> @@ -77,12 +49,12 @@ const CustomControls = ({examples, registry, manualLifecycleEventEmitter}: Custo setClearModalOpen(true)}> - handleRun()}> - + + - setLoadModalOpen(false)} examples={examples} /> - setSaveModalOpen(false)} /> + setLoadModalOpen(false)} setBehaviorGraph={setBehaviorGraph} examples={examples} /> + {specJson && ( setSaveModalOpen(false)} />)} setHelpModalOpen(false)} /> = ({ graph, examples, }) => { - const [nodePickerVisibility, setNodePickerVisibility] = - useState(); - const [lastConnectStart, setLastConnectStart] = - useState(); - - const [initialNodes, initialEdges] = useMemo(() => behaveToFlow(graph), [graph]); - - const [nodes, , onNodesChange] = useNodesState(initialNodes); - const [edges, , onEdgesChange] = useEdgesState(initialEdges); - - const {registry, logger, manualLifecycleEventEmitter} = useRegistry(); - const nodeSpecJson = useNodeSpecJson({registry}); - - const onConnect = useCallback( - (connection: Connection) => { - if (connection.source === null) return; - if (connection.target === null) return; - - const newEdge = { - id: uuidv4(), - source: connection.source, - target: connection.target, - sourceHandle: connection.sourceHandle, - targetHandle: connection.targetHandle, - }; - onEdgesChange([ - { - type: "add", - item: newEdge, - }, - ]); - }, - [onEdgesChange] - ); - - const handleAddNode = useCallback( - (nodeType: string, position: XYPosition) => { - closeNodePicker(); - const newNode = { - id: uuidv4(), - type: nodeType, - position, - data: {}, - }; - onNodesChange([ - { - type: "add", - item: newNode, - }, - ]); - - if (lastConnectStart === undefined) return; - - // add an edge if we started on a socket - const originNode = nodes.find( - (node) => node.id === lastConnectStart.nodeId - ); - if (originNode === undefined) return; - onEdgesChange([ - { - type: "add", - item: calculateNewEdge( - originNode, - nodeType, - newNode.id, - lastConnectStart - ), - }, - ]); - }, - [lastConnectStart, nodes, onEdgesChange, onNodesChange] - ); - - const handleStartConnect = ( - e: ReactMouseEvent, - params: OnConnectStartParams - ) => { - setLastConnectStart(params); - }; - - const handleStopConnect = (e: MouseEvent) => { - const element = e.target as HTMLElement; - if (element.classList.contains("react-flow__pane")) { - setNodePickerVisibility({ x: e.clientX, y: e.clientY }); - } else { - setLastConnectStart(undefined); - } - }; - - const closeNodePicker = () => { - setLastConnectStart(undefined); - setNodePickerVisibility(undefined); - }; - - const handlePaneClick = () => closeNodePicker(); + initialGraph: GraphJSON; + examples: Examples; +} - const handlePaneContextMenu = (e: ReactMouseEvent) => { - e.preventDefault(); - setNodePickerVisibility({ x: e.clientX, y: e.clientY }); - }; +export const Flow: FC = ({ initialGraph: graph, examples }) => { + const { nodeDefinitions, valuesDefinitions, dependencies: dependencies } = useCoreRegistry(); + + const specJson = useNodeSpecJson({ nodes: nodeDefinitions, values: valuesDefinitions, dependencies }); + + const { + nodes, + edges, + onNodesChange, + onEdgesChange, + graphJson, + setGraphJson, + nodeTypes + } = useBehaveGraphFlow({ + initialGraphJson: graph, + specJson + }); + + const { onConnect, handleStartConnect, handleStopConnect, handlePaneClick, handlePaneContextMenu, nodePickerVisibility, handleAddNode, lastConnectStart, closeNodePicker, nodePickFilters } = useFlowHandlers({ + nodes, + onEdgesChange, + onNodesChange, + specJSON: specJson + }) + + const { togglePlay, playing } = useGraphRunner({ + graphJson, + valueTypeDefinitions: valuesDefinitions, + nodeDefinitions, + eventEmitter: dependencies.lifecycleEventEmitter, + dependencies + }); return ( - + {nodePickerVisibility && ( )} ); } + + diff --git a/packages/flow/src/components/InputSocket.tsx b/packages/flow/src/components/InputSocket.tsx index d37ef941..eb279137 100644 --- a/packages/flow/src/components/InputSocket.tsx +++ b/packages/flow/src/components/InputSocket.tsx @@ -3,25 +3,88 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Connection, Handle, Position, useReactFlow } from "reactflow"; import cx from "classnames"; import { colors, valueTypeColorMap } from "../util/colors"; -import { InputSocketSpecJSON } from "@behave-graph/core"; +import { InputSocketSpecJSON, NodeSpecJSON } from "@behave-graph/core"; import { isValidConnection } from "../util/isValidConnection"; import { AutoSizeInput } from "./AutoSizeInput"; +import { useEffect } from "react"; export type InputSocketProps = { connected: boolean; value: any | undefined; onChange: (key: string, value: any) => void; + specJSON: NodeSpecJSON[]; } & InputSocketSpecJSON; +const InputFieldForValue = ({ choices, value, defaultValue, onChange, name, valueType }: Pick) => { + const showChoices = choices?.length; + const inputVal = String(value) ?? defaultValue ?? ""; + + if (showChoices) + return + + return <> + {valueType === "string" && ( + onChange(name, e.currentTarget.value)} + /> + )} + {valueType === "number" && ( + onChange(name, e.currentTarget.value)} + /> + )} + {valueType === "float" && ( + onChange(name, e.currentTarget.value)} + /> + )} + {valueType === "integer" && ( + onChange(name, e.currentTarget.value)} + /> + )} + {valueType === "boolean" && ( + onChange(name, e.currentTarget.checked)} + /> + )} + ; +} + export default function InputSocket({ connected, - value, - onChange, - name, - valueType, - defaultValue, + + specJSON, + ...rest }: InputSocketProps) { + const { value, + name, + valueType, + defaultValue, + choices, } = rest; const instance = useReactFlow(); + const isFlowSocket = valueType === "flow"; let colorName = valueTypeColorMap[valueType]; @@ -29,6 +92,9 @@ export default function InputSocket({ colorName = "red"; } + const inputVal = String(value) ?? defaultValue ?? ""; + + // @ts-ignore const [backgroundColor, borderColor] = colors[colorName]; const showName = isFlowSocket === false || name !== "flow"; @@ -38,49 +104,8 @@ export default function InputSocket({ )} {showName &&
{name}
} - {isFlowSocket === false && connected === false && ( - <> - {valueType === "string" && ( - onChange(name, e.currentTarget.value)} - /> - )} - {valueType === "number" && ( - onChange(name, e.currentTarget.value)} - /> - )} - {valueType === "float" && ( - onChange(name, e.currentTarget.value)} - /> - )} - {valueType === "integer" && ( - onChange(name, e.currentTarget.value)} - /> - )} - {valueType === "boolean" && ( - onChange(name, e.currentTarget.checked)} - /> - )} - + {!isFlowSocket && ( + !connected && )} - isValidConnection(connection, instance) + isValidConnection(connection, instance, specJSON) } /> diff --git a/packages/flow/src/components/Node.tsx b/packages/flow/src/components/Node.tsx index 968e06ca..04c29261 100644 --- a/packages/flow/src/components/Node.tsx +++ b/packages/flow/src/components/Node.tsx @@ -9,6 +9,7 @@ import { isHandleConnected } from "../util/isHandleConnected"; type NodeProps = FlowNodeProps & { spec: NodeSpecJSON; + allSpecs: NodeSpecJSON[]; }; const getPairs = (arr1: T[], arr2: U[]) => { @@ -21,7 +22,7 @@ const getPairs = (arr1: T[], arr2: U[]) => { return pairs; }; -export const Node = ({ id, data, spec, selected }: NodeProps) => { +export const Node = ({ id, data, spec, selected, allSpecs }: NodeProps) => { const edges = useEdges(); const handleChange = useChangeNodeData(id); const pairs = getPairs(spec.inputs, spec.outputs); @@ -40,6 +41,7 @@ export const Node = ({ id, data, spec, selected }: NodeProps) => { {input && ( { {output && ( )} diff --git a/packages/flow/src/components/NodePicker.tsx b/packages/flow/src/components/NodePicker.tsx index 70f7acb1..054dd4ec 100644 --- a/packages/flow/src/components/NodePicker.tsx +++ b/packages/flow/src/components/NodePicker.tsx @@ -12,23 +12,24 @@ type NodePickerProps = { position: XYPosition; filters?: NodePickerFilters; onPickNode: (type: string, position: XYPosition) => void; - onClose: () => void; - specJSON: NodeSpecJSON[]|undefined; + onClose: () => void; + specJSON: NodeSpecJSON[] | undefined; }; -const NodePicker = ({ +export const NodePicker = ({ position, onPickNode, onClose, filters, - specJSON: nodes, + specJSON }: NodePickerProps) => { const [search, setSearch] = useState(""); const instance = useReactFlow(); useOnPressKey("Escape", onClose); - let filtered = nodes; + if (!specJSON) return null; + let filtered = specJSON; if (filters !== undefined) { filtered = filtered?.filter((node) => { const sockets = @@ -72,5 +73,3 @@ const NodePicker = ({ ); }; - -export default NodePicker; diff --git a/packages/flow/src/components/OutputSocket.tsx b/packages/flow/src/components/OutputSocket.tsx index 91f4dee3..bad60baf 100644 --- a/packages/flow/src/components/OutputSocket.tsx +++ b/packages/flow/src/components/OutputSocket.tsx @@ -3,14 +3,16 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Connection, Handle, Position, useReactFlow } from "reactflow"; import cx from "classnames"; import { colors, valueTypeColorMap } from "../util/colors"; -import { OutputSocketSpecJSON } from "@behave-graph/core"; +import { NodeSpecJSON, OutputSocketSpecJSON } from "@behave-graph/core"; import { isValidConnection } from "../util/isValidConnection"; export type OutputSocketProps = { connected: boolean; + specJSON: NodeSpecJSON[]; } & OutputSocketSpecJSON; export default function OutputSocket({ + specJSON, connected, valueType, name, @@ -21,6 +23,7 @@ export default function OutputSocket({ if (colorName === undefined) { colorName = "red"; } + // @ts-ignore const [backgroundColor, borderColor] = colors[colorName]; const showName = isFlowSocket === false || name !== "flow"; @@ -42,7 +45,7 @@ export default function OutputSocket({ position={Position.Right} className={cx(borderColor, connected ? backgroundColor : "bg-gray-800")} isValidConnection={(connection: Connection) => - isValidConnection(connection, instance) + isValidConnection(connection, instance, specJSON) } /> diff --git a/packages/flow/src/components/modals/LoadModal.tsx b/packages/flow/src/components/modals/LoadModal.tsx index 83c69559..d49ebb59 100644 --- a/packages/flow/src/components/modals/LoadModal.tsx +++ b/packages/flow/src/components/modals/LoadModal.tsx @@ -1,30 +1,35 @@ import { GraphJSON } from "@behave-graph/core"; -import { FC, useState } from "react"; +import { useEffect } from "react"; +import { FC, useCallback, useState } from "react"; import { useReactFlow } from "reactflow"; -import { behaveToFlow } from "../../transformers/behaveToFlow"; -import { autoLayout } from "../../util/autoLayout"; -import { hasPositionMetaData } from "../../util/hasPositionMetaData"; import { Modal } from "./Modal"; +export type Examples = { + [key: string]: GraphJSON; +} + export type LoadModalProps = { open?: boolean; onClose: () => void; - examples: Examples + setBehaviorGraph: (value: GraphJSON) => void; + examples: Examples; }; -export type Examples = { - [key: string]: GraphJSON; -} - -export const LoadModal: FC = ({ open = false, onClose, examples}) => { +export const LoadModal: FC = ({ open = false, onClose, setBehaviorGraph, examples }) => { const [value, setValue] = useState(); const [selected, setSelected] = useState(""); const instance = useReactFlow(); - const handleLoad = () => { + useEffect(() => { + if (selected) { + setValue(JSON.stringify(examples[selected], null, 2)); + } + }, [selected, examples]); + + const handleLoad = useCallback(() => { let graph; if (value !== undefined) { graph = JSON.parse(value) as GraphJSON; @@ -34,14 +39,7 @@ export const LoadModal: FC = ({ open = false, onClose, examples} if (graph === undefined) return; - const [nodes, edges] = behaveToFlow(graph); - - if (hasPositionMetaData(graph) === false) { - autoLayout(nodes, edges); - } - - instance.setNodes(nodes); - instance.setEdges(edges); + setBehaviorGraph(graph) // TODO better way to call fit vew after edges render setTimeout(() => { @@ -49,7 +47,7 @@ export const LoadModal: FC = ({ open = false, onClose, examples} }, 100); handleClose(); - }; + }, [setBehaviorGraph, value, instance]); const handleClose = () => { setValue(undefined); @@ -83,11 +81,9 @@ export const LoadModal: FC = ({ open = false, onClose, examples} - - - - - + {Object.keys(examples).map(key => ( + + ))} ); diff --git a/packages/flow/src/components/modals/SaveModal.tsx b/packages/flow/src/components/modals/SaveModal.tsx index 6b9a8252..55cdaca7 100644 --- a/packages/flow/src/components/modals/SaveModal.tsx +++ b/packages/flow/src/components/modals/SaveModal.tsx @@ -1,18 +1,19 @@ +import { NodeSpecJSON } from '@behave-graph/core' import { FC, useMemo, useRef, useState } from "react"; import { useEdges, useNodes } from "reactflow"; import { flowToBehave } from "../../transformers/flowToBehave"; import { Modal } from "./Modal"; -export type SaveModalProps = { open?: boolean; onClose: () => void }; +export type SaveModalProps = { open?: boolean; onClose: () => void, specJson: NodeSpecJSON[] }; -export const SaveModal: FC = ({ open = false, onClose }) => { +export const SaveModal: FC = ({ open = false, onClose, specJson }) => { const ref = useRef(null); const [copied, setCopied] = useState(false); const edges = useEdges(); const nodes = useNodes(); - const flow = useMemo(() => flowToBehave(nodes, edges), [nodes, edges]); + const flow = useMemo(() => flowToBehave(nodes, edges, specJson), [nodes, edges, specJson]); const jsonString = JSON.stringify(flow, null, 2); diff --git a/packages/flow/src/hooks/useBehaveGraphFlow.ts b/packages/flow/src/hooks/useBehaveGraphFlow.ts new file mode 100644 index 00000000..b874225d --- /dev/null +++ b/packages/flow/src/hooks/useBehaveGraphFlow.ts @@ -0,0 +1,75 @@ +import { GraphJSON, NodeSpecJSON } from '@behave-graph/core'; +import { useCallback, useEffect, useState } from 'react'; +import { useEdgesState, useNodesState } from 'reactflow'; + +import { behaveToFlow } from '../transformers/behaveToFlow'; +import { flowToBehave } from '../transformers/flowToBehave'; +import { autoLayout } from '../util/autoLayout'; +import { hasPositionMetaData } from '../util/hasPositionMetaData'; +import { useCustomNodeTypes } from './useCustomNodeTypes'; + +export const fetchBehaviorGraphJson = async (url: string) => + // eslint-disable-next-line unicorn/no-await-expression-member + (await (await fetch(url)).json()) as GraphJSON; + +/** + * Hook that returns the nodes and edges for react-flow, and the graphJson for the behave-graph. + * If nodes or edges are changes, the graph json is updated automatically. + * The graph json can be set manually, in which case the nodes and edges are updated to match the graph json. + * @param param0 + * @returns + */ +export const useBehaveGraphFlow = ({ + initialGraphJson, + specJson +}: { + initialGraphJson: GraphJSON; + specJson: NodeSpecJSON[] | undefined; +}) => { + const [graphJson, setStoredGraphJson] = useState(); + const [nodes, setNodes, onNodesChange] = useNodesState([]); + const [edges, setEdges, onEdgesChange] = useEdgesState([]); + + const setGraphJson = useCallback( + (graphJson: GraphJSON) => { + if (!graphJson) return; + + const [nodes, edges] = behaveToFlow(graphJson); + + if (hasPositionMetaData(graphJson) === false) { + autoLayout(nodes, edges); + } + + setNodes(nodes); + setEdges(edges); + setStoredGraphJson(graphJson); + }, + [setEdges, setNodes] + ); + + useEffect(() => { + if (!initialGraphJson) return; + setGraphJson(initialGraphJson); + }, [initialGraphJson, setGraphJson]); + + useEffect(() => { + if (!specJson) return; + // when nodes and edges are updated, update the graph json with the flow to behave behavior + const graphJson = flowToBehave(nodes, edges, specJson); + setStoredGraphJson(graphJson); + }, [nodes, edges, specJson]); + + const nodeTypes = useCustomNodeTypes({ + specJson + }); + + return { + nodes, + edges, + onEdgesChange, + onNodesChange, + setGraphJson, + graphJson, + nodeTypes + }; +}; diff --git a/packages/flow/src/hooks/useCoreRegistry.ts b/packages/flow/src/hooks/useCoreRegistry.ts new file mode 100644 index 00000000..f3b0d0df --- /dev/null +++ b/packages/flow/src/hooks/useCoreRegistry.ts @@ -0,0 +1,34 @@ +import { + getCoreNodeDefinitions, + getCoreValueTypes, + ValueTypeMap +} from '@behave-graph/core'; +import { useMemo } from 'react'; + +import { useCoreDependencies } from './useDependencies'; + +export const useCoreValueDefinitions = () => { + return useMemo(() => getCoreValueTypes(), []); +}; + +export const useCoreNodeDefinitions = ({ + values +}: { + values: ValueTypeMap; +}) => { + return useMemo(() => getCoreNodeDefinitions(values), [values]); +}; + +export const useCoreRegistry = () => { + const valuesDefinitions = useCoreValueDefinitions(); + const nodeDefinitions = useCoreNodeDefinitions({ + values: valuesDefinitions + }); + const dependencies = useCoreDependencies(); + + return { + nodeDefinitions, + valuesDefinitions, + dependencies + }; +}; diff --git a/packages/flow/src/hooks/useCustomNodeTypes.tsx b/packages/flow/src/hooks/useCustomNodeTypes.tsx new file mode 100644 index 00000000..48b3c087 --- /dev/null +++ b/packages/flow/src/hooks/useCustomNodeTypes.tsx @@ -0,0 +1,35 @@ +import { NodeSpecJSON } from '@behave-graph/core'; +import { useEffect, useState } from 'react'; +import { NodeTypes } from 'reactflow'; +import { Node } from '../components/Node'; + +const getCustomNodeTypes = ( + allSpecs: NodeSpecJSON[], +) => { + return allSpecs.reduce((nodes: NodeTypes, node) => { + nodes[node.type] = (props) => ( + + ); + return nodes; + }, {}); +}; + +export const useCustomNodeTypes = ({ + specJson, +}: { + specJson: NodeSpecJSON[] | undefined; +}) => { + const [customNodeTypes, setCustomNodeTypes] = useState(); + useEffect(() => { + if (!specJson) return; + const customNodeTypes = getCustomNodeTypes(specJson); + + setCustomNodeTypes(customNodeTypes); + }, [specJson]); + + return customNodeTypes; +}; diff --git a/packages/flow/src/hooks/useDependencies.ts b/packages/flow/src/hooks/useDependencies.ts new file mode 100644 index 00000000..f31e3d1b --- /dev/null +++ b/packages/flow/src/hooks/useDependencies.ts @@ -0,0 +1,50 @@ +import { + DefaultLogger, + Dependencies, + makeCoreDependencies, + ManualLifecycleEventEmitter +} from '@behave-graph/core'; +import { useEffect, useState } from 'react'; + +export const useCoreDependencies = () => { + const [dependencies] = useState(() => + makeCoreDependencies({ + lifecyleEmitter: new ManualLifecycleEventEmitter(), + logger: new DefaultLogger() + }) + ); + + return dependencies; +}; + +export const useMergeDependencies = ( + a: Dependencies | undefined, + b: Dependencies | undefined +): Dependencies | undefined => { + const [merged, setMerged] = useState(); + + useEffect(() => { + if (!a || !b) setMerged(undefined); + else + setMerged({ + ...a, + ...b + }); + }, [a, b]); + + return merged; +}; + +export const useDependency = ( + dependency: any, + createDependency: (dependency: any) => Dependencies +) => { + const [dependencies, setDependencies] = useState(); + + useEffect(() => { + if (typeof dependency === 'undefined') setDependencies(undefined); + else setDependencies(createDependency(dependency)); + }, [dependency, createDependency]); + + return dependencies; +}; diff --git a/packages/flow/src/hooks/useFlowHandlers.ts b/packages/flow/src/hooks/useFlowHandlers.ts new file mode 100644 index 00000000..635e2109 --- /dev/null +++ b/packages/flow/src/hooks/useFlowHandlers.ts @@ -0,0 +1,169 @@ +import { NodeSpecJSON } from '@behave-graph/core'; +import { + MouseEvent as ReactMouseEvent, + useCallback, + useEffect, + useState +} from 'react'; +import { Connection, Node, OnConnectStartParams, XYPosition } from 'reactflow'; +import { v4 as uuidv4 } from 'uuid'; + +import { calculateNewEdge } from '../util/calculateNewEdge'; +import { getNodePickerFilters } from '../util/getPickerFilters'; +import { useBehaveGraphFlow } from './useBehaveGraphFlow'; + +type BehaveGraphFlow = ReturnType; + +const useNodePickFilters = ({ + nodes, + lastConnectStart, + specJSON +}: { + nodes: Node[]; + lastConnectStart: OnConnectStartParams | undefined; + specJSON: NodeSpecJSON[] | undefined; +}) => { + const [nodePickFilters, setNodePickFilters] = useState( + getNodePickerFilters(nodes, lastConnectStart, specJSON) + ); + + useEffect(() => { + setNodePickFilters(getNodePickerFilters(nodes, lastConnectStart, specJSON)); + }, [nodes, lastConnectStart, specJSON]); + + return nodePickFilters; +}; + +export const useFlowHandlers = ({ + onEdgesChange, + onNodesChange, + nodes, + specJSON +}: Pick & { + nodes: Node[]; + specJSON: NodeSpecJSON[] | undefined; +}) => { + const [lastConnectStart, setLastConnectStart] = + useState(); + const [nodePickerVisibility, setNodePickerVisibility] = + useState(); + + const onConnect = useCallback( + (connection: Connection) => { + if (connection.source === null) return; + if (connection.target === null) return; + + const newEdge = { + id: uuidv4(), + source: connection.source, + target: connection.target, + sourceHandle: connection.sourceHandle, + targetHandle: connection.targetHandle + }; + onEdgesChange([ + { + type: 'add', + item: newEdge + } + ]); + }, + [onEdgesChange] + ); + + const closeNodePicker = useCallback(() => { + setLastConnectStart(undefined); + setNodePickerVisibility(undefined); + }, []); + + const handleAddNode = useCallback( + (nodeType: string, position: XYPosition) => { + closeNodePicker(); + const newNode = { + id: uuidv4(), + type: nodeType, + position, + data: {} + }; + onNodesChange([ + { + type: 'add', + item: newNode + } + ]); + + if (lastConnectStart === undefined) return; + + // add an edge if we started on a socket + const originNode = nodes.find( + (node) => node.id === lastConnectStart.nodeId + ); + if (originNode === undefined) return; + if (!specJSON) return; + onEdgesChange([ + { + type: 'add', + item: calculateNewEdge( + originNode, + nodeType, + newNode.id, + lastConnectStart, + specJSON + ) + } + ]); + }, + [ + closeNodePicker, + lastConnectStart, + nodes, + onEdgesChange, + onNodesChange, + specJSON + ] + ); + + const handleStartConnect = useCallback( + (e: ReactMouseEvent, params: OnConnectStartParams) => { + setLastConnectStart(params); + }, + [] + ); + + const handleStopConnect = useCallback((e: MouseEvent) => { + const element = e.target as HTMLElement; + if (element.classList.contains('react-flow__pane')) { + setNodePickerVisibility({ x: e.clientX, y: e.clientY }); + } else { + setLastConnectStart(undefined); + } + }, []); + + const handlePaneClick = useCallback( + () => closeNodePicker(), + [closeNodePicker] + ); + + const handlePaneContextMenu = useCallback((e: ReactMouseEvent) => { + e.preventDefault(); + setNodePickerVisibility({ x: e.clientX, y: e.clientY }); + }, []); + + const nodePickFilters = useNodePickFilters({ + nodes, + lastConnectStart, + specJSON + }); + + return { + onConnect, + handleStartConnect, + handleStopConnect, + handlePaneClick, + handlePaneContextMenu, + lastConnectStart, + nodePickerVisibility, + handleAddNode, + closeNodePicker, + nodePickFilters + }; +}; diff --git a/packages/flow/src/hooks/useGraphRunner.ts b/packages/flow/src/hooks/useGraphRunner.ts new file mode 100644 index 00000000..aa60adbe --- /dev/null +++ b/packages/flow/src/hooks/useGraphRunner.ts @@ -0,0 +1,111 @@ +import { + Dependencies, + Engine, + GraphJSON, + GraphNodes, + ILifecycleEventEmitter, + NodeDefinitionsMap, + readGraphFromJSON, + ValueTypeMap +} from '@behave-graph/core'; +import { useCallback, useEffect, useState } from 'react'; + +/** Runs the behavior graph by building the execution + * engine and triggering start on the lifecycle event emitter. + */ +export const useGraphRunner = ({ + graphJson, + eventEmitter, + autoRun = false, + nodeDefinitions, + valueTypeDefinitions, + dependencies +}: { + graphJson: GraphJSON | undefined; + eventEmitter: ILifecycleEventEmitter; + autoRun?: boolean; + nodeDefinitions: NodeDefinitionsMap; + valueTypeDefinitions: ValueTypeMap; + dependencies: Dependencies | undefined; +}) => { + const [engine, setEngine] = useState(); + + const [run, setRun] = useState(autoRun); + + const play = useCallback(() => { + setRun(true); + }, []); + + const pause = useCallback(() => { + setRun(false); + }, []); + + const togglePlay = useCallback(() => { + setRun((existing) => !existing); + }, []); + + useEffect(() => { + if (!graphJson || !valueTypeDefinitions || !run || !dependencies) return; + + let graphNodes: GraphNodes; + try { + graphNodes = readGraphFromJSON({ + graphJson, + nodes: nodeDefinitions, + values: valueTypeDefinitions, + dependencies + }).nodes; + } catch (e) { + console.error(e); + return; + } + const engine = new Engine(graphNodes); + + setEngine(engine); + + return () => { + engine.dispose(); + setEngine(undefined); + }; + }, [graphJson, valueTypeDefinitions, nodeDefinitions, run, dependencies]); + + useEffect(() => { + if (!engine || !run) return; + + engine.executeAllSync(); + + let timeout: number; + + const onTick = async () => { + eventEmitter.tickEvent.emit(); + + // eslint-disable-next-line no-await-in-loop + await engine.executeAllAsync(500); + + timeout = window.setTimeout(onTick, 50); + }; + + (async () => { + if (eventEmitter.startEvent.listenerCount > 0) { + eventEmitter.startEvent.emit(); + + await engine.executeAllAsync(5); + } else { + console.log('has no listener count'); + } + onTick(); + })(); + + return () => { + window.clearTimeout(timeout); + }; + }, [engine, eventEmitter.startEvent, eventEmitter.tickEvent, run]); + + return { + engine, + playing: run, + play, + togglePlay, + pause + }; +}; diff --git a/packages/flow/src/hooks/useMergeMap.ts b/packages/flow/src/hooks/useMergeMap.ts new file mode 100644 index 00000000..e832b026 --- /dev/null +++ b/packages/flow/src/hooks/useMergeMap.ts @@ -0,0 +1,14 @@ +import { useEffect, useState } from 'react'; + +export function useMergeMap>( + mapA: TMap, + mapB: TMap +): TMap { + const [result, setResult] = useState(() => ({ ...mapA, ...mapB })); + + useEffect(() => { + setResult({ ...mapA, ...mapB }); + }, [mapA, mapB]); + + return result; +} diff --git a/packages/flow/src/hooks/useNodeSpecJson.ts b/packages/flow/src/hooks/useNodeSpecJson.ts index 94a952af..cad5edd4 100644 --- a/packages/flow/src/hooks/useNodeSpecJson.ts +++ b/packages/flow/src/hooks/useNodeSpecJson.ts @@ -1,24 +1,30 @@ import { - IRegistry, + Dependencies, + NodeDefinitionsMap, NodeSpecJSON, + ValueTypeMap, writeNodeSpecsToJSON } from '@behave-graph/core'; import { useEffect, useState } from 'react'; export const useNodeSpecJson = ({ - registry + values, + nodes, + dependencies }: { - registry: IRegistry | undefined; + values: ValueTypeMap; + nodes: NodeDefinitionsMap; + dependencies: Dependencies | undefined; }) => { const [specJson, setSpecJson] = useState(); useEffect(() => { - if (!registry) { + if (!nodes || !values || !dependencies) { setSpecJson(undefined); return; } - setSpecJson(writeNodeSpecsToJSON(registry)); - }, [registry]); + setSpecJson(writeNodeSpecsToJSON({ nodes, values, dependencies })); + }, [nodes, values, dependencies]); return specJson; }; diff --git a/packages/flow/src/hooks/useQueriableDefinitions.ts b/packages/flow/src/hooks/useQueriableDefinitions.ts new file mode 100644 index 00000000..9a54a68e --- /dev/null +++ b/packages/flow/src/hooks/useQueriableDefinitions.ts @@ -0,0 +1,22 @@ +import { IQueriableRegistry } from 'packages/core/src'; +import { useMemo } from 'react'; + +export const toQueriableDefinitions = (definitionsMap: { + [id: string]: T; +}): IQueriableRegistry => ({ + get: (id: string) => definitionsMap[id], + getAll: () => Object.values(definitionsMap), + getAllNames: () => Object.keys(definitionsMap), + contains: (id: string) => definitionsMap[id] !== undefined +}); + +export const useQueriableDefinitions = (definitionsMap: { + [id: string]: T; +}): IQueriableRegistry => { + const queriableDefinitions = useMemo( + () => toQueriableDefinitions(definitionsMap), + [definitionsMap] + ); + + return queriableDefinitions; +}; diff --git a/packages/flow/src/index.ts b/packages/flow/src/index.ts index f6dd656d..b737099a 100644 --- a/packages/flow/src/index.ts +++ b/packages/flow/src/index.ts @@ -3,17 +3,27 @@ export * from './components/modals/HelpModal'; export * from './components/modals/LoadModal'; export * from './components/modals/Modal'; export * from './components/modals/SaveModal'; +export * from './components/Controls'; export * from './components/AutoSizeInput'; export * from './components/Controls'; export * from './components/InputSocket'; export * from './components/Node'; +export * from './components/Flow'; export * from './components/NodeContainer'; export * from './components/NodePicker'; export * from './components/OutputSocket'; export * from './hooks/useChangeNodeData'; export * from './hooks/useOnPressKey'; +export * from './hooks/useFlowHandlers'; +export * from './hooks/useGraphRunner'; +export * from './hooks/useDependencies'; +export * from './hooks/useBehaveGraphFlow'; +export * from './hooks/useNodeSpecJson'; +export * from './hooks/useCoreRegistry'; +export * from './hooks/useCustomNodeTypes'; +export * from './hooks/useMergeMap'; export * from './transformers/behaveToFlow'; export * from './transformers/flowToBehave'; @@ -21,13 +31,9 @@ export * from './transformers/flowToBehave'; export * from './util/autoLayout'; export * from './util/calculateNewEdge'; export * from './util/colors'; -export * from './util/customNodeTypes'; -export * from './util/getNodeSpecJSON'; export * from './util/getPickerFilters'; export * from './util/getSocketsByNodeTypeAndHandleType'; export * from './util/hasPositionMetaData'; export * from './util/isHandleConnected'; export * from './util/isValidConnection'; export * from './util/sleep'; - -export * from './components/Flow'; diff --git a/packages/flow/src/transformers/flowToBehave.test.ts b/packages/flow/src/transformers/flowToBehave.test.ts index 2f81348d..e73263e8 100644 --- a/packages/flow/src/transformers/flowToBehave.test.ts +++ b/packages/flow/src/transformers/flowToBehave.test.ts @@ -1,4 +1,8 @@ -import { GraphJSON } from '@behave-graph/core'; +import { + getCoreRegistry, + GraphJSON, + writeNodeSpecsToJSON +} from '@behave-graph/core'; import rawFlowGraph from '../../../../graphs/react-flow/graph.json'; import { behaveToFlow } from './behaveToFlow'; @@ -9,6 +13,12 @@ const flowGraph = rawFlowGraph as GraphJSON; const [nodes, edges] = behaveToFlow(flowGraph); it('transforms from flow to behave', () => { - const output = flowToBehave(nodes, edges); + const { values: valueTypes, nodes: nodeDefinitions } = getCoreRegistry(); + const specJSON = writeNodeSpecsToJSON({ + values: valueTypes, + nodes: nodeDefinitions, + dependencies: {} + }); + const output = flowToBehave(nodes, edges, specJSON); expect(output).toEqual(flowGraph); }); diff --git a/packages/flow/src/transformers/flowToBehave.ts b/packages/flow/src/transformers/flowToBehave.ts index 4567d6fc..60ca2a76 100644 --- a/packages/flow/src/transformers/flowToBehave.ts +++ b/packages/flow/src/transformers/flowToBehave.ts @@ -1,14 +1,14 @@ -import { GraphJSON, NodeJSON } from '@behave-graph/core'; +import { GraphJSON, NodeJSON, NodeSpecJSON } from '@behave-graph/core'; import { Edge, Node } from 'reactflow'; -import { getNodeSpecJSON } from '../util/getNodeSpecJSON'; - -const nodeSpecJSON = getNodeSpecJSON(); - const isNullish = (value: any): value is null | undefined => value === undefined || value === null; -export const flowToBehave = (nodes: Node[], edges: Edge[]): GraphJSON => { +export const flowToBehave = ( + nodes: Node[], + edges: Edge[], + nodeSpecJSON: NodeSpecJSON[] +): GraphJSON => { const graph: GraphJSON = { nodes: [], variables: [], customEvents: [] }; nodes.forEach((node) => { diff --git a/packages/flow/src/util/calculateNewEdge.ts b/packages/flow/src/util/calculateNewEdge.ts index 4eb9be82..22c247f9 100644 --- a/packages/flow/src/util/calculateNewEdge.ts +++ b/packages/flow/src/util/calculateNewEdge.ts @@ -1,16 +1,15 @@ +import { NodeSpecJSON } from 'packages/core/dist/behave-graph-core.cjs'; import { Node, OnConnectStartParams } from 'reactflow'; import { v4 as uuidv4 } from 'uuid'; -import { getNodeSpecJSON } from './getNodeSpecJSON'; import { getSocketsByNodeTypeAndHandleType } from './getSocketsByNodeTypeAndHandleType'; -const specJSON = getNodeSpecJSON(); - export const calculateNewEdge = ( originNode: Node, destinationNodeType: string, destinationNodeId: string, - connection: OnConnectStartParams + connection: OnConnectStartParams, + specJSON: NodeSpecJSON[] ) => { const sockets = getSocketsByNodeTypeAndHandleType( specJSON, diff --git a/packages/flow/src/util/colors.ts b/packages/flow/src/util/colors.ts index 7b9eb9d1..785cfb48 100644 --- a/packages/flow/src/util/colors.ts +++ b/packages/flow/src/util/colors.ts @@ -1,9 +1,18 @@ import { NodeSpecJSON } from '@behave-graph/core'; -export const colors: Record = { +export type color = + | 'red' + | 'green' + | 'lime' + | 'purple' + | 'blue' + | 'gray' + | 'white'; + +export const colors: Record = { red: ['bg-orange-700', 'border-orange-700', 'text-white'], green: ['bg-green-600', 'border-green-600', 'text-white'], - lime: ['bg-lime-500', 'border-lime-500', 'text-white'], + lime: ['bg-lime-500', 'border-lime-500', 'text-gray-900'], purple: ['bg-purple-500', 'border-purple-500', 'text-white'], blue: ['bg-cyan-600', 'border-cyan-600', 'text-white'], gray: ['bg-gray-500', 'border-gray-500', 'text-white'], @@ -19,13 +28,14 @@ export const valueTypeColorMap: Record = { string: 'purple' }; -export const categoryColorMap: Record = { +export const categoryColorMap: Record = { Event: 'red', Logic: 'green', Variable: 'purple', Query: 'purple', Action: 'blue', Flow: 'gray', + Effect: 'lime', Time: 'gray', None: 'gray' }; diff --git a/packages/flow/src/util/getPickerFilters.ts b/packages/flow/src/util/getPickerFilters.ts index e5889cb9..81a35f3b 100644 --- a/packages/flow/src/util/getPickerFilters.ts +++ b/packages/flow/src/util/getPickerFilters.ts @@ -1,25 +1,26 @@ +import { NodeSpecJSON } from '@behave-graph/core'; import { Node, OnConnectStartParams } from 'reactflow'; import { NodePickerFilters } from '../components/NodePicker'; -import { getNodeSpecJSON } from './getNodeSpecJSON'; import { getSocketsByNodeTypeAndHandleType } from './getSocketsByNodeTypeAndHandleType'; -const specJSON = getNodeSpecJSON(); - export const getNodePickerFilters = ( nodes: Node[], - params: OnConnectStartParams | undefined + params: OnConnectStartParams | undefined, + specJSON: NodeSpecJSON[] | undefined ): NodePickerFilters | undefined => { if (params === undefined) return; const originNode = nodes.find((node) => node.id === params.nodeId); if (originNode === undefined) return; - const sockets = getSocketsByNodeTypeAndHandleType( - specJSON, - originNode.type, - params.handleType - ); + const sockets = specJSON + ? getSocketsByNodeTypeAndHandleType( + specJSON, + originNode.type, + params.handleType + ) + : undefined; const socket = sockets?.find((socket) => socket.name === params.handleId); diff --git a/packages/flow/src/util/isValidConnection.ts b/packages/flow/src/util/isValidConnection.ts index e52706f4..1e472ada 100644 --- a/packages/flow/src/util/isValidConnection.ts +++ b/packages/flow/src/util/isValidConnection.ts @@ -1,14 +1,13 @@ +import { NodeSpecJSON } from '@behave-graph/core'; import { Connection, ReactFlowInstance } from 'reactflow'; -import { getNodeSpecJSON } from './getNodeSpecJSON'; import { getSocketsByNodeTypeAndHandleType } from './getSocketsByNodeTypeAndHandleType'; import { isHandleConnected } from './isHandleConnected'; -const specJSON = getNodeSpecJSON(); - export const isValidConnection = ( connection: Connection, - instance: ReactFlowInstance + instance: ReactFlowInstance, + specJSON: NodeSpecJSON[] ) => { if (connection.source === null || connection.target === null) return false; diff --git a/packages/flow/tsconfig.json b/packages/flow/tsconfig.json index a23055e1..665c0e86 100644 --- a/packages/flow/tsconfig.json +++ b/packages/flow/tsconfig.json @@ -3,7 +3,12 @@ "compilerOptions": { "jsx": "react-jsx", "declaration": false, - "emitDeclarationOnly": false + "emitDeclarationOnly": false, + "baseUrl": ".", + "paths": { + "@behave-graph/core": ["../core/src"], + "@/*": ["src/*"], + } }, "include": ["./src"] } diff --git a/packages/scene/package.json b/packages/scene/package.json index 6cfa2b2b..0484fdb8 100644 --- a/packages/scene/package.json +++ b/packages/scene/package.json @@ -21,7 +21,7 @@ "@types/jest": "^29.1.1", "@types/node": "^18.0.6", "@types/offscreencanvas": "^2019.7.0", - "@types/three": "^0.144.0", + "@types/three": "^0.149.0", "@typescript-eslint/eslint-plugin": "^5.38.1", "eslint": "^8.24.0", "eslint-config-prettier": "^8.5.0", @@ -40,7 +40,13 @@ "description": "Simple, extensible behavior graph engine", "author": "", "license": "ISC", - "dependencies": { - "@behave-graph/core": "*" + "peerDependencies": { + "@behave-graph/core": "*", + "three": "^0.144.0", + "@types/three": "^0.144.0", + "three-stdlib": "^2.21.8" + }, + "publishConfig": { + "access": "public" } } diff --git a/packages/scene/src/Abstractions/Drivers/DummyScene.ts b/packages/scene/src/Abstractions/Drivers/DummyScene.ts index 5d9fac3e..f47424d7 100644 --- a/packages/scene/src/Abstractions/Drivers/DummyScene.ts +++ b/packages/scene/src/Abstractions/Drivers/DummyScene.ts @@ -4,7 +4,7 @@ import { FloatValue, IntegerValue, StringValue, - ValueTypeRegistry + ValueType } from '@behave-graph/core'; import { ColorValue } from '../../Values/ColorValue'; @@ -17,25 +17,29 @@ import { IScene } from '../IScene'; export class DummyScene implements IScene { public onSceneChanged = new EventEmitter(); - private valueRegistry = new ValueTypeRegistry(); + + private valueRegistry: Record; constructor() { - const values = this.valueRegistry; + this.valueRegistry = Object.fromEntries( + [ + BooleanValue, + StringValue, + IntegerValue, + FloatValue, + Vec2Value, + Vec3Value, + Vec4Value, + ColorValue, + EulerValue, + QuatValue + ].map((valueType) => [valueType.name, valueType]) + ); // pull in value type nodes - values.register(BooleanValue); - values.register(StringValue); - values.register(IntegerValue); - values.register(FloatValue); - values.register(Vec2Value); - values.register(Vec3Value); - values.register(Vec4Value); - values.register(ColorValue); - values.register(EulerValue); - values.register(QuatValue); } getProperty(jsonPath: string, valueTypeName: string): any { - return this.valueRegistry.get(valueTypeName).creator(); + return this.valueRegistry[valueTypeName]?.creator(); } setProperty(): void { this.onSceneChanged.emit(); @@ -52,4 +56,24 @@ export class DummyScene implements IScene { ): void { console.log('removed on clicked listener'); } + + getQueryableProperties() { + return []; + } + + getRaycastableProperties() { + return []; + } + + getProperties() { + return []; + } + + addOnSceneChangedListener() { + console.log('added on scene changed listener'); + } + + removeOnSceneChangedListener(): void { + console.log('removed on scene changed listener'); + } } diff --git a/packages/scene/src/Abstractions/IScene.ts b/packages/scene/src/Abstractions/IScene.ts index ea3cdc58..7f12d7e9 100644 --- a/packages/scene/src/Abstractions/IScene.ts +++ b/packages/scene/src/Abstractions/IScene.ts @@ -1,3 +1,5 @@ +import { Choices } from '@behave-graph/core'; + export interface IScene { getProperty(jsonPath: string, valueTypeName: string): any; setProperty(jsonPath: string, valueTypeName: string, value: any): void; @@ -9,4 +11,8 @@ export interface IScene { jsonPath: string, callback: (jsonPath: string) => void ): void; + getRaycastableProperties: () => Choices; + getProperties: () => Choices; + addOnSceneChangedListener(listener: () => void): void; + removeOnSceneChangedListener(listener: () => void): void; } diff --git a/packages/scene/src/GLTFJson.ts b/packages/scene/src/GLTFJson.ts new file mode 100644 index 00000000..4278e3cd --- /dev/null +++ b/packages/scene/src/GLTFJson.ts @@ -0,0 +1,34 @@ +export type GLTFAssetJson = { + generator: string; + version: string; +}; + +export type GLTFSceneJson = { + name: string; + nodes: number[]; +}; + +export type GLTFNodeJson = { + name?: string; + mesh?: number; + translation?: number[]; + children?: number[]; +}; + +export type GLTFMaterialJson = { + name?: string; + doubleSided?: boolean; +}; + +export type GLTFMeshJson = { + name?: string; +}; + +export type GLTFJson = { + asset: GLTFAssetJson; + scene: number; + scenes: GLTFSceneJson[]; + nodes: GLTFNodeJson[]; + materials: GLTFMaterialJson[]; + meshes: GLTFMeshJson[]; +}; diff --git a/packages/scene/src/Nodes/Actions/SetSceneProperty.ts b/packages/scene/src/Nodes/Actions/SetSceneProperty.ts index fea465c4..6feffae6 100644 --- a/packages/scene/src/Nodes/Actions/SetSceneProperty.ts +++ b/packages/scene/src/Nodes/Actions/SetSceneProperty.ts @@ -1,13 +1,22 @@ -import { makeFlowNodeDefinition } from '@behave-graph/core'; +import { makeFlowNodeDefinition, NodeCategory } from '@behave-graph/core'; -import { IScene } from '../../Abstractions/IScene'; +import { getSceneDependencey } from '../../dependencies'; export const SetSceneProperty = (valueTypeNames: string[]) => valueTypeNames.map((valueTypeName) => makeFlowNodeDefinition({ typeName: `scene/set/${valueTypeName}`, + category: NodeCategory.Effect, + label: `Set Scene ${valueTypeName}`, in: { - jsonPath: 'string', + jsonPath: (_, graphApi) => { + const scene = getSceneDependencey(graphApi.getDependency); + + return { + valueType: 'string', + choices: scene?.getProperties() + }; + }, value: valueTypeName, flow: 'flow' }, @@ -16,8 +25,8 @@ export const SetSceneProperty = (valueTypeNames: string[]) => }, initialState: undefined, triggered: ({ commit, read, graph: { getDependency } }) => { - const scene = getDependency('scene'); - scene.setProperty(read('jsonPath'), valueTypeName, read('value')); + const scene = getSceneDependencey(getDependency); + scene?.setProperty(read('jsonPath'), valueTypeName, read('value')); commit('flow'); } }) diff --git a/packages/scene/src/Nodes/Events/OnSceneNodeClick.ts b/packages/scene/src/Nodes/Events/OnSceneNodeClick.ts index d6832c13..9492a521 100644 --- a/packages/scene/src/Nodes/Events/OnSceneNodeClick.ts +++ b/packages/scene/src/Nodes/Events/OnSceneNodeClick.ts @@ -5,6 +5,7 @@ import { } from '@behave-graph/core'; import { IScene } from '../../Abstractions/IScene'; +import { getSceneDependencey } from '../../dependencies'; type State = { jsonPath?: string | undefined; @@ -17,8 +18,16 @@ const initialState = (): State => ({}); export const OnSceneNodeClick = makeEventNodeDefinition({ typeName: 'scene/nodeClick', category: NodeCategory.Event, + label: 'On Scene Node Click', in: { - jsonPath: 'string' + jsonPath: (_, graphApi) => { + const scene = getSceneDependencey(graphApi.getDependency); + + return { + valueType: 'string', + choices: scene?.getRaycastableProperties() + }; + } }, out: { flow: 'flow' @@ -31,8 +40,8 @@ export const OnSceneNodeClick = makeEventNodeDefinition({ const jsonPath = read('jsonPath'); - const scene = getDependency('scene'); - scene.addOnClickedListener(jsonPath, handleNodeClick); + const scene = getSceneDependencey(getDependency); + scene?.addOnClickedListener(jsonPath, handleNodeClick); const state: State = { handleNodeClick, @@ -51,7 +60,7 @@ export const OnSceneNodeClick = makeEventNodeDefinition({ if (!jsonPath || !handleNodeClick) return {}; const scene = getDependency('scene'); - scene.removeOnClickedListener(jsonPath, handleNodeClick); + scene?.removeOnClickedListener(jsonPath, handleNodeClick); return {}; } diff --git a/packages/scene/src/Nodes/Queries/GetSceneProperty.ts b/packages/scene/src/Nodes/Queries/GetSceneProperty.ts index b995071d..964a36ac 100644 --- a/packages/scene/src/Nodes/Queries/GetSceneProperty.ts +++ b/packages/scene/src/Nodes/Queries/GetSceneProperty.ts @@ -1,19 +1,28 @@ -import { makeFunctionNodeDefinition } from '@behave-graph/core'; +import { makeFunctionNodeDefinition, NodeCategory } from '@behave-graph/core'; -import { IScene } from '../../Abstractions/IScene'; +import { getSceneDependencey } from '../../dependencies'; export const GetSceneProperty = (valueTypeNames: string[]) => valueTypeNames.map((valueTypeName) => makeFunctionNodeDefinition({ - typeName: `scene/get${valueTypeName}`, + typeName: `scene/get/${valueTypeName}`, + category: NodeCategory.Query, + label: `Scene set ${valueTypeName}`, in: { - jsonPath: 'string' + jsonPath: (_, graphApi) => { + const scene = getSceneDependencey(graphApi.getDependency); + + return { + valueType: 'string', + choices: scene.getProperties() + }; + } }, out: { value: valueTypeName }, exec: ({ graph: { getDependency }, read, write }) => { - const scene = getDependency('scene'); + const scene = getSceneDependencey(getDependency); const propertyValue = scene.getProperty( read('jsonPath'), valueTypeName diff --git a/packages/scene/src/buildScene.ts b/packages/scene/src/buildScene.ts new file mode 100644 index 00000000..2de5a0a3 --- /dev/null +++ b/packages/scene/src/buildScene.ts @@ -0,0 +1,477 @@ +import { Choices, EventEmitter } from '@behave-graph/core'; +import { + Event, + Material, + MeshBasicMaterial, + Object3D, + Quaternion, + Vector3, + Vector4 +} from 'three'; +import { GLTF } from 'three-stdlib'; + +import { IScene } from './Abstractions/IScene'; +import { GLTFJson } from './GLTFJson'; +import { Vec3 } from './Values/Internal/Vec3'; +import { Vec4 } from './Values/Internal/Vec4'; + +enum Resource { + nodes = 'nodes', + materials = 'materials', + animations = 'animations' +} + +function toVec3(value: Vector3): Vec3 { + return new Vec3(value.x, value.y, value.z); +} +function toVec4(value: Vector4 | Quaternion): Vec4 { + return new Vec4(value.x, value.y, value.z, value.w); +} + +export declare type ObjectMap = { + nodes: { + [name: string]: Object3D; + }; + materials: { + [name: string]: Material; + }; +}; + +const shortPathRegEx = /^\/?(?[^/]+)\/(?\d+)$/; +const jsonPathRegEx = + /^\/?(?[^/]+)\/(?\d+)\/(?[^/]+)$/; + +export type Optional = { + [K in keyof T]: T[K] | undefined; +}; + +export type Path = { + resource: Resource; + index: number; + property: string; +}; + +export function toJsonPathString( + { index, property, resource: resourceType }: Optional, + short: boolean +) { + if (short) { + if (!resourceType || typeof index === undefined) return; + return `${resourceType}/${index}`; + } else { + if (!resourceType || typeof index === undefined || !property) return; + return `${resourceType}/${index}/${property}`; + } +} + +export function parseJsonPath(jsonPath: string, short = false): Path { + // hack = for now we see if there are 2 segments to know if its short + const regex = short ? shortPathRegEx : jsonPathRegEx; + const matches = regex.exec(jsonPath); + if (matches === null) throw new Error(`can not parse jsonPath: ${jsonPath}`); + if (matches.groups === undefined) + throw new Error(`can not parse jsonPath (no groups): ${jsonPath}`); + return { + resource: matches.groups.resource as Resource, + index: +matches.groups.index, + property: matches.groups.property + }; +} + +export function applyPropertyToModel( + { resource, index, property }: Path, + gltf: GLTF & ObjectMap, + value: any, + properties: Properties, + setActiveAnimations: + | ((animation: string, active: boolean) => void) + | undefined +) { + const nodeName = getResourceName({ resource, index }, properties); + if (!nodeName) throw new Error(`could not get node at index ${index}`); + if (resource === Resource.nodes) { + const node = gltf.nodes[nodeName] as unknown as Object3D | undefined; + + if (!node) { + console.error(`no node at path ${nodeName}`); + return; + } + + applyNodeModifier(property, node, value); + + return; + } + if (resource === Resource.materials) { + const node = gltf.materials[nodeName] as unknown as Material | undefined; + + if (!node) { + console.error(`no node at path ${nodeName}`); + return; + } + + applyMaterialModifier(property, node, value); + + return; + } + + if (resource === Resource.animations) { + if (!setActiveAnimations) { + console.error( + 'cannot apply animation property without setActiveAnimations' + ); + return; + } + + setActiveAnimations(nodeName, value as boolean); + return; + } + + console.error(`unknown resource type ${resource}`); +} + +const getResourceName = ( + { resource, index }: Pick, + properties: Properties +) => { + return properties[resource]?.options[index].name; +}; + +const getPropertyFromModel = ( + { resource, index, property }: Path, + gltf: GLTF & ObjectMap, + properties: Properties +) => { + if (resource === Resource.nodes) { + const nodeName = getResourceName({ resource, index }, properties); + if (!nodeName) throw new Error(`could not get node at index ${index}`); + const node = gltf.nodes[nodeName] as unknown as Object3D | undefined; + + if (!node) { + console.error(`no node at path ${nodeName}`); + return; + } + + getPropertyValue(property, node); + + return; + } +}; + +function applyNodeModifier(property: string, objectRef: Object3D, value: any) { + switch (property) { + case 'visible': { + objectRef.visible = value as boolean; + break; + } + case 'translation': { + const v = value as Vec3; + objectRef.position.set(v.x, v.y, v.z); + break; + } + case 'scale': { + const v = value as Vec3; + console.log(v.x); + objectRef.scale.set(v.x, v.y, v.z); + break; + } + case 'rotation': { + const v = value as Vec4; + objectRef.quaternion.set(v.x, v.y, v.z, v.w); + break; + } + } +} + +function applyMaterialModifier( + property: string, + materialRef: Material, + value: any +) { + switch (property) { + case 'color': { + const basic = materialRef as MeshBasicMaterial; + + if (basic.color) { + const v = value as Vec3; + basic.color.setRGB(v.x, v.y, v.z); + basic.needsUpdate = true; + } + break; + } + } +} + +function getPropertyValue(property: string, objectRef: Object3D) { + switch (property) { + case 'visible': { + return objectRef.visible; + } + case 'translation': { + return toVec3(objectRef.position); + } + case 'scale': { + return toVec3(objectRef.scale); + } + case 'rotation': { + return toVec4(objectRef.quaternion); + } + default: + throw new Error(`unrecognized property: ${property}`); + } +} + +export type ResourceOption = { + name: string; + index: number; +}; + +export type ResourceProperties = { + options: ResourceOption[]; + properties: string[]; +}; + +type Properties = { + [key in Resource]?: ResourceProperties; +}; + +export type ParsableScene = GLTF & + ObjectMap & { + json?: GLTFJson; + }; + +export const extractProperties = (gltf: ParsableScene): Properties => { + const nodeProperties = [ + 'visible', + 'translation', + 'scale', + 'rotation', + 'color' + ]; + const animationProperties = ['playing']; + const materialProperties = ['color']; + + const gltfJson = gltf.parser.json as GLTFJson; + + const nodeOptions = gltfJson.nodes?.map(({ name }, index) => ({ + name: name || index.toString(), + index + })); + const materialOptions = gltfJson.materials?.map(({ name }, index) => ({ + name: name || index.toString(), + index + })); + const animationOptions = gltf.animations?.map(({ name }, index) => ({ + name: name || index.toString(), + index + })); + + const properties: Properties = {}; + + properties.nodes = { options: nodeOptions, properties: nodeProperties }; + + if (materialOptions) { + properties.materials = { + options: materialOptions, + properties: materialProperties + }; + } + + if (animationOptions) { + properties.animations = { + options: animationOptions, + properties: animationProperties + }; + } + + return properties; +}; + +function createPropertyChoice( + resource: string, + name: string, + property: string, + index: number +): { text: string; value: any } { + return { + text: `${resource}/${name}/${property}`, + value: `${resource}/${index}/${property}` + }; +} + +function generateChoicesForProperty( + property: ResourceProperties | undefined, + resource: Resource +) { + if (!property) return []; + const choices: { text: string; value: any }[] = []; + + property.options.forEach(({ index, name }) => { + property.properties.forEach((property) => { + choices.push(createPropertyChoice(resource, name, property, index)); + }); + }); + + return choices; +} + +export function generateSettableChoices(properties: Properties): Choices { + const choices: { text: string; value: any }[] = [ + ...generateChoicesForProperty(properties.nodes, Resource.nodes), + ...generateChoicesForProperty(properties.materials, Resource.materials), + ...generateChoicesForProperty(properties.animations, Resource.animations) + ]; + + return choices; +} + +export function generateRaycastableChoices(properties: Properties): Choices { + const choices: { text: string; value: any }[] = []; + + properties.nodes?.options.forEach(({ index, name }) => { + choices.push({ + text: `nodes/${name}`, + value: `nodes/${index}` + }); + }); + + return choices; +} + +export type OnClickCallback = (jsonPath: string) => void; + +export type OnClickListener = { + path: Path; + elementName: string; + callbacks: OnClickCallback[]; +}; + +export type OnClickListeners = { + [jsonPath: string]: OnClickListener; +}; + +export const buildScene = ({ + gltf, + setOnClickListeners, + setActiveAnimations +}: { + gltf: GLTF & ObjectMap; + setOnClickListeners: + | ((cb: (existing: OnClickListeners) => OnClickListeners) => void) + | undefined; + setActiveAnimations: + | ((animation: string, active: boolean) => void) + | undefined; +}) => { + const properties = extractProperties(gltf); + + const onSceneChanged = new EventEmitter(); + + const addOnClickedListener = ( + jsonPath: string, + callback: (jsonPath: string) => void + ) => { + if (!setOnClickListeners) return; + const path = parseJsonPath(jsonPath, true); + + setOnClickListeners((existing) => { + const listenersForPath = existing[jsonPath] || { + path, + elementName: getResourceName( + { resource: path.resource, index: path.index }, + properties + ), + callbacks: [] + }; + + const updatedListeners: OnClickListener = { + ...listenersForPath, + callbacks: [...listenersForPath.callbacks, callback] + }; + + const result: OnClickListeners = { + ...existing, + [jsonPath]: updatedListeners + }; + + return result; + }); + }; + + const removeOnClickedListener = ( + jsonPath: string, + callback: (jsonPath: string) => void + ) => { + if (!setOnClickListeners) return; + setOnClickListeners((existing) => { + const listenersForPath = existing[jsonPath]; + + if (!listenersForPath) return existing; + + const updatedCallbacks = listenersForPath.callbacks.filter( + (x) => x !== callback + ); + + if (updatedCallbacks.length > 0) { + const updatedListeners = { + ...listenersForPath, + callback: updatedCallbacks + }; + + return { + ...existing, + [jsonPath]: updatedListeners + }; + } + + const result = { + ...existing + }; + + delete result[jsonPath]; + + return result; + }); + }; + + const getProperty = (jsonPath: string, valueTypeName: string) => { + const path = parseJsonPath(jsonPath); + + return getPropertyFromModel(path, gltf, properties); + }; + + const setProperty = (jsonPath: string, valueTypeName: string, value: any) => { + const path = parseJsonPath(jsonPath); + + applyPropertyToModel(path, gltf, value, properties, setActiveAnimations); + + onSceneChanged.emit(); + }; + + const settableChoices = generateSettableChoices(properties); + const raycastableChoices = generateRaycastableChoices(properties); + + const addOnSceneChangedListener: IScene['addOnSceneChangedListener'] = ( + listener + ) => { + onSceneChanged.addListener(listener); + }; + + const removeOnSceneChangedListener: IScene['removeOnSceneChangedListener'] = ( + listener + ) => { + onSceneChanged.removeListener(listener); + }; + + const scene: IScene = { + getProperty, + setProperty, + getProperties: () => settableChoices, + getRaycastableProperties: () => raycastableChoices, + addOnClickedListener, + removeOnClickedListener, + addOnSceneChangedListener, + removeOnSceneChangedListener + }; + + return scene; +}; diff --git a/packages/scene/src/dependencies.ts b/packages/scene/src/dependencies.ts new file mode 100644 index 00000000..1a0d0d98 --- /dev/null +++ b/packages/scene/src/dependencies.ts @@ -0,0 +1,9 @@ +import { IGraphApi } from '@behave-graph/core'; + +import { IScene } from './Abstractions/IScene'; + +export const sceneDepdendencyKey = 'scene'; + +export const getSceneDependencey = ( + getDependency: IGraphApi['getDependency'] +) => getDependency(sceneDepdendencyKey); diff --git a/packages/scene/src/index.ts b/packages/scene/src/index.ts index 7caa3b59..d09b00b7 100644 --- a/packages/scene/src/index.ts +++ b/packages/scene/src/index.ts @@ -35,3 +35,4 @@ export * from './Nodes/Logic/VecElements'; export * from './Nodes/Queries/GetSceneProperty'; export * from './registerSceneProfile'; +export * from './buildScene'; diff --git a/packages/scene/src/loadScene.ts b/packages/scene/src/loadScene.ts new file mode 100644 index 00000000..c5b578d9 --- /dev/null +++ b/packages/scene/src/loadScene.ts @@ -0,0 +1,81 @@ +import { Object3D } from 'three'; +import { DRACOLoader, GLTF, GLTFLoader } from 'three-stdlib'; + +import { IScene } from './Abstractions/IScene'; +import { buildScene, ObjectMap } from './buildScene'; + +// Taken from react-three-fiber +// Collects nodes and materials from a THREE.Object3D +export function buildGraph(object: Object3D) { + const data: ObjectMap = { nodes: {}, materials: {} }; + if (object) { + object.traverse((obj: any) => { + if (obj.name) data.nodes[obj.name] = obj; + if (obj.material && !data.materials[obj.material.name]) + data.materials[obj.material.name] = obj.material; + }); + } + return data; +} + +type ThreeSceneReturn = { + scene: IScene; + gltf: GLTF & ObjectMap; +}; + +/** + * Loads a gltf, and corresponding IScene from a url + * @param url + * @param onProgress invoked on progress of loading the gltf + * @returns + */ +export const loadGltfAndBuildScene = ( + url: string, + onProgress?: (progress: number) => void +): Promise => { + const loader = new GLTFLoader(); + + // Optional: Provide a DRACOLoader instance to decode compressed mesh data + const dracoLoader = new DRACOLoader(); + dracoLoader.setDecoderPath( + 'https://www.gstatic.com/draco/versioned/decoders/1.4.3/' + ); + loader.setDRACOLoader(dracoLoader); + + // Load a glTF resource + + // eslint-disable-next-line promise/avoid-new + const result = new Promise((resolve, reject) => { + loader.load( + // resource URL + url, + // called when the resource is loaded + function (gltf) { + Object.assign(gltf, buildGraph(gltf.scene)); + const asObjectMap = gltf as GLTF & ObjectMap; + + const scene = buildScene({ + gltf: asObjectMap, + setOnClickListeners: undefined, + setActiveAnimations: undefined + }); + + resolve({ + scene, + gltf: asObjectMap + }); + }, + // called while loading is progressing + function (xhr) { + const progress = (xhr.loaded / xhr.total) * 100; + if (onProgress) onProgress(progress); + }, + // called when loading has errors + function (error) { + reject(error); + } + ); + }); + + return result; +}; diff --git a/packages/scene/src/readSceneGraphs.test.ts b/packages/scene/src/readSceneGraphs.test.ts index a7331de7..cba4f541 100644 --- a/packages/scene/src/readSceneGraphs.test.ts +++ b/packages/scene/src/readSceneGraphs.test.ts @@ -8,7 +8,7 @@ import * as vector2Json from '../../../../../graphs/scene/logic/Vector2.json'; import * as vector3Json from '../../../../../graphs/scene/logic/Vector3.json'; import * as vector4Json from '../../../../../graphs/scene/logic/Vector4.json'; import { Logger } from '../../Diagnostics/Logger'; -import { Graph } from '../../Graphs/Graph'; +import { GraphInstance } from '../../Graphs/Graph'; import { GraphJSON } from '../../Graphs/IO/GraphJSON'; import { readGraphFromJSON } from '../../Graphs/IO/readGraphFromJSON'; import { validateGraphAcyclic } from '../../Graphs/Validation/validateGraphAcyclic'; @@ -39,15 +39,15 @@ for (const key in exampleMap) { describe(`${key}`, () => { const exampleJson = exampleMap[key] as GraphJSON; - let parsedGraphJson: Graph | undefined; + let parsedGraphJson: GraphInstance | undefined; test('parse json to graph', () => { expect(() => { - parsedGraphJson = readGraphFromJSON(exampleJson, registry); + parsedGraphJson = readGraphFromJSON({ graphJson: exampleJson, registry }); }).not.toThrow(); // await fs.writeFile('./examples/test.json', JSON.stringify(writeGraphToJSON(graph), null, ' '), { encoding: 'utf-8' }); if (parsedGraphJson !== undefined) { - expect(validateGraphLinks(parsedGraphJson)).toHaveLength(0); - expect(validateGraphAcyclic(parsedGraphJson)).toHaveLength(0); + expect(validateGraphLinks(parsedGraphJson.nodes)).toHaveLength(0); + expect(validateGraphAcyclic(parsedGraphJson.nodes)).toHaveLength(0); } else { expect(parsedGraphJson).toBeDefined(); } diff --git a/packages/scene/src/registerSceneProfile.ts b/packages/scene/src/registerSceneProfile.ts index 7aad3b14..1e3c75e3 100644 --- a/packages/scene/src/registerSceneProfile.ts +++ b/packages/scene/src/registerSceneProfile.ts @@ -1,11 +1,14 @@ /* eslint-disable max-len */ import { + Dependencies, getNodeDescriptions, - IRegistry, - registerSerializersForValueType + getStringConversionsForValueType, + NodeDefinition, + ValueType } from '@behave-graph/core'; import { IScene } from './Abstractions/IScene'; +import { sceneDepdendencyKey } from './dependencies'; import { SetSceneProperty } from './Nodes/Actions/SetSceneProperty'; import { OnSceneNodeClick } from './Nodes/Events/OnSceneNodeClick'; import * as ColorNodes from './Nodes/Logic/ColorNodes'; @@ -26,63 +29,62 @@ import { Vec2Value } from './Values/Vec2Value'; import { Vec3Value } from './Values/Vec3Value'; import { Vec4Value } from './Values/Vec4Value'; -export function registerSceneDependency( - dependencies: IRegistry['dependencies'], - scene: IScene -) { - dependencies.register('scene', scene); -} +export const createSceneDependency = (scene: IScene): Dependencies => ({ + [sceneDepdendencyKey]: scene +}); -export function registerSceneProfile( - registry: Pick -) { - const { values, nodes } = registry; +export const getSceneValueTypes = (): ValueType[] => [ + Vec2Value, + Vec3Value, + Vec4Value, + ColorValue, + EulerValue, + QuatValue, + Mat3Value, + Mat4Value +]; - // pull in value type nodes - values.register(Vec2Value); - values.register(Vec3Value); - values.register(Vec4Value); - values.register(ColorValue); - values.register(EulerValue); - values.register(QuatValue); - values.register(Mat3Value); - values.register(Mat4Value); +export const getSceneNodeDefinitions = ( + values: Record +): NodeDefinition[] => { + const allValueTypeNames = Object.keys(values); + return [ + // pull in value type nodes - // pull in value type nodes - nodes.register(...getNodeDescriptions(Vec2Nodes)); - nodes.register(...getNodeDescriptions(Vec3Nodes)); - nodes.register(...getNodeDescriptions(Vec4Nodes)); - nodes.register(...getNodeDescriptions(ColorNodes)); - nodes.register(...getNodeDescriptions(EulerNodes)); - nodes.register(...getNodeDescriptions(QuatNodes)); - nodes.register(...getNodeDescriptions(Mat3Nodes)); - nodes.register(...getNodeDescriptions(Mat4Nodes)); - - // events - - nodes.register(OnSceneNodeClick); - - // actions - const allValueTypeNames = values.getAllNames(); - nodes.register(...SetSceneProperty(allValueTypeNames)); - nodes.register(...GetSceneProperty(allValueTypeNames)); - - const newValueTypeNames = [ - 'vec2', - 'vec3', - 'vec4', - 'quat', - 'euler', - 'color', - 'mat3', - 'mat4' + ...getNodeDescriptions(Vec2Nodes), + ...getNodeDescriptions(Vec3Nodes), + ...getNodeDescriptions(Vec4Nodes), + ...getNodeDescriptions(ColorNodes), + ...getNodeDescriptions(EulerNodes), + ...getNodeDescriptions(QuatNodes), + ...getNodeDescriptions(Mat3Nodes), + ...getNodeDescriptions(Mat4Nodes), + // events + OnSceneNodeClick, + // actions + ...SetSceneProperty(allValueTypeNames), + ...GetSceneProperty(allValueTypeNames) ]; +}; - // variables +const newValueTypeNames = [ + 'vec2', + 'vec3', + 'vec4', + 'quat', + 'euler', + 'color', + 'mat3', + 'mat4' +]; - newValueTypeNames.forEach((valueTypeName) => { - registerSerializersForValueType(registry, valueTypeName); - }); +export const makeSceneDependencies = ({ scene }: { scene: IScene }) => ({ + [sceneDepdendencyKey]: scene +}); - return registry; -} +export const getStringConversions = ( + values: Record +): NodeDefinition[] => + newValueTypeNames.flatMap((valueTypeName) => + getStringConversionsForValueType({ values, valueTypeName }) + ); diff --git a/packages/scene/tsconfig.json b/packages/scene/tsconfig.json new file mode 100644 index 00000000..665c0e86 --- /dev/null +++ b/packages/scene/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "declaration": false, + "emitDeclarationOnly": false, + "baseUrl": ".", + "paths": { + "@behave-graph/core": ["../core/src"], + "@/*": ["src/*"], + } + }, + "include": ["./src"] +}