diff --git a/.changeset/brown-melons-fetch.md b/.changeset/brown-melons-fetch.md new file mode 100644 index 000000000..bc5dab05a --- /dev/null +++ b/.changeset/brown-melons-fetch.md @@ -0,0 +1,5 @@ +--- +"@tokens-studio/graph-editor": major +--- + +Removes the debugger and any reference to it. It was error prone and required patches to get it to compile correctly due to a problem with react-virtualized. A new debugger as a standalone package for testing and development will be created diff --git a/.changeset/curvy-ghosts-love.md b/.changeset/curvy-ghosts-love.md new file mode 100644 index 000000000..47673dbb7 --- /dev/null +++ b/.changeset/curvy-ghosts-love.md @@ -0,0 +1,5 @@ +--- +"@tokens-studio/graph-engine": major +--- + +Removed the COLOR_OBJECT, VEC3, DIMENSION schemas diff --git a/.changeset/cyan-wombats-drum.md b/.changeset/cyan-wombats-drum.md new file mode 100644 index 000000000..5f386bbd3 --- /dev/null +++ b/.changeset/cyan-wombats-drum.md @@ -0,0 +1,5 @@ +--- +"@tokens-studio/graph-engine": major +--- + +Removed the `checkCapabilitites` function on the Graph. diff --git a/.changeset/fluffy-dragons-matter.md b/.changeset/fluffy-dragons-matter.md new file mode 100644 index 000000000..f0c1f8d46 --- /dev/null +++ b/.changeset/fluffy-dragons-matter.md @@ -0,0 +1,5 @@ +--- +"@tokens-studio/graph-editor": patch +--- + +Adds a shortcut panel to visualize the shortcuts diff --git a/.changeset/foo-bar.md b/.changeset/foo-bar.md new file mode 100644 index 000000000..aac0e7355 --- /dev/null +++ b/.changeset/foo-bar.md @@ -0,0 +1,5 @@ +--- +"@tokens-studio/graph-editor": major +--- + +Editor now accepts a `nodeLoader` which handles loading nodes instead of a static lookup diff --git a/.changeset/giant-steaks-jump.md b/.changeset/giant-steaks-jump.md new file mode 100644 index 000000000..39fecd6e2 --- /dev/null +++ b/.changeset/giant-steaks-jump.md @@ -0,0 +1,6 @@ +--- +"@tokens-studio/graph-editor": major +--- + +Removed `initialGraph` from the editor. This can now be passed through with the Frame.graph option instead + diff --git a/.changeset/green-ladybugs-warn.md b/.changeset/green-ladybugs-warn.md new file mode 100644 index 000000000..90c7e0556 --- /dev/null +++ b/.changeset/green-ladybugs-warn.md @@ -0,0 +1,5 @@ +--- +"@tokens-studio/graph-engine": major +--- + +Removed the setNode method on the Port diff --git a/.changeset/hot-feet-do.md b/.changeset/hot-feet-do.md new file mode 100644 index 000000000..f0622ab9f --- /dev/null +++ b/.changeset/hot-feet-do.md @@ -0,0 +1,5 @@ +--- +"@tokens-studio/graph-engine": major +--- + +Changed the Changed the internal conversion of the engine to no longer attempt to convert types. This should be handle in the editor rather diff --git a/.changeset/hot-swans-hug.md b/.changeset/hot-swans-hug.md new file mode 100644 index 000000000..6a3540827 --- /dev/null +++ b/.changeset/hot-swans-hug.md @@ -0,0 +1,5 @@ +--- +"@tokens-studio/graph-engine": major +--- + +.predecessors is no longer a method on the Graph object, but is instead exported as a standalone function diff --git a/.changeset/khaki-timers-brush.md b/.changeset/khaki-timers-brush.md new file mode 100644 index 000000000..8269098d1 --- /dev/null +++ b/.changeset/khaki-timers-brush.md @@ -0,0 +1,5 @@ +--- +"@tokens-studio/graph-engine": patch +--- + +Removed the .load method from Nodes, the .loadResource method from the Graph object and the externalLoader from the graph diff --git a/.changeset/lemon-clocks-double.md b/.changeset/lemon-clocks-double.md new file mode 100644 index 000000000..aff5b6dcc --- /dev/null +++ b/.changeset/lemon-clocks-double.md @@ -0,0 +1,5 @@ +--- +"@tokens-studio/graph-engine": major +--- + +.update, .propagate, and .execute are now no longer methods on the Graph object and instead belong to the dataflow capability diff --git a/.changeset/light-avocados-hammer.md b/.changeset/light-avocados-hammer.md new file mode 100644 index 000000000..f7c33e073 --- /dev/null +++ b/.changeset/light-avocados-hammer.md @@ -0,0 +1,5 @@ +--- +"@tokens-studio/graph-engine": major +--- + +The engine now allows for async node loading during deserialization using a nodeLoader instead of a static lookup diff --git a/.changeset/light-donkeys-lie.md b/.changeset/light-donkeys-lie.md new file mode 100644 index 000000000..876dbee49 --- /dev/null +++ b/.changeset/light-donkeys-lie.md @@ -0,0 +1,5 @@ +--- +"@tokens-studio/graph-engine": major +--- + +The groups property on the node base class has been removed diff --git a/.changeset/long-shoes-occur.md b/.changeset/long-shoes-occur.md new file mode 100644 index 000000000..a62806a9d --- /dev/null +++ b/.changeset/long-shoes-occur.md @@ -0,0 +1,5 @@ +--- +"@tokens-studio/graph-engine": major +--- + +Removed .start, .stop, .pause, .resume from the graph and instead created a new ControlFlow class which can be added to a graph as a capability diff --git a/.changeset/old-birds-attend.md b/.changeset/old-birds-attend.md new file mode 100644 index 000000000..0e7b5e04e --- /dev/null +++ b/.changeset/old-birds-attend.md @@ -0,0 +1,5 @@ +--- +"@tokens-studio/graph-engine": major +--- + +extractTypes no longer exists as a static method on the Graph class and can instead be imported as a standalone function diff --git a/.changeset/poor-glasses-greet.md b/.changeset/poor-glasses-greet.md new file mode 100644 index 000000000..7b8131ac6 --- /dev/null +++ b/.changeset/poor-glasses-greet.md @@ -0,0 +1,5 @@ +--- +"@tokens-studio/graph-engine": major +--- + +Removes the `visible` value in the edge. This should be handled in an editor and is not important to the the core engine diff --git a/.changeset/popular-seas-pretend.md b/.changeset/popular-seas-pretend.md new file mode 100644 index 000000000..bc8335d79 --- /dev/null +++ b/.changeset/popular-seas-pretend.md @@ -0,0 +1,5 @@ +--- +"@tokens-studio/graph-engine": major +--- + +hasConnectedInput has been removed as a method on the graph. It can be imported now as a standalone function diff --git a/.changeset/quick-jeans-hammer.md b/.changeset/quick-jeans-hammer.md new file mode 100644 index 000000000..b131e620e --- /dev/null +++ b/.changeset/quick-jeans-hammer.md @@ -0,0 +1,5 @@ +--- +"@tokens-studio/graph-editor": patch +--- + +Made the context menu styling consistent diff --git a/.changeset/silver-dolls-joke.md b/.changeset/silver-dolls-joke.md new file mode 100644 index 000000000..e6a0789a8 --- /dev/null +++ b/.changeset/silver-dolls-joke.md @@ -0,0 +1,5 @@ +--- +"@tokens-studio/graph-engine": major +--- + +Capabilities are now no longer shared by default and are instead registered again on any cloned graphs diff --git a/.changeset/slow-humans-fold.md b/.changeset/slow-humans-fold.md new file mode 100644 index 000000000..9cb260e3f --- /dev/null +++ b/.changeset/slow-humans-fold.md @@ -0,0 +1,5 @@ +--- +"@tokens-studio/graph-engine": major +--- + +Remove the clearOutputs method on Node diff --git a/.changeset/tame-queens-relate.md b/.changeset/tame-queens-relate.md new file mode 100644 index 000000000..938d7b555 --- /dev/null +++ b/.changeset/tame-queens-relate.md @@ -0,0 +1,5 @@ +--- +"@tokens-studio/graph-engine": major +--- + +Removed the `groups` property on the Node object diff --git a/.changeset/thin-files-beg.md b/.changeset/thin-files-beg.md new file mode 100644 index 000000000..980b09c1c --- /dev/null +++ b/.changeset/thin-files-beg.md @@ -0,0 +1,5 @@ +--- +"@tokens-studio/graph-engine": major +--- + +Removed the buffer type and schema from the engine diff --git a/.changeset/wild-cars-kiss.md b/.changeset/wild-cars-kiss.md new file mode 100644 index 000000000..529b3c565 --- /dev/null +++ b/.changeset/wild-cars-kiss.md @@ -0,0 +1,16 @@ +--- +"@tokens-studio/graph-editor": major +--- + +Removes the External Loader from the editor as this will likely be scoped per Frame + +You should instead attach your external loader directly to the graph you created when creating a frame + +```ts +const graph = new Graph(); + +graph.externaloLoader = myExternalLoader; + +const Frame = new Frame({ graph , ...etc}) + +``` diff --git a/.changeset/yellow-carrots-tan.md b/.changeset/yellow-carrots-tan.md new file mode 100644 index 000000000..823582082 --- /dev/null +++ b/.changeset/yellow-carrots-tan.md @@ -0,0 +1,5 @@ +--- +"@tokens-studio/graph-editor": major +--- + +Removed the Vec3 Control diff --git a/adrs/0005-engines.md b/adrs/0005-engines.md new file mode 100644 index 000000000..425a03ba4 --- /dev/null +++ b/adrs/0005-engines.md @@ -0,0 +1,21 @@ +# Engines + +## Context and Problem Statement + +We previously only supported dataflow style graphs, however this is limiting as there are problem types that are not ideal for dataflow, such as conditionals or loops. We need to support different graph types. + +As part of this we want to support additional engine types like (behaviour/event/blueprint)[https://dev.epicgames.com/documentation/en-us/unreal-engine/blueprints-visual-scripting-in-unreal-engine?application_version=5.4] graphs where we can control the current pointer to execution and have more control over the flow of the graph. + +## Considered Options + +- Seperate packages for different graph types +- Reuse generic graph package and implement specific engines + +## Decision Outcome + +While it is possible to have seperate packages for different graph types, we decided to go with the second option as it allows us to reuse the same graph package and implement specific engines for different graph types. There is a common core to all graphs and we can use the same base classes and abstractions to implement different engines. + +What this entails is using generics and OOP principles to create a base engine class that can be extended to implement different engines. We would then also make generic editor systems that can be extended to support different graph types. This will be done using a `Frame` object which represents all the information specific to a certain graph type + +## Links + diff --git a/packages/documentation/docs/guides/developers/engine/programmatic.md b/packages/documentation/docs/guides/developers/engine/programmatic.md index a6e21e99a..0d301944f 100644 --- a/packages/documentation/docs/guides/developers/engine/programmatic.md +++ b/packages/documentation/docs/guides/developers/engine/programmatic.md @@ -25,11 +25,6 @@ const output = new Output({ graph }); const add = new Add({ graph }); const div = new Divide({ graph }); -//Lets add the nodes explicitly. This makes sure if they apply side effects to the graph on entry that they are seperated from being defined -graph.addNode(input); -graph.addNode(output); -graph.addNode(add); -graph.addNode(div); //Create an input port on the input. This would be a dynamic property input.addInput("y", { diff --git a/packages/graph-editor/package.json b/packages/graph-editor/package.json index 4779ca9de..add3e0759 100644 --- a/packages/graph-editor/package.json +++ b/packages/graph-editor/package.json @@ -34,6 +34,7 @@ "format": "npm run format:eslint && npm run format:prettier", "format:eslint": "eslint . --fix", "format:prettier": "prettier \"**/*.{tsx,ts,js,md,json}\" --write", + "test": "vitest run", "test:e2e": "cypress run", "test:e2e:manual": "cypress open", "lint": "npm run lint:prettier && npm run lint:eslint", @@ -59,7 +60,6 @@ "@tokens-studio/tokens": "^0.3.7", "@tokens-studio/types": "^0.5.1", "@tokens-studio/ui": "^1.0.13", - "@xzdarcy/react-timeline-editor": "^0.1.9", "array-move": "^4.0.0", "clsx": "^2.1.1", "cmdk": "^0.2.0", @@ -89,7 +89,7 @@ "reselect": "^4.1.8", "sentence-case": "^3.0.4", "undo-manager": "^1.1.1", - "uuid": "^9.0.0" + "nanoid": "^5.0.7" }, "peerDependencies": { "react": "^18.2.0", @@ -121,6 +121,7 @@ "cypress": "^13.9.0", "cypress-react-selector": "^3.0.0", "glob": "^11.0.0", + "happy-dom": "^16.3.0", "postcss": "^8.4.47", "postcss-cli": "^11.0.0", "postcss-css-variables": "^0.19.0", diff --git a/packages/graph-editor/readme.md b/packages/graph-editor/readme.md index 274985faa..41dd56bac 100644 --- a/packages/graph-editor/readme.md +++ b/packages/graph-editor/readme.md @@ -83,3 +83,7 @@ In the example above, we use the useRef hook to create a reference to the Editor ## Development We need to force the cypress react selector to use a version of `resq` that supports 18.2.0. This is done through resolutions and by manually adding it as a dev dependency + +## Notes + +This project uses [CSS modules](https://github.com/css-modules/css-modules) to handle encapsulating css. We distribute this in the dist with imports to `*.module.css` files still remaining without transformation to the css files. We assume that the implementing system will handle this transformation and export of the mangled css classnames if needed. diff --git a/packages/graph-editor/src/components/commandPalette/index.tsx b/packages/graph-editor/src/components/commandPalette/index.tsx index d44657282..fa629029d 100644 --- a/packages/graph-editor/src/components/commandPalette/index.tsx +++ b/packages/graph-editor/src/components/commandPalette/index.tsx @@ -13,18 +13,19 @@ import { observer } from 'mobx-react-lite'; import { showNodesCmdPaletteSelector } from '@/redux/selectors/ui.js'; import { useDispatch, useSelector } from 'react-redux'; import { useSelectAddedNodes } from '@/hooks/useSelectAddedNodes.js'; -import React from 'react'; +import React, { useCallback } from 'react'; import Search from '@tokens-studio/icons/Search.js'; import styles from './index.module.css'; export interface ICommandMenu { items: DropPanelStore; - handleSelectNewNodeType: (node: NodeRequest) => + handleSelectNewNodeType: (node: NodeRequest) => Promise< | { graphNode: Node; flowNode: ReactFlowNode; } - | undefined; + | undefined + >; } const CommandItem = observer( @@ -80,7 +81,10 @@ const CommandMenuGroup = observer( }, ); -const CommandMenu = ({ items, handleSelectNewNodeType }: ICommandMenu) => { +export const CommandMenu = ({ + items, + handleSelectNewNodeType, +}: ICommandMenu) => { const showNodesCmdPalette = useSelector(showNodesCmdPaletteSelector); const dispatch = useDispatch(); const cursorPositionRef = React.useRef<{ x: number; y: number }>({ @@ -91,8 +95,8 @@ const CommandMenu = ({ items, handleSelectNewNodeType }: ICommandMenu) => { const reactflow = useReactFlow(); const selectAddedNodes = useSelectAddedNodes(); - const handleSelectItem = (item) => { - const newNode = handleSelectNewNodeType({ + const handleSelectItem = async (item) => { + const newNode = await handleSelectNewNodeType({ position: reactflow.screenToFlowPosition(cursorPositionRef.current), ...item, }); @@ -132,13 +136,16 @@ const CommandMenu = ({ items, handleSelectNewNodeType }: ICommandMenu) => { }, [dispatch.ui, showNodesCmdPalette]); // Close the menu when Escape key is pressed inside the input - const handleKeyDown = (e) => { - if (e.key === 'Escape') { - e.preventDefault(); + const handleKeyDown = useCallback( + (e) => { + if (e.key === 'Escape') { + e.preventDefault(); - dispatch.ui.setShowNodesCmdPalette(false); - } - }; + dispatch.ui.setShowNodesCmdPalette(false); + } + }, + [dispatch.ui], + ); return ( ); } - -export { CommandMenu }; diff --git a/packages/graph-editor/src/components/contextMenus/nodeContextMenu.tsx b/packages/graph-editor/src/components/contextMenus/nodeContextMenu.tsx index 0e4a6a876..b3ca0b9ee 100644 --- a/packages/graph-editor/src/components/contextMenus/nodeContextMenu.tsx +++ b/packages/graph-editor/src/components/contextMenus/nodeContextMenu.tsx @@ -1,5 +1,7 @@ +import { ContextMenuItem } from './ContextMenuStyles.js'; +import { DataflowNode } from '@tokens-studio/graph-engine'; import { Graph } from 'graphlib'; -import { Item, Menu, Separator } from 'react-contexify'; +import { Menu, Separator } from 'react-contexify'; import { Node, ReactFlowInstance, useReactFlow } from 'reactflow'; import { useAction } from '@/editor/actions/provider.js'; import { useCanDeleteNode } from '@/hooks/useCanDeleteNode.js'; @@ -153,7 +155,7 @@ export const NodeContextMenu = ({ id, nodes }: INodeContextMenuProps) => { nodes.forEach((node) => { const graphNode = graph.getNode(node.id); if (graphNode) { - graphNode.run(); + (graphNode as DataflowNode).dataflow?.run(); } }); } @@ -167,11 +169,15 @@ export const NodeContextMenu = ({ id, nodes }: INodeContextMenuProps) => { {nodes?.length == 1 && ( <> - Trace Upstream - Trace Downstream + + Trace Upstream + + + Trace Downstream + )} - Reset Trace + Reset Trace Delete diff --git a/packages/graph-editor/src/components/contextMenus/paneContextMenu.tsx b/packages/graph-editor/src/components/contextMenus/paneContextMenu.tsx index b64d011a8..f828b14f5 100644 --- a/packages/graph-editor/src/components/contextMenus/paneContextMenu.tsx +++ b/packages/graph-editor/src/components/contextMenus/paneContextMenu.tsx @@ -1,13 +1,12 @@ import { ContextMenuItem } from './ContextMenuStyles.js'; import { Menu, Separator } from 'react-contexify'; import { clear } from '../../editor/actions/clear.js'; -import { showGrid, snapGrid } from '@/redux/selectors/settings.js'; import { useAction } from '@/editor/actions/provider.js'; import { useAutoLayout } from '../../editor/hooks/useAutolayout.js'; import { useDispatch } from '@/hooks/index.js'; +import { useFrame } from '@/system/frame/hook.js'; import { useLocalGraph } from '@/context/graph.js'; import { useReactFlow } from 'reactflow'; -import { useSelector } from 'react-redux'; import React, { useCallback } from 'react'; export interface IPaneContextMenu { @@ -17,8 +16,8 @@ export interface IPaneContextMenu { export const PaneContextMenu = ({ id }: IPaneContextMenu) => { const reactFlowInstance = useReactFlow(); - const showGridValue = useSelector(showGrid); - const snapGridValue = useSelector(snapGrid); + + const frame = useFrame(); const dispatch = useDispatch(); const graph = useLocalGraph(); const createNode = useAction('createNode'); @@ -51,12 +50,12 @@ export const PaneContextMenu = ({ id }: IPaneContextMenu) => { }, [reactFlowInstance]); const setShowGrid = useCallback(() => { - dispatch.settings.setShowGrid(!showGridValue); - }, [dispatch.settings, showGridValue]); + frame.settings.setShowGrid(!frame.settings.showGrid); + }, [frame.settings]); const setSnapGrid = useCallback(() => { - dispatch.settings.setSnapGrid(!snapGridValue); - }, [dispatch.settings, snapGridValue]); + frame.settings.setSnapGrid(!frame.settings.snapGrid); + }, [frame.settings]); const clearCallback = useCallback(() => { clear(reactFlowInstance, graph); @@ -70,7 +69,7 @@ export const PaneContextMenu = ({ id }: IPaneContextMenu) => { Apply Layout - {showGridValue ? 'Hide' : 'Show'} Grid + {frame.settings.showGrid ? 'Hide' : 'Show'} Grid Recenter diff --git a/packages/graph-editor/src/components/contextMenus/selectionContextMenu.tsx b/packages/graph-editor/src/components/contextMenus/selectionContextMenu.tsx index ee764bfa9..bd50fa562 100644 --- a/packages/graph-editor/src/components/contextMenus/selectionContextMenu.tsx +++ b/packages/graph-editor/src/components/contextMenus/selectionContextMenu.tsx @@ -1,4 +1,10 @@ -import { Edge, Graph } from '@tokens-studio/graph-engine'; +import { + DataflowNode, + Edge, + Graph, + Input, + Output, +} from '@tokens-studio/graph-engine'; import { Item, Menu, Separator } from 'react-contexify'; import { Node, getRectOfNodes, useReactFlow, useStoreApi } from 'reactflow'; import { NodeTypes } from '../flow/types.js'; @@ -6,7 +12,7 @@ import { deletable } from '@/annotations/index.js'; import { getId } from '../flow/utils.js'; import { useAction } from '@/editor/actions/provider.js'; import { useLocalGraph } from '@/hooks/index.js'; -import { v4 as uuid } from 'uuid'; +import { nanoid as uuid } from 'nanoid'; import React, { useCallback } from 'react'; export type INodeContextMenuProps = { @@ -74,7 +80,7 @@ export const SelectionContextMenu = ({ id, nodes }: INodeContextMenuProps) => { }); }, [nodes, reactFlowInstance, selectedNodeIds, store]); - const onCreateSubgraph = useCallback(() => { + const onCreateSubgraph = useCallback(async () => { //We need to work out which nodes do not have parents in the selection const lookup = new Set(selectedNodeIds); @@ -96,7 +102,7 @@ export const SelectionContextMenu = ({ id, nodes }: INodeContextMenuProps) => { y: position.y / selectedNodes.length, }; - const nodes = createNode({ + const nodes = await createNode({ type: 'studio.tokens.generic.subgraph', position: finalPosition, }); @@ -120,10 +126,10 @@ export const SelectionContextMenu = ({ id, nodes }: INodeContextMenuProps) => { const existingInternal = Object.values(internalGraph.nodes); const inputNode = existingInternal.find( (x) => x.factory.type == 'studio.tokens.generic.input', - ); + ) as DataflowNode | undefined; const outputNode = existingInternal.find( (x) => x.factory.type == 'studio.tokens.generic.output', - ); + ) as DataflowNode | undefined; //Now we need to determine the inputs and outputs of the subgraph //We need to find the entry nodes and the exit nodes @@ -174,8 +180,8 @@ export const SelectionContextMenu = ({ id, nodes }: INodeContextMenuProps) => { //We need to create a input for each of the entry edges entryEdges.forEach((edge) => { //Get the output node of the edge - const outputNode = graph.getNode(edge.target); - const input = outputNode?.inputs[edge.targetHandle]; + const outputNode = graph.getNode(edge.target) as DataflowNode; + const input = outputNode?.inputs[edge.targetHandle] as Input; //Its possible that we have a collision with the name so we need to rename it const initialName = edge.targetHandle; @@ -186,15 +192,13 @@ export const SelectionContextMenu = ({ id, nodes }: INodeContextMenuProps) => { name = initialName + '_' + count++; } - const newInput = inputNode?.addInput(name, { + inputNode?.dataflow.addInput(name, { type: input!.type, }); - if (newInput) { - newInput.annotations[deletable] = true; - } + inputNode?.inputs[name]?.setAnnotation(deletable, true); //The output won't be dynamically generated until the node is executed, so we add it manually - inputNode?.addOutput(name, { + inputNode?.dataflow.addOutput(name, { type: input!.type, }); @@ -223,7 +227,7 @@ export const SelectionContextMenu = ({ id, nodes }: INodeContextMenuProps) => { exitEdges.forEach((edge) => { //Get the output node of the edge const sourceNode = graph.getNode(edge.source); - const sourceOutput = sourceNode?.outputs[edge.sourceHandle]; + const sourceOutput = sourceNode?.outputs[edge.sourceHandle] as Output; //Its possible that we have a collision with the name so we need to rename it const initialName = edge.targetHandle; @@ -234,15 +238,15 @@ export const SelectionContextMenu = ({ id, nodes }: INodeContextMenuProps) => { name = initialName + '_' + count++; } - outputNode?.addInput(name, { + outputNode?.dataflow.addInput(name, { type: sourceOutput!.type, }); - outputNode?.addOutput(name, { + outputNode?.dataflow.addOutput(name, { type: sourceOutput!.type, }); //Ensure the output exists on the outer graph node as it will only be dynamically populated once the subgraph has run - graphNode.addOutput(name, { + (graphNode as DataflowNode).dataflow.addOutput(name, { type: sourceOutput!.type, }); diff --git a/packages/graph-editor/src/components/controls/array.tsx b/packages/graph-editor/src/components/controls/array.tsx index 7fba5ac2c..47ad0b9d7 100644 --- a/packages/graph-editor/src/components/controls/array.tsx +++ b/packages/graph-editor/src/components/controls/array.tsx @@ -21,10 +21,8 @@ import { import { ColorPickerPopover } from '../colorPicker/index.js'; import { IField } from './interface.js'; import { JSONTree } from 'react-json-tree'; -import { delayedUpdateSelector } from '@/redux/selectors/index.js'; import { observer } from 'mobx-react-lite'; import { toJS } from 'mobx'; -import { useSelector } from 'react-redux'; import FloppyDisk from '@tokens-studio/icons/FloppyDisk.js'; import Minus from '@tokens-studio/icons/Minus.js'; import Plus from '@tokens-studio/icons/Plus.js'; @@ -40,7 +38,7 @@ const NEW_ITEM_DEFAULTS = { }, }; -export const ArrayField = observer(({ port, readOnly }: IField) => { +export const ArrayField = observer(({ port, readOnly, settings }: IField) => { const [value, setValue] = React.useState(port.value); const [selectItemsType, setSelectItemsType] = React.useState( port.type.items.$id, @@ -49,7 +47,6 @@ export const ArrayField = observer(({ port, readOnly }: IField) => { const [autofocusIndex, setAutofocusIndex] = React.useState( null, ); - const useDelayed = useSelector(delayedUpdateSelector); React.useEffect(() => { setValue(port.value); @@ -82,13 +79,13 @@ export const ArrayField = observer(({ port, readOnly }: IField) => { setValue(newValueArray); - if (useDelayed) { + if (settings.delayedUpdate) { return; } (port as Input).setValue(newValueArray); }, - [port, useDelayed, value], + [port, settings.delayedUpdate, value], ); const onColorChange = useCallback( @@ -106,12 +103,12 @@ export const ArrayField = observer(({ port, readOnly }: IField) => { setAutofocusIndex(newValueArray.length - 1); - if (useDelayed) { + if (settings.delayedUpdate) { return; } (port as Input).setValue(newValueArray); - }, [itemsType, port, useDelayed, value]); + }, [itemsType, port, settings.delayedUpdate, value]); const removeItem = useCallback( (index: number) => { @@ -121,13 +118,13 @@ export const ArrayField = observer(({ port, readOnly }: IField) => { setValue(newValueArray); setAutofocusIndex(null); - if (useDelayed) { + if (settings.delayedUpdate) { return; } (port as Input).setValue(newValueArray); }, - [port, useDelayed, value], + [port, settings.delayedUpdate, value], ); const itemList = React.useMemo(() => { @@ -255,7 +252,7 @@ export const ArrayField = observer(({ port, readOnly }: IField) => { onClick={addItem} /> - {useDelayed && ( + {settings.delayedUpdate && ( } diff --git a/packages/graph-editor/src/components/controls/color.tsx b/packages/graph-editor/src/components/controls/color.tsx index 20f4815de..4e51cc16c 100644 --- a/packages/graph-editor/src/components/controls/color.tsx +++ b/packages/graph-editor/src/components/controls/color.tsx @@ -2,15 +2,12 @@ import { ColorPickerPopover } from '../colorPicker/index.js'; import { IField } from './interface.js'; import { IconButton, Stack, Text } from '@tokens-studio/ui'; import { Input, hexToColor, toColor, toHex } from '@tokens-studio/graph-engine'; -import { delayedUpdateSelector } from '@/redux/selectors/index.js'; import { observer } from 'mobx-react-lite'; -import { useSelector } from 'react-redux'; import FloppyDisk from '@tokens-studio/icons/FloppyDisk.js'; import React, { useCallback } from 'react'; import styles from './color.module.css'; -export const ColorField = observer(({ port, readOnly }: IField) => { - const useDelayed = useSelector(delayedUpdateSelector); +export const ColorField = observer(({ port, readOnly, settings }: IField) => { const [val, setVal] = React.useState(''); React.useEffect(() => { @@ -33,14 +30,14 @@ export const ColorField = observer(({ port, readOnly }: IField) => { col = e.target.value; } setVal(col); - if (useDelayed) { + if (settings.delayedUpdate) { return; } //We need to convert from hex (port as Input).setValue(hexToColor(col)); }, - [port, useDelayed], + [port, settings.delayedUpdate], ); if (readOnly) { @@ -62,7 +59,7 @@ export const ColorField = observer(({ port, readOnly }: IField) => { {val} - {useDelayed && ( + {settings.delayedUpdate && ( } onClick={() => (port as Input).setValue(val)} diff --git a/packages/graph-editor/src/components/controls/curve.tsx b/packages/graph-editor/src/components/controls/curve.tsx index fc96a719b..b3bce2a9a 100644 --- a/packages/graph-editor/src/components/controls/curve.tsx +++ b/packages/graph-editor/src/components/controls/curve.tsx @@ -4,21 +4,23 @@ import { Input } from '@tokens-studio/graph-engine'; import { observer } from 'mobx-react-lite'; import React from 'react'; -export const CurveField = observer(({ port, readOnly }: IField) => { - const onChange = (index: number, value: number[]) => { - if (!readOnly) { - const points = [...port.value.curves[0].points]; - points[index] = value; - (port as Input).setValue({ - curves: [ - { - points, - }, - ], - }); - } - }; - return ( - - ); -}); +export const CurveField = observer( + ({ port, readOnly }: Omit) => { + const onChange = (index: number, value: number[]) => { + if (!readOnly) { + const points = [...port.value.curves[0].points]; + points[index] = value; + (port as Input).setValue({ + curves: [ + { + points, + }, + ], + }); + } + }; + return ( + + ); + }, +); diff --git a/packages/graph-editor/src/components/controls/interface.tsx b/packages/graph-editor/src/components/controls/interface.tsx index 6cb097cee..01b62db37 100644 --- a/packages/graph-editor/src/components/controls/interface.tsx +++ b/packages/graph-editor/src/components/controls/interface.tsx @@ -1,5 +1,7 @@ -import { Port } from '@tokens-studio/graph-engine'; +import type { DataFlowPort } from '@tokens-studio/graph-engine'; +import { SystemSettings } from '@/system/frame/settings.js'; export interface IField { - port: Port; + port: DataFlowPort; readOnly?: boolean; + settings: SystemSettings; } diff --git a/packages/graph-editor/src/components/controls/numeric.tsx b/packages/graph-editor/src/components/controls/numeric.tsx index 6bb608bd9..a51012629 100644 --- a/packages/graph-editor/src/components/controls/numeric.tsx +++ b/packages/graph-editor/src/components/controls/numeric.tsx @@ -1,18 +1,16 @@ import { IField } from './interface.js'; import { IconButton, Stack, TextInput } from '@tokens-studio/ui'; import { Input } from '@tokens-studio/graph-engine'; -import { delayedUpdateSelector } from '@/redux/selectors/index.js'; import { observer } from 'mobx-react-lite'; -import { useSelector } from 'react-redux'; + import FloppyDisk from '@tokens-studio/icons/FloppyDisk.js'; import React, { useCallback } from 'react'; -export const NumericField = observer(({ port, readOnly }: IField) => { +export const NumericField = observer(({ port, readOnly, settings }: IField) => { const [intermediate, setIntermediate] = React.useState( undefined, ); const [hadErr, setHadErr] = React.useState(false); - const useDelayed = useSelector(delayedUpdateSelector); const [val, setVal] = React.useState(port.value); React.useEffect(() => { @@ -24,7 +22,7 @@ export const NumericField = observer(({ port, readOnly }: IField) => { if (!readOnly) { const number = Number.parseFloat(e.target.value); if (!Number.isNaN(number)) { - if (!useDelayed) { + if (!settings.delayedUpdate) { (port as Input).setValue(number); } else { setVal(number); @@ -37,7 +35,7 @@ export const NumericField = observer(({ port, readOnly }: IField) => { setIntermediate(e.target.value); } }, - [port, readOnly, useDelayed], + [readOnly, settings.delayedUpdate, port], ); return ( @@ -49,7 +47,7 @@ export const NumericField = observer(({ port, readOnly }: IField) => { onChange={onChange} disabled={readOnly} /> - {useDelayed && ( + {settings.delayedUpdate && ( } onClick={() => (port as Input).setValue(val)} diff --git a/packages/graph-editor/src/components/controls/slider.tsx b/packages/graph-editor/src/components/controls/slider.tsx index a7ee58a4d..1509f67fe 100644 --- a/packages/graph-editor/src/components/controls/slider.tsx +++ b/packages/graph-editor/src/components/controls/slider.tsx @@ -2,18 +2,16 @@ import { IField } from './interface.js'; import { IconButton, Stack, Text } from '@tokens-studio/ui'; import { Input } from '@tokens-studio/graph-engine'; import { Slider } from '../slider/index.js'; -import { delayedUpdateSelector } from '@/redux/selectors/index.js'; + import { observer } from 'mobx-react-lite'; -import { useSelector } from 'react-redux'; import FloppyDisk from '@tokens-studio/icons/FloppyDisk.js'; import React, { useCallback } from 'react'; -export const SliderField = observer(({ port, readOnly }: IField) => { +export const SliderField = observer(({ port, readOnly, settings }: IField) => { const min = port.type.minimum || 0; const max = port.type.maximum || 1; const step = port.type.multipleOf || (max - min) / 100; - const useDelayed = useSelector(delayedUpdateSelector); const [val, setVal] = React.useState(port.value); React.useEffect(() => { @@ -23,14 +21,14 @@ export const SliderField = observer(({ port, readOnly }: IField) => { const onChange = useCallback( (value: number[]) => { if (!readOnly) { - if (!useDelayed) { + if (!settings.delayedUpdate) { (port as Input).setValue(value[0]); } else { setVal(value[0]); } } }, - [port, readOnly, useDelayed], + [port, readOnly, settings.delayedUpdate], ); return ( @@ -43,7 +41,7 @@ export const SliderField = observer(({ port, readOnly }: IField) => { onValueChange={onChange} /> {val} - {useDelayed && ( + {settings.delayedUpdate && ( } onClick={() => (port as Input).setValue(val)} diff --git a/packages/graph-editor/src/components/controls/string.tsx b/packages/graph-editor/src/components/controls/string.tsx index 438b53bdc..41640f5b6 100644 --- a/packages/graph-editor/src/components/controls/string.tsx +++ b/packages/graph-editor/src/components/controls/string.tsx @@ -1,14 +1,11 @@ import { IField } from './interface.js'; import { IconButton, Stack, Text, TextInput } from '@tokens-studio/ui'; import { Input } from '@tokens-studio/graph-engine'; -import { delayedUpdateSelector } from '@/redux/selectors/index.js'; import { observer } from 'mobx-react-lite'; -import { useSelector } from 'react-redux'; import FloppyDisk from '@tokens-studio/icons/FloppyDisk.js'; import React, { useEffect } from 'react'; -export const Textfield = observer(({ port, readOnly }: IField) => { - const useDelayed = useSelector(delayedUpdateSelector); +export const Textfield = observer(({ port, readOnly, settings }: IField) => { const [val, setVal] = React.useState(port.value); useEffect(() => { @@ -18,7 +15,7 @@ export const Textfield = observer(({ port, readOnly }: IField) => { const onChange = (e: React.ChangeEvent) => { const str = e.target.value; setVal(str); - if (useDelayed) { + if (settings.delayedUpdate) { return; } @@ -32,7 +29,7 @@ export const Textfield = observer(({ port, readOnly }: IField) => { return ( - {useDelayed && ( + {settings.delayedUpdate && ( } onClick={() => (port as Input).setValue(val)} diff --git a/packages/graph-editor/src/components/controls/text.tsx b/packages/graph-editor/src/components/controls/text.tsx index 33b5ad6c8..e64df340c 100644 --- a/packages/graph-editor/src/components/controls/text.tsx +++ b/packages/graph-editor/src/components/controls/text.tsx @@ -1,14 +1,12 @@ import { IField } from './interface.js'; import { IconButton, Textarea as UITextarea } from '@tokens-studio/ui'; import { Input } from '@tokens-studio/graph-engine'; -import { delayedUpdateSelector } from '@/redux/selectors/index.js'; import { observer } from 'mobx-react-lite'; -import { useSelector } from 'react-redux'; + import FloppyDisk from '@tokens-studio/icons/FloppyDisk.js'; import React, { useEffect } from 'react'; -export const TextArea = observer(({ port, readOnly }: IField) => { - const useDelayed = useSelector(delayedUpdateSelector); +export const TextArea = observer(({ settings, port, readOnly }: IField) => { const [val, setVal] = React.useState(port.value); useEffect(() => { @@ -17,7 +15,7 @@ export const TextArea = observer(({ port, readOnly }: IField) => { const onChange = (str: string) => { setVal(str); - if (useDelayed) { + if (settings.delayedUpdate) { return; } @@ -31,7 +29,7 @@ export const TextArea = observer(({ port, readOnly }: IField) => { return ( <> - {useDelayed && ( + {settings.delayedUpdate && ( } onClick={() => (port as Input).setValue(val)} diff --git a/packages/graph-editor/src/components/controls/vec3.tsx b/packages/graph-editor/src/components/controls/vec3.tsx deleted file mode 100644 index fdb943e53..000000000 --- a/packages/graph-editor/src/components/controls/vec3.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { IField } from './interface.js'; -import { Input } from '@tokens-studio/graph-engine'; -import { Label, Stack, TextInput } from '@tokens-studio/ui'; -import { observer } from 'mobx-react-lite'; -import React from 'react'; - -export const Vec3field = observer(({ port, readOnly }: IField) => { - const onChange = - (index: number) => (e: React.ChangeEvent) => { - const newArr = [...port.value]; - newArr[index] = e.target.value; - - (port as Input).setValue(newArr); - }; - - return ( - - - - - - - - - ); -}); diff --git a/packages/graph-editor/src/components/debugger/data.ts b/packages/graph-editor/src/components/debugger/data.ts deleted file mode 100644 index d776ca27a..000000000 --- a/packages/graph-editor/src/components/debugger/data.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { - CustomTimeLineAction, - CustomTimelineRow, -} from '@/components/debugger/index.js'; -import { action, computed, makeObservable, observable } from 'mobx'; - -export interface IDebugInfo { - rows?: CustomTimelineRow[]; -} - -export class DebugInfo { - _rows: CustomTimelineRow[] = []; - offset = 0; - first = true; - - constructor(init?: IDebugInfo) { - const { rows = [] } = init || {}; - - makeObservable(this, { - _rows: observable.ref, - rows: computed, - addRow: action, - addAction: action, - }); - - this._rows = rows; - } - - get rows() { - return this._rows; - } - - addRow(row: CustomTimelineRow) { - this._rows = [...this._rows, row]; - } - - addAction(rowId: string, action: CustomTimeLineAction) { - if (this.first) { - this.first = false; - this.offset = action.start; - } - - this._rows = this._rows.map((row) => { - if (row.id === rowId) { - return { - ...row, - actions: [ - ...row.actions, - { - ...action, - start: action.start - this.offset, - end: action.end - this.offset, - }, - ], - }; - } - return row; - }); - } - - clear() { - this.first = true; - this._rows = []; - } -} - -export const debugInfo = new DebugInfo(); diff --git a/packages/graph-editor/src/components/debugger/index.css b/packages/graph-editor/src/components/debugger/index.css deleted file mode 100644 index c71ff0343..000000000 --- a/packages/graph-editor/src/components/debugger/index.css +++ /dev/null @@ -1,89 +0,0 @@ -.timeline-editor-time-unit-scale { - color: var(--color-neutral-canvas-default-fg-default) !important -} - -.timeline-editor-time-unit { - border-right: 1px solid var(--color-neutral-stroke-subtle); -} - -.timeline-editor-action { - background-color: unset; -} - -.timeline-editor-time-area .ReactVirtualized__Grid{ - overflow: hidden !important; -} - -.timeline-editor { - font-family: var(--font-body-md) !important; - height: 100% !important; - width: unset !important; - flex: 1; - background-color: inherit !important; -} - -.timeline-editor-edit-row { - background: var(--color-neutral-canvas-minimal-bg); -} - -.timeline-player { - height: 32px; - - padding: 0 10px; - display: flex; - flex-direction: row; - align-items: center; - background-color: #3a3a3a; - color: #ddd; -} - -.timeline-player .play-control { - width: 24px; - height: 24px; - border-radius: 4px; - display: flex; - background-color: #666; - justify-content: center; - align-items: center; -} - -.timeline-player .time { - font-size: 12px; - margin: 0 20px; - width: 70px; -} - -.timeline-player.rate-control { - justify-self: flex-end; -} - - -.timeline-list { - min-width: 150px; - margin-top: 42px; - height: 258px; - flex: 0 1 auto; - overflow-y: auto; - box-sizing: border-box; - overflow-x: clip; -} - -.timeline-list-item { - height: 32px; - align-items: center; - width: 100%; - display: flex; - padding: 0 10px; - box-sizing: border-box; -} - -.timeline-list-item:hover { - background-color: var(--colors-focus); -} - -.timeline-list-item .text { - height: 28px; - width: 100%; - padding-left: 10px; - border-radius: 4px; -} \ No newline at end of file diff --git a/packages/graph-editor/src/components/debugger/index.stories.tsx b/packages/graph-editor/src/components/debugger/index.stories.tsx deleted file mode 100644 index 324bdad8e..000000000 --- a/packages/graph-editor/src/components/debugger/index.stories.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { CustomTimelineRow, Debugger } from './index.js'; -import { DebugInfo } from './data.js'; -import { TimelineEffect } from '@xzdarcy/react-timeline-editor'; -import React, { useMemo } from 'react'; -import type { Meta, StoryObj } from '@storybook/react'; - -const meta: Meta = { - title: 'Components/Debugger', - component: Debugger, -}; - -const mockEffect: Record = { - effect0: { - id: 'effect0', - name: 'foo', - }, - effect1: { - id: 'effect1', - name: 'bar', - }, -}; - -const mockRows: CustomTimelineRow[] = new Array(20).fill(0).map((_, i) => { - return { - id: i.toString(), - name: 'Row ' + i, - actions: new Array(1).fill(0).map((y) => { - return { - id: `${i}-${y}`, - start: i * 2, - effectId: 'effect0', - end: i * 2 + 2, - }; - }), - }; -}); - -export default meta; -type Story = StoryObj; -export const Default: Story = { - render: () => { - // eslint-disable-next-line react-hooks/rules-of-hooks - const mockData = useMemo(() => { - return new DebugInfo({ - rows: mockRows, - }); - }, []); - - return ; - }, - args: {}, -}; diff --git a/packages/graph-editor/src/components/debugger/index.tsx b/packages/graph-editor/src/components/debugger/index.tsx deleted file mode 100644 index c9fefa1ae..000000000 --- a/packages/graph-editor/src/components/debugger/index.tsx +++ /dev/null @@ -1,182 +0,0 @@ -import './index.css'; -import { DebugInfo, debugInfo } from './data.js'; -import { Stack, Text } from '@tokens-studio/ui'; -import { - Timeline, - TimelineAction, - TimelineEffect, - TimelineRow, - TimelineState, -} from '@xzdarcy/react-timeline-editor'; -import { observer } from 'mobx-react-lite'; -import { useGraph } from '@/hooks/useGraph.js'; -import React, { MutableRefObject, useEffect, useRef, useState } from 'react'; -import TimelinePlayer from './player.js'; - -export interface CustomTimelineRow extends TimelineRow { - name: string; - actions: CustomTimeLineAction[]; -} - -export interface CustomTimeLineAction extends TimelineAction { - /** - * The color to render the action - */ - color?: string; -} - -export interface DebuggerProps { - data: DebugInfo; - effects: Record; -} - -const DebuggerInner = observer( - /** @ts-expect-error observer not typed here...? */ - ({ data, domRef, timeline, scale }) => { - return ( - -
{ - const target = e.target as HTMLDivElement; - timeline.current?.setScrollTop(target.scrollTop); - }} - className={'timeline-list'} - > - {data.rows.map((item) => { - return ( -
- {item.name} -
- ); - })} -
- {}} - disableDrag - onScroll={({ scrollTop }) => { - if (domRef.current) { - domRef.current.scrollTop = scrollTop; - } - }} - autoScroll={true} - scaleSplitCount={~~(scale / 10)} - // scale={scale} - scaleWidth={scale} - ref={timeline} - effects={{}} - getActionRender={(action, row) => { - return ( - - ); - }} - /> -
- ); - }, -); - -export const Debugger = ({ data }: DebuggerProps) => { - const timelineState = - useRef() as MutableRefObject; - const autoScrollWhenPlay = useRef(true); - const domRef = useRef(); - - const graph = useGraph(); - - useEffect(() => { - if (!graph) { - return; - } - - const NodeStartListener = graph.on('nodeExecuted', (run) => { - const existing = debugInfo.rows.find((x) => x.id == run.node.id); - - if (!existing) { - debugInfo.addRow({ - id: run.node.id, - name: run.node.factory.type, - actions: [], - }); - } - - //Now we need to add the actions - debugInfo.addAction(run.node.id, { - id: `${run.node.id}-${Date.now()}`, - start: run.start, - end: run.end, - effectId: 'effect0', - }); - }); - - return () => { - NodeStartListener(); - }; - }, [graph]); - - const [scale, setScale] = useState(100); - - return ( - - - - - ); -}; - -const colors = [ - '#F94144', - '#F3722C', - '#F8961E', - '#F9844A', - '#F9C74F', - '#90BE6D', - '#43AA8B', - '#577590', - '#277DA1', - '#2A9D8F', - '#2B9348', -]; - -export const CustomRender0: React.FC<{ - action: CustomTimeLineAction; - row: TimelineRow; - index: number; -}> = ({ action, index }) => { - return ( - - {Math.round(action.end - action.start)}ms - - ); -}; diff --git a/packages/graph-editor/src/components/debugger/player.tsx b/packages/graph-editor/src/components/debugger/player.tsx deleted file mode 100644 index d27be0cbd..000000000 --- a/packages/graph-editor/src/components/debugger/player.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import { DebugInfo } from './data.js'; -import { IconButton, Select, Stack, Text } from '@tokens-studio/ui'; -import { TimelineState } from '@xzdarcy/react-timeline-editor'; -import Pause from '@tokens-studio/icons/Pause.js'; -import Play from '@tokens-studio/icons/Play.js'; -import React, { useEffect, useState } from 'react'; -import Trash from '@tokens-studio/icons/Trash.js'; -import UndoAction from '@tokens-studio/icons/UndoAction.js'; -import ZoomIn from '@tokens-studio/icons/ZoomIn.js'; -import ZoomOut from '@tokens-studio/icons/ZoomOut.js'; - -export const scaleWidth = 160; -export const scale = 5; -export const startLeft = 20; - -export const Rates = [0.2, 0.5, 1.0, 1.5, 2.0]; - -export interface TimelinePlayerProps { - timelineState: React.MutableRefObject | undefined; - autoScrollWhenPlay: React.MutableRefObject; - setScale: React.Dispatch>; - data: DebugInfo; -} - -const TimelinePlayer = (props: TimelinePlayerProps) => { - const { timelineState, autoScrollWhenPlay, setScale, data } = props; - const [isPlaying, setIsPlaying] = useState(false); - const [time, setTime] = useState(0); - const [rate, setRate] = useState(1); - - useEffect(() => { - if (!timelineState?.current) return; - const engine = timelineState.current; - engine.listener.on('play', () => setIsPlaying(true)); - engine.listener.on('paused', () => setIsPlaying(false)); - engine.listener.on('afterSetTime', ({ time }) => setTime(time)); - engine.listener.on('setTimeByTick', ({ time }) => { - setTime(time); - - if (autoScrollWhenPlay.current) { - const autoScrollFrom = 500; - const left = time * (scaleWidth / scale) + startLeft - autoScrollFrom; - timelineState.current.setScrollLeft(left); - } - }); - - return () => { - if (!engine) return; - engine.pause(); - engine.listener.offAll(); - }; - }, []); - - const handlePlayOrPause = () => { - if (!timelineState?.current) return; - if (timelineState.current.isPlaying) { - timelineState.current.pause(); - } else { - timelineState.current.play({ autoEnd: true }); - } - }; - - const handleRateChange = (rate: string) => { - if (!timelineState?.current) return; - const asNum = parseFloat(rate); - setRate(asNum); - timelineState.current.setPlayRate(asNum); - }; - - const timeRender = (time: number) => { - const float = (parseInt((time % 1) * 100 + '') + '').padStart(2, '0'); - const min = (parseInt(time / 60 + '') + '').padStart(2, '0'); - const second = (parseInt((time % 60) + '') + '').padStart(2, '0'); - return <>{`${min}:${second}.${float.replace('0.', '')}`}; - }; - - const reset = () => { - if (!timelineState?.current) return; - timelineState.current.setTime(0); - }; - const zoomIn = () => { - setScale((prev) => prev * 1.2); - }; - const zoomOut = () => { - setScale((prev) => prev * 0.8); - }; - const trash = () => { - data.clear(); - }; - - return ( - - : } - onClick={handlePlayOrPause} - emphasis="high" - > - - - } onClick={reset}> - } onClick={() => zoomIn()}> - } onClick={() => zoomOut()}> - } onClick={() => trash()}> - {timeRender(time)} - - ); -}; - -export default TimelinePlayer; diff --git a/packages/graph-editor/src/components/dialogs/findDialog.tsx b/packages/graph-editor/src/components/dialogs/findDialog.tsx index a16e4d58a..07fad3b88 100644 --- a/packages/graph-editor/src/components/dialogs/findDialog.tsx +++ b/packages/graph-editor/src/components/dialogs/findDialog.tsx @@ -1,10 +1,8 @@ import { Button, Dialog, IconButton, Text, TextInput } from '@tokens-studio/ui'; import { title as annotatedTitle } from '@/annotations/index.js'; -import { - graphEditorSelector, - showSearchSelector, -} from '@/redux/selectors/index.js'; -import { useDispatch, useGraph } from '@/hooks/index.js'; +import { graphEditorSelector } from '@/redux/selectors/index.js'; +import { useFrame } from '@/system/frame/hook.js'; +import { useGraph } from '@/hooks/index.js'; import { useSelector } from 'react-redux'; import React from 'react'; import Xmark from '@tokens-studio/icons/Xmark.js'; @@ -13,12 +11,11 @@ export const FindDialog = () => { const [id, setId] = React.useState(''); const [title, setTitle] = React.useState(''); const localGraph = useGraph(); - const dispatch = useDispatch(); const graph = useSelector(graphEditorSelector); - const open = useSelector(showSearchSelector); + const frame = useFrame(); const setOpen = (value: boolean) => { - dispatch.settings.setShowSearch(value); + frame.settings.setShowSearch(value); }; const onClick = () => { @@ -65,7 +62,7 @@ export const FindDialog = () => { }; return ( - + diff --git a/packages/graph-editor/src/components/flow/edges/edge.tsx b/packages/graph-editor/src/components/flow/edges/edge.tsx index e86b056e8..879169763 100644 --- a/packages/graph-editor/src/components/flow/edges/edge.tsx +++ b/packages/graph-editor/src/components/flow/edges/edge.tsx @@ -1,14 +1,14 @@ -import { EdgeType } from '../../../redux/models/settings.js'; +import { EdgeType, SystemSettings } from '@/system/frame/settings.js'; import { Port } from '@tokens-studio/graph-engine'; -import { edgeType as edgeTypeSelector } from '../../../redux/selectors/settings.js'; import { getBetterBezierPath } from './offsetBezier.js'; import { getSimpleBezierPath, getSmoothStepPath, getStraightPath, } from 'reactflow'; +import { observer } from 'mobx-react-lite'; +import { useFrame } from '@/system/frame/hook.js'; import { useLocalGraph } from '@/context/graph.js'; -import { useSelector } from 'react-redux'; import React from 'react'; import colors from '@/tokens/colors.js'; @@ -26,89 +26,112 @@ const extractColor = (port: Port) => { return { color, backgroundColor }; }; -export default function CustomEdge({ - id, - sourceX, - sourceY, - targetX, - targetY, - sourcePosition, - targetPosition, - style = {}, - data, - markerEnd, -}) { - const edgeType = useSelector(edgeTypeSelector); - const graph = useLocalGraph(); - - const edge = graph.getEdge(id); - let col = undefined; - - if (edge) { - const sourceNode = graph.getNode(edge?.source); - - const sourcePort = sourceNode?.outputs[edge?.sourceHandle]; - - if (sourcePort) { - const { backgroundColor } = extractColor(sourcePort as Port); - col = backgroundColor; - } - } +export default (props) => { + const frame = useFrame(); + return ; +}; - let edgeFn; - switch (edgeType) { - case EdgeType.bezier: - edgeFn = getBetterBezierPath; - break; - case EdgeType.simpleBezier: - edgeFn = getSimpleBezierPath; - break; - case EdgeType.smoothStep: - edgeFn = getSmoothStepPath; - break; - case EdgeType.straight: - edgeFn = getStraightPath; - break; - default: - edgeFn = getBetterBezierPath; - } +export interface ICustomEdge { + settings: SystemSettings; + id: string; + sourceX: number; + sourceY: number; + targetX: number; + targetY: number; + sourcePosition: object; + targetPosition: object; + style: object; + data?: { + text: string; + }; + markerEnd: string; +} - const [edgePath] = edgeFn({ +export const CustomEdgeInner = observer( + ({ + settings, + id, sourceX, sourceY, - sourcePosition, targetX, targetY, + sourcePosition, targetPosition, - }); + style = {}, + data, + markerEnd, + }: ICustomEdge) => { + const graph = useLocalGraph(); - return ( - <> - - - - - - {data?.text} - - - - ); -} + const edge = graph.getEdge(id); + let col = undefined; + + if (edge) { + const sourceNode = graph.getNode(edge?.source); + + const sourcePort = sourceNode?.outputs[edge?.sourceHandle]; + + if (sourcePort) { + const { backgroundColor } = extractColor(sourcePort as Port); + col = backgroundColor; + } + } + + let edgeFn; + switch (settings.edgeType) { + case EdgeType.bezier: + edgeFn = getBetterBezierPath; + break; + case EdgeType.simpleBezier: + edgeFn = getSimpleBezierPath; + break; + case EdgeType.smoothStep: + edgeFn = getSmoothStepPath; + break; + case EdgeType.straight: + edgeFn = getStraightPath; + break; + default: + edgeFn = getBetterBezierPath; + } + + const [edgePath] = edgeFn({ + sourceX, + sourceY, + sourcePosition, + targetX, + targetY, + targetPosition, + }); + + return ( + <> + + + + + + {data?.text} + + + + ); + }, +); diff --git a/packages/graph-editor/src/components/flow/handles.tsx b/packages/graph-editor/src/components/flow/handles.tsx index 7d01d8ddf..0efc360f3 100644 --- a/packages/graph-editor/src/components/flow/handles.tsx +++ b/packages/graph-editor/src/components/flow/handles.tsx @@ -84,7 +84,7 @@ export const Handle = (props: HandleProps) => { shouldHideHandles = false, error, backgroundColor, - isArray, + variant, type: dataType, variadic, isAnchor, diff --git a/packages/graph-editor/src/components/flow/nodes/passthroughNode.tsx b/packages/graph-editor/src/components/flow/nodes/passthroughNode.tsx index e456a6501..ea83df2dd 100644 --- a/packages/graph-editor/src/components/flow/nodes/passthroughNode.tsx +++ b/packages/graph-editor/src/components/flow/nodes/passthroughNode.tsx @@ -2,21 +2,20 @@ import { Handle } from '../handles.js'; import { HandleContainer } from '../handles.js'; import { Stack } from '@tokens-studio/ui'; import { extractType, extractTypeIcon } from '../wrapper/nodeV2.js'; -import { icons } from '@/registry/icon.js'; +import { useFrame } from '@/system/frame/hook.js'; import { useLocalGraph } from '@/context/graph.js'; -import { useSelector } from 'react-redux'; import React from 'react'; export const PassthroughNode = (args) => { const { id } = args; const graph = useLocalGraph(); const node = graph.getNode(id); - const iconTypeRegistry = useSelector(icons); + const frame = useFrame(); if (!node) return null; const port = node.inputs.value; - const typeCol = extractTypeIcon(port, iconTypeRegistry); + const typeCol = extractTypeIcon(port, frame.icons); const type = extractType(port.type); return ( diff --git a/packages/graph-editor/src/components/flow/wrapper/nodeV2.tsx b/packages/graph-editor/src/components/flow/wrapper/nodeV2.tsx index 3cf7ee226..a41e4768b 100644 --- a/packages/graph-editor/src/components/flow/wrapper/nodeV2.tsx +++ b/packages/graph-editor/src/components/flow/wrapper/nodeV2.tsx @@ -2,10 +2,13 @@ import { BaseNodeWrapper } from './base.js'; import { COLOR, + CONTROL_PORT, Color, + ControlFlowPort, + DataFlowPort, + DataflowNode, Input, OBJECT, - Port, SchemaObject, annotatedDynamicInputs, annotatedNodeRunning, @@ -14,19 +17,13 @@ import { } from '@tokens-studio/graph-engine'; import { ErrorBoundary } from 'react-error-boundary'; import { ErrorBoundaryContent } from '@/components/ErrorBoundaryContent.js'; -import { Node as GraphNode } from '@tokens-studio/graph-engine'; import { Handle, HandleContainer, useHandle } from '../handles.js'; import { Stack, Text } from '@tokens-studio/ui'; -import { icons, nodeSpecifics } from '@/redux/selectors/registry.js'; -import { - inlineTypes, - inlineValues, - showTimings, -} from '@/redux/selectors/settings.js'; +import { SystemSettings } from '@/system/frame/settings.js'; import { observer } from 'mobx-react-lite'; import { title } from '@/annotations/index.js'; +import { useFrame } from '@/system/frame/hook.js'; import { useLocalGraph } from '@/context/graph.js'; -import { useSelector } from 'react-redux'; import React from 'react'; import clsx from 'clsx'; import colors from '@/tokens/colors.js'; @@ -56,7 +53,8 @@ export type WrappedNodeDefinition = { export const NodeV2 = (args) => { const { id } = args; const graph = useLocalGraph(); - const node = graph.getNode(id); + const node = graph.getNode(id) as DataflowNode; + const frame = useFrame(); if (!node) { return
Node not found
; @@ -64,7 +62,11 @@ export const NodeV2 = (args) => { return ( }> - + ); }; @@ -76,11 +78,41 @@ export interface INodeWrap { title?: string; subtitle?: string; icon?: React.ReactNode; + node: DataflowNode; + settings: SystemSettings; +} + +export interface ISpecificWrapper { + specifics: Record< + string, + React.FC<{ + node: GraphNode; + }> + >; node: GraphNode; } -const NodeWrap = observer(({ node, icon }: INodeWrap) => { - const showTimingsValue = useSelector(showTimings); - const specifics = useSelector(nodeSpecifics); + +export const SpecificWrapper = observer( + ({ specifics, node }: ISpecificWrapper) => { + const Specific = specifics[node.factory.type]; + + if (!Specific) { + return null; + } + return ( + + + + ); + }, +); + +const NodeWrap = observer(({ settings, node, icon }: INodeWrap) => { + const frame = useFrame(); //Check if the input allows for dynamic inputs const isInput = !!( @@ -88,8 +120,6 @@ const NodeWrap = observer(({ node, icon }: INodeWrap) => { node.annotations[annotatedDynamicInputs] ); - const Specific = specifics[node.factory.type]; - return ( { (node.annotations[title] as string) || node.factory.title || 'Node' } subtitle={node.annotations[title] ? node.factory.title : ''} - error={node.error || null} + error={node.dataflow?.error || null} controls={''} style={{ minWidth: '200px' }} > @@ -110,45 +140,44 @@ const NodeWrap = observer(({ node, icon }: INodeWrap) => { style={{ padding: 'var(--component-spacing-md) 0' }} > - + } /> {isInput && } - + } />
- {Specific && ( - - - - )} +
- {showTimingsValue && ( + {settings.showTimings && (
- {node.lastExecutedDuration}ms + {node.dataflow?.lastExecutedDuration}ms
)} ); }); + export interface IPortArray { - ports: Record; + ports: Record; hideNames?: boolean; } export const PortArray = observer(({ ports, hideNames }: IPortArray) => { + const frame = useFrame(); const entries = Object.values(ports).sort(); return ( <> {entries .filter((x) => x.visible != false || x.isConnected) .map((input) => ( - + ))} ); @@ -169,7 +198,7 @@ export const DynamicOutput = () => { }; export const extractTypeIcon = ( - port: Port, + port: DataFlowPort | ControlFlowPort, iconLookup: Record, ) => { let id = port.type.$id || ''; @@ -208,7 +237,7 @@ export const extractType = (schema: SchemaObject) => { return schema.type; }; -export const InlineTypeLabel = ({ port }: { port: Port }) => { +export const InlineTypeLabel = ({ port }: { port: DataFlowPort }) => { //Try lookup the id if possible const typeName = extractType(port.type); const handleInformation = useHandle(); @@ -313,16 +342,27 @@ const getValuePreview = (value, type) => { : valuePreview; }; +export interface IInputHandle { + port: Port; + hideName?: boolean; + inlineTypes: boolean; + inlineValues: boolean; +} + const InputHandle = observer( - ({ port, hideName }: { port: Port; hideName?: boolean }) => { - const inlineTypesValue = useSelector(inlineTypes); - const iconTypeRegistry = useSelector(icons); - const inlineValuesValue = useSelector(inlineValues); - const typeCol = extractTypeIcon(port, iconTypeRegistry); + ({ port, hideName, inlineTypes, inlineValues }: IInputHandle) => { + const frame = useFrame(); + const typeCol = extractTypeIcon(port, frame.icons); const input = port as unknown as Input; const type = extractType(port.type); const handleInformation = useHandle(); + const variant = typeCol.isArray + ? 'array' + : port.pType == CONTROL_PORT + ? 'message' + : undefined; + if (input.variadic) { return ( <> @@ -331,17 +371,18 @@ const InputHandle = observer( type={type} visible={port.visible != false || port.isConnected} id={port.name} + variant={variant} variadic > {!hideName && {port.name} + } - {inlineTypesValue && } + {inlineTypes && } {port._edges.map((edge, i) => { return ( - {inlineValuesValue && ( + {inlineValues && ( )} - {inlineTypesValue && } + {inlineTypes && } ); })} @@ -378,6 +419,7 @@ const InputHandle = observer( return ( {input.name} - {inlineValuesValue && ( + {inlineValues && ( )} - {inlineTypesValue && } + {inlineTypes && } ); }, diff --git a/packages/graph-editor/src/components/hotKeys/index.tsx b/packages/graph-editor/src/components/hotKeys/index.tsx index 7fbb3e2d6..c3e8c7172 100644 --- a/packages/graph-editor/src/components/hotKeys/index.tsx +++ b/packages/graph-editor/src/components/hotKeys/index.tsx @@ -1,20 +1,14 @@ import { HotKeys as HotKeysComp } from 'react-hotkeys'; import { SerializedNode } from '@/types/serializedNode.js'; import { annotatedDeleteable } from '@tokens-studio/graph-engine'; -import { - inlineTypes, - inlineValues, - showGrid, - snapGrid, -} from '@/redux/selectors/settings.js'; import { savedViewports } from '@/annotations/index.js'; import { useAction } from '@/editor/actions/provider.js'; import { useAutoLayout } from '@/editor/hooks/useAutolayout.js'; import { useDispatch } from '@/hooks/useDispatch.js'; +import { useFrame } from '@/system/frame/hook.js'; import { useLocalGraph } from '@/hooks/index.js'; import { useMemo } from 'react'; import { useReactFlow } from 'reactflow'; -import { useSelector } from 'react-redux'; import { useToast } from '@/hooks/useToast.js'; import React from 'react'; import copy from 'copy-to-clipboard'; @@ -72,6 +66,40 @@ export const keyMap = { RECALL_VIEWPORT: ['1', '2', '3', '4', '5', '6', '7', '8', '9'], }; +export const keyMapDescriptions = { + AUTO_LAYOUT: 'Auto Layout', + COPY: 'Copy', + PASTE: 'Paste', + DELETE: 'Delete', + UNDO: 'Undo', + REDO: 'Redo', + SELECT_ALL: 'Select All', + DUPLICATE: 'Duplicate', + GROUP: 'Group', + UNGROUP: 'Ungroup', + SAVE: 'Save', + LOAD: 'Load', + FIND: 'Find', + RESET: 'Reset', + ZOOM_IN: 'Zoom In', + ZOOM_OUT: 'Zoom Out', + ZOOM_RESET: 'Zoom Reset', + TOGGLE_GRID: 'Toggle Grid', + TOGGLE_MINIMAP: 'Toggle Minimap', + TOGGLE_CONTROLS: 'Toggle Controls', + TOGGLE_SIDEBAR: 'Toggle Sidebar', + TOGGLE_HIDE: 'Toggle Hide', + TOGGLE_FULLSCREEN: 'Toggle Fullscreen', + TOGGLE_HELP: 'Toggle Help', + TOGGLE_THEME: 'Toggle Theme', + TOGGLE_SNAP_GRID: 'Toggle Snap Grid', + TOGGLE_NODES_PANEL: 'Toggle Nodes Panel', + TOGGLE_TYPES: 'Toggle Types', + TOGGLE_VALUES: 'Toggle Values', + SAVE_VIEWPORT: 'Save Viewport', + RECALL_VIEWPORT: 'Recall Viewport', +}; + export const getViewports = (graph) => { const viewports = graph.annotations[savedViewports] || new Array(9).fill(null); @@ -79,10 +107,7 @@ export const getViewports = (graph) => { }; export const useHotkeys = () => { - const showGridValue = useSelector(showGrid); - const snapGridValue = useSelector(snapGrid); - const inlineTypesValue = useSelector(inlineTypes); - const inlineValuesValue = useSelector(inlineValues); + const frame = useFrame(); const duplicateNodes = useAction('duplicateNodes'); const deleteNode = useAction('deleteNode'); const copyNodes = useAction('copyNodes'); @@ -232,20 +257,20 @@ export const useHotkeys = () => { }, TOGGLE_GRID: (event) => { event.preventDefault(); - dispatch.settings.setShowGrid(!showGridValue); + frame.settings.setShowGrid(!frame.settings.showGrid); }, FIND: (event) => { event.preventDefault(); dispatch.settings.setShowSearch(true); }, TOGGLE_SNAP_GRID: () => { - dispatch.settings.setSnapGrid(!snapGridValue); + frame.settings.setSnapGrid(!frame.settings.snapGrid); }, TOGGLE_TYPES: () => { - dispatch.settings.setInlineTypes(!inlineTypesValue); + frame.settings.setInlineTypes(!frame.settings.inlineTypes); }, TOGGLE_VALUES: () => { - dispatch.settings.setInlineValues(!inlineValuesValue); + dispatch.settings.setInlineValues(!frame.settings.inlineValues); }, }), @@ -257,10 +282,7 @@ export const useHotkeys = () => { graph, layout, reactFlowInstance, - showGridValue, - snapGridValue, - inlineTypesValue, - inlineValuesValue, + frame.settings, trigger, ], ); diff --git a/packages/graph-editor/src/components/index.ts b/packages/graph-editor/src/components/index.ts index d0d76dbf5..af58c8915 100644 --- a/packages/graph-editor/src/components/index.ts +++ b/packages/graph-editor/src/components/index.ts @@ -3,7 +3,6 @@ export * from './commandPalette/index.js'; export * from './contextMenus/index.js'; export * from './controls/index.js'; export * from './curveEditor/index.js'; -export * from './debugger/index.js'; export * from './dialogs/findDialog.js'; export * from './flow/index.js'; export * from './hotKeys/index.js'; diff --git a/packages/graph-editor/src/components/menubar/defaults.tsx b/packages/graph-editor/src/components/menubar/defaults.tsx index 1d54ed83f..5e573fc93 100644 --- a/packages/graph-editor/src/components/menubar/defaults.tsx +++ b/packages/graph-editor/src/components/menubar/defaults.tsx @@ -1,15 +1,12 @@ import { AlignmentPanel } from '../panels/alignment/index.js'; -import { DebugPanel } from '../panels/debugger/index.js'; import { DropPanel } from '../panels/dropPanel/index.js'; import { GraphPanel } from '../panels/graph/index.js'; import { ImperativeEditorRef } from '@/editor/editorTypes.js'; -import { Inputsheet } from '../panels/inputs/index.js'; import { Legend } from '../panels/legend/index.js'; import { LogsPanel } from '../panels/logs/index.js'; import { Menu, MenuItem, Seperator, SubMenu } from './data.js'; import { MenuItemElement } from './menuItem.js'; import { NodeSettingsPanel } from '../panels/nodeSettings/index.js'; -import { OutputSheet } from '../panels/output/index.js'; import { PlayPanel } from '../panels/play/index.js'; import { Settings } from '../panels/settings/index.js'; import { TabData } from 'rc-dock'; @@ -223,18 +220,6 @@ export const defaultMenuDataFactory = (): Menu => name: 'window', title: 'Window', items: [ - windowButton({ - name: 'inputs', - id: 'inputs', - title: 'Inputs', - content: , - }), - windowButton({ - name: 'outputs', - id: 'outputs', - title: 'Outputs', - content: , - }), windowButton({ name: 'nodeSettings', id: 'nodeSettings', @@ -274,12 +259,6 @@ export const defaultMenuDataFactory = (): Menu => title: 'Alignment + Distribution', content: , }), - windowButton({ - name: 'debugger', - id: 'debugger', - title: 'Debugger', - content: , - }), windowButton({ name: 'settings', id: 'settings', diff --git a/packages/graph-editor/src/components/panels/debugger/index.tsx b/packages/graph-editor/src/components/panels/debugger/index.tsx deleted file mode 100644 index 0c24234b7..000000000 --- a/packages/graph-editor/src/components/panels/debugger/index.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react'; - -import { Debugger } from '@/components/debugger/index.js'; -import { debugInfo } from '../../debugger/data.js'; - -export const DebugPanel = () => { - return ; -}; diff --git a/packages/graph-editor/src/components/panels/dropPanel/DragItem.tsx b/packages/graph-editor/src/components/panels/dropPanel/DragItem.tsx index e77c5896c..f61f31ae3 100644 --- a/packages/graph-editor/src/components/panels/dropPanel/DragItem.tsx +++ b/packages/graph-editor/src/components/panels/dropPanel/DragItem.tsx @@ -1,4 +1,5 @@ import { GrabberIcon } from '@/components/icons/GrabberIcon.js'; +import { NodeEntry } from './NodeEntry.js'; import { NodeHoverCard } from '@/components/NodeHoverCard.js'; import React, { useCallback } from 'react'; import styles from './DragItem.module.css'; @@ -6,8 +7,7 @@ import styles from './DragItem.module.css'; type DragItemProps = { data?: unknown; type: string; - children: React.ReactNode; - title?: string; + title: string; description?: string; docs?: string; icon?: React.ReactNode | string; @@ -20,7 +20,6 @@ export const DragItem = ({ description, icon, docs, - children, ...rest }: DragItemProps) => { const [isDragging, setIsDragging] = React.useState(false); @@ -62,7 +61,7 @@ export const DragItem = ({ - {children} + diff --git a/packages/graph-editor/src/components/panels/dropPanel/NodeEntry.tsx b/packages/graph-editor/src/components/panels/dropPanel/NodeEntry.tsx index 23118af8e..cbfdc0f5e 100644 --- a/packages/graph-editor/src/components/panels/dropPanel/NodeEntry.tsx +++ b/packages/graph-editor/src/components/panels/dropPanel/NodeEntry.tsx @@ -1,39 +1,18 @@ -import { Text } from '@tokens-studio/ui'; +import { Text } from '@tokens-studio/ui/Text.js'; import React from 'react'; +import styles from './nodeEntry.module.css'; -export const NodeEntry = ({ - icon, - text, -}: { +export interface INodeEntry { icon?: React.ReactNode | string; text: string; -}) => { +} + +export const NodeEntry = ({ icon, text }: INodeEntry) => { return ( <> - {icon && ( -
- {icon} -
- )} + {icon &&
{icon}
} - + {text} diff --git a/packages/graph-editor/src/components/panels/dropPanel/dropPanel.module.css b/packages/graph-editor/src/components/panels/dropPanel/dropPanel.module.css index 0adbdf244..f6574c606 100644 --- a/packages/graph-editor/src/components/panels/dropPanel/dropPanel.module.css +++ b/packages/graph-editor/src/components/panels/dropPanel/dropPanel.module.css @@ -7,6 +7,19 @@ flex-direction: column; } +.vertical { + padding-top: var(--component-spacing-xs); + width: 100%; + flex: 1; + overflow: auto; + box-sizing: border-box; +} + +.search { + padding: 0 var(--component-spacing-md); + padding-top: var(--component-spacing-lg) +} + .accordion { width: 100%; display: flex; @@ -17,10 +30,12 @@ border: 0; } } + .accordionItem { border: 0; } + .accordionTrigger { display: flex; align-items: center; diff --git a/packages/graph-editor/src/components/panels/dropPanel/dropPanel.tsx b/packages/graph-editor/src/components/panels/dropPanel/dropPanel.tsx index 3377567e0..b5b71f924 100644 --- a/packages/graph-editor/src/components/panels/dropPanel/dropPanel.tsx +++ b/packages/graph-editor/src/components/panels/dropPanel/dropPanel.tsx @@ -1,10 +1,8 @@ import { Accordion, Stack, TextInput } from '@tokens-studio/ui'; import { DragItem } from './DragItem.js'; import { DropPanelStore } from './data.js'; -import { NodeEntry } from './NodeEntry.js'; import { observer } from 'mobx-react-lite'; -import { panelItemsSelector } from '@/redux/selectors/registry.js'; -import { useSelector } from 'react-redux'; +import { useFrame } from '@/system/frame/hook.js'; import NavArrowRight from '@tokens-studio/icons/NavArrowRight.js'; import React, { useState } from 'react'; import styles from './dropPanel.module.css'; @@ -24,8 +22,8 @@ export interface IDropPanel { } export const DropPanel = () => { - const data = useSelector(panelItemsSelector); - return ; + const frame = useFrame(); + return ; }; export const DropPanelInner = observer(({ data }: IDropPanel) => { @@ -43,25 +41,8 @@ export const DropPanelInner = observer(({ data }: IDropPanel) => { return (
- - + + @@ -73,14 +54,13 @@ export const DropPanelInner = observer(({ data }: IDropPanel) => { .map((item) => ( - - + /> )); if (filteredValues.length === 0) { diff --git a/packages/graph-editor/src/components/panels/dropPanel/index.ts b/packages/graph-editor/src/components/panels/dropPanel/index.ts index c0c286a75..14952d6c2 100644 --- a/packages/graph-editor/src/components/panels/dropPanel/index.ts +++ b/packages/graph-editor/src/components/panels/dropPanel/index.ts @@ -1,2 +1,3 @@ export * from './dropPanel.js'; export * from './data.js'; +export * from './DragItem.js'; diff --git a/packages/graph-editor/src/components/panels/dropPanel/nodeEntry.module.css b/packages/graph-editor/src/components/panels/dropPanel/nodeEntry.module.css new file mode 100644 index 000000000..1a0742e93 --- /dev/null +++ b/packages/graph-editor/src/components/panels/dropPanel/nodeEntry.module.css @@ -0,0 +1,15 @@ +.icon { + color: var(--color-neutral-canvas-default-fg-subtle); + width: var(--size-100); + height: var(--size-100); + display: flex; + align-items: center; + justify-content: center; + font: var(--font-body-sm); +} + +.title { + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; +} \ No newline at end of file diff --git a/packages/graph-editor/src/components/panels/index.tsx b/packages/graph-editor/src/components/panels/index.tsx index af9aad22d..a883ead65 100644 --- a/packages/graph-editor/src/components/panels/index.tsx +++ b/packages/graph-editor/src/components/panels/index.tsx @@ -1,5 +1,4 @@ export * from './alignment/index.js'; -export * from './debugger/index.js'; export * from './dropPanel/index.js'; export * from './graph/index.js'; export * from './inputs/index.js'; diff --git a/packages/graph-editor/src/components/panels/inputs/dynamicInputs.tsx b/packages/graph-editor/src/components/panels/inputs/dynamicInputs.tsx index e8cc5e78d..c7ab04606 100644 --- a/packages/graph-editor/src/components/panels/inputs/dynamicInputs.tsx +++ b/packages/graph-editor/src/components/panels/inputs/dynamicInputs.tsx @@ -1,7 +1,6 @@ import { Button, Checkbox, - Heading, Label, Select, Stack, @@ -77,7 +76,6 @@ export const DynamicInputs = observer(({ node }: { node: Node }) => { return ( - Add Input - - - - {selectedNode.factory.title} - - - -
- {dynamicInputs && } + + {dynamicInputs && } - {SpecificInput ? : null} - - {/* The purpose of the key is to invalidate the port panel if the selected node changes */} - - -
+ {SpecificInput ? : null} + + {/* The purpose of the key is to invalidate the port panel if the selected node changes */} + -
+
); } diff --git a/packages/graph-editor/src/components/panels/legend/index.tsx b/packages/graph-editor/src/components/panels/legend/index.tsx index 2fed1030f..b27d1159f 100644 --- a/packages/graph-editor/src/components/panels/legend/index.tsx +++ b/packages/graph-editor/src/components/panels/legend/index.tsx @@ -1,12 +1,11 @@ import { Stack, Text } from '@tokens-studio/ui'; -import { icons } from '@/redux/selectors/registry.js'; -import { useSelector } from 'react-redux'; +import { useFrame } from '@/system/frame/hook.js'; import React, { useMemo } from 'react'; import colors from '@/tokens/colors.js'; export const Legend = () => { - const iconsRegistry = useSelector(icons); - return ; + const frame = useFrame(); + return ; }; export interface ILegendInner { diff --git a/packages/graph-editor/src/components/panels/output/index.tsx b/packages/graph-editor/src/components/panels/output/index.tsx index 8621fbf08..1256baf77 100644 --- a/packages/graph-editor/src/components/panels/output/index.tsx +++ b/packages/graph-editor/src/components/panels/output/index.tsx @@ -1,11 +1,12 @@ -import { Heading, Stack } from '@tokens-studio/ui'; import { Node } from '@tokens-studio/graph-engine'; import { PortPanel } from '@/components/portPanel/index.js'; +import { Stack } from '@tokens-studio/ui/Stack.js'; import { currentNode } from '@/redux/selectors/graph.js'; import { observer } from 'mobx-react-lite'; import { useGraph } from '@/hooks/useGraph.js'; import { useSelector } from 'react-redux'; import React, { useMemo } from 'react'; +import styles from './styles.module.css'; export function OutputSheet() { const graph = useGraph(); @@ -23,43 +24,10 @@ export function OutputSheet() { */ const OutputSheetObserver = observer(({ node }: { node: Node }) => { return ( -
- - - - {node.factory.title} - - - -
- - - -
+ + + -
+ ); }); diff --git a/packages/graph-editor/src/components/panels/output/styles.module.css b/packages/graph-editor/src/components/panels/output/styles.module.css new file mode 100644 index 000000000..61e2800ca --- /dev/null +++ b/packages/graph-editor/src/components/panels/output/styles.module.css @@ -0,0 +1,10 @@ +.outer { + height: 100%; + flex: 1; + padding: var(--component-spacing-md); +} + +.inner { + padding-top: var(--component-spacing-md); + padding-bottom: var(--component-spacing-md); +} \ No newline at end of file diff --git a/packages/graph-editor/src/components/panels/settings/index.tsx b/packages/graph-editor/src/components/panels/settings/index.tsx index 5b7659377..04b14d41a 100644 --- a/packages/graph-editor/src/components/panels/settings/index.tsx +++ b/packages/graph-editor/src/components/panels/settings/index.tsx @@ -1,17 +1,9 @@ import { Checkbox, Label, Select, Stack, Text } from '@tokens-studio/ui'; -import { EdgeType, LayoutType } from '@/redux/models/settings.js'; -import { - connectOnClickSelector, - delayedUpdateSelector, - edgeType, - inlineTypes, - inlineValues, - layoutType, - showMinimapSelector, - showTimings, -} from '@/redux/selectors/settings.js'; +import { EdgeType, LayoutType, SystemSettings } from '@/system/frame/settings.js'; import { contextMenuSelector } from '@/redux/selectors/ui.js'; +import { observer } from 'mobx-react-lite'; import { useDispatch } from '@/hooks/useDispatch.js'; +import { useFrame } from '@/system/frame/hook.js'; import { useSelector } from 'react-redux'; import React from 'react'; @@ -19,173 +11,174 @@ const EdgeValues = Object.values(EdgeType); const LayoutValues = Object.values(LayoutType); export const Settings = () => { - const edgeTypeValue = useSelector(edgeType); - const layoutTypeValue = useSelector(layoutType); - const showTimingsValue = useSelector(showTimings); - const inlineTypesValue = useSelector(inlineTypes); - const inlineValuesValue = useSelector(inlineValues); - const delayedUpdateValue = useSelector(delayedUpdateSelector); - const connectOnClick = useSelector(connectOnClickSelector); - const contextMenus = useSelector(contextMenuSelector); - const showMinimap = useSelector(showMinimapSelector); - const dispatch = useDispatch(); + const frame = useFrame(); - return ( -
- - - - dispatch.settings.setInlineTypes(Boolean(checked)) - } - checked={inlineTypesValue} - /> - - - - Adds additional labels to help differentiate types for colorblind - users. - - - - - - dispatch.settings.setInlineValues(Boolean(checked)) - } - checked={inlineValuesValue} - /> - - - - Shows values directly on the node. Useful for debugging but can be - cluttered. - - - - - - dispatch.settings.setDelayedUpdate(Boolean(checked)) - } - checked={delayedUpdateValue} - /> - - - - Forces a user to click save to update port. - + return ; +}; + +export const SettingsInner = observer( + ({ settings }: { settings: SystemSettings }) => { + const contextMenus = useSelector(contextMenuSelector); + const dispatch = useDispatch(); + + return ( +
+ + + + settings.setInlineTypes(Boolean(checked)) + } + checked={settings.inlineTypes} + /> + + + + Adds additional labels to help differentiate types for + colorblind users. + + - - - - dispatch.settings.setConnectOnClick(Boolean(checked)) - } - checked={connectOnClick} - /> - - - - Allows you to quick connect nodes by clicking on the 2 port. - + + + settings.setInlineValues(Boolean(checked)) + } + checked={settings.inlineValues} + /> + + + + Shows values directly on the node. Useful for debugging but can + be cluttered. + + - - - - dispatch.settings.setShowTimings(Boolean(checked)) - } - checked={showTimingsValue} - /> - - - - Shows how long it takes for a node to process. - + + + settings.setDelayedUpdate(Boolean(checked)) + } + checked={settings.delayedUpdate} + /> + + + + Forces a user to click save to update port. + + - - - - dispatch.settings.setShowMinimap(Boolean(checked)) - } - checked={showMinimap} - /> - - - - Shows the minimap in the graph editing area - + + + settings.setConnectOnClick(Boolean(checked)) + } + checked={settings.connectOnClick} + /> + + + + Allows you to quick connect nodes by clicking on the 2 port. + + - - - - dispatch.ui.setContextMenus(Boolean(checked)) - } - checked={contextMenus} - /> - - - - Provides right click context menus. - + + + settings.setShowTimings(Boolean(checked)) + } + checked={settings.showTimings} + /> + + + + Shows how long it takes for a node to process. + + - - -
- + checked={settings.showMinimap} + /> + + + + Shows the minimap in the graph editing area + +
-
- + checked={contextMenus} + /> + + + + Provides right click context menus. + + +
+ + +
+ +
+ +
+ +
-
-
- ); -}; +
+ ); + }, +); diff --git a/packages/graph-editor/src/components/panels/shortcuts/index.tsx b/packages/graph-editor/src/components/panels/shortcuts/index.tsx new file mode 100644 index 000000000..54f84127e --- /dev/null +++ b/packages/graph-editor/src/components/panels/shortcuts/index.tsx @@ -0,0 +1,62 @@ +import { Box, Heading, Stack, Text } from '@tokens-studio/ui'; +import { keyMap, keyMapDescriptions } from '../../hotKeys/index.js'; +import { styled } from '@/lib/stitches/index.js'; +import { useMemo } from 'react'; +import React from 'react'; + +const Code = styled('code', { + color: '$fgSubtle', + padding: '$1', + border: '1px solid $borderSubtle', +}); + +export const ShortcutsPanel = () => { + const entries = useMemo(() => { + return Object.entries(keyMap).map(([id, triggers]) => { + //Get the description + const description = keyMapDescriptions[id]; + //Get the first trigger + const trigger = Array.isArray(triggers) ? triggers[0] : triggers; + + //Now split the trigger + const pieces = trigger.split('+'); + + return ( + + + {/* Place a + between the values */} + {pieces.map((piece, index) => ( + + {index > 0 && + } + {piece} + + ))} + + {description} + + ); + }); + }, []); + + return ( + + Keyboard Shortcuts +
+ + {entries} + +
+ ); +}; diff --git a/packages/graph-editor/src/components/panels/unified/index.tsx b/packages/graph-editor/src/components/panels/unified/index.tsx new file mode 100644 index 000000000..a740cff91 --- /dev/null +++ b/packages/graph-editor/src/components/panels/unified/index.tsx @@ -0,0 +1,59 @@ +import { + Accordion, + Heading, + IconButton, + Separator, + Stack, +} from '@tokens-studio/ui'; +import { InfoCircle } from 'iconoir-react'; +import { Inputsheet } from '../inputs/index.js'; +import { OutputSheet } from '../output/index.js'; +import { currentNode } from '@/redux/selectors/graph.js'; +import { useGraph } from '@/hooks/useGraph.js'; +import { useSelector } from 'react-redux'; +import React, { useMemo } from 'react'; +import styles from './styles.module.css'; + +export function UnifiedSheet() { + const graph = useGraph(); + const nodeID = useSelector(currentNode); + const selectedNode = useMemo(() => graph?.getNode(nodeID), [graph, nodeID]); + + if (!selectedNode) { + return <>; + } + + return ( +
+ + + + {selectedNode.factory.title} + } + /> + + + + + Inputs + + + + + + + + Outputs + + + + + + + +
+ ); +} diff --git a/packages/graph-editor/src/components/panels/unified/styles.module.css b/packages/graph-editor/src/components/panels/unified/styles.module.css new file mode 100644 index 000000000..1569b3c66 --- /dev/null +++ b/packages/graph-editor/src/components/panels/unified/styles.module.css @@ -0,0 +1,14 @@ +.container { + height: 100%; + width: 100%; + flex: 1; + display: flex; + overflow: auto; + flex-direction: column; +} + +.inner { + height: 100%; + flex: 1; + padding: var(--component-spacing-md); +} \ No newline at end of file diff --git a/packages/graph-editor/src/components/portPanel/index.tsx b/packages/graph-editor/src/components/portPanel/index.tsx index 4fcc84818..c04de2b30 100644 --- a/packages/graph-editor/src/components/portPanel/index.tsx +++ b/packages/graph-editor/src/components/portPanel/index.tsx @@ -1,3 +1,4 @@ +import { DATAFLOW_PORT, DataFlowPort, Port } from '@tokens-studio/graph-engine'; import { DropdownMenu, IconButton, @@ -5,28 +6,30 @@ import { Stack, Tooltip, } from '@tokens-studio/ui'; -import { Port as GraphPort } from '@tokens-studio/graph-engine'; -import { Input } from '@tokens-studio/graph-engine'; -import { observer } from 'mobx-react-lite'; -import { useSelector } from 'react-redux'; -import React, { useCallback, useMemo } from 'react'; - import { IField } from '@/components/controls/interface.js'; import { InlineTypeLabel } from '@/components/flow/index.js'; -import { controls } from '@/redux/selectors/registry.js'; +import { Input } from '@tokens-studio/graph-engine'; import { deletable, hidden, resetable } from '@/annotations/index.js'; +import { observer } from 'mobx-react-lite'; +import { useFrame } from '@/system/frame/hook.js'; import { useGraph } from '@/hooks/useGraph.js'; import Download from '@tokens-studio/icons/Download.js'; import Eye from '@tokens-studio/icons/Eye.js'; import EyeClosed from '@tokens-studio/icons/EyeClosed.js'; import MoreVert from '@tokens-studio/icons/MoreVert.js'; import Puzzle from '@tokens-studio/icons/Puzzle.js'; +import React, { useCallback, useMemo } from 'react'; import Undo from '@tokens-studio/icons/Undo.js'; import Xmark from '@tokens-studio/icons/Xmark.js'; import copy from 'copy-to-clipboard'; export interface IPortPanel { - ports: Record; + ports: Record; + readOnly?: boolean; +} + +export interface IPort { + port: GraphPort; readOnly?: boolean; } @@ -34,32 +37,39 @@ export const PortPanel = observer(({ ports, readOnly }: IPortPanel) => { const entries = Object.values(ports).sort(); return ( - + {entries .filter((x) => !x.annotations[hidden]) .map((x) => ( - + ))} ); }); -export const Port = observer(({ port, readOnly: isReadOnly }: IField) => { +export const Port = observer(({ port, readOnly: isReadOnly }: IPort) => { const readOnly = isReadOnly || port.isConnected; - const controlSelector = useSelector(controls); + const frame = useFrame(); const graph = useGraph(); + const dataflowPort = port as DataFlowPort; const isInput = 'studio.tokens.generic.input' === port.node.factory.type; const isDynamicInput = Boolean(port.annotations[deletable]); const resettable = Boolean(port.annotations[resetable]); const inner = useMemo(() => { + if (port.pType !== DATAFLOW_PORT) { + return <>; + } + const field = controlSelector.find((x) => x.matcher(port, { readOnly })); const Component = field?.component as React.FC; - return ; + return ( + + ); //We use an explicit dependency on the type // eslint-disable-next-line react-hooks/exhaustive-deps - }, [controlSelector, port, readOnly, port.type]); + }, [controlSelector, port, readOnly, (port as DataFlowPort).type!]); const onClick = useCallback(() => { port.setVisible(!port.visible); @@ -85,20 +95,20 @@ export const Port = observer(({ port, readOnly: isReadOnly }: IField) => { }, [port]); const onCopySchema = useCallback(() => { - const type = port.type; + const type = dataflowPort.type; copy(JSON.stringify(type, null, 4), { debug: true, }); - }, [port]); + }, [dataflowPort.type]); const onCopyValue = useCallback(() => { - const value = port.value; + const value = dataflowPort.value; copy(JSON.stringify(value, null, 4), { debug: true, }); - }, [port]); + }, [dataflowPort.value]); return ( @@ -112,12 +122,12 @@ export const Port = observer(({ port, readOnly: isReadOnly }: IField) => { icon={port.visible ? : } /> )} - + - + { - const data = useSelector(panelItemsSelector); + const frame = useFrame(); const createNode = useAction('createNode'); const reactFlowInstance = useReactFlow(); const dispatch = useDispatch(); @@ -37,7 +36,7 @@ export const AddDropdown = () => { }, [dispatch.ui]); const addNode = useCallback( - (type: string) => { + async (type: string) => { const newNode = { type, position: reactFlowInstance.screenToFlowPosition( @@ -45,7 +44,7 @@ export const AddDropdown = () => { ), }; - const node = createNode(newNode); + const node = await createNode(newNode); if (node) { selectAddedNodes([node.flowNode]); @@ -55,7 +54,7 @@ export const AddDropdown = () => { ); const nodes = React.useMemo(() => { - return data.groups.map((group) => { + return frame.panelItems.groups.map((group) => { return ( @@ -82,7 +81,7 @@ export const AddDropdown = () => { ); }); - }, [data.groups, addNode]); + }, [frame.panelItems.groups, addNode]); return ( diff --git a/packages/graph-editor/src/components/toolbar/dropdowns/layout.tsx b/packages/graph-editor/src/components/toolbar/dropdowns/layout.tsx index 4a51af251..df0f53dd4 100644 --- a/packages/graph-editor/src/components/toolbar/dropdowns/layout.tsx +++ b/packages/graph-editor/src/components/toolbar/dropdowns/layout.tsx @@ -53,11 +53,8 @@ export const LayoutDropdown = () => { - onClick('inputs')}> - Input - - onClick('outputs')}> - Output + onClick('nodeSheet')}> + Node Sheet onClick('nodeSettings')}> Node Settings @@ -71,13 +68,12 @@ export const LayoutDropdown = () => { onClick('legend')}> Legend - onClick('debugger')}> - Debugger - onClick('dropPanel')}> Nodes - + onClick('keyboardShortcuts')}> + Keyboard Shortcuts + Save Layout diff --git a/packages/graph-editor/src/components/toolbar/layoutButtons.tsx b/packages/graph-editor/src/components/toolbar/layoutButtons.tsx index 8a7ff794d..28341d6dc 100644 --- a/packages/graph-editor/src/components/toolbar/layoutButtons.tsx +++ b/packages/graph-editor/src/components/toolbar/layoutButtons.tsx @@ -1,12 +1,12 @@ -import { DebugPanel } from '../panels/debugger/index.js'; import { DropPanel } from '../panels/dropPanel/index.js'; import { GraphPanel } from '../panels/graph/index.js'; -import { Inputsheet } from '../panels/inputs/index.js'; + import { Legend } from '../panels/legend/index.js'; import { LogsPanel } from '../panels/logs/index.js'; import { NodeSettingsPanel } from '../panels/nodeSettings/index.js'; -import { OutputSheet } from '../panels/output/index.js'; import { Settings } from '../panels/settings/index.js'; +import { ShortcutsPanel } from '../panels/shortcuts/index.js'; +import { UnifiedSheet } from '../panels/unified/index.js'; import React from 'react'; export const layoutButtons = { @@ -15,21 +15,21 @@ export const layoutButtons = { title: 'Settings', content: , }, - inputs: { - id: 'inputs', - title: 'Inputs', - content: , - }, - outputs: { - id: 'outputs', - title: 'Outputs', - content: , + nodeSheet: { + id: 'nodeSheet', + title: 'Node', + content: , }, nodeSettings: { id: 'nodeSettings', title: 'Node Settings', content: , }, + keyboardShortcuts: { + id: 'keyboardShortcuts', + title: 'Keyboard Shortcuts', + content: , + }, graphSettings: { id: 'graphSettings', title: 'Graph Settings', @@ -45,11 +45,6 @@ export const layoutButtons = { title: 'Legend', content: , }, - debugger: { - id: 'debugger', - title: 'Debugger', - content: , - }, dropPanel: { id: 'dropPanel', title: 'Nodes', diff --git a/packages/graph-editor/src/components/toolbar/toolbar.tsx b/packages/graph-editor/src/components/toolbar/toolbar.tsx index fce1acc44..76cf0b74b 100644 --- a/packages/graph-editor/src/components/toolbar/toolbar.tsx +++ b/packages/graph-editor/src/components/toolbar/toolbar.tsx @@ -1,12 +1,13 @@ import * as Toolbar from '@radix-ui/react-toolbar'; -import { ToolBarButtonsSelector } from '@/redux/selectors/index.js'; -import { useSelector } from 'react-redux'; +import { useFrame } from '@/system/frame/hook.js'; import React from 'react'; import styles from './toolbar.module.css'; export const GraphToolbar = () => { - const toolbarButtons = useSelector(ToolBarButtonsSelector); - return {toolbarButtons}; + const frame = useFrame(); + return ( + {frame.toolbarButtons} + ); }; export const ToolbarSeparator = ({ className = '', ...props }) => ( diff --git a/packages/graph-editor/src/context/ExternalDataContext.tsx b/packages/graph-editor/src/context/ExternalDataContext.tsx deleted file mode 100644 index 773bcb971..000000000 --- a/packages/graph-editor/src/context/ExternalDataContext.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import React, { createContext, useContext, useMemo } from 'react'; - -interface ExternalSetData { - tokens: Record[]; -} - -export interface EditorExternalSet { - name: string; - identifier: string; -} - -type ExternalDataContextType = { - tokenSets?: EditorExternalSet[]; - loadSetTokens: (identifier: string) => Promise; -}; - -const ExternalDataContext = createContext({ - tokenSets: undefined, - loadSetTokens: async () => ({ tokens: [] }), -}); - -function ExternalDataContextProvider({ - children, - tokenSets, - loadSetTokens, -}: { - children: React.ReactNode; - tokenSets?: EditorExternalSet[]; - loadSetTokens: (identifier: string) => Promise; -}) { - const providerValue = useMemo( - () => ({ tokenSets, loadSetTokens }), - [tokenSets, loadSetTokens], - ); - - return ( - - {children} - - ); -} - -function useExternalData() { - const context = useContext(ExternalDataContext); - - if (context === undefined) { - console.error( - 'useExternalData must be used within a ExternalDataContextProvider', - ); - } - return context; -} - -export { ExternalDataContextProvider, useExternalData }; diff --git a/packages/graph-editor/src/context/ExternalLoaderContext.tsx b/packages/graph-editor/src/context/ExternalLoaderContext.tsx deleted file mode 100644 index 1c01083e2..000000000 --- a/packages/graph-editor/src/context/ExternalLoaderContext.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { ExternalLoader } from '@tokens-studio/graph-engine'; -import React, { createContext, useContext } from 'react'; - -const ExternalLoaderContext = createContext( - undefined, -); - -function ExternalLoaderProvider({ - children, - externalLoader, -}: { - children: React.ReactNode; - externalLoader?: ExternalLoader; -}) { - return ( - - {children} - - ); -} - -function useExternalLoader() { - const context = useContext(ExternalLoaderContext); - - return context; -} - -export { ExternalLoaderProvider, useExternalLoader }; diff --git a/packages/graph-editor/src/context/graph.tsx b/packages/graph-editor/src/context/graph.tsx index 767a26081..a4ffb315f 100644 --- a/packages/graph-editor/src/context/graph.tsx +++ b/packages/graph-editor/src/context/graph.tsx @@ -1,19 +1,20 @@ +import { FullyFeaturedGraph } from '@/types/index.js'; import { Graph } from '@tokens-studio/graph-engine'; import React, { createContext, useContext, useMemo } from 'react'; type GraphContextType = { - graph: Graph; + graph: FullyFeaturedGraph; }; const GraphContext = createContext({ - graph: new Graph(), + graph: new Graph() as unknown as FullyFeaturedGraph, }); function GraphContextProvider({ graph, children, }: { - graph: Graph; + graph: FullyFeaturedGraph; children: React.ReactNode; }) { const providerValue = useMemo(() => ({ graph }), [graph]); @@ -25,7 +26,7 @@ function GraphContextProvider({ ); } -function useLocalGraph(): Graph { +function useLocalGraph(): FullyFeaturedGraph { return useContext(GraphContext).graph; } diff --git a/packages/graph-editor/src/context/index.ts b/packages/graph-editor/src/context/index.ts deleted file mode 100644 index 36973211f..000000000 --- a/packages/graph-editor/src/context/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './ExternalDataContext.js'; diff --git a/packages/graph-editor/src/data/version.ts b/packages/graph-editor/src/data/version.ts index 1a2aa36dc..0fad64366 100644 --- a/packages/graph-editor/src/data/version.ts +++ b/packages/graph-editor/src/data/version.ts @@ -1 +1 @@ -export const version = '4.3.11'; +export const version = '4.3.12'; diff --git a/packages/graph-editor/src/editor/actions/connect.ts b/packages/graph-editor/src/editor/actions/connect.ts index 73c374154..535ea0487 100644 --- a/packages/graph-editor/src/editor/actions/connect.ts +++ b/packages/graph-editor/src/editor/actions/connect.ts @@ -1,6 +1,6 @@ import { Connection, Edge } from 'reactflow'; import { Dispatch } from '@/redux/store.js'; -import { Graph } from '@tokens-studio/graph-engine'; +import { FullyFeaturedGraph } from '@/types/index.js'; import { deletable } from '@/annotations/index.js'; import { getVariadicIndex, stripVariadic } from '@/utils/stripVariadic.js'; @@ -10,7 +10,7 @@ export const connectNodes = setEdges, dispatch, }: { - graph: Graph; + graph: FullyFeaturedGraph; setEdges: React.Dispatch>; dispatch: Dispatch; }) => diff --git a/packages/graph-editor/src/editor/actions/copyNodes.tsx b/packages/graph-editor/src/editor/actions/copyNodes.tsx index f8bbdc220..482e556af 100644 --- a/packages/graph-editor/src/editor/actions/copyNodes.tsx +++ b/packages/graph-editor/src/editor/actions/copyNodes.tsx @@ -1,17 +1,17 @@ import { Node, ReactFlowInstance } from 'reactflow'; import { SerializedNode } from '@/types/serializedNode.js'; -import { v4 as uuidv4 } from 'uuid'; +import { nanoid as uuid } from 'nanoid'; export const copyNodeAction = ( reactFlowInstance: ReactFlowInstance, graph, - nodeLookup, + nodeLoader, ) => { return async (nodes: SerializedNode[]) => { const { addNodes } = await nodes.reduce( async (acc, node) => { const resolvedAcc = await acc; - const newID = uuidv4(); + const newID = uuid(); if (node.engine) { const graphNode = graph.getNode(node.engine.id); const newGraphNode = await graphNode?.factory.deserialize( @@ -19,7 +19,7 @@ export const copyNodeAction = ( ...node.engine, id: newID, }, - nodeLookup, + nodeLoader, ); graph.addNode(newGraphNode); diff --git a/packages/graph-editor/src/editor/actions/createNode.tsx b/packages/graph-editor/src/editor/actions/createNode.tsx index e3d762220..c55e04e95 100644 --- a/packages/graph-editor/src/editor/actions/createNode.tsx +++ b/packages/graph-editor/src/editor/actions/createNode.tsx @@ -1,6 +1,8 @@ import { Dispatch } from '@/redux/store.js'; -import { Graph, Node, NodeFactory } from '@tokens-studio/graph-engine'; +import { FullyFeaturedGraph } from '@/types/index.js'; +import { Graph, Input, Node, NodeLoader } from '@tokens-studio/graph-engine'; import { ReactFlowInstance, Node as ReactFlowNode } from 'reactflow'; +import { xpos, ypos } from '@/annotations/index.js'; export type NodeRequest = { type: string; @@ -10,8 +12,8 @@ export type NodeRequest = { export interface ICreateNode { reactFlowInstance: ReactFlowInstance; - graph: Graph; - nodeLookup: Record; + graph: FullyFeaturedGraph; + nodeLoader: NodeLoader; iconLookup: Record; /** * If a customized node would be created in the editor, it would be created using this UI lookup. @@ -25,13 +27,13 @@ export interface ICreateNode { export const createNode = ({ reactFlowInstance, graph, - nodeLookup, + nodeLoader, iconLookup, customUI, dropPanelPosition, dispatch, }: ICreateNode) => { - return (nodeRequest: NodeRequest) => { + return async (nodeRequest: NodeRequest) => { const position = nodeRequest.position || { x: dropPanelPosition.x, y: dropPanelPosition.y, @@ -59,18 +61,19 @@ export const createNode = ({ } //Lookup the node type - const Factory = nodeLookup[nodeRequest.type]; + const Factory = await nodeLoader(nodeRequest.type); //Generate the new node - const node = new Factory({ - graph: graph, + const node = await new Factory({ + graph: graph as unknown as Graph, }); + graph.addNode(node); const finalPos = position || { x: 0, y: 0 }; - node.annotations['xpos'] = finalPos.x; - node.annotations['ypos'] = finalPos.y; + node.setAnnotation(xpos, finalPos.x); + node.setAnnotation(ypos, finalPos.y); if (customUI[nodeRequest.type]) { node.annotations['uiNodeType'] = customUI[nodeRequest.type]; @@ -83,11 +86,11 @@ export const createNode = ({ if (!input) { return; } - node.inputs[name].setValue(value); + (node.inputs[name] as Input).setValue(value); }); //Update immediately - graph.update(node.id); + graph.capabilities.dataFlow?.update(node.id); //Add the node to the react flow instance const reactFlowNode = { diff --git a/packages/graph-editor/src/editor/actions/deleteNode.tsx b/packages/graph-editor/src/editor/actions/deleteNode.tsx index 4b4e7b582..55581e79a 100644 --- a/packages/graph-editor/src/editor/actions/deleteNode.tsx +++ b/packages/graph-editor/src/editor/actions/deleteNode.tsx @@ -1,9 +1,9 @@ import { Dispatch } from '@/redux/store.js'; -import { Graph } from '@tokens-studio/graph-engine'; +import { FullyFeaturedGraph } from '@/types/index.js'; import { ReactFlowInstance } from 'reactflow'; export const deleteNode = ( - graph: Graph, + graph: FullyFeaturedGraph, dispatch: Dispatch, reactFlowInstance: ReactFlowInstance, ) => { diff --git a/packages/graph-editor/src/editor/actions/duplicate.ts b/packages/graph-editor/src/editor/actions/duplicate.ts index 6e19a0163..7766298e2 100644 --- a/packages/graph-editor/src/editor/actions/duplicate.ts +++ b/packages/graph-editor/src/editor/actions/duplicate.ts @@ -1,16 +1,16 @@ import { Edge, Node, ReactFlowInstance } from 'reactflow'; +import { FullyFeaturedGraph } from '@/types/index.js'; import { - Graph, NodeFactory, Port, annotatedSingleton, } from '@tokens-studio/graph-engine'; -import { v4 as uuidv4 } from 'uuid'; +import { nanoid as uuid } from 'nanoid'; +import { xpos, ypos } from '@/annotations/index.js'; export interface IDuplicate { reactFlowInstance: ReactFlowInstance; - graph: Graph; - nodeLookup: Record; + graph: FullyFeaturedGraph; } /** @@ -42,8 +42,8 @@ export const duplicateNodes = y: node.position.y + 100, }; - clonedNode.annotations['ui.position.x'] = newPosition.x; - clonedNode.annotations['ui.position.y'] = newPosition.y; + clonedNode.setAnnotation(xpos, newPosition.x); + clonedNode.setAnnotation(ypos, newPosition.y); graph.addNode(clonedNode); @@ -51,7 +51,7 @@ export const duplicateNodes = .map(([key, value]) => { // value._edges. return (value as Port)._edges.map((edge) => { - const newEdgeId = uuidv4(); + const newEdgeId = uuid(); const vals = { id: newEdgeId, diff --git a/packages/graph-editor/src/editor/actions/provider.tsx b/packages/graph-editor/src/editor/actions/provider.tsx index de86a28b9..311ba5a04 100644 --- a/packages/graph-editor/src/editor/actions/provider.tsx +++ b/packages/graph-editor/src/editor/actions/provider.tsx @@ -5,12 +5,13 @@ import React from 'react'; import type { NodeRequest } from './createNode.js'; export type Actions = { - createNode: (nodeRequest: NodeRequest) => + createNode: (nodeRequest: NodeRequest) => Promise< | undefined | { graphNode: Node; flowNode: FlowNode; - }; + } + >; deleteNode: (nodeId: string) => void; copyNodes: (nodes: SerializedNode[]) => void; duplicateNodes: (nodeIds: string[]) => void; @@ -35,6 +36,6 @@ export const ActionProvider = ({ children, actions }: ActionProviderProps) => { export const useAction = ( actionName: T, ): Actions[T] => { - const actions = React.useContext(ContextProvider); + const actions = React.useContext(ContextProvider) as Actions; return actions[actionName]; }; diff --git a/packages/graph-editor/src/editor/editorTypes.ts b/packages/graph-editor/src/editor/editorTypes.ts index c613b6ae3..b9fa23aa0 100644 --- a/packages/graph-editor/src/editor/editorTypes.ts +++ b/packages/graph-editor/src/editor/editorTypes.ts @@ -1,22 +1,24 @@ import { CapabilityFactory, - ExternalLoader, - Graph, Node as GraphNode, SchemaObject, SerializedGraph, } from '@tokens-studio/graph-engine'; -import { Control } from '../types/controls.js'; -import { DropPanelStore } from '@/components/panels/dropPanel/index.js'; + import { Edge, Node, ReactFlowInstance } from 'reactflow'; +import { FullyFeaturedGraph } from '@/types/index.js'; +import { LayoutBase } from 'rc-dock'; import { Menu } from '@/components/menubar/data.js'; -import type { LayoutBase, TabBase, TabData } from 'rc-dock'; +import { System } from '@/system/index.js'; +import type { LayoutBase } from 'rc-dock'; export interface EditorProps { id?: string; - tabLoader?: (tab: TabBase) => TabData | undefined; - + /** + * The system to use for the editor + */ + system: System; /** * A lookup of the custom node types to display in the editor. * Not populating this will result in the default items being displayed. @@ -31,10 +33,6 @@ export interface EditorProps { emptyContent?: React.ReactNode; children?: React.ReactNode; onOutputChange?: (output: Record) => void; - /** - * An external loader to use for loading the graphs or node data - */ - externalLoader?: ExternalLoader; /** * Whether or not to show the menu */ @@ -44,31 +42,6 @@ export interface EditorProps { * A custom menu to display in the editor. */ menuItems?: Menu; - - /** - * Capabilities to load into the graphs. Each factory is loaded into each graph individually - */ - capabilities?: CapabilityFactory[]; - /** - * Items to display in the drop panel. - * Not populating this will result in the default items being displayed. - */ - panelItems: DropPanelStore; - /** - * Customize the controls that are displayed in the editor - */ - controls?: Control[]; - - /** - * A lookup of the custom node ui types to display in the editor. - */ - customNodeUI?: Record; - - /** - * Additional specifics to display in the editor for custom types - */ - specifics?: Record>; - /** * An initial layout to use */ @@ -79,21 +52,11 @@ export interface EditorProps { */ schemas?: SchemaObject[]; - /** - * Additional icons to display in the editor for custom types - */ - icons?: Record; - /** - * Additional buttons to display in the toolbar - */ - toolbarButtons?: React.ReactElement; - /** * Additional colors to display in the editor for custom types */ typeColors?: Record; - initialGraph?: Graph; } export interface GraphEditorProps { @@ -111,7 +74,6 @@ export interface GraphEditorProps { */ nodeTypes?: Record; children?: React.ReactNode; - initialGraph?: SerializedGraph; } export type ImperativeEditorRef = { @@ -121,10 +83,10 @@ export type ImperativeEditorRef = { */ clear: () => void; save: () => SerializedGraph; - load: (state: Graph) => void; + load: (state: FullyFeaturedGraph) => void; loadRaw: (state: SerializedGraph) => void; getFlow: () => ReactFlowInstance; - getGraph: () => Graph; + getGraph: () => FullyFeaturedGraph; }; export type EditorNode = Node; diff --git a/packages/graph-editor/src/editor/graph.tsx b/packages/graph-editor/src/editor/graph.tsx index 000dc8cdc..8998e18b2 100644 --- a/packages/graph-editor/src/editor/graph.tsx +++ b/packages/graph-editor/src/editor/graph.tsx @@ -24,7 +24,7 @@ import { } from '../components/flow/utils.js'; import { handleDrop } from './actions/handleDrop.js'; import { useDispatch } from '../hooks/index.js'; -import { v4 as uuidv4 } from 'uuid'; +import { nanoid as uuid } from 'nanoid'; import CustomEdge from '../components/flow/edges/edge.js'; import React, { MouseEvent, @@ -42,9 +42,15 @@ import groupNode from '../components/flow/nodes/groupNode.js'; import noteNode from '../components/flow/nodes/noteNode.js'; import { ActionProvider } from './actions/provider.js'; -import { BatchRunError, Graph } from '@tokens-studio/graph-engine'; +import { + BatchRunError, + DataFlowCapabilityFactory, + Graph, +} from '@tokens-studio/graph-engine'; import { CommandMenu } from '@/components/commandPalette/index.js'; +import { ControlFlowCapabilityFactory } from '@tokens-studio/graph-engine'; import { EdgeContextMenu } from '../components/contextMenus/edgeContextMenu.js'; +import { FullyFeaturedGraph } from '@/types/index.js'; import { GraphContextProvider } from '@/context/graph.js'; import { GraphEditorProps, ImperativeEditorRef } from './editorTypes.js'; import { GraphToolbar } from '@/components/toolbar/index.js'; @@ -55,19 +61,8 @@ import { NodeV2 } from '@/components/index.js'; import { PaneContextMenu } from '../components/contextMenus/paneContextMenu.js'; import { PassthroughNode } from '@/components/flow/nodes/passthroughNode.js'; import { SelectionContextMenu } from '@/components/contextMenus/selectionContextMenu.js'; -import { - capabilitiesSelector, - nodeTypesSelector, - panelItemsSelector, -} from '@/redux/selectors/registry.js'; import { clear } from './actions/clear.js'; import { connectNodes } from './actions/connect.js'; -import { - connectOnClickSelector, - showGrid, - showMinimapSelector, - snapGrid, -} from '@/redux/selectors/settings.js'; import { contextMenuSelector } from '@/redux/selectors/ui.js'; import { copyNodeAction } from './actions/copyNodes.js'; import { currentPanelIdSelector } from '@/redux/selectors/graph.js'; @@ -82,8 +77,12 @@ import { ypos, } from '@/annotations/index.js'; import { duplicateNodes } from './actions/duplicate.js'; +import { + nodeTypesSelector, + panelItemsSelector, +} from '@/redux/selectors/registry.js'; import { useContextMenu } from 'react-contexify'; -import { useExternalLoader } from '@/context/ExternalLoaderContext.js'; +import { useFrame } from '@/system/frame/hook.js'; import { useSelectAddedNodes } from '@/hooks/useSelectAddedNodes.js'; import { useSelector } from 'react-redux'; import { useSetCurrentNode } from '@/hooks/useSetCurrentNode.js'; @@ -112,47 +111,30 @@ export const EditorApp = React.forwardRef< ImperativeEditorRef, GraphEditorProps >((props: GraphEditorProps, ref) => { - const panelItems = useSelector(panelItemsSelector); - const fullNodeLookup = useSelector(nodeTypesSelector); - const { id, customNodeUI = {}, children } = props; + const frame = useFrame(); + + const { id, children } = props; - const externalLoader = useExternalLoader(); - const showMinimap = useSelector(showMinimapSelector); - const capabilities = useSelector(capabilitiesSelector); const contextMenus = useSelector(contextMenuSelector); - const connectOnClick = useSelector(connectOnClickSelector); const reactFlowWrapper = useRef(null); const reactFlowInstance = useReactFlow(); const dispatch = useDispatch(); const { getIntersectingNodes } = reactFlowInstance; const store = useStoreApi(); - const initialGraph: Graph = useMemo(() => { - //Set defaults - const graph = new Graph(); - graph.annotations[title] = 'Untitled Graph'; - graph.annotations[description] = ''; - return graph; - }, []); - const [graph, setTheGraph] = useState(initialGraph); - - const showGridValue = useSelector(showGrid); - const snapGridValue = useSelector(snapGrid); + + const [graph, setTheGraph] = useState(frame.graph); + const internalRef = useRef(null); const activeGraphId = useSelector(currentPanelIdSelector); - //Update the external loader - useEffect(() => { - graph.externalLoader = externalLoader; - }, [graph, externalLoader]); - const iconLookup = useMemo(() => { - return panelItems.groups.reduce((acc, group) => { + return frame.panelItems.groups.reduce((acc, group) => { group.items.forEach((item) => { acc[item.type] = item.icon || group.icon; }); return acc; }, {}); - }, [panelItems]); + }, [frame.panelItems.groups]); const refProxy = useCallback( (v) => { @@ -173,8 +155,6 @@ export const EditorApp = React.forwardRef< //Attach sideeffect listeners useEffect(() => { - capabilities.forEach((factory) => graph.registerCapability(factory)); - graph.onFinalize('serialize', (serialized) => { const nodes = reactFlowInstance.getNodes(); @@ -346,12 +326,14 @@ export const EditorApp = React.forwardRef< break; case 'position': if ((change as NodePositionChange).position) { - node.annotations['ui.position.x'] = ( - change as NodePositionChange - ).position?.x; - node.annotations['ui.position.y'] = ( - change as NodePositionChange - ).position?.y; + node.setAnnotation( + xpos, + (change as NodePositionChange).position!.x, + ); + node.setAnnotation( + ypos, + (change as NodePositionChange).position!.y, + ); } break; } @@ -370,7 +352,7 @@ export const EditorApp = React.forwardRef< // Create flow node types here, instead of the global scope to ensure that custom nodes added by the user are available in nodeTypes const fullNodeTypesRef = useRef({ - ...customNodeUI, + ...frame.customNodeUI, GenericNode: NodeV2, [PASSTHROUGH]: PassthroughNode, [EditorNodeTypes.GROUP]: groupNode, @@ -381,12 +363,12 @@ export const EditorApp = React.forwardRef< //Turn it into an O(1) lookup object return Object.fromEntries( Object.entries({ - ...customNodeUI, + ...frame.customNodeUI, [NOTE]: NOTE, 'studio.tokens.generic.preview': 'studio.tokens.generic.preview', }).map(([k]) => [k, k]), ); - }, [customNodeUI]); + }, [frame.customNodeUI]); const handleDeleteNode = useMemo(() => { return deleteNode(graph, dispatch, reactFlowInstance); @@ -397,7 +379,7 @@ export const EditorApp = React.forwardRef< createNode({ reactFlowInstance, graph, - nodeLookup: fullNodeLookup, + nodeLoader: frame.nodeLoader, iconLookup, customUI: customNodeMap, dropPanelPosition, @@ -406,7 +388,7 @@ export const EditorApp = React.forwardRef< [ reactFlowInstance, graph, - fullNodeLookup, + frame.nodeLoader, iconLookup, customNodeMap, dropPanelPosition, @@ -429,11 +411,11 @@ export const EditorApp = React.forwardRef< }, loadRaw: async (serializedGraph) => { if (internalRef.current) { - await graph.deserialize(serializedGraph, fullNodeLookup); + await graph.deserialize(serializedGraph, frame.nodeLoader); internalRef?.current.load(graph); } }, - load: (loadedGraph: Graph) => { + load: (loadedGraph: FullyFeaturedGraph) => { //Read the annotaions const viewport = loadedGraph.annotations[uiViewport]; @@ -490,17 +472,19 @@ export const EditorApp = React.forwardRef< }); //Execute the graph once to propagate values and update the UI - loadedGraph.execute().catch((e: BatchRunError) => { - dispatch.graph.appendLog({ - type: 'error', - time: new Date(), - data: { - node: e.nodeId, - error: e.message, - msg: `Error executing graph`, - }, + loadedGraph.capabilities.dataFlow + .execute() + .catch((e: BatchRunError) => { + dispatch.graph.appendLog({ + type: 'error', + time: new Date(), + data: { + node: e.nodeId, + error: e.message, + msg: `Error executing graph`, + }, + }); }); - }); setTheGraph(loadedGraph); }, @@ -510,7 +494,7 @@ export const EditorApp = React.forwardRef< [ reactFlowInstance, graph, - fullNodeLookup, + frame.nodeLoader, setNodes, setEdges, dispatch.graph, @@ -520,12 +504,6 @@ export const EditorApp = React.forwardRef< useSetCurrentNode(); - useEffect(() => { - if (props.initialGraph) { - internalRef.current?.loadRaw(props.initialGraph); - } - }, []); - const onConnect = useMemo( () => connectNodes({ graph, setEdges, dispatch }), [dispatch, graph, setEdges], @@ -589,16 +567,17 @@ export const EditorApp = React.forwardRef< ); const onEdgeDblClick = useCallback( - (event, clickedEdge) => { + async (event, clickedEdge) => { event.stopPropagation(); const position = reactFlowInstance?.screenToFlowPosition({ x: event.clientX, y: event.clientY, }); + const PassthroughFactory = await frame.nodeLoader(PASSTHROUGH); - const newNode = new fullNodeLookup[PASSTHROUGH]({ - graph, + const newNode = new PassthroughFactory({ + graph: graph as unknown as Graph, }); newNode.annotations[uiNodeType] = PASSTHROUGH; @@ -615,14 +594,14 @@ export const EditorApp = React.forwardRef< graph.removeEdge(clickedEdge.id); const aEdge = { - id: uuidv4(), + id: uuid(), source: clickedEdge.source, target: newNode.id, sourceHandle: clickedEdge.sourceHandle, targetHandle: 'value', }; const bEdge = { - id: uuidv4(), + id: uuid(), source: newNode.id, sourceHandle: 'value', target: clickedEdge.target, @@ -632,7 +611,7 @@ export const EditorApp = React.forwardRef< graph.createEdge(aEdge); graph.createEdge(bEdge); - graph.update(newNode.id); + graph.capabilities.data.update(newNode.id); setNodes((nds) => [...nds, editorNode]); setEdges((eds) => { @@ -657,7 +636,7 @@ export const EditorApp = React.forwardRef< return [...filtered, newEdge, newEdge2]; }); }, - [fullNodeLookup, graph, reactFlowInstance, setEdges, setNodes], + [reactFlowInstance, frame, graph, setNodes, setEdges], ); const onNodeDrag = useCallback( @@ -698,10 +677,9 @@ export const EditorApp = React.forwardRef< const duplicateNodesAction = duplicateNodes({ graph, reactFlowInstance, - nodeLookup: fullNodeLookup, }); - const copyNodes = copyNodeAction(reactFlowInstance, graph, fullNodeLookup); + const copyNodes = copyNodeAction(reactFlowInstance, graph, frame.nodeLoader); const selectAddedNodes = useSelectAddedNodes(); const onDrop = useCallback( @@ -721,14 +699,17 @@ export const EditorApp = React.forwardRef< }); //Some of the nodes might be invalid, so remember to filter them out - const newNodes = positionUpdated - .map((nodeRequest) => handleSelectNewNodeType(nodeRequest)) - .filter((x) => !!x) - .map((x) => x?.flowNode ?? ({} as Node)); + const newNodes = await Promise.all( + positionUpdated.map((nodeRequest) => + handleSelectNewNodeType(nodeRequest), + ), + ); - selectAddedNodes(newNodes); + selectAddedNodes( + newNodes.filter((x) => !!x).map((x) => x?.flowNode ?? ({} as Node)), + ); }, - [handleSelectNewNodeType, reactFlowInstance], + [handleSelectNewNodeType, reactFlowInstance, selectAddedNodes], ); const nodeCount = nodes.length; return ( @@ -762,10 +743,10 @@ export const EditorApp = React.forwardRef< onEdgeDoubleClick={onEdgeDblClick} onEdgesDelete={onEdgesDeleted} edges={edges} - connectOnClick={connectOnClick} + connectOnClick={frame.settings.connectOnClick} elevateNodesOnSelect={true} onNodeDragStop={onNodeDragStop} - snapToGrid={snapGridValue} + snapToGrid={frame.settings.snapGrid} edgeTypes={edgeTypes} nodeTypes={fullNodeTypesRef.current} snapGrid={snapGridCoords} @@ -798,7 +779,7 @@ export const EditorApp = React.forwardRef< maxZoom={Infinity} proOptions={proOptions} > - {showGridValue && ( + {frame.settings.showGrid && ( )} @@ -820,7 +801,7 @@ export const EditorApp = React.forwardRef< - {showMinimap && } + {frame.settings.showMinimap && } { + const system = useFrame(); const dagreAutoLayout = useDagreLayout(); - const layoutType = useSelector(layoutTypeSelector); - return useCallback(() => { - switch (layoutType) { + switch (system.settings.layoutType) { case LayoutType.dagre: dagreAutoLayout(); break; } - }, [dagreAutoLayout, layoutType]); + }, [dagreAutoLayout, system.settings.layoutType]); }; diff --git a/packages/graph-editor/src/editor/index.tsx b/packages/graph-editor/src/editor/index.tsx index 13275df10..99ea807a8 100644 --- a/packages/graph-editor/src/editor/index.tsx +++ b/packages/graph-editor/src/editor/index.tsx @@ -2,10 +2,8 @@ import { EditorProps, ImperativeEditorRef } from './editorTypes.js'; import { LayoutController } from './layoutController.js'; import { ReduxProvider } from '../redux/index.js'; import { ToastProvider } from '@/hooks/useToast.js'; -import { defaultControls } from '@/registry/control.js'; -import { nodeLookup as defaultNodeLookup } from '@tokens-studio/graph-engine'; -import { defaultPanelGroupsFactory } from '@/components/index.js'; -import { defaultSpecifics } from '@/registry/specifics.js'; + +import { SystemContext } from '@/system/hook.js'; import React from 'react'; /** @@ -14,33 +12,16 @@ import React from 'react'; */ export const Editor = React.forwardRef( (props: EditorProps, ref) => { - const { - panelItems = defaultPanelGroupsFactory(), - capabilities, - - toolbarButtons, - schemas, - nodeTypes = defaultNodeLookup, - controls = [...defaultControls], - specifics = defaultSpecifics, - icons, - } = props; + const { schemas } = props; // Note that the provider exists around the layout controller so that the layout controller can register itself during mount return ( - - - + + + + + ); }, diff --git a/packages/graph-editor/src/editor/layout/data.tsx b/packages/graph-editor/src/editor/layout/data.tsx new file mode 100644 index 000000000..4f9d972fd --- /dev/null +++ b/packages/graph-editor/src/editor/layout/data.tsx @@ -0,0 +1,166 @@ +import { DropPanel } from '@/components/panels/dropPanel/dropPanel.js'; +import { ErrorBoundary } from 'react-error-boundary'; +import { ErrorBoundaryContent } from '@/components/ErrorBoundaryContent.js'; +import { GraphEditor } from '../graphEditor.js'; +import { Inputsheet } from '@/components/panels/inputs/index.js'; +import { LayoutData, TabBase, TabData } from 'rc-dock'; +import { MAIN_GRAPH_ID } from '@/constants.js'; +import { OutputSheet } from '@/components/panels/output/index.js'; +import React from 'react'; + +export const layoutDataFactory = (): LayoutData => { + return { + dockbox: { + mode: 'vertical', + children: [ + { + mode: 'horizontal', + children: [ + { + size: 2, + mode: 'vertical', + children: [ + { + mode: 'horizontal', + children: [ + { + size: 3, + mode: 'vertical', + children: [ + { + tabs: [ + { + id: 'dropPanel', + title: '', + content: <>, + }, + { + id: 'previewNodesPanel', + title: '', + content: <>, + }, + ], + }, + ], + }, + { + size: 17, + mode: 'vertical', + children: [ + { + id: 'graphs', + size: 700, + group: 'graph', + panelLock: { panelStyle: 'graph' }, + tabs: [ + { + closable: true, + cached: true, + id: MAIN_GRAPH_ID, + group: 'graph', + title: 'Graph', + content: <>, + }, + ], + }, + ], + }, + + { + size: 4, + mode: 'vertical', + children: [ + { + size: 12, + tabs: [ + { + id: 'input', + title: '', + content: <>, + }, + ], + }, + { + size: 12, + tabs: [ + { + id: 'outputs', + title: '', + content: <>, + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + }; +}; + +export const layoutLoader = + (props, ref) => + (tab: TabBase): TabData => { + const { id } = tab; + switch (id) { + case MAIN_GRAPH_ID: + return { + closable: true, + cached: true, + id: MAIN_GRAPH_ID, + group: 'graph', + title: 'Graph', + content: ( + }> + + + ), + }; + case 'input': + return { + closable: true, + cached: true, + group: 'popout', + id: 'input', + title: 'Inputs', + content: ( + }> + + + ), + }; + case 'outputs': + return { + closable: true, + cached: true, + group: 'popout', + id: 'outputs', + title: 'Outputs', + content: ( + }> + + + ), + }; + + case 'dropPanel': + return { + group: 'popout', + id: 'dropPanel', + title: 'Nodes', + content: ( + }> + + + ), + closable: true, + }; + default: + return tab as TabData; + } + }; diff --git a/packages/graph-editor/src/editor/layout/groups.tsx b/packages/graph-editor/src/editor/layout/groups.tsx new file mode 100644 index 000000000..378d6dee2 --- /dev/null +++ b/packages/graph-editor/src/editor/layout/groups.tsx @@ -0,0 +1,78 @@ +import { IconButton } from '@tokens-studio/ui/IconButton.js'; +import { Stack } from '@tokens-studio/ui/Stack.js'; +import { TabGroup } from 'rc-dock'; +import ArrowUpRight from '@tokens-studio/icons/ArrowUpRight.js'; +import Maximize from '@tokens-studio/icons/Maximize.js'; +import React from 'react'; +import Reduce from '@tokens-studio/icons/Reduce.js'; +import Xmark from '@tokens-studio/icons/Xmark.js'; + +const DockButton = (rest) => { + return ; +}; + +export const groups: Record = { + popout: { + animated: true, + floatable: true, + + panelExtra: (panelData, context) => { + const buttons: React.ReactElement[] = []; + if (panelData?.parent?.mode !== 'window') { + const maxxed = panelData?.parent?.mode === 'maximize'; + buttons.push( + : } + onClick={() => context.dockMove(panelData, null, 'maximize')} + >, + ); + buttons.push( + } + onClick={() => context.dockMove(panelData, null, 'new-window')} + >, + ); + } + buttons.push( + } + onClick={() => context.dockMove(panelData, null, 'remove')} + >, + ); + return {buttons}; + }, + }, + /** + * Note that the graph has a huge issue when ran in a popout window, as such we disable it for now + */ + graph: { + animated: true, + floatable: true, + panelExtra: (panelData, context) => { + const buttons: React.ReactElement[] = []; + if (panelData?.parent?.mode !== 'window') { + const maxxed = panelData?.parent?.mode === 'maximize'; + buttons.push( + : } + onClick={() => context.dockMove(panelData, null, 'maximize')} + >, + ); + } + + return {buttons}; + }, + }, +}; diff --git a/packages/graph-editor/src/editor/layout/utils.ts b/packages/graph-editor/src/editor/layout/utils.ts new file mode 100644 index 000000000..0ac3069cf --- /dev/null +++ b/packages/graph-editor/src/editor/layout/utils.ts @@ -0,0 +1,45 @@ +import { BoxBase, LayoutBase, PanelBase } from 'rc-dock'; + +export function recurseFindGraphPanel( + base: BoxBase | PanelBase, +): PanelBase | null { + if (base.id === 'graphs') { + return base as PanelBase; + } + //Check if it has children + if ((base as BoxBase).children) { + for (const child of (base as BoxBase).children) { + const found = recurseFindGraphPanel(child); + if (found) { + return found; + } + } + } + return null; +} + +export function findGraphPanel(layout: LayoutBase): PanelBase | null { + //We need to recursively search for the graph panel + // It is most likely in the dockbox + const dockbox = recurseFindGraphPanel(layout.dockbox); + if (dockbox) { + return dockbox; + } + if (layout.floatbox) { + const floatBox = recurseFindGraphPanel(layout.floatbox); + if (floatBox) { + return floatBox; + } + } else if (layout.maxbox) { + const tab = recurseFindGraphPanel(layout.maxbox); + if (tab) { + return tab; + } + } else if (layout.windowbox) { + const tab = recurseFindGraphPanel(layout.windowbox); + if (tab) { + return tab; + } + } + return null; +} diff --git a/packages/graph-editor/src/editor/layoutController.tsx b/packages/graph-editor/src/editor/layoutController.tsx index 30d7a5a6d..ae96ed005 100644 --- a/packages/graph-editor/src/editor/layoutController.tsx +++ b/packages/graph-editor/src/editor/layoutController.tsx @@ -12,18 +12,18 @@ import { DropPanel } from '@/components/panels/dropPanel/dropPanel.js'; import { EditorProps, ImperativeEditorRef } from './editorTypes.js'; import { ErrorBoundary } from 'react-error-boundary'; import { ErrorBoundaryContent } from '@/components/ErrorBoundaryContent.js'; -import { ExternalLoaderProvider } from '@/context/ExternalLoaderContext.js'; import { FindDialog } from '@/components/dialogs/findDialog.js'; +import { FrameContext } from '@/system/frame/hook.js'; import { GraphEditor } from './graphEditor.js'; import { IconButton, Stack, Tooltip } from '@tokens-studio/ui'; -import { Inputsheet } from '@/components/panels/inputs/index.js'; import { MAIN_GRAPH_ID } from '@/constants.js'; import { MenuBar, defaultMenuDataFactory } from '@/components/menubar/index.js'; -import { OutputSheet } from '@/components/panels/output/index.js'; +import { UnifiedSheet } from '@/components/panels/unified/index.js'; import { dockerSelector } from '@/redux/selectors/refs.js'; import { useDispatch } from '@/hooks/useDispatch.js'; import { useRegisterRef } from '@/hooks/useRegisterRef.js'; import { useSelector } from 'react-redux'; +import { useSystem } from '@/system/hook.js'; import Maximize from '@tokens-studio/icons/Maximize.js'; import React, { MutableRefObject, useCallback, useEffect } from 'react'; import Reduce from '@tokens-studio/icons/Reduce.js'; @@ -200,15 +200,7 @@ const defaultLayout: LayoutBase = { size: 12, tabs: [ { - id: 'input', - }, - ], - }, - { - size: 12, - tabs: [ - { - id: 'outputs', + id: 'unifiedPorts', }, ], }, @@ -225,20 +217,20 @@ const defaultLayout: LayoutBase = { }; const layoutLoader = (tab: TabBase, props, ref): TabData => { - const { id, ...rest } = tab; + const { id } = tab; switch (id) { case 'graphs': return { - id: 'graphs', + ...tab, //@ts-expect-error size: 700, group: 'graph', panelLock: { panelStyle: 'graph' }, - ...rest, }; case MAIN_GRAPH_ID: return { + ...tab, closable: true, cached: true, group: 'graph', @@ -250,37 +242,23 @@ const layoutLoader = (tab: TabBase, props, ref): TabData => { ), }; - case 'input': - return { - closable: true, - cached: true, - group: 'popout', - id: 'input', - title: 'Inputs', - content: ( - }> - - - ), - }; - case 'outputs': + case 'unifiedPorts': return { + ...tab, closable: true, cached: true, group: 'popout', - id: 'outputs', - title: 'Outputs', + title: 'Ports', content: ( }> - + ), }; - case 'dropPanel': return { + ...tab, group: 'popout', - id: 'dropPanel', title: 'Nodes', content: ( }> @@ -299,12 +277,9 @@ export const LayoutController = React.forwardRef< ImperativeEditorRef, EditorProps >((props: EditorProps, ref) => { - const { - tabLoader, - externalLoader, - initialLayout, - menuItems = defaultMenuDataFactory(), - } = props; + const { initialLayout, menuItems = defaultMenuDataFactory() } = props; + + const system = useSystem(); const registerDocker = useRegisterRef('docker'); const dispatch = useDispatch(); @@ -313,13 +288,13 @@ export const LayoutController = React.forwardRef< const loadTab = useCallback( (tab): TabData => { - const loaded = tabLoader?.(tab); + const loaded = system.tabLoader?.(tab); if (!loaded) { return layoutLoader(tab, props, ref); } return loaded; }, - [tabLoader, props, ref], + [system, props, ref], ); useEffect(() => { @@ -330,7 +305,7 @@ export const LayoutController = React.forwardRef< const onLayoutChange = (newLayout: LayoutBase) => { //We need to find the graph tab container in the newlayout - const graphContainer = findGraphPanel(newLayout); + const graphContainer = findGraphPanel(newLayout) if (graphContainer?.activeId) { //Get the active Id to find the currently selected graph @@ -339,7 +314,7 @@ export const LayoutController = React.forwardRef< }; return ( - + - + ); }); LayoutController.displayName = 'LayoutController'; diff --git a/packages/graph-editor/src/hooks/useIsValidConnection.ts b/packages/graph-editor/src/hooks/useIsValidConnection.ts index 449b936eb..971bf60c8 100644 --- a/packages/graph-editor/src/hooks/useIsValidConnection.ts +++ b/packages/graph-editor/src/hooks/useIsValidConnection.ts @@ -1,4 +1,9 @@ -import { ANY, canConvertSchemaTypes } from '@tokens-studio/graph-engine'; +import { + ANY, + ControlFlowPort, + DataFlowPort, + canConvertSchemaTypes, +} from '@tokens-studio/graph-engine'; import { Connection, useReactFlow } from 'reactflow'; import { stripVariadic } from '@/utils/stripVariadic.js'; import { useCallback } from 'react'; @@ -14,6 +19,8 @@ export interface IuseIsValidConnection { postProcessor?: (connection: Connection) => boolean; } +type TypedPort = ControlFlowPort | DataFlowPort; + export const useIsValidConnection = ({ postProcessor, }: IuseIsValidConnection = {}) => { @@ -36,7 +43,7 @@ export const useIsValidConnection = ({ const strippedVariadic = stripVariadic(connection.targetHandle!); - let targetType = target?.inputs[strippedVariadic].type; + let targetType = (target?.inputs[strippedVariadic] as TypedPort).type; //They are trying to create a dynamic input. Allow non dynamic sets to connect if ( @@ -46,11 +53,19 @@ export const useIsValidConnection = ({ return !target.inputs[strippedVariadic]?.isConnected; } - let sourceType = connection.sourceHandle - ? source?.outputs[connection.sourceHandle].type - : null; + const srcPort = source.outputs[connection.sourceHandle!]; + const tgtPort = target.inputs[strippedVariadic]; + + //Do not allow connections of different port types + if (srcPort && tgtPort) { + if (srcPort.pType !== tgtPort.pType) { + return false; + } + } + + let sourceType = (srcPort as TypedPort).type || null; - if (target.inputs[strippedVariadic].type.items) { + if ((target.inputs[strippedVariadic] as TypedPort).type.items) { targetType = target.inputs[strippedVariadic].type.items; } if (source.outputs[connection.sourceHandle!].type.items) { diff --git a/packages/graph-editor/src/hooks/useOpenPanel.tsx b/packages/graph-editor/src/hooks/useOpenPanel.tsx index d1a9b3d53..06f6b4201 100644 --- a/packages/graph-editor/src/hooks/useOpenPanel.tsx +++ b/packages/graph-editor/src/hooks/useOpenPanel.tsx @@ -5,7 +5,7 @@ import React, { MutableRefObject, useCallback } from 'react'; import type { DockLayout } from 'rc-dock'; export type PanelFactory = { - group: string; + group: string | undefined; id: string; title: React.ReactElement | string; content: React.ReactElement; diff --git a/packages/graph-editor/src/index.tsx b/packages/graph-editor/src/index.tsx index b508c6315..5a56a4dc9 100644 --- a/packages/graph-editor/src/index.tsx +++ b/packages/graph-editor/src/index.tsx @@ -3,7 +3,6 @@ export * from './utils/index.js'; export * from './types/index.js'; export * from './redux/index.js'; export * from './redux/selectors/index.js'; -export * from './context/index.js'; export * from './editor/graph.js'; export * from './editor/index.js'; export * from './editor/editorTypes.js'; @@ -19,4 +18,6 @@ export * from './registry/control.js'; export * from './registry/specifics.js'; export * from './registry/toolbar.js'; export * from './types/index.js'; -export { DropPanelStore } from './components/panels/dropPanel/data.js'; + +export * from './system/index.js'; +export * from './system/frame/index.js'; diff --git a/packages/graph-editor/src/redux/index.tsx b/packages/graph-editor/src/redux/index.tsx index 8a824681e..d0185779a 100644 --- a/packages/graph-editor/src/redux/index.tsx +++ b/packages/graph-editor/src/redux/index.tsx @@ -4,20 +4,14 @@ import React, { useEffect } from 'react'; export const ReduxProvider = ({ children, - nodeTypes, - panelItems, - capabilities, - icons, schemas, - controls, - specifics, - toolbarButtons, + // toolbarButtons, }) => { - useEffect(() => { - if (toolbarButtons) { - store.dispatch.registry.setToolbarButtons(toolbarButtons); - } - }, [toolbarButtons]); + // useEffect(() => { + // if (toolbarButtons) { + // store.dispatch.registry.setToolbarButtons(toolbarButtons); + // } + // }, [toolbarButtons]); useEffect(() => { if (schemas) { @@ -25,29 +19,5 @@ export const ReduxProvider = ({ } }, [schemas]); - useEffect(() => { - store.dispatch.registry.registerIcons(icons || {}); - }, [icons]); - - useEffect(() => { - store.dispatch.registry.setControls(controls); - }, [controls]); - - useEffect(() => { - store.dispatch.registry.setNodeTypes(nodeTypes); - }, [nodeTypes]); - - useEffect(() => { - store.dispatch.registry.setSpecifics(specifics); - }, [specifics]); - - useEffect(() => { - store.dispatch.registry.setPanelItems(panelItems); - }, [panelItems]); - - useEffect(() => { - store.dispatch.registry.setCapabilities(capabilities || []); - }, [capabilities]); - return {children}; }; diff --git a/packages/graph-editor/src/redux/models/graph.ts b/packages/graph-editor/src/redux/models/graph.ts index a176d9a11..26a30d3ad 100644 --- a/packages/graph-editor/src/redux/models/graph.ts +++ b/packages/graph-editor/src/redux/models/graph.ts @@ -1,7 +1,8 @@ -import { Graph, annotatedPlayState } from '@tokens-studio/graph-engine'; +import { FullyFeaturedGraph } from '@/types/index.js'; import { ImperativeEditorRef } from '@/editor/editorTypes.js'; import { MAIN_GRAPH_ID } from '@/constants.js'; import { RootModel } from './root.js'; +import { annotatedPlayState } from '@tokens-studio/graph-engine'; import { createModel } from '@rematch/core'; import type { PlayState } from '@tokens-studio/graph-engine'; @@ -12,12 +13,12 @@ export interface ILog { } export interface IPanel { - graph: Graph; + graph: FullyFeaturedGraph; ref: ImperativeEditorRef; } export interface GraphState { - graph: Graph | undefined; + graph: FullyFeaturedGraph | undefined; currentNode: string; logs: ILog[]; graphPlayState: PlayState; @@ -83,28 +84,28 @@ export const graphState = createModel()({ }; }, startGraph(state) { - state.graph?.start(); + state.graph?.capabilities.controlFlow.start(); return { ...state, graphPlayState: state.graph?.annotations[annotatedPlayState], }; }, pauseGraph(state) { - state.graph?.pause(); + state.graph?.capabilities.controlFlow.pause(); return { ...state, graphPlayState: state.graph?.annotations[annotatedPlayState], }; }, resumeGraph(state) { - state.graph?.resume(); + state.graph?.capabilities.controlFlow.resume(); return { ...state, graphPlayState: state.graph?.annotations[annotatedPlayState], }; }, stopGraph(state) { - state.graph?.stop(); + state.graph?.capabilities.controlFlow.stop(); return { ...state, graphPlayState: state.graph?.annotations[annotatedPlayState], @@ -116,7 +117,7 @@ export const graphState = createModel()({ logs: [], }; }, - setGraph(state, graph: Graph) { + setGraph(state, graph: FullyFeaturedGraph) { return { ...state, graph, diff --git a/packages/graph-editor/src/redux/models/index.ts b/packages/graph-editor/src/redux/models/index.ts index 0afb0f6b2..b8d86b01d 100644 --- a/packages/graph-editor/src/redux/models/index.ts +++ b/packages/graph-editor/src/redux/models/index.ts @@ -3,12 +3,10 @@ import { RootModel } from './root.js'; import { graphState } from './graph.js'; import { refState } from './refs.js'; import { registryState } from './registry.js'; -import { settingsState } from './settings.js'; import { uiState } from './ui.js'; export const models: RootModel = { graph: graphState, - settings: settingsState, ui: uiState, refs: refState, registry: registryState, diff --git a/packages/graph-editor/src/redux/models/registry.ts b/packages/graph-editor/src/redux/models/registry.ts index 742830230..42c738a5a 100644 --- a/packages/graph-editor/src/redux/models/registry.ts +++ b/packages/graph-editor/src/redux/models/registry.ts @@ -1,48 +1,23 @@ import { AllSchemas, - CapabilityFactory, Node, SchemaObject, } from '@tokens-studio/graph-engine'; -import { Control } from '@/types/controls.js'; -import { DefaultToolbarButtons } from '@/registry/toolbar.js'; -import { - DropPanelStore, - defaultPanelGroupsFactory, -} from '@/components/panels/dropPanel/index.js'; -import { ReactElement } from 'react'; import { RootModel } from './root.js'; import { createModel } from '@rematch/core'; -import { defaultControls } from '@/registry/control.js'; -import { defaultSpecifics } from '@/registry/specifics.js'; -import { icons } from '@/registry/icon.js'; + import { inputControls } from '@/registry/inputControls.js'; export interface RegistryState { - //Additional specific controls for nodes. Appended to the end of the default controls - nodeSpecifics: Record>; - icons: Record; inputControls: Record>; - controls: Control[]; - nodeTypes: Record; - panelItems: DropPanelStore; - capabilities: CapabilityFactory[]; - toolbarButtons: ReactElement[]; schemas: SchemaObject[]; } export const registryState = createModel()({ state: { - nodeSpecifics: defaultSpecifics, - icons: icons(), inputControls: { ...inputControls }, - controls: [...(defaultControls as Control[])], - panelItems: defaultPanelGroupsFactory(), - nodeTypes: {}, - capabilities: [], - toolbarButtons: DefaultToolbarButtons(), schemas: AllSchemas, - } as RegistryState, + } as unknown as RegistryState, reducers: { setSchemas(state, schemas: SchemaObject[]) { return { @@ -50,65 +25,5 @@ export const registryState = createModel()({ schemas, }; }, - setToolbarButtons(state, toolbarButtons: ReactElement[]) { - return { - ...state, - toolbarButtons, - }; - }, - setCapabilities(state, payload: CapabilityFactory[]) { - return { - ...state, - capabilities: payload, - }; - }, - setNodeTypes: (state, payload: Record) => { - return { - ...state, - nodeTypes: payload, - }; - }, - setSpecifics: ( - state, - payload: Record>, - ) => { - return { - ...state, - nodeSpecifics: payload, - }; - }, - setControls(state, payload: Control[]) { - return { - ...state, - controls: payload, - }; - }, - registerIcons(state, payload: Record) { - return { - ...state, - icons: { - ...state.icons, - ...payload, - }, - }; - }, - registerInputControl( - state, - payload: { key: string; value: React.FC<{ node: Node }> }, - ) { - return { - ...state, - inputControls: { - ...state.inputControls, - [payload.key]: payload.value, - }, - }; - }, - setPanelItems(state, payload: DropPanelStore) { - return { - ...state, - panelItems: payload, - }; - }, }, }); diff --git a/packages/graph-editor/src/redux/models/root.ts b/packages/graph-editor/src/redux/models/root.ts index df2bbe604..e586803e0 100644 --- a/packages/graph-editor/src/redux/models/root.ts +++ b/packages/graph-editor/src/redux/models/root.ts @@ -2,11 +2,9 @@ import { Models } from '@rematch/core'; import { graphState } from './graph.js'; import { refState } from './refs.js'; import { registryState } from './registry.js'; -import { settingsState } from './settings.js'; import { uiState } from './ui.js'; export interface RootModel extends Models { - settings: typeof settingsState; ui: typeof uiState; graph: typeof graphState; refs: typeof refState; diff --git a/packages/graph-editor/src/redux/models/settings.ts b/packages/graph-editor/src/redux/models/settings.ts deleted file mode 100644 index b9ff22b05..000000000 --- a/packages/graph-editor/src/redux/models/settings.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { RootModel } from './root.js'; -import { createModel } from '@rematch/core'; - -export enum EdgeType { - bezier = 'Bezier', - smoothStep = 'Smooth step', - straight = 'Straight', - simpleBezier = 'Simple Bezier', -} -export enum LayoutType { - dagre = 'Dagre', - elkForce = 'Elk - Force', - elkRect = 'Elk - Rect', - elkLayered = 'Elk - Layered', - elkStress = 'Elk - Stress', -} - -export interface SettingsState { - edgeType: EdgeType; - layoutType: LayoutType; - debugMode: boolean; - showTimings: boolean; - showMinimap: boolean; - showGrid: boolean; - showSearch: boolean; - /** - * Whether to delay the update of a node when a value is changed - */ - delayedUpdate: boolean; - /** - * Whether to show the types inline with the nodes - */ - inlineTypes: boolean; - /** - * Whether to show the values inline with the nodes - */ - inlineValues: boolean; - connectOnClick: boolean; - snapGrid: boolean; -} - -export const settingsState = createModel()({ - state: { - edgeType: EdgeType.bezier, - layoutType: LayoutType.dagre, - showGrid: true, - connectOnClick: true, - showTimings: false, - showSearch: false, - inlineTypes: false, - inlineValues: false, - snapGrid: false, - debugMode: false, - showMinimap: false, - delayedUpdate: false, - } as SettingsState, - reducers: { - setConnectOnClick(state, connectOnClick: boolean) { - return { - ...state, - connectOnClick, - }; - }, - setShowSearch(state, showSearch: boolean) { - return { - ...state, - showSearch, - }; - }, - setShowMinimap(state, showMinimap: boolean) { - return { - ...state, - showMinimap, - }; - }, - - setDelayedUpdate(state, delayedUpdate: boolean) { - return { - ...state, - delayedUpdate, - }; - }, - - setSnapGrid(state, snapGrid: boolean) { - return { - ...state, - snapGrid, - }; - }, - setShowGrid(state, showGrid: boolean) { - return { - ...state, - showGrid, - }; - }, - setDebugMode(state, debugMode: boolean) { - return { - ...state, - debugMode, - }; - }, - setInlineTypes(state, inlineTypes: boolean) { - return { - ...state, - inlineTypes, - }; - }, - setInlineValues(state, inlineValues: boolean) { - return { - ...state, - inlineValues, - }; - }, - setEdgeType(state, edgeType: EdgeType) { - return { - ...state, - edgeType, - }; - }, - setShowTimings(state, showTimings: boolean) { - return { - ...state, - showTimings, - }; - }, - setLayoutType(state, layoutType: LayoutType) { - return { - ...state, - layoutType, - }; - }, - }, -}); diff --git a/packages/graph-editor/src/redux/selectors/index.ts b/packages/graph-editor/src/redux/selectors/index.ts index 9365f451f..40ee7fda4 100644 --- a/packages/graph-editor/src/redux/selectors/index.ts +++ b/packages/graph-editor/src/redux/selectors/index.ts @@ -2,5 +2,4 @@ export * from './graph.js'; export * from './refs.js'; export * from './roots.js'; export * from './registry.js'; -export * from './settings.js'; export * from './ui.js'; diff --git a/packages/graph-editor/src/redux/selectors/registry.ts b/packages/graph-editor/src/redux/selectors/registry.ts index ed9e71f27..5e2db078a 100644 --- a/packages/graph-editor/src/redux/selectors/registry.ts +++ b/packages/graph-editor/src/redux/selectors/registry.ts @@ -1,37 +1,11 @@ import { createSelector } from 'reselect'; import { registry } from './roots.js'; -export const icons = createSelector(registry, (state) => state.icons); export const inputControls = createSelector( registry, (state) => state.inputControls, ); -export const controls = createSelector(registry, (state) => state.controls); -export const nodeSpecifics = createSelector( - registry, - (state) => state.nodeSpecifics, -); - -export const panelItemsSelector = createSelector( - registry, - (state) => state.panelItems, -); - -export const capabilitiesSelector = createSelector( - registry, - (state) => state.capabilities, -); - -export const nodeTypesSelector = createSelector( - registry, - (state) => state.nodeTypes, -); - -export const ToolBarButtonsSelector = createSelector( - registry, - (state) => state.toolbarButtons, -); export const SchemaSelector = createSelector( registry, diff --git a/packages/graph-editor/src/redux/selectors/roots.ts b/packages/graph-editor/src/redux/selectors/roots.ts index abcf184a6..dbc75ccff 100644 --- a/packages/graph-editor/src/redux/selectors/roots.ts +++ b/packages/graph-editor/src/redux/selectors/roots.ts @@ -1,5 +1,4 @@ import { RootState } from '../store.js'; -export const settings = (state: RootState) => state.settings; export const ui = (state: RootState) => state.ui; export const graph = (state: RootState) => state.graph; export const refs = (state: RootState) => state.refs; diff --git a/packages/graph-editor/src/redux/selectors/settings.ts b/packages/graph-editor/src/redux/selectors/settings.ts deleted file mode 100644 index f1685b829..000000000 --- a/packages/graph-editor/src/redux/selectors/settings.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { createSelector } from 'reselect'; -import { settings } from './roots.js'; - -export const edgeType = createSelector(settings, (state) => state.edgeType); - -export const layoutType = createSelector(settings, (state) => state.layoutType); - -export const debugMode = createSelector(settings, (state) => state.debugMode); -export const inlineTypes = createSelector( - settings, - (state) => state.inlineTypes, -); -export const inlineValues = createSelector( - settings, - (state) => state.inlineValues, -); - -export const showGrid = createSelector(settings, (state) => state.showGrid); - -export const snapGrid = createSelector(settings, (state) => state.snapGrid); -export const showTimings = createSelector( - settings, - (state) => state.showTimings, -); - -export const showMinimapSelector = createSelector( - settings, - (state) => state.showMinimap, -); - -export const delayedUpdateSelector = createSelector( - settings, - (state) => state.delayedUpdate, -); -export const showSearchSelector = createSelector( - settings, - (state) => state.showSearch, -); -export const connectOnClickSelector = createSelector( - settings, - (state) => state.connectOnClick, -); diff --git a/packages/graph-editor/src/redux/store.tsx b/packages/graph-editor/src/redux/store.tsx index 3ec8c5195..fef9bacef 100644 --- a/packages/graph-editor/src/redux/store.tsx +++ b/packages/graph-editor/src/redux/store.tsx @@ -3,7 +3,6 @@ import { RefState } from './models/refs.js'; import { RegistryState } from './models/registry.js'; import { RematchDispatch, init } from '@rematch/core'; import { RootModel, models } from './models/index.js'; -import { SettingsState } from './models/settings.js'; import { UIState } from './models/ui.js'; export const store = init({ @@ -20,7 +19,6 @@ export const store = init({ export type Dispatch = RematchDispatch; export type RootState = { graph: GraphState; - settings: SettingsState; ui: UIState; refs: RefState; registry: RegistryState; diff --git a/packages/graph-editor/src/registry/control.tsx b/packages/graph-editor/src/registry/control.tsx index 75a53331e..42919d968 100644 --- a/packages/graph-editor/src/registry/control.tsx +++ b/packages/graph-editor/src/registry/control.tsx @@ -3,13 +3,12 @@ import { BOOLEAN, COLOR, CURVE, + DataFlowPort, FLOATCURVE, Input, NUMBER, - Port, STRING, VEC2, - VEC3, } from '@tokens-studio/graph-engine'; import { AnyField } from '@/components/controls/any.js'; import { ArrayField } from '@/components/controls/array.js'; @@ -27,9 +26,8 @@ import { VariadicAny } from '@/components/controls/variadicAny.js'; import { VariadicColor } from '@/components/controls/variadicColor.js'; import { VariadicNumber } from '@/components/controls/variadicNumber.js'; import { Vec2field } from '@/components/controls/vec2.js'; -import { Vec3field } from '@/components/controls/vec3.js'; -export const variadicMatcher = (id) => (port: Port) => +export const variadicMatcher = (id) => (port: DataFlowPort) => port.type.type === 'array' && port.type.items.$id === id && (port as Input).variadic; @@ -39,57 +37,54 @@ export const variadicMatcher = (id) => (port: Port) => */ export const defaultControls = [ { - matcher: (port: Port) => port.annotations['ui.control'] === 'slider', + matcher: (port: DataFlowPort) => + port.annotations['ui.control'] === 'slider', component: SliderField, }, { - matcher: (port: Port) => port.type.$id === FLOATCURVE, + matcher: (port: DataFlowPort) => port.type.$id === FLOATCURVE, component: FloatCurveField, }, { - matcher: (port: Port) => port.type.$id === CURVE, + matcher: (port: DataFlowPort) => port.type.$id === CURVE, component: CurveField, }, { - matcher: (port: Port) => port.type.$id === BOOLEAN, + matcher: (port: DataFlowPort) => port.type.$id === BOOLEAN, component: BooleanField, }, { - matcher: (port: Port) => port.type.$id === COLOR, + matcher: (port: DataFlowPort) => port.type.$id === COLOR, component: ColorField, }, { - matcher: (port: Port) => port.type.$id === NUMBER, + matcher: (port: DataFlowPort) => port.type.$id === NUMBER, component: NumericField, }, { - matcher: (port: Port) => port.type.$id === STRING && port.type.enum, + matcher: (port: DataFlowPort) => port.type.$id === STRING && port.type.enum, component: EnumeratedTextfield, }, { - matcher: (port: Port) => + matcher: (port: DataFlowPort) => port.type.$id === STRING && port.annotations['ui.control'] === 'textarea', component: TextArea, }, { - matcher: (port: Port) => port.type.$id === STRING, + matcher: (port: DataFlowPort) => port.type.$id === STRING, component: Textfield, }, { - matcher: (port: Port) => port.type.$id === VEC2, + matcher: (port: DataFlowPort) => port.type.$id === VEC2, component: Vec2field, }, { - matcher: (port: Port) => port.type.$id === VEC3, - component: Vec3field, - }, - { - matcher: (port: Port) => port.type.$id === ANY, + matcher: (port: DataFlowPort) => port.type.$id === ANY, component: AnyField, }, { - matcher: (port: Port) => + matcher: (port: DataFlowPort) => port.type.type === 'array' && !(port as Input).variadic, component: ArrayField, }, diff --git a/packages/graph-editor/src/registry/icon.tsx b/packages/graph-editor/src/registry/icon.tsx index aa2014ad7..bb5b2a4b1 100644 --- a/packages/graph-editor/src/registry/icon.tsx +++ b/packages/graph-editor/src/registry/icon.tsx @@ -20,7 +20,7 @@ import Text from '@tokens-studio/icons/Text.js'; * Default icons for the graph editor * These icons are used to represent the different types of custom types in the graph editor */ -export const icons = () => +export const iconsFactory = () => ({ [COLOR]: , [CURVE]: , diff --git a/packages/graph-editor/src/registry/inputControls.tsx b/packages/graph-editor/src/registry/inputControls.tsx index f1bfd9069..771e0cc2d 100644 --- a/packages/graph-editor/src/registry/inputControls.tsx +++ b/packages/graph-editor/src/registry/inputControls.tsx @@ -1,5 +1,5 @@ import { Button, Heading, Select, Stack } from '@tokens-studio/ui'; -import { Node } from '@tokens-studio/graph-engine'; +import { DataflowNode } from '@tokens-studio/graph-engine'; import { StringSchema } from '@tokens-studio/graph-engine'; import { deletable } from '@/annotations/index.js'; import { observer } from 'mobx-react-lite'; @@ -10,17 +10,18 @@ import properties from 'mdn-data/css/properties.json' with { type: 'json' }; const CSSProperties = Object.keys(properties); -const CSSMapSpecifics = observer(({ node }: { node: Node }) => { +const CSSMapSpecifics = observer(({ node }: { node: DataflowNode }) => { const [inputName, setInputName] = React.useState('-'); const onClick = () => { - const input = node.addInput(inputName, { + node.dataflow.addInput(inputName, { type: StringSchema, visible: true, }); - input.annotations[deletable] = true; + const input = node.inputs[inputName]; + input.setAnnotation(deletable, true); setInputName(''); - node.run(); + node.dataflow?.run(); }; const isDisabled = useMemo(() => { @@ -52,6 +53,6 @@ const CSSMapSpecifics = observer(({ node }: { node: Node }) => { ); }); -export const inputControls = { +export const inputControls: Record> = { 'studio.tokens.css.map': CSSMapSpecifics, -} as Record>; +}; diff --git a/packages/graph-editor/src/registry/specifics.tsx b/packages/graph-editor/src/registry/specifics.tsx index 055019de4..6d5d0e8b2 100644 --- a/packages/graph-editor/src/registry/specifics.tsx +++ b/packages/graph-editor/src/registry/specifics.tsx @@ -3,12 +3,12 @@ import { ColorCompare } from '@/components/preview/colorCompare.js'; import { ColorScale } from '@/components/preview/colorScale.js'; import { ColorSwatch } from '@/components/preview/swatch.js'; import { CurveField } from '@/components/controls/curve.js'; +import { DataflowNode, Input } from '@tokens-studio/graph-engine'; import { ErrorBoundary } from 'react-error-boundary'; import { ErrorBoundaryContent } from '@/components/ErrorBoundaryContent.js'; import { GraphEditor } from '@/editor/graphEditor.js'; import { ImperativeEditorRef } from '../index.js'; import { MathExpression } from '@/components/preview/mathExpression.js'; -import { Node } from '@tokens-studio/graph-engine'; import { title as annotatedTitle } from '@/annotations/index.js'; import { dockerSelector } from '@/redux/selectors/refs.js'; import { observer } from 'mobx-react-lite'; @@ -67,34 +67,34 @@ const SubgraphExplorer = ({ node }) => { ); }; -const ColorComparePreview = observer(({ node }: { node: Node }) => { +const ColorComparePreview = observer(({ node }: { node: DataflowNode }) => { return ( ); }); -const ColorScalePreview = observer(({ node }: { node: Node }) => { - return ; +const ColorScalePreview = observer(({ node }: { node: DataflowNode }) => { + return ; }); -const CurvePreview = observer(({ node }: { node: Node }) => { - return ; +const CurvePreview = observer(({ node }: { node: DataflowNode }) => { + return ; }); -const MathExpressionPreview = observer(({ node }: { node: Node }) => { - return ; +const MathExpressionPreview = observer(({ node }: { node: DataflowNode }) => { + return ; }); -const SwatchPreview = observer(({ node }: { node: Node }) => { - return ; +const SwatchPreview = observer(({ node }: { node: DataflowNode }) => { + return ; }); -const NumberPreview = observer(({ node }: { node: Node }) => { +const NumberPreview = observer(({ node }: { node: DataflowNode }) => { const { precision, value } = node.getAllInputs(); const shift = 10 ** precision; diff --git a/packages/graph-editor/src/system/frame/hook.tsx b/packages/graph-editor/src/system/frame/hook.tsx new file mode 100644 index 000000000..39c4786bc --- /dev/null +++ b/packages/graph-editor/src/system/frame/hook.tsx @@ -0,0 +1,8 @@ +import { Frame } from './index.js'; +import { createContext, useContext } from 'react'; + +export const FrameContext = createContext(undefined); + +export const useFrame = (): Frame => { + return useContext(FrameContext)!; +}; diff --git a/packages/graph-editor/src/system/frame/index.tsx b/packages/graph-editor/src/system/frame/index.tsx new file mode 100644 index 000000000..afed16062 --- /dev/null +++ b/packages/graph-editor/src/system/frame/index.tsx @@ -0,0 +1,104 @@ +import { + CapabilityFactory, + Graph, + Node, + NodeLoader, +} from '@tokens-studio/graph-engine'; +import { Control } from '@/types/controls.js'; +import { DefaultToolbarButtons } from '@/registry/toolbar.js'; +import { DropPanelStore } from '@/components/index.js'; +import { SystemSettings } from './settings.js'; +import { defaultSpecifics } from '@/registry/specifics.js'; +import { iconsFactory } from '@/registry/icon.js'; +import { makeAutoObservable } from 'mobx'; +import React from 'react'; + + +export interface IFrame { + /** + * The underlying graph to use for the frame + */ + graph :Graph; + /** + * Items to display in the drop panel. + * Not populating this will result in the default items being displayed. + */ + panelItems?: DropPanelStore; + /** + * Customize the controls that are displayed in the editor + */ + controls?: Control[]; + /** + * Additional specifics to display in the editor for custom types + */ + specifics?: Record< + string, + React.FC<{ + node: Node; + }> + >; + /** + * The loader for nodes to display in the editor + */ + nodeLoader: NodeLoader; + + /** + * Capabilities to load into the graphs. Each factory is loaded into each graph individually. + */ + capabilities?: CapabilityFactory[]; + /** + * A lookup of the custom node ui types to display in the editor. + */ + customNodeUI?: Record; + /** + * An icon lookup to be used for legends, etc + */ + icons?: Record; + + settings?: SystemSettings; + + /** + * Additional buttons to display in the toolbar + */ + toolbarButtons?: React.ReactElement[]; +} + +export class Frame { + specifics!: Record< + string, + React.FC<{ + node: Node; + }> + >; + nodeLoader!: NodeLoader; + graph!: Graph; + panelItems!: DropPanelStore; + capabilities!: CapabilityFactory[]; + customNodeUI!: Record; + controls!: Control[]; + settings!: SystemSettings; + icons!: Record; + toolbarButtons!: React.ReactElement[]; + + constructor(config: IFrame) { + const defaultConfig: Partial