diff --git a/packages/core/src/Values/ValueTypeRegistry.ts b/packages/core/src/Values/ValueTypeRegistry.ts index 5e9072b3..ef2d2818 100644 --- a/packages/core/src/Values/ValueTypeRegistry.ts +++ b/packages/core/src/Values/ValueTypeRegistry.ts @@ -22,4 +22,8 @@ export class ValueTypeRegistry { getAllNames(): string[] { return Object.keys(this.valueTypeNameToValueType); } + + getAll(): ValueType[] { + return Object.values(this.valueTypeNameToValueType); + } } diff --git a/website/.gitignore b/website/.gitignore new file mode 100644 index 00000000..86cf7ea2 --- /dev/null +++ b/website/.gitignore @@ -0,0 +1,27 @@ +# Dynamically generated content +/docs/profiles/Core/Nodes +/docs/profiles/Core/Values +/docs/profiles/Scene/Nodes +/docs/profiles/Scene/Values +/docs/examples + +# Dependencies +/node_modules + +# Production +/build + +# Generated files +.docusaurus +.cache-loader + +# Misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/website/README.md b/website/README.md new file mode 100644 index 00000000..aaba2fa1 --- /dev/null +++ b/website/README.md @@ -0,0 +1,41 @@ +# Website + +This website is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator. + +### Installation + +``` +$ yarn +``` + +### Local Development + +``` +$ yarn start +``` + +This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. + +### Build + +``` +$ yarn build +``` + +This command generates static content into the `build` directory and can be served using any static contents hosting service. + +### Deployment + +Using SSH: + +``` +$ USE_SSH=true yarn deploy +``` + +Not using SSH: + +``` +$ GIT_USER= yarn deploy +``` + +If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. diff --git a/website/babel.config.js b/website/babel.config.js new file mode 100644 index 00000000..e00595da --- /dev/null +++ b/website/babel.config.js @@ -0,0 +1,3 @@ +module.exports = { + presets: [require.resolve('@docusaurus/core/lib/babel/preset')], +}; diff --git a/website/blog/2021-08-26-welcome/behave-graph-flow.png b/website/blog/2021-08-26-welcome/behave-graph-flow.png new file mode 100644 index 00000000..539a7bf0 Binary files /dev/null and b/website/blog/2021-08-26-welcome/behave-graph-flow.png differ diff --git a/website/blog/2021-08-26-welcome/index.md b/website/blog/2021-08-26-welcome/index.md new file mode 100644 index 00000000..72809b28 --- /dev/null +++ b/website/blog/2021-08-26-welcome/index.md @@ -0,0 +1,25 @@ +--- +slug: welcome +title: Welcome +authors: [bhouston, aitorllj93] +tags: [behave-graph, hello] +--- + +[Docusaurus blogging features](https://docusaurus.io/docs/blog) are powered by the [blog plugin](https://docusaurus.io/docs/api/plugins/@docusaurus/plugin-content-blog). + +Simply add Markdown files (or folders) to the `blog` directory. + +Regular blog authors can be added to `authors.yml`. + +The blog post date can be extracted from filenames, such as: + +- `2019-05-30-welcome.md` +- `2019-05-30-welcome/index.md` + +A blog post folder can be convenient to co-locate blog post images: + +![Behave-Graph Flow](./behave-graph-flow.png) + +The blog supports tags as well! + +**And if you don't want a blog**: just delete this directory, and use `blog: false` in your Docusaurus config. diff --git a/website/blog/authors.yml b/website/blog/authors.yml new file mode 100644 index 00000000..213a06b9 --- /dev/null +++ b/website/blog/authors.yml @@ -0,0 +1,11 @@ +bhouston: + name: Ben Houston + title: Behave-Graph maintainer + url: https://github.com/bhouston + image_url: https://github.com/bhouston.png + +aitorllj93: + name: Aitor Llamas + title: Behave-Graph maintainer + url: https://github.com/aitorllj93 + image_url: https://github.com/aitorllj93.png \ No newline at end of file diff --git a/website/docs/core-concepts/_category_.json b/website/docs/core-concepts/_category_.json new file mode 100644 index 00000000..5aaf4b07 --- /dev/null +++ b/website/docs/core-concepts/_category_.json @@ -0,0 +1,7 @@ +{ + "label": "Core Concepts", + "position": 4, + "link": { + "type": "generated-index" + } +} diff --git a/website/docs/core-concepts/abstractions.md b/website/docs/core-concepts/abstractions.md new file mode 100644 index 00000000..270d5a77 --- /dev/null +++ b/website/docs/core-concepts/abstractions.md @@ -0,0 +1,51 @@ +--- +sidebar_position: 5 +--- + +# Abstractions + +Behave-graph is designed as a light weight library that can be plugged into other engines, such as Three.js or Babylon.js. In order to simplify pluggin into other engines, it defines the functionality required for interfacing with these engines as "abstractions", which can then be implemented by the engines. + +This design is inspired by [Hardware Abstraction layers](https://en.wikipedia.org/wiki/Hardware_abstraction) present in operating systems. HALs, as they are called, are interfaces which are then implemented by drivers that enable that functionality. HALs allow for operating systems to easily be ported to different systems because the machine specific implementations for the operating systems are cordened off from the main operating system behind these HALs. Behave-graph takes this same approach. + +Example abstractions in behave-graph: + +* **ILogger**. The logging interface allows for behave-graph to report verbose, info, warning and error text messages. The command line graph-exec tool uses the driver DefaultLogger to output these messages to the console. +* **ILifecycleEventEmitter**. This interface is responsible for emitting when the behave-graph should start, tick and stop. +* **IScene**. This interface is responsible for doing basic scene manipulations. In the three example, this interface is implemented by the ThreeScene class that maps these operations to a Three.js-based scene graph. + +For example here is the ILogger abstraction, you can see it is just a standard Typescript interface: + +```ts +export interface ILogger { + verbose(text: string): void; + info(text: string): void; + warn(text: string): void; + error(text: string): void; +} +``` + +Here is the DefaultLogger implementation of the abstraction, you can see it is just a standard Typescript class that implements the above interface: + +```ts +import { Logger } from '../../../../Diagnostics/Logger.js'; +import { ILogger } from '../ILogger.js'; + +export class DefaultLogger implements ILogger { + verbose(text: string): void { + Logger.verbose(text); + } + + info(text: string): void { + Logger.info(text); + } + + warn(text: string): void { + Logger.warn(text); + } + + error(text: string): void { + Logger.error(text); + } +} +``` \ No newline at end of file diff --git a/website/docs/core-concepts/nodes.md b/website/docs/core-concepts/nodes.md new file mode 100644 index 00000000..6ecdec61 --- /dev/null +++ b/website/docs/core-concepts/nodes.md @@ -0,0 +1,311 @@ +--- +sidebar_position: 4 +--- + +# Nodes + +Nodes are the building blocks of the system. They are used to perform operations on the data in the system. + +## Types of Nodes + +The types of nodes in behave-graph mimic those found in Unreal Engine Blueprints and Unity Visual Scripting. + +The different node types have different execution models and are also declared differently. The main two categories of nodes are "flow" nodes, that are nodes who are activate participants in the control flow of the behavior graph, and function nodes, who are evaluated on demand when their results are required by "flow" nodes. + +### Function Nodes + +Function nodes are the simplest type of node. They do not support "flow" input or output sockets, rather they only have non-flow sockets. These nodes are evaluated on demand when an output of theirs are required. Thes are most often used for mathematical operators, or for queries of context or state. + +If there is a network of function nodes, execution proceeds by first evaluating the outer most leaf nodes and then proceeding downwards through the graph until you have evaluated the output sockets of the function nodes you require. + +### Basic Flow Nodes + +There are a couple types of flow nodes, but the main flow node will take a flow input and also have one or more flow outputs. When it's flow input is triggered, it will evaluate and then synchrously trigger one of its flow outputs, continuing execution of the graph. Most action nodes are basic flow nodes, such as if you want to log a message, or modify a scene graph property. Basic flow nodes can also wait for the downstream execution triggered by its flow output to complete and then do another operation, which is usually to trigger another downstream output flow. Some control flow nodes are basic flow nodes, such as a branch/condition node as well as both the for-loop and sequence nodes (the later two use the basic flow node's ability to wait for the completion of the downstream node graph execution.) + +### Event Nodes + +Event nodes are nodes that cause execution in the graph to start. They do not have any input "flow" sockets, but they are allow to have non-flow input sockets that may set the parameters for the event node's operation. + +Event nodes are all initialized at the start of graph execution. It is expected that after this initialization, the event nodes will trigger their "flow" output sockets when there is an event, and this will happen at any time. These events nodes will not be revisited by the graph execution engine, rather they are assumed to just be active behind the scenes. + +### Async Nodes + +Async nodes are nodes whom when are triggered will remain activate. They are not synchrous. The simplest example is the delay node. When this node is triggered, it is not required to functionly trigger an output "flow" socket, but rather it can return the trigger function without calling an output. And then later, when it so wants to trigger an output "flow" socket, it can. This can implement a delay node, where you set a timer when the input "flow" socket is triggered and when that timer callback occurs, you can trigger the output "flow" socket. + +## Categories + +The nodes are also divided into the following categories: + +### Event + +Event nodes are used to trigger events in the system. + +Some examples of event nodes are: + +- [lifecycle/onStart](../profiles/Core/Nodes/lifecycle/on-start) +- [lifecycle/onEnd](../profiles/Core/Nodes/lifecycle/on-end) +- [lifecycle/onTick](../profiles/Core/Nodes/lifecycle/on-tick) +- [customEvent/onTriggered](../profiles/Core/Nodes/custom-event/on-triggered) + +### Logic + +Logic nodes are used to perform logic operations on the data in the system. + +Some examples of logic nodes are: + +- [logic/concat/string](../profiles/Core/Nodes/logic/concat/string) +- [logic/includes/string](../profiles/Core/Nodes/logic/includes/string) +- [logic/length/string](../profiles/Core/Nodes/logic/length/string) +- [math/add/float](../profiles/Core/Nodes/math/add/float) +- [math/add/integer](../profiles/Core/Nodes/math/add/integer) +- [math/round/float](../profiles/Core/Nodes/math/round/float) +- [math/toInteger/boolean](../profiles/Core/Nodes/math/to-integer/boolean) +- [math/toString/integer](../profiles/Core/Nodes/math/to-string/integer) + +### Variable + +Variable nodes are used to store data in the system. + +Some examples of variable nodes are: + +- [variable/set](../profiles/Core/Nodes/variable/set) +- [variable/get](../profiles/Core/Nodes/variable/get) + +### Query + +Query nodes are used to query data from the system. + +### Action + +Action nodes are used to perform actions in the system. + +Some examples of action nodes are: + +- [customEvent/trigger](../profiles/Core/Nodes/custom-event/trigger) +- [debug/log](../profiles/Core/Nodes/debug/log) + +### Flow + +Flow nodes are used to control the flow of the system. + +Some examples of flow nodes are: + +- [flow/branch](../profiles/Core/Nodes/flow/branch) +- [flow/gate](../profiles/Core/Nodes/flow/gate) +- [flow/multiGate](../profiles/Core/Nodes/flow/multi-gate) +- [flow/sequence](../profiles/Core/Nodes/flow/sequence) +- [flow/waitAll](../profiles/Core/Nodes/flow/wait-all) + +### Time + +Time nodes are used to perform time operations in the system. + +Some examples of time nodes are: + +- [time/delay](../profiles/Core/Nodes/time/delay) +- [time/now](../profiles/Core/Nodes/time/now) + +## Creating a custom node + +To create a custom node, you need to create a node description, there are several ways to do this, depending on the kind of node you want to create. + +### Constants + +If you want to create a node that defines constants, you can use the `NodeDescription` constructor with the `In1Out1FuncNode` helper. + +```ts +import { + In1Out1FuncNode, + NodeDescription, +} from '@behave-graph/core'; + +const Constant = new NodeDescription( + 'logic/object', + 'Logic', + 'Object', + (description, graph) => + new In1Out1FuncNode( + description, + graph, + ['object'], + 'object', + (a: object) => a + ) +); +``` + +### Binary functions + +The same way, if you want to create a node that defines binary functions, you can use the `NodeDescription` constructor with the `In2Out1FuncNode` helper. (There are also `In3Out1FuncNode` and `In4Out1FuncNode` helpers) + +```ts +import { + In2Out1FuncNode, + NodeDescription, +} from '@behave-graph/core'; +import { path } from 'rambdax'; + +const Path = new NodeDescription( + 'logic/path/object', + 'Logic', + 'Path', + (description, graph) => + new In2Out1FuncNode( + description, + graph, + ['string', 'object'], + 'object', + (a: string, b: object) => { + const key = a.split('.'); + return path(key, b); + }, + ['path', 'object'] + ) +); +``` + +### Flow nodes + +If you want to create a flow node, you can extend the `FlowNode` class. In the NodeDescription, you should have a `flow` socket as input and output. Then you should override the `triggered` method, which is called when the node is triggered. + +```ts +import { + Graph, + FlowNode, + NodeDescription, + Socket, + ILogger, +} from '@behave-graph/core'; + +class LogObject extends FlowNode { + public static Description = (logger: ILogger) => + new NodeDescription( + 'debug/log/object', + 'Action', + 'Log', + (description, graph) => new LogObject(description, graph, logger) + ); + + constructor( + description: NodeDescription, + graph: Graph, + private readonly logger: ILogger + ) { + super( + description, + graph, + [ + new Socket('flow', 'flow'), + new Socket('string', 'text'), + new Socket('string', 'severity', 'info'), + new Socket('object', 'payload'), + ], + [new Socket('flow', 'flow')] + ); + } + + override triggered(fiber: any) { + const text = this.readInput('text'); + const payload = this.readInput('payload'); + + const message = `${text} ${JSON.stringify(payload)}`; + + switch (this.readInput('severity')) { + case 'verbose': + this.logger.verbose(message); + break; + case 'info': + this.logger.info(message); + break; + case 'warning': + this.logger.warn(message); + break; + case 'error': + this.logger.error(message); + break; + } + + fiber.commit(this, 'flow'); + } +} +``` + +### Async Nodes + +Async Nodes are similar to Flow Nodes, but in this case you need to extend the AsyncNode class. You should also control the internal state of the node, and call the `engine.commitToNewFiber(this, 'flow')` and `finished()` methods when the node is done. + +```ts +import { + AsyncNode, + Engine, + Graph, + NodeDescription, + Socket, +} from '@behave-graph/core'; +import { JSONTemplateEngine } from 'json-template-engine'; + +export type ITemplateEngineFactory = () => JSONTemplateEngine; + +export class Template extends AsyncNode { + public static Description = (templateEngineFactory: ITemplateEngineFactory) => + new NodeDescription( + 'logic/template/object', + 'Logic', + 'Template', + (description, graph) => new Template(description, graph, engine) + ); + + constructor( + description: NodeDescription, + graph: Graph, + private readonly templateEngineFactory: ITemplateEngineFactory + ) { + super( + description, + graph, + [ + new Socket('flow', 'flow'), + new Socket('object', 'template', ''), + new Socket('object', 'data', ''), + ], + [new Socket('flow', 'flow'), new Socket('object', 'result', '')] + ); + } + + private templateIsRendering = false; + + override triggered( + engine: Engine, + triggeringSocketName: string, + finished: () => void + ) { + // if there is a valid rendering running, leave it. + if (this.templateIsRendering) { + return; + } + + const parser = this.templateEngineFactory(); + this.templateIsRendering = true; + + parser + .compile(this.readInput('template'), this.readInput('data')) + .then((result) => { + // check if cancelled + if (!this.templateIsRendering) return; + this.templateIsRendering = false; + + const output = this.outputSockets.find((s) => s.name === 'result'); + if (output) { + output.value = result; + } + + engine.commitToNewFiber(this, 'flow'); + finished(); + }); + } + + override dispose() { + this.templateIsRendering = false; + } +} +``` \ No newline at end of file diff --git a/website/docs/core-concepts/profiles.md b/website/docs/core-concepts/profiles.md new file mode 100644 index 00000000..e87d0d50 --- /dev/null +++ b/website/docs/core-concepts/profiles.md @@ -0,0 +1,53 @@ +--- +sidebar_position: 2 +--- + +# Profiles + +A profile is a function that updates the registry with the nodes and value types that are available in the system. The profile usually also includes the [Abstractions](./abstractions) that are required for the nodes to work as parameters. + +## Using official Profiles + +The official profiles are available in the `@behave-graph/core` package. + +```ts +import { + DefaultLogger, + DummyScene, + ManualLifecycleEventEmitter, + Registry, + registerCoreProfile, + registerSceneProfile, +} from '@behave-graph/core'; + +const registry = new Registry(); +const logger = new DefaultLogger(); +const lifecycleEventEmitter = new ManualLifecycleEventEmitter(); +const scene = new DummyScene(); + +registerCoreProfile(registry, logger, lifecycleEventEmitter); +registerSceneProfile(registry, scene); +``` + +## Creating a Custom Profile + +```ts +import { Registry, ILogger } from '@behave-graph/core'; + +const registerMyProfile = ( + registry: Registry, + logger: ILogger, +) => { + const { nodes, values } = registry; + + // Register nodes + nodes.register(MyNodeDescription); + nodes.register(MyNodeWithDependenciesDescription(logger)); + + // Register value types + values.register(MyValueTypeDescription); + + return registry; +}; +``` + diff --git a/website/docs/core-concepts/registry.md b/website/docs/core-concepts/registry.md new file mode 100644 index 00000000..11464bd6 --- /dev/null +++ b/website/docs/core-concepts/registry.md @@ -0,0 +1,24 @@ +--- +sidebar_position: 1 +--- + +# Registry + +The registry is a collection of all the nodes and value types that are available in the system. + +In order to add a node or value type to the registry, you need to use the register function, usually inside your [profile's register function](./profiles.md). + + +```ts +import { Registry } from '@behave-graph/core'; + +const registry = new Registry(); + +const { nodes, values } = registry; + +// Register a value type +values.register(MyValueTypeDescription); + +// Register a node +nodes.register(MyNodeDescription); +``` \ No newline at end of file diff --git a/website/docs/core-concepts/turing-completeness.md b/website/docs/core-concepts/turing-completeness.md new file mode 100644 index 00000000..6e95a7c0 --- /dev/null +++ b/website/docs/core-concepts/turing-completeness.md @@ -0,0 +1,21 @@ +--- +sidebar_position: 6 +--- + +# Turing Completeness + +The execution model and node choices based for the Core profile mean that behave-graph is turing complete. This means that this enging can execute any computation and it is also hard to predict if it will run forever (e.g. halt or not.) + +While this may sound scary, it is not a major hindrance and can be safely mitigated so that any tool using behave-graph does not become succeptible to denial of services by badly behaving behave-graphs, whether intention or not. + +Limiting Execution Resources + +The main way to mitigate the risk of non-halting behave-graphs is to limit the amount of time given to them for execution, both in terms of individual time slice as well as overall execution time. Limiting time to behave-graph for a single time slice is done by passing in either node limits or time limits into the main execution functions. + +For example, the included command line tool exec-graph limits runtime to 5 seconds after the end lifecycle event is fired. It does so like this: + +```typescript +const limitInSeconds = 5; +const limitInSteps = 1000; +await engine.executeAllAsync(limitInSeconds, limitInSteps); +``` diff --git a/website/docs/core-concepts/values.md b/website/docs/core-concepts/values.md new file mode 100644 index 00000000..a73f4fa0 --- /dev/null +++ b/website/docs/core-concepts/values.md @@ -0,0 +1,82 @@ +--- +sidebar_position: 3 +--- + +# Values + +Behave-graph supports a pluggable value system where you can easily add new values to the system. Values are what are passed between nodes via sockets. + +Values are registered into the central registry as instances of the ValueType class. The value type class controls creation, serialization, deserialization. + +```ts +export class ValueType { + constructor( + public readonly name: string, + public readonly creator: () => TValue, + public readonly deserialize: (value: TJson) => TValue, + public readonly serialize: (value: TValue) => TJson + ) {} +} +``` + +This ValueType system can support basic types, like string, boolean, integer or float types. For example here is the implementation of the Boolean type: + +```ts +new ValueType( + 'boolean', + () => false, + (value: string | boolean) => + typeof value === 'string' ? value.toLowerCase() === 'true' : value, + (value: boolean) => value +); +``` + +It can also be used to register more complex types that have sub-elements to them, such as Vec2, Vec3, Quaternion, Euler and Color. For example, here is the implementation of the Vec3 type: + +```ts +new ValueType( + 'vec3', + () => new Vec3(), + (value: string | Vec3JSON) => + typeof value === 'string' + ? vec3Parse(value) + : new Vec3(value.x, value.y, value.z), + (value) => ({ x: value.x, y: value.y, z: value.z } as Vec3JSON) +); +``` + +### Future Improvements + +Currently the sub-elements of values types are not registered and thus during execution from the point of view of behave-graph, Vec3 is not composed of 3 separate elements of type float. In the future, we may allow for more detailed registration of type internals, but for now this current method works sufficiently well. + +## Core Value Types + +The Core profile contains the following value types: + +- [Boolean](../profiles/Core/Values/boolean) +- [Float](../profiles/Core/Values/float) +- [Integer](../profiles/Core/Values/integer) +- [String](../profiles/Core/Values/string) + + +## Creating a custom value type + +To create a custom value type, you need to create a value type description with serializer and deserializer functions. + +```ts +import { ValueType } from '@behave-graph/core'; + +export const ObjectValue = new ValueType( + 'object', + () => ({}), + (value: string | object) => + typeof value === 'string' ? JSON.parse(value) : value, + (value: object) => JSON.stringify(value) +); +```` + +And register it in the registry. + +```ts +values.register(ObjectValue); +``` diff --git a/website/docs/defining-a-graph.md b/website/docs/defining-a-graph.md new file mode 100644 index 00000000..afeb8c57 --- /dev/null +++ b/website/docs/defining-a-graph.md @@ -0,0 +1,54 @@ +--- +sidebar_position: 2 +--- + +# Defining a Graph + +A graph is a collection of nodes that define the flow of your application. The graph is defined in JSON format and can be created using the [Graph Editor](./graph-editor) or by writing it manually. + +Here's an example of how a Graph should look like: + +```json +{ + "nodes": [ + { + "type": "lifecycle/onStart", + "id": "0", + "flows": { + "flow": { + "nodeId": "1", + "socket": "flow" + } + } + }, + { + "type": "debug/log", + "id": "1", + "parameters": { + "text": { + "value": "Hello World!" + } + } + } + ] +} +``` + +Each node has a `type` and an `id`. The `type` is the name of the node and the `id` is a unique identifier for the node. + +The nodes are connected using `flows`. Each node has a `flows` property that is an object with the name of the flow as the key and the value is an object with the `nodeId` and the `socket` of the node that the flow is connected to. + +The nodes can also accept an arbitrary number of parameters. Each parameter can have a `value` or a `link` to another node's output. + +The syntax to define a link is: + +```json +"parameters": { + "text": { + "link": { + "nodeId": "1", + "socket": "result" + } + } +} +``` \ No newline at end of file diff --git a/website/docs/getting-started.md b/website/docs/getting-started.md new file mode 100644 index 00000000..87e30556 --- /dev/null +++ b/website/docs/getting-started.md @@ -0,0 +1,58 @@ +--- +sidebar_position: 1 +--- + +# Getting Started + +## About this project + +Behave-Graph is a standalone library that implements the concept of "behavior graphs" as a portable TypeScript library with no required external run-time dependencies. Behavior graphs are expressive, deterministic, and extensible state machines that can encode arbitrarily complex behavior. + +Behavior graphs are used extensively in game development as a visual scripting language. For example, look at Unreal Engine Blueprints or Unity's Visual Scripting or NVIDIA Omniverse's OmniGraph behavior graphs. + +This library is intended to follow industry best practices in terms of behavior graphs. It is also designed to be compatible with these existing implementations in terms of capabilities. Although, like all node-based systems, behavior graphs are always limited by their node implementations. + +Another neat fact about behavior graphs is that they offer a sand boxed execution model. Because one can only execute what is defined by nodes exposed by the host system, you can restrict what can be executed by these graphs. This type of sand-boxing is not possible when you just load and execute arbitrary scripts. + +### Features + +* **Customizable** While this library contains a lot of nodes, you do not have to expose all of them. For example, just because this supports for-loops and state, does not mean you have to register that node type as being available. +* **Type Safe** This library is implemented in TypeScript and fully makes use of its type safety features. +* **Small** This is a very small library with no external dependencies. +* **Simple** This library is implemented in a forward fashion without unnecessary complexity. +* **High Performance** Currently in performance testing, the library achieves over 2M node executions per second. + +### Node Types + +* **Events** You can implement arbitrary events that start execution: Start, Tick +* **Actions** You can implement actions that trigger animations, scene scene variations, or update internal state: Log +* **Logic** You can do arithmetic, trigonometry as well as vector operations and string manipulation: Add, Subtract, Multiply, Divide, Pow, Exp, Log, Log2, Log10, Min, Max, Round, Ceil, Floor, Sign, Abs, Trunc, Sqrt, Negate, And, Or, Not, ==, >, >=, <, <=, isNan, isInfinity, concat, includes. +* **Queries** You can query the state from the system. +* **Flow Control** Control execution flow using familiar structures: Branch, Delay, Debounce, Throttle, FlipFlop, Sequence, Gate, MultiGate, DoOnce, DoN, ForLoop +* **Variables** You can create, set and get variable values. +* **Custom Events** You can create, listen to and trigger custom events. + +### Designed for Integration into Other Systems + +This library is designed to be extended with context dependent nodes, specifically Actions, Events and Queries that match the capabilities and requirements of your system. For example, if you integrate into a 3D engine, you can query for player state or 3D positions of your scene graph, set scene graph properties and also react to overlaps, and player movements. Or if you want to integrate into an AR system, you can react to face-detected, tracking-loss. + +## Installation + +Get started by **creating a new TypeScript/JavaScript project**. + +### What you'll need + +- [Node.js](https://nodejs.org/en/download/) version 16.14 or above: + - When installing Node.js, you are recommended to check all checkboxes related to dependencies. + +## Install Behave-Graph + +In order to use behave-graph in your project you need to install it from npm: + +```bash +npm install @behave-graph/core +``` + +You can type this command into Command Prompt, Powershell, Terminal, or any other integrated terminal of your code editor. + +The command also installs all necessary dependencies you need to run Behave-Graph. \ No newline at end of file diff --git a/website/docs/graph-editor.md b/website/docs/graph-editor.md new file mode 100644 index 00000000..48729049 --- /dev/null +++ b/website/docs/graph-editor.md @@ -0,0 +1,7 @@ +--- +sidebar_position: 7 +--- + +# Visual Editor + +There's a visual editor for the graph started by [@beeglebug](https://github.com/beeglebug/behave-flow). It's a WIP, but you can take a look at it [here](https://behave-flow.netlify.app/). \ No newline at end of file diff --git a/website/docs/profiles/Core/_category_.json b/website/docs/profiles/Core/_category_.json new file mode 100644 index 00000000..2b8e19c4 --- /dev/null +++ b/website/docs/profiles/Core/_category_.json @@ -0,0 +1,7 @@ +{ + "label": "Core", + "position": 1, + "link": { + "type": "generated-index" + } +} diff --git a/website/docs/profiles/Scene/_category_.json b/website/docs/profiles/Scene/_category_.json new file mode 100644 index 00000000..7c77b014 --- /dev/null +++ b/website/docs/profiles/Scene/_category_.json @@ -0,0 +1,7 @@ +{ + "label": "Scene", + "position": 2, + "link": { + "type": "generated-index" + } +} diff --git a/website/docs/profiles/_category_.json b/website/docs/profiles/_category_.json new file mode 100644 index 00000000..369f6b55 --- /dev/null +++ b/website/docs/profiles/_category_.json @@ -0,0 +1,7 @@ +{ + "label": "Profiles", + "position": 5, + "link": { + "type": "generated-index" + } +} diff --git a/website/docs/running-the-engine.md b/website/docs/running-the-engine.md new file mode 100644 index 00000000..e545891b --- /dev/null +++ b/website/docs/running-the-engine.md @@ -0,0 +1,60 @@ +--- +sidebar_position: 3 +--- + +# Running The Engine + +To be able to run Behave Graph, you need to have a graph in JSON format. You can create a graph using the [Graph Editor](./graph-editor) or by writing it manually. + +You will also need to configure the registry with the profiles you want to use. You can read more about profiles [here](./core-concepts/profiles). + +Here's an example code of how should look like: + +```ts +import { + DefaultLogger, + Engine, + readGraphFromJSON, + registerCoreProfile, + Registry, + ManualLifecycleEventEmitter, +} from '@behave-graph/core'; + +import myGraphJson from './myGraph.json'; + +/** Setup the Registry **/ +const registry = new Registry(); +const logger = new DefaultLogger(); +const manualLifecycleEventEmitter = new ManualLifecycleEventEmitter(); + +registerCoreProfile(registry, logger, manualLifecycleEventEmitter); + +/** Prepare the Graph **/ +const graph = readGraphFromJSON(myGraphJson, registry); + +/** Run the Graph **/ +const engine = new Engine(graph); + +/** Trigger events **/ + +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); +} + +``` \ No newline at end of file diff --git a/website/docusaurus.config.js b/website/docusaurus.config.js new file mode 100644 index 00000000..9426a98e --- /dev/null +++ b/website/docusaurus.config.js @@ -0,0 +1,128 @@ +// @ts-check +// Note: type annotations allow type checking and IDEs autocompletion + +const lightCodeTheme = require('prism-react-renderer/themes/github'); +const darkCodeTheme = require('prism-react-renderer/themes/dracula'); + +/** @type {import('@docusaurus/types').Config} */ +const config = { + title: 'Behave-Graph', + tagline: + '"behavior graphs" as a portable TypeScript library with no required external run-time dependencies', + url: 'https://your-docusaurus-test-site.com', + baseUrl: '/', + onBrokenLinks: 'throw', + onBrokenMarkdownLinks: 'warn', + favicon: 'img/favicon.ico', + + // GitHub pages deployment config. + // If you aren't using GitHub pages, you don't need these. + organizationName: 'bhouston', // Usually your GitHub org/user name. + projectName: 'behave-graph', // Usually your repo name. + + // Even if you don't use internalization, you can use this field to set useful + // metadata like html lang. For example, if your site is Chinese, you may want + // to replace "en" with "zh-Hans". + i18n: { + defaultLocale: 'en', + locales: ['en'] + }, + + presets: [ + [ + 'classic', + /** @type {import('@docusaurus/preset-classic').Options} */ + ({ + docs: { + sidebarPath: require.resolve('./sidebars.js'), + // Please change this to your repo. + // Remove this to remove the "edit this page" links. + editUrl: 'https://github.com/bhouston/behave-graph/tree/main/website/' + }, + blog: { + showReadingTime: true, + // Please change this to your repo. + // Remove this to remove the "edit this page" links. + editUrl: 'https://github.com/bhouston/behave-graph/tree/main/website/' + }, + theme: { + customCss: require.resolve('./src/css/custom.css') + } + }) + ] + ], + + themeConfig: + /** @type {import('@docusaurus/preset-classic').ThemeConfig} */ + ({ + navbar: { + title: 'Behave-Graph', + // TODO: Include a logo + logo: { + alt: 'Behave-Graph Logo', + src: 'img/logo.png' + }, + items: [ + { + type: 'doc', + docId: 'getting-started', + position: 'left', + label: 'Docs' + }, + { to: '/blog', label: 'Blog', position: 'left' }, + { + href: 'https://github.com/bhouston/behave-graph', + label: 'GitHub', + position: 'right' + } + ] + }, + footer: { + style: 'dark', + links: [ + { + title: 'Docs', + items: [ + { + label: 'Getting Started', + to: '/docs/getting-started' + } + ] + }, + { + title: 'Community', + items: [ + { + label: 'Stack Overflow', + href: 'https://stackoverflow.com/questions/tagged/behave-graph' + }, + { + label: 'Discord', + href: 'https://discord.gg/mrags8WyuH' + } + ] + }, + { + title: 'More', + items: [ + { + label: 'Blog', + to: '/blog' + }, + { + label: 'GitHub', + href: 'https://github.com/bhouston/behave-graph' + } + ] + } + ], + copyright: `Copyright © ${new Date().getFullYear()} Behave-Graph, Inc. Built with Docusaurus.` + }, + prism: { + theme: lightCodeTheme, + darkTheme: darkCodeTheme + } + }) +}; + +module.exports = config; diff --git a/website/package.json b/website/package.json new file mode 100644 index 00000000..be4ea334 --- /dev/null +++ b/website/package.json @@ -0,0 +1,49 @@ +{ + "name": "website", + "version": "0.0.0", + "private": true, + "scripts": { + "docusaurus": "docusaurus", + "start": "docusaurus start", + "build": "docusaurus build", + "swizzle": "docusaurus swizzle", + "deploy": "docusaurus deploy", + "clear": "docusaurus clear", + "serve": "docusaurus serve", + "write-translations": "docusaurus write-translations", + "write-heading-ids": "docusaurus write-heading-ids", + "typecheck": "tsc", + "generate-dynamic-pages": "ts-node scripts/generate-dynamic-pages/index.ts" + }, + "dependencies": { + "@docusaurus/core": "2.2.0", + "@docusaurus/preset-classic": "2.2.0", + "@mdx-js/react": "^1.6.22", + "clsx": "^1.2.1", + "prism-react-renderer": "^1.3.5", + "react": "^17.0.2", + "react-dom": "^17.0.2" + }, + "devDependencies": { + "@docusaurus/module-type-aliases": "2.2.0", + "@iconify/react": "^4.0.1", + "@tsconfig/docusaurus": "^1.0.5", + "case": "^1.6.3", + "typescript": "^4.7.4" + }, + "browserslist": { + "production": [ + ">0.5%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "engines": { + "node": ">=16.14" + } +} diff --git a/website/scripts/generate-dynamic-pages/generate-pages-from-examples.ts b/website/scripts/generate-dynamic-pages/generate-pages-from-examples.ts new file mode 100644 index 00000000..da9a6614 --- /dev/null +++ b/website/scripts/generate-dynamic-pages/generate-pages-from-examples.ts @@ -0,0 +1,77 @@ +import { + existsSync, + mkdirSync, + readdirSync, + readFileSync, + writeFileSync +} from 'node:fs'; +import { dirname, join } from 'node:path'; + +import { title } from 'case'; + +const exampleTemplate = (name: string, graph: string) => ` +# ${title(name)} + +\`\`\`json +${graph} +\`\`\` +`; + +const getExamplesRecursive = (dir: string): string[] => { + const files = readdirSync(dir); + const examples = files + .filter((file) => file.endsWith('.json')) + .map((file) => join(dir, file)); + + const dirs = files.filter((file) => !file.endsWith('.json')); + + dirs.forEach((sub) => { + examples.push(...getExamplesRecursive(join(dir, sub))); + }); + + return examples; +}; + +export default (examplesDir: string, outDir: string) => { + if (!existsSync(outDir)) { + console.log('Creating directory', outDir); + mkdirSync(outDir, { recursive: true }); + } + + console.log('Writing category file', join(outDir, '_category_.json')); + writeFileSync( + join(outDir, '_category_.json'), + `{ + "label": "Examples", + "position": 6, + "link": { + "type": "generated-index" + } +} +` + ); + const examples = getExamplesRecursive(examplesDir); + + examples.forEach((example) => { + const fileContent = readFileSync(example, 'utf-8'); + const filePath = join( + outDir, + example.replace(examplesDir, '').replace('.json', '.mdx') + ); + const dirName = dirname(filePath); + + if (!existsSync(dirName)) { + console.log('Creating directory', dirName); + mkdirSync(dirName, { recursive: true }); + } + + console.log('Writing file', filePath); + writeFileSync( + filePath, + exampleTemplate( + filePath.split('/').pop()!.replace('.mdx', ''), + fileContent + ) + ); + }); +}; diff --git a/website/scripts/generate-dynamic-pages/generate-pages-from-registry.ts b/website/scripts/generate-dynamic-pages/generate-pages-from-registry.ts new file mode 100644 index 00000000..647a2132 --- /dev/null +++ b/website/scripts/generate-dynamic-pages/generate-pages-from-registry.ts @@ -0,0 +1,157 @@ +import { existsSync, mkdirSync, writeFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; + +// We need to transform directories to kebab case because otherwise Docusaurus won't generate the toString one +import { kebab, pascal } from 'case'; + +import { NodeSpecJSON } from '../../../packages/core/src/Graphs/IO/NodeSpecJSON'; +import { writeNodeSpecsToJSON } from '../../../packages/core/src/Graphs/IO/writeNodeSpecsToJSON'; +import { NodeDescription } from '../../../packages/core/src/Nodes/Registry/NodeDescription'; +import { Registry } from '../../../packages/core/src/Registry'; +import { ValueType } from '../../../packages/core/src/Values/ValueType'; +import nodeTemplate from './templates/node'; + +const generateValuePages = (values: ValueType[], baseDir: string) => { + const valuesDir = join(baseDir, 'Values'); + if (!existsSync(valuesDir)) { + console.log('Creating directory', valuesDir); + mkdirSync(valuesDir, { recursive: true }); + } + + console.log('Writing category file', join(valuesDir, '_category_.json')); + writeFileSync( + join(valuesDir, '_category_.json'), + `{ + "label": "Values", + "position": 1, + "link": { + "type": "generated-index" + } +} +` + ); + + values.forEach((desc) => { + const { name, serialize, deserialize } = desc; + const filePath = join(valuesDir, `${kebab(name)}.mdx`); + const dirName = dirname(filePath); + + if (!existsSync(dirName)) { + console.log('Creating directory', dirName); + mkdirSync(dirName, { recursive: true }); + } + + console.log('Writing file', filePath); + writeFileSync( + filePath, + ` +# ${name} + +## Serialize + +\`\`\`ts +${serialize} +\`\`\` + +## Deserialize + +\`\`\`ts +${deserialize} +\`\`\` + +` + ); + }); +}; + +const generateNodePages = ( + nodes: NodeDescription[], + baseDir: string, + nodeSpecJson: NodeSpecJSON[] +) => { + const nodesDir = join(baseDir, 'Nodes'); + if (!existsSync(nodesDir)) { + console.log('Creating directory', nodesDir); + mkdirSync(nodesDir, { recursive: true }); + } + + console.log('Writing category file', join(nodesDir, '_category_.json')); + writeFileSync( + join(nodesDir, '_category_.json'), + `{ + "label": "Nodes", + "position": 2, + "link": { + "type": "generated-index" + } +} +` + ); + + nodes.forEach((desc) => { + const { typeName, factory } = desc; + const kebabName = typeName.split('/').map(kebab).join('/'); + const specJSON = nodeSpecJson.find((n) => n.type === typeName); + + const filePath = join(nodesDir, `${kebabName}.mdx`); + const dirName = dirname(filePath); + + if (!factory) { + console.warn('desc', desc); + throw new Error('desc.factory is undefined'); + } + + if (!existsSync(dirName)) { + console.log('Creating directory', dirName); + mkdirSync(dirName, { recursive: true }); + } + + const folderName = dirName.split('/').pop(); + console.log('Writing category file', join(dirName, '_category_.json')); + writeFileSync( + join(dirName, '_category_.json'), + `{ + "label": "${pascal(folderName)}", + "link": { + "type": "generated-index" + } +} +` + ); + + console.log('Writing file', filePath); + writeFileSync( + filePath, + nodeTemplate( + factory( + desc, + { + customEvents: {}, + variables: {} + } as any, + { + numInputs: 2, + numOutputs: 2 + } + ), + specJSON + ) + ); + }); +}; + +// First registry includes only the nodes for that specific profile, second registry includes all nodes required to run writeNodeSpecsToJSON +export default ( + registry: Registry, + baseDir: string, + functionalRegistry?: Registry +) => { + const nodes = registry.nodes.getAllDescriptions(); + + const values = registry.values.getAll(); + + const nodeSpecJson = writeNodeSpecsToJSON(functionalRegistry || registry); + + generateValuePages(values, baseDir); + generateNodePages(nodes, baseDir, nodeSpecJson); +}; diff --git a/website/scripts/generate-dynamic-pages/index.ts b/website/scripts/generate-dynamic-pages/index.ts new file mode 100644 index 00000000..61cf5762 --- /dev/null +++ b/website/scripts/generate-dynamic-pages/index.ts @@ -0,0 +1,40 @@ +import { join } from 'node:path'; + +// import generatePagesFromDescriptions from './generate-pages-from-descriptions'; +// import { descriptions as coreDescriptions } from './profiles/core'; +// generatePagesFromDescriptions( +// coreDescriptions, +// join(__dirname, '../../docs/profiles/Core') +// ); +import { registerCoreProfile } from '../../../packages/core/src/Profiles/Core/registerCoreProfile'; +import { registerSceneProfile } from '../../../packages/core/src/Profiles/Scene/registerSceneProfile'; +import { Registry } from '../../../packages/core/src/Registry'; +import generatePagesFromExamples from './generate-pages-from-examples'; +import generatePagesFromRegistry from './generate-pages-from-registry'; + +const coreRegistry = new Registry(); + +registerCoreProfile(coreRegistry); + +generatePagesFromRegistry( + coreRegistry, + join(__dirname, '../../docs/profiles/Core') +); + +const sceneRegistry = new Registry(); +const sceneFunctionalRegistry = new Registry(); + +registerSceneProfile(sceneRegistry); +registerCoreProfile(sceneFunctionalRegistry); +registerSceneProfile(sceneFunctionalRegistry); + +generatePagesFromRegistry( + sceneRegistry, + join(__dirname, '../../docs/profiles/Scene'), + sceneFunctionalRegistry +); + +generatePagesFromExamples( + join(__dirname, '../../../graphs'), + join(__dirname, '../../docs/examples') +); diff --git a/website/scripts/generate-dynamic-pages/templates/inputs-table.ts b/website/scripts/generate-dynamic-pages/templates/inputs-table.ts new file mode 100644 index 00000000..406310d6 --- /dev/null +++ b/website/scripts/generate-dynamic-pages/templates/inputs-table.ts @@ -0,0 +1,21 @@ +import { NodeSpecJSON } from '../../../../packages/core/src/Graphs/IO/NodeSpecJSON'; +import { Socket } from '../../../../packages/core/src/Sockets/Socket'; + +export default (inputs: Socket[], specJSON: NodeSpecJSON) => { + if (inputs.length === 0) { + return ''; + } + + return ` +| Name | Type | Default Value | Choices | +|------|------|---------------|---------| +${inputs + .map( + (i) => + `| ${i.name} | ${i.valueTypeName} | ${ + i.value ?? '' + } | ${i.valueChoices?.join(', ')} |` + ) + .join('\n')} +`; +}; diff --git a/website/scripts/generate-dynamic-pages/templates/inputs.ts b/website/scripts/generate-dynamic-pages/templates/inputs.ts new file mode 100644 index 00000000..583252d3 --- /dev/null +++ b/website/scripts/generate-dynamic-pages/templates/inputs.ts @@ -0,0 +1,17 @@ +import { NodeSpecJSON } from '../../../../packages/core/src/Graphs/IO/NodeSpecJSON'; +import { Node } from '../../../../packages/core/src/Nodes/Node'; +import socketsTable from './inputs-table'; + +export default (node: Node, specJSON: NodeSpecJSON) => { + if ( + !('inputs' in node) || + !Array.isArray(node.inputs) || + node.inputs.length === 0 + ) { + return ''; + } + + return `## Inputs + +${socketsTable(node.inputs, specJSON)}`; +}; diff --git a/website/scripts/generate-dynamic-pages/templates/node.ts b/website/scripts/generate-dynamic-pages/templates/node.ts new file mode 100644 index 00000000..4b27425b --- /dev/null +++ b/website/scripts/generate-dynamic-pages/templates/node.ts @@ -0,0 +1,75 @@ +import { NodeSpecJSON } from '../../../../packages/core/src/Graphs/IO/NodeSpecJSON'; +import { Node } from '../../../../packages/core/src/Nodes/Node'; +import inputsTemplate from './inputs'; +import outputsTemplate from './outputs'; + +const buildPage = ( + node: Node, + specJSON: NodeSpecJSON, + displayDynamicInputsOutputsLegend: boolean +) => ` +import NodePreview from '@site/src/components/NodePreview'; + +# ${node.description.typeName} + +${node.description.helpDescription || node.description.label} + + + +${inputsTemplate(node, specJSON)} + +${outputsTemplate(node, specJSON)} + +${displayDynamicInputsOutputsLegend ? '*: Dynamic I/O' : ''} + +`; + +export default (node: Node, specJSON: NodeSpecJSON) => { + const hasVariableInputs = + node.inputs.some((i) => i.name === '1') && + node.inputs.length !== specJSON.inputs.length; + + const hasVariableOutputs = + node.outputs.some((i) => i.name === '1') && + node.outputs.length !== specJSON.outputs.length; + + if (hasVariableInputs) { + specJSON.inputs = node.inputs.map((i) => { + const inSpec = specJSON.inputs.find((s) => s.name === i.name); + const returnValue = inSpec ?? { + name: i.name, + valueType: i.valueTypeName, + defaultValue: i.value + }; + + if (Number.isInteger(Number.parseInt(i.name, 10))) { + (i as any).name = `${i.name} *`; + } + + return returnValue; + }); + } + + if (hasVariableOutputs) { + specJSON.outputs = node.outputs.map((i) => { + const inSpec = specJSON.outputs.find((s) => s.name === i.name); + const returnValue = inSpec ?? { + name: i.name, + valueType: i.valueTypeName, + defaultValue: i.value + }; + + if (Number.isInteger(Number.parseInt(i.name, 10))) { + (i as any).name = `${i.name} *`; + } + + return returnValue; + }); + } + + return buildPage(node, specJSON, hasVariableInputs || hasVariableOutputs); +}; diff --git a/website/scripts/generate-dynamic-pages/templates/outputs-table.ts b/website/scripts/generate-dynamic-pages/templates/outputs-table.ts new file mode 100644 index 00000000..c0aebc98 --- /dev/null +++ b/website/scripts/generate-dynamic-pages/templates/outputs-table.ts @@ -0,0 +1,13 @@ +import { Socket } from '../../../../packages/core/src/Sockets/Socket'; + +export default (sockets: Socket[]) => { + if (sockets.length === 0) { + return ''; + } + + return ` +| Name | Type | +|------|------| +${sockets.map((i) => `| ${i.name} | ${i.valueTypeName} |`).join('\n')} +`; +}; diff --git a/website/scripts/generate-dynamic-pages/templates/outputs.ts b/website/scripts/generate-dynamic-pages/templates/outputs.ts new file mode 100644 index 00000000..9f4bf417 --- /dev/null +++ b/website/scripts/generate-dynamic-pages/templates/outputs.ts @@ -0,0 +1,17 @@ +import { NodeSpecJSON } from '../../../../packages/core/src/Graphs/IO/NodeSpecJSON'; +import { Node } from '../../../../packages/core/src/Nodes/Node'; +import socketsTable from './outputs-table'; + +export default (node: Node, specJSON: NodeSpecJSON) => { + if ( + !('outputs' in node) || + !Array.isArray(node.outputs) || + node.outputs.length === 0 + ) { + return ''; + } + + return `## Outputs + +${socketsTable(node.outputs)}`; +}; diff --git a/website/sidebars.js b/website/sidebars.js new file mode 100644 index 00000000..26a65eae --- /dev/null +++ b/website/sidebars.js @@ -0,0 +1,33 @@ +/** + * Creating a sidebar enables you to: + - create an ordered group of docs + - render a sidebar for each doc of that group + - provide next/previous navigation + + The sidebars can be generated from the filesystem, or explicitly defined here. + + Create as many sidebars as you want. + */ + +// @ts-check + +/** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */ +const sidebars = { + // By default, Docusaurus generates a sidebar from the docs folder structure + docsSidebar: [{ type: 'autogenerated', dirName: '.' }] + + // But you can create a sidebar manually + /* + tutorialSidebar: [ + 'intro', + 'hello', + { + type: 'category', + label: 'Tutorial', + items: ['tutorial-basics/create-a-document'], + }, + ], + */ +}; + +module.exports = sidebars; diff --git a/website/src/components/HomepageFeatures/index.tsx b/website/src/components/HomepageFeatures/index.tsx new file mode 100644 index 00000000..8d7cd8c4 --- /dev/null +++ b/website/src/components/HomepageFeatures/index.tsx @@ -0,0 +1,72 @@ +import React from 'react'; +import clsx from 'clsx'; +import styles from './styles.module.css'; + +type FeatureItem = { + title: string; + Svg: React.ComponentType>; + description: JSX.Element; +}; + +const FeatureList: FeatureItem[] = [ + { + title: 'Customizable', + Svg: require('@site/static/img/undraw_mind_map_re_nlb6.svg').default, + description: ( + <> + While this library contains a lot of nodes, you do not have to expose + all of them. You can also include your custom nodes. + + ) + }, + { + title: 'High Performance', + Svg: require('@site/static/img/undraw_performance_overview_re_mqrq.svg') + .default, + description: ( + <> + Currently in performance testing, the library achieves over 2M node + executions per second. + + ) + }, + { + title: 'Designed for Integration', + Svg: require('@site/static/img/undraw_code_typing_re_p8b9.svg').default, + description: ( + <> + This library is designed to be extended with context dependent nodes, + specifically Actions, Events and Queries that match the capabilities and + requirements of your system. + + ) + } +]; + +function Feature({title, Svg, description}: FeatureItem) { + return ( +
+
+ +
+
+

{title}

+

{description}

+
+
+ ); +} + +export default function HomepageFeatures(): JSX.Element { + return ( +
+
+
+ {FeatureList.map((props, idx) => ( + + ))} +
+
+
+ ); +} diff --git a/website/src/components/HomepageFeatures/styles.module.css b/website/src/components/HomepageFeatures/styles.module.css new file mode 100644 index 00000000..b248eb2e --- /dev/null +++ b/website/src/components/HomepageFeatures/styles.module.css @@ -0,0 +1,11 @@ +.features { + display: flex; + align-items: center; + padding: 2rem 0; + width: 100%; +} + +.featureSvg { + height: 200px; + width: 200px; +} diff --git a/website/src/components/NodePreview/AutoSizeInput.tsx b/website/src/components/NodePreview/AutoSizeInput.tsx new file mode 100644 index 00000000..934f63e4 --- /dev/null +++ b/website/src/components/NodePreview/AutoSizeInput.tsx @@ -0,0 +1,63 @@ +import React, { + CSSProperties, + FC, + HTMLProps, + useCallback, + useEffect, + useRef, + useState +} from 'react'; + +export type AutoSizeInputProps = HTMLProps & { + minWidth?: number; +}; + +const baseStyles: CSSProperties = { + position: 'absolute', + top: 0, + left: 0, + visibility: 'hidden', + height: 0, + width: 'auto', + whiteSpace: 'pre' +}; + +export const AutoSizeInput: FC = ({ + minWidth = 30, + ...props +}) => { + const inputRef = useRef(null); + const measureRef = useRef(null); + const [styles, setStyles] = useState({}); + + // grab the font size of the input on ref mount + const setRef = useCallback((input: HTMLInputElement | null) => { + if (input) { + const styles = window.getComputedStyle(input); + setStyles({ + fontSize: styles.getPropertyValue('font-size'), + paddingLeft: styles.getPropertyValue('padding-left'), + paddingRight: styles.getPropertyValue('padding-right') + }); + } + inputRef.current = input; + }, []); + + // measure the text on change and update input + useEffect(() => { + if (measureRef.current === null) return; + if (inputRef.current === null) return; + + const width = measureRef.current.clientWidth; + inputRef.current.style.width = Math.max(minWidth, width) + 'px'; + }, [props.value, minWidth, styles]); + + return ( + <> + + + {props.value} + + + ); +}; diff --git a/website/src/components/NodePreview/InputSocket.tsx b/website/src/components/NodePreview/InputSocket.tsx new file mode 100644 index 00000000..578f9ae9 --- /dev/null +++ b/website/src/components/NodePreview/InputSocket.tsx @@ -0,0 +1,138 @@ + +import React from 'react'; +import { Icon } from '@iconify/react'; +import { colors, valueTypeColorMap } from './utils/colors'; +import { InputSocketSpecJSON } from 'packages/core/src/Graphs/IO/NodeSpecJSON'; +import { AutoSizeInput } from './AutoSizeInput'; + +export type InputSocketProps = { + value: any | undefined; + onChange: (key: string, value: any) => void; +} & InputSocketSpecJSON; + +export default function InputSocket({ + value, + onChange, + name, + valueType, + defaultValue +}: InputSocketProps) { + const isFlowSocket = valueType === 'flow'; + + let colorName = valueTypeColorMap[valueType]; + if (colorName === undefined) { + colorName = 'red'; + } + + const [_, borderColor] = colors[colorName]; + const showName = isFlowSocket === false || name !== 'flow'; + + return ( +
+ {isFlowSocket && ( + + )} + {!isFlowSocket && ( + + )} + {showName && ( +
+ {name} +
+ )} + {isFlowSocket === 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)} + /> + )} + + )} +
+ ); +} diff --git a/website/src/components/NodePreview/Node.tsx b/website/src/components/NodePreview/Node.tsx new file mode 100644 index 00000000..6398645f --- /dev/null +++ b/website/src/components/NodePreview/Node.tsx @@ -0,0 +1,54 @@ +import React from 'react'; +import { NodeSpecJSON } from 'packages/core/src/Graphs/IO/NodeSpecJSON'; +import NodeContainer from './NodeContainer'; +import InputSocket from './InputSocket'; +import OutputSocket from './OutputSocket'; + +type NodeProps = { + spec: NodeSpecJSON; +}; + +const getPairs = (arr1: T[], arr2: U[]) => { + const max = Math.max(arr1.length, arr2.length); + const pairs = []; + for (let i = 0; i < max; i++) { + const pair: [T | undefined, U | undefined] = [arr1[i], arr2[i]]; + pairs.push(pair); + } + return pairs; +}; + +const Node = ({ spec }: NodeProps) => { + const pairs = getPairs(spec.inputs, spec.outputs); + return ( + + {pairs.map(([input, output], ix) => ( +
+ {input && ( + + )} + {output && ( + + )} +
+ ))} +
+ ); +}; + +export default Node; \ No newline at end of file diff --git a/website/src/components/NodePreview/NodeContainer.tsx b/website/src/components/NodePreview/NodeContainer.tsx new file mode 100644 index 00000000..7bdea65e --- /dev/null +++ b/website/src/components/NodePreview/NodeContainer.tsx @@ -0,0 +1,57 @@ +import { NodeSpecJSON } from 'packages/core/src/Graphs/IO/NodeSpecJSON'; +import React, { PropsWithChildren } from 'react'; +import { categoryColorMap, colors } from './utils/colors'; + +type NodeProps = { + title: string; + category?: NodeSpecJSON['category']; +}; + +export default function NodeContainer({ + title, + category = 'None', + children +}: PropsWithChildren) { + let colorName = categoryColorMap[category]; + if (colorName === undefined) { + colorName = 'red'; + } + let [backgroundColor, borderColor, textColor] = colors[colorName]; + return ( +
+
+ {title} +
+
+ {children} +
+
+ ); +} diff --git a/website/src/components/NodePreview/OutputSocket.tsx b/website/src/components/NodePreview/OutputSocket.tsx new file mode 100644 index 00000000..041437df --- /dev/null +++ b/website/src/components/NodePreview/OutputSocket.tsx @@ -0,0 +1,54 @@ +import React from 'react'; +import { Icon } from '@iconify/react'; +import { colors, valueTypeColorMap } from './utils/colors'; +import { OutputSocketSpecJSON } from 'packages/core/src/Graphs/IO/NodeSpecJSON'; + +export type OutputSocketProps = OutputSocketSpecJSON; + +export default function OutputSocket({ + valueType, + name +}: OutputSocketProps) { + const isFlowSocket = valueType === 'flow'; + let colorName = valueTypeColorMap[valueType]; + if (colorName === undefined) { + colorName = 'red'; + } + const [_, borderColor] = colors[colorName]; + const showName = isFlowSocket === false || name !== 'flow'; + + return ( +
+ {showName && ( +
+ {name} +
+ )} + {isFlowSocket && ( + + )} + {!isFlowSocket && ( + + )} +
+ ); +} diff --git a/website/src/components/NodePreview/index.tsx b/website/src/components/NodePreview/index.tsx new file mode 100644 index 00000000..43bb9c5b --- /dev/null +++ b/website/src/components/NodePreview/index.tsx @@ -0,0 +1,39 @@ + + +import { NodeSpecJSON } from 'packages/core/src/Graphs/IO/NodeSpecJSON'; +import { NodeDescription } from 'packages/core/src/Nodes/Registry/NodeDescription'; +import { Socket } from 'packages/core/src/Sockets/Socket'; +import React from 'react'; + +import Node from './Node'; + +const NodePreview = ({ + description, + inputs, + outputs, + spec +}: Props) => { + return ( +
+ +
+ ); +}; + +export type Props = { + description: NodeDescription, + inputs: Socket[], + outputs: Socket[], + spec: NodeSpecJSON, +} + +export default NodePreview; diff --git a/website/src/components/NodePreview/utils/colors.ts b/website/src/components/NodePreview/utils/colors.ts new file mode 100644 index 00000000..1ac0bcdb --- /dev/null +++ b/website/src/components/NodePreview/utils/colors.ts @@ -0,0 +1,31 @@ +import { NodeSpecJSON } from 'packages/core/src/Graphs/IO/NodeSpecJSON'; + +export const colors: Record = { + red: ['#f56565', '#ed64a6', '#ffffff'], + green: ['#48bb78', '#38a169', '#ffffff'], + lime: ['#68d391', '#4fd1c5', '#ffffff'], + purple: ['#805ad5', '#667eea', '#ffffff'], + blue: ['#4299e1', '#63b3ed', '#ffffff'], + gray: ['#718096', '#a0aec0', '#ffffff'], + white: ['#ffffff', '#ffffff', '#2d3748'] +}; + +export const valueTypeColorMap: Record = { + flow: 'white', + number: 'green', + float: 'green', + integer: 'lime', + boolean: 'red', + string: 'purple' +}; + +export const categoryColorMap: Record = { + Event: 'red', + Logic: 'green', + Variable: 'purple', + Query: 'purple', + Action: 'blue', + Flow: 'gray', + Time: 'gray', + None: 'gray' +}; diff --git a/website/src/css/custom.css b/website/src/css/custom.css new file mode 100644 index 00000000..f6a26842 --- /dev/null +++ b/website/src/css/custom.css @@ -0,0 +1,30 @@ +/** + * Any CSS included here will be global. The classic template + * bundles Infima by default. Infima is a CSS framework designed to + * work well for content-centric websites. + */ + +/* You can override the default Infima variables here. */ +:root { + --ifm-color-primary: #0891b2; + --ifm-color-primary-dark: #0a7c99; + --ifm-color-primary-darker: #0a6c8a; + --ifm-color-primary-darkest: #0a5470; + --ifm-color-primary-light: #0b8fb2; + --ifm-color-primary-lighter: #0d9db4; + --ifm-color-primary-lightest: #0fb2bf; + --ifm-code-font-size: 95%; + --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1); +} + +/* For readability concerns, you should choose a lighter palette in dark mode. */ +[data-theme='dark'] { + --ifm-color-primary: #0891b2; + --ifm-color-primary-dark: #0a7c99; + --ifm-color-primary-darker: #0a6c8a; + --ifm-color-primary-darkest: #0a5470; + --ifm-color-primary-light: #0b8fb2; + --ifm-color-primary-lighter: #0d9db4; + --ifm-color-primary-lightest: #0fb2bf; + --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3); +} diff --git a/website/src/pages/index.module.css b/website/src/pages/index.module.css new file mode 100644 index 00000000..9f71a5da --- /dev/null +++ b/website/src/pages/index.module.css @@ -0,0 +1,23 @@ +/** + * CSS files with the .module.css suffix will be treated as CSS modules + * and scoped locally. + */ + +.heroBanner { + padding: 4rem 0; + text-align: center; + position: relative; + overflow: hidden; +} + +@media screen and (max-width: 996px) { + .heroBanner { + padding: 2rem; + } +} + +.buttons { + display: flex; + align-items: center; + justify-content: center; +} diff --git a/website/src/pages/index.tsx b/website/src/pages/index.tsx new file mode 100644 index 00000000..b203c543 --- /dev/null +++ b/website/src/pages/index.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import clsx from 'clsx'; +import Link from '@docusaurus/Link'; +import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; +import Layout from '@theme/Layout'; +import HomepageFeatures from '@site/src/components/HomepageFeatures'; + +import styles from './index.module.css'; + +function HomepageHeader() { + const {siteConfig} = useDocusaurusContext(); + return ( +
+
+

{siteConfig.title}

+

{siteConfig.tagline}

+
+ + Read the Docs + +
+
+
+ ); +} + +export default function Home(): JSX.Element { + const {siteConfig} = useDocusaurusContext(); + return ( + + +
+ +
+
+ ); +} diff --git a/website/src/pages/markdown-page.md b/website/src/pages/markdown-page.md new file mode 100644 index 00000000..9756c5b6 --- /dev/null +++ b/website/src/pages/markdown-page.md @@ -0,0 +1,7 @@ +--- +title: Markdown page example +--- + +# Markdown page example + +You don't need React to write simple standalone pages. diff --git a/website/static/.nojekyll b/website/static/.nojekyll new file mode 100644 index 00000000..e69de29b diff --git a/website/static/img/behave-graph-flow.png b/website/static/img/behave-graph-flow.png new file mode 100644 index 00000000..539a7bf0 Binary files /dev/null and b/website/static/img/behave-graph-flow.png differ diff --git a/website/static/img/docusaurus.png b/website/static/img/docusaurus.png new file mode 100644 index 00000000..f458149e Binary files /dev/null and b/website/static/img/docusaurus.png differ diff --git a/website/static/img/favicon.ico b/website/static/img/favicon.ico new file mode 100644 index 00000000..085f6e6b Binary files /dev/null and b/website/static/img/favicon.ico differ diff --git a/website/static/img/logo.png b/website/static/img/logo.png new file mode 100644 index 00000000..10cced39 Binary files /dev/null and b/website/static/img/logo.png differ diff --git a/website/static/img/undraw_code_typing_re_p8b9.svg b/website/static/img/undraw_code_typing_re_p8b9.svg new file mode 100644 index 00000000..6e791b67 --- /dev/null +++ b/website/static/img/undraw_code_typing_re_p8b9.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/website/static/img/undraw_mind_map_re_nlb6.svg b/website/static/img/undraw_mind_map_re_nlb6.svg new file mode 100644 index 00000000..0ab55070 --- /dev/null +++ b/website/static/img/undraw_mind_map_re_nlb6.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/website/static/img/undraw_performance_overview_re_mqrq.svg b/website/static/img/undraw_performance_overview_re_mqrq.svg new file mode 100644 index 00000000..cc83b673 --- /dev/null +++ b/website/static/img/undraw_performance_overview_re_mqrq.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/website/tsconfig.json b/website/tsconfig.json new file mode 100644 index 00000000..2b373c63 --- /dev/null +++ b/website/tsconfig.json @@ -0,0 +1,11 @@ +{ + // This file is not used in compilation. It is here just for a nice editor experience. + "extends": "@tsconfig/docusaurus/tsconfig.json", + "compilerOptions": { + "baseUrl": ".", + "paths": { + "packages/*": ["../packages/*"], + }, + "target": "ES2020", + } +}