mirror of
https://github.com/stan-smith/FossFLOW.git
synced 2025-12-24 06:58:48 -05:00
feat: exposes api to update single node and hook into scene changes
This commit is contained in:
24
package-lock.json
generated
24
package-lock.json
generated
@@ -15,7 +15,6 @@
|
||||
"@mui/material": "^5.11.10",
|
||||
"auto-bind": "^5.0.1",
|
||||
"chroma-js": "^2.4.2",
|
||||
"deep-diff": "^1.0.2",
|
||||
"gsap": "^3.11.4",
|
||||
"immer": "^10.0.2",
|
||||
"paper": "^0.12.17",
|
||||
@@ -31,7 +30,6 @@
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@types/chroma-js": "^2.4.0",
|
||||
"@types/deep-diff": "^1.0.2",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/jsdom": "^21.1.0",
|
||||
"@types/react": "^18.0.28",
|
||||
@@ -2579,12 +2577,6 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/deep-diff": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/deep-diff/-/deep-diff-1.0.2.tgz",
|
||||
"integrity": "sha512-WD2O611C7Oz7RSwKbSls8LaznKfWfXh39CHY9Amd8FhQz+NJRe20nUHhYpOopVq9M2oqDZd4L6AzqJIXQycxiA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/eslint": {
|
||||
"version": "8.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.21.1.tgz",
|
||||
@@ -4812,11 +4804,6 @@
|
||||
"integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/deep-diff": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/deep-diff/-/deep-diff-1.0.2.tgz",
|
||||
"integrity": "sha512-aWS3UIVH+NPGCD1kki+DCU9Dua032iSsO43LqQpcs4R3+dVv7tX0qBGjiVHJHjplsoUM2XRO/KB92glqc68awg=="
|
||||
},
|
||||
"node_modules/deep-equal": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.0.tgz",
|
||||
@@ -16001,12 +15988,6 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/deep-diff": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/deep-diff/-/deep-diff-1.0.2.tgz",
|
||||
"integrity": "sha512-WD2O611C7Oz7RSwKbSls8LaznKfWfXh39CHY9Amd8FhQz+NJRe20nUHhYpOopVq9M2oqDZd4L6AzqJIXQycxiA==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/eslint": {
|
||||
"version": "8.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.21.1.tgz",
|
||||
@@ -17730,11 +17711,6 @@
|
||||
"integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==",
|
||||
"dev": true
|
||||
},
|
||||
"deep-diff": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/deep-diff/-/deep-diff-1.0.2.tgz",
|
||||
"integrity": "sha512-aWS3UIVH+NPGCD1kki+DCU9Dua032iSsO43LqQpcs4R3+dVv7tX0qBGjiVHJHjplsoUM2XRO/KB92glqc68awg=="
|
||||
},
|
||||
"deep-equal": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.0.tgz",
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@types/chroma-js": "^2.4.0",
|
||||
"@types/deep-diff": "^1.0.2",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/jsdom": "^21.1.0",
|
||||
"@types/react": "^18.0.28",
|
||||
@@ -65,7 +64,6 @@
|
||||
"@mui/material": "^5.11.10",
|
||||
"auto-bind": "^5.0.1",
|
||||
"chroma-js": "^2.4.2",
|
||||
"deep-diff": "^1.0.2",
|
||||
"gsap": "^3.11.4",
|
||||
"immer": "^10.0.2",
|
||||
"paper": "^0.12.17",
|
||||
|
||||
36
src/App.tsx
36
src/App.tsx
@@ -7,12 +7,12 @@ import { SceneInput } from 'src/validation/SceneSchema';
|
||||
import { useSceneStore, Scene } from 'src/stores/useSceneStore';
|
||||
import { GlobalStyles } from 'src/styles/GlobalStyles';
|
||||
import { Renderer } from 'src/renderer/Renderer';
|
||||
import { nodeInputToNode } from 'src/utils';
|
||||
import { Coords } from 'src/utils/Coords';
|
||||
import { sceneInputtoScene, sceneToSceneInput } from 'src/utils';
|
||||
import { ItemControlsManager } from './components/ItemControls/ItemControlsManager';
|
||||
|
||||
interface Props {
|
||||
initialScene: SceneInput;
|
||||
onSceneUpdated?: (scene: SceneInput, prevScene: SceneInput) => void;
|
||||
width?: number | string;
|
||||
height: number | string;
|
||||
}
|
||||
@@ -37,26 +37,30 @@ const InnerApp = React.memo(
|
||||
)
|
||||
);
|
||||
|
||||
const App = ({ initialScene, width, height }: Props) => {
|
||||
const App = ({ initialScene, width, height, onSceneUpdated }: Props) => {
|
||||
const sceneActions = useSceneStore((state) => state.actions);
|
||||
// const setOnSceneChange = useAppState((state) => state.setOnSceneChange);
|
||||
|
||||
// useEffect(() => {
|
||||
// if (!onSceneChange) return;
|
||||
|
||||
// setOnSceneChange(onSceneChange);
|
||||
// }, [setOnSceneChange, onSceneChange]);
|
||||
|
||||
useEffect(() => {
|
||||
const nodes = initialScene.nodes.map((nodeInput) =>
|
||||
nodeInputToNode(nodeInput)
|
||||
);
|
||||
|
||||
sceneActions.set({ ...initialScene, nodes, gridSize: new Coords(51, 51) });
|
||||
const convertedInput = sceneInputtoScene(initialScene);
|
||||
sceneActions.set(convertedInput);
|
||||
}, [initialScene, sceneActions]);
|
||||
|
||||
useSceneStore.subscribe((scene, prevScene) => {
|
||||
if (!onSceneUpdated) return;
|
||||
|
||||
onSceneUpdated(sceneToSceneInput(scene), sceneToSceneInput(prevScene));
|
||||
});
|
||||
|
||||
return <InnerApp height={height} width={width} />;
|
||||
};
|
||||
|
||||
export { Scene };
|
||||
const useIsoflow = () => {
|
||||
const updateNode = useSceneStore((state) => state.actions.updateNode);
|
||||
|
||||
return {
|
||||
updateNode
|
||||
};
|
||||
};
|
||||
|
||||
export { Scene, SceneInput, useIsoflow };
|
||||
export default App;
|
||||
|
||||
@@ -1,31 +1,47 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect, useCallback } from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import GlobalStyles from '@mui/material/GlobalStyles';
|
||||
import App from './App';
|
||||
import Isoflow, { useIsoflow, SceneInput } from './App';
|
||||
import { mockScene } from './mockData';
|
||||
|
||||
const root = ReactDOM.createRoot(
|
||||
document.getElementById('root') as HTMLElement
|
||||
);
|
||||
|
||||
const DataLayer = () => (
|
||||
// const onSceneChange = useCallback<OnSceneChange>(() => {}, []);
|
||||
const DataLayer = () => {
|
||||
const { updateNode } = useIsoflow();
|
||||
|
||||
<>
|
||||
<GlobalStyles
|
||||
styles={{
|
||||
body: {
|
||||
margin: 0
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<App
|
||||
initialScene={mockScene}
|
||||
// onSceneChange={onSceneChange}
|
||||
height="100vh"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
const onSceneUpdated = useCallback((scene: SceneInput) => {
|
||||
console.log(scene);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => {
|
||||
updateNode('Node1', { label: Date.now().toString() });
|
||||
}, 1000);
|
||||
|
||||
return () => {
|
||||
clearInterval(timer);
|
||||
};
|
||||
}, [updateNode]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<GlobalStyles
|
||||
styles={{
|
||||
body: {
|
||||
margin: 0
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Isoflow
|
||||
initialScene={mockScene}
|
||||
height="100vh"
|
||||
onSceneUpdated={onSceneUpdated}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<DataLayer />
|
||||
|
||||
@@ -42,6 +42,10 @@ export const DragItems: InteractionReducer = {
|
||||
|
||||
if (nodeIndex === -1) return;
|
||||
|
||||
draftState.scene.nodes = draftState.scene.nodes.map((node) => ({
|
||||
...node,
|
||||
isSelected: false
|
||||
}));
|
||||
draftState.scene.nodes[nodeIndex].isSelected = true;
|
||||
draftState.contextMenu = draftState.scene.nodes[nodeIndex];
|
||||
draftState.itemControls = {
|
||||
|
||||
@@ -72,7 +72,7 @@ export const mockScene: SceneInput = {
|
||||
connectors: [],
|
||||
groups: [],
|
||||
gridSize: {
|
||||
x: 51,
|
||||
y: 51
|
||||
width: 51,
|
||||
height: 51
|
||||
}
|
||||
};
|
||||
|
||||
@@ -32,13 +32,14 @@ const InitialisedRenderer = () => {
|
||||
const { position: scrollPosition } = scroll;
|
||||
|
||||
useEffect(() => {
|
||||
initRenderer();
|
||||
console.log('init renderer');
|
||||
initRenderer(gridSize);
|
||||
setIsReady(true);
|
||||
|
||||
return () => {
|
||||
if (activeLayer) gsap.killTweensOf(activeLayer.view);
|
||||
};
|
||||
}, [initRenderer, activeLayer]);
|
||||
}, [initRenderer, activeLayer, gridSize.toString()]);
|
||||
|
||||
useEffect(() => {
|
||||
zoomTo(zoom);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { Path, Point, Group } from 'paper';
|
||||
import { useSceneStore } from 'src/stores/useSceneStore';
|
||||
import { Coords } from 'src/utils/Coords';
|
||||
import { applyProjectionMatrix } from '../../utils/projection';
|
||||
import { TILE_SIZE, PIXEL_UNIT, SCALING_CONST } from '../../utils/constants';
|
||||
|
||||
@@ -51,15 +51,14 @@ const drawGrid = (width: number, height: number) => {
|
||||
|
||||
export const useGrid = () => {
|
||||
const container = useRef(new Group());
|
||||
const gridSize = useSceneStore((state) => state.gridSize);
|
||||
|
||||
const init = useCallback(() => {
|
||||
const init = useCallback((gridSize: Coords) => {
|
||||
container.current.removeChildren();
|
||||
const grid = drawGrid(gridSize.x, gridSize.y);
|
||||
container.current.addChild(grid);
|
||||
|
||||
return container.current;
|
||||
}, [gridSize]);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
init,
|
||||
|
||||
@@ -43,7 +43,7 @@ export const Node = ({ node, parentContainer }: NodeProps) => {
|
||||
updateHeight: updateLabelHeight,
|
||||
setVisible: setLabelConnectorVisible
|
||||
} = labelConnector;
|
||||
const { init: initNodeTile, updateColor } = nodeTile;
|
||||
const { init: initNodeTile, updateColor, setActive } = nodeTile;
|
||||
|
||||
useEffect(() => {
|
||||
const nodeIconContainer = initNodeIcon();
|
||||
@@ -116,6 +116,10 @@ export const Node = ({ node, parentContainer }: NodeProps) => {
|
||||
updateColor(node.color);
|
||||
}, [node.color, updateColor]);
|
||||
|
||||
useEffect(() => {
|
||||
setActive(node.isSelected);
|
||||
}, [setActive, node.isSelected]);
|
||||
|
||||
if (!node.label) return null;
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,48 +1,90 @@
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { Group, Shape } from 'paper';
|
||||
import gsap from 'gsap';
|
||||
import { TILE_SIZE, PIXEL_UNIT } from 'src/renderer/utils/constants';
|
||||
import { applyProjectionMatrix } from 'src/renderer/utils/projection';
|
||||
import { getColorVariant } from 'src/utils';
|
||||
|
||||
export const useNodeTile = () => {
|
||||
const containerRef = useRef(new Group());
|
||||
const shapeRef = useRef<paper.Shape.Rectangle>();
|
||||
const tileRef = useRef<paper.Shape.Rectangle>();
|
||||
const highlightRef = useRef<paper.Shape.Rectangle>();
|
||||
|
||||
const updateColor = useCallback((color: string) => {
|
||||
if (!shapeRef.current) return;
|
||||
if (!tileRef.current || !highlightRef.current) return;
|
||||
|
||||
shapeRef.current.set({
|
||||
tileRef.current.set({
|
||||
fillColor: color,
|
||||
strokeColor: getColorVariant(color, 'dark', 2)
|
||||
});
|
||||
|
||||
highlightRef.current.set({
|
||||
strokeColor: getColorVariant(color, 'dark', 2)
|
||||
});
|
||||
}, []);
|
||||
|
||||
const setActive = useCallback((isActive: boolean) => {
|
||||
if (!highlightRef.current) return;
|
||||
|
||||
if (isActive) {
|
||||
highlightRef.current.set({ visible: true });
|
||||
|
||||
gsap
|
||||
.fromTo(
|
||||
highlightRef.current,
|
||||
{ dashOffset: 0 },
|
||||
{
|
||||
dashOffset: PIXEL_UNIT * 12,
|
||||
duration: 0.25,
|
||||
ease: 'none'
|
||||
}
|
||||
)
|
||||
.repeat(-1);
|
||||
} else {
|
||||
highlightRef.current.set({ visible: false });
|
||||
gsap.killTweensOf(highlightRef.current);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const init = useCallback(() => {
|
||||
containerRef.current.removeChildren();
|
||||
containerRef.current.set({ pivot: [0, 0] });
|
||||
|
||||
shapeRef.current = new Shape.Rectangle({
|
||||
tileRef.current = new Shape.Rectangle({
|
||||
strokeCap: 'round',
|
||||
size: [TILE_SIZE, TILE_SIZE],
|
||||
size: [TILE_SIZE * 1.2, TILE_SIZE * 1.2],
|
||||
radius: PIXEL_UNIT * 8,
|
||||
strokeWidth: 1,
|
||||
strokeColor: 'black',
|
||||
pivot: [0, 0],
|
||||
position: [0, 0],
|
||||
dashArray: null
|
||||
});
|
||||
|
||||
containerRef.current.addChild(shapeRef.current);
|
||||
highlightRef.current = tileRef.current.clone().set({
|
||||
radius: PIXEL_UNIT * 12,
|
||||
strokeWidth: PIXEL_UNIT * 3,
|
||||
pivot: [0, 0],
|
||||
dashArray: [PIXEL_UNIT * 6, PIXEL_UNIT * 6],
|
||||
scaling: 1.2,
|
||||
visible: false
|
||||
});
|
||||
|
||||
containerRef.current.addChild(highlightRef.current);
|
||||
containerRef.current.addChild(tileRef.current);
|
||||
applyProjectionMatrix(containerRef.current);
|
||||
|
||||
shapeRef.current.position.set(0, 0);
|
||||
shapeRef.current.scaling.set(1.2);
|
||||
shapeRef.current.applyMatrix = true;
|
||||
tileRef.current.position.set(0, 0);
|
||||
highlightRef.current.position.set(0, 0);
|
||||
tileRef.current.applyMatrix = true;
|
||||
highlightRef.current.applyMatrix = true;
|
||||
|
||||
return containerRef.current;
|
||||
}, []);
|
||||
|
||||
return {
|
||||
init,
|
||||
updateColor
|
||||
updateColor,
|
||||
setActive
|
||||
};
|
||||
};
|
||||
|
||||
@@ -22,21 +22,24 @@ export const useRenderer = () => {
|
||||
Paper.project.activeLayer.view.zoom = zoom;
|
||||
}, []);
|
||||
|
||||
const init = useCallback(() => {
|
||||
const gridContainer = initGrid();
|
||||
const cursorContainer = initCursor();
|
||||
const init = useCallback(
|
||||
(gridSize: Coords) => {
|
||||
const gridContainer = initGrid(gridSize);
|
||||
const cursorContainer = initCursor();
|
||||
|
||||
innerContainer.current.addChild(gridContainer);
|
||||
innerContainer.current.addChild(cursorContainer);
|
||||
innerContainer.current.addChild(nodeManager.container);
|
||||
container.current.addChild(innerContainer.current);
|
||||
container.current.set({ position: [0, 0] });
|
||||
Paper.project.activeLayer.addChild(container.current);
|
||||
setScroll({
|
||||
position: new Coords(0, 0),
|
||||
offset: new Coords(0, 0)
|
||||
});
|
||||
}, [initGrid, initCursor, setScroll, nodeManager.container]);
|
||||
innerContainer.current.addChild(gridContainer);
|
||||
innerContainer.current.addChild(cursorContainer);
|
||||
innerContainer.current.addChild(nodeManager.container);
|
||||
container.current.addChild(innerContainer.current);
|
||||
container.current.set({ position: [0, 0] });
|
||||
Paper.project.activeLayer.addChild(container.current);
|
||||
setScroll({
|
||||
position: new Coords(0, 0),
|
||||
offset: new Coords(0, 0)
|
||||
});
|
||||
},
|
||||
[initGrid, initCursor, setScroll, nodeManager.container]
|
||||
);
|
||||
|
||||
const scrollTo = useCallback((to: Coords) => {
|
||||
const { center: viewCenter } = Paper.project.view.bounds;
|
||||
|
||||
4
src/tests/fixtures/scene.ts
vendored
4
src/tests/fixtures/scene.ts
vendored
@@ -2,8 +2,8 @@ import { SceneInput } from 'src/validation/SceneSchema';
|
||||
|
||||
export const scene: SceneInput = {
|
||||
gridSize: {
|
||||
x: 10,
|
||||
y: 10
|
||||
width: 10,
|
||||
height: 10
|
||||
},
|
||||
icons: [
|
||||
{
|
||||
|
||||
@@ -57,6 +57,10 @@ export class Coords {
|
||||
return `x: ${this.x}, y: ${this.y}`;
|
||||
}
|
||||
|
||||
toObject() {
|
||||
return { x: this.x, y: this.y };
|
||||
}
|
||||
|
||||
static fromObject({ x, y }: { x: number; y: number }) {
|
||||
return new Coords(x, y);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import { customVars } from '../styles/theme';
|
||||
|
||||
export const GRID_DEFAULTS = {
|
||||
size: {
|
||||
x: 51,
|
||||
y: 51
|
||||
}
|
||||
};
|
||||
|
||||
export const NODE_DEFAULTS = {
|
||||
label: '',
|
||||
labelHeight: 100,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import gsap from 'gsap';
|
||||
import { Coords } from 'src/utils/Coords';
|
||||
import chroma from 'chroma-js';
|
||||
import type { NodeInput } from 'src/validation/SceneSchema';
|
||||
import { Node, SceneItemTypeEnum } from 'src/stores/useSceneStore';
|
||||
import { NODE_DEFAULTS } from 'src/utils/defaults';
|
||||
import type { NodeInput, SceneInput } from 'src/validation/SceneSchema';
|
||||
import { Node, SceneItemTypeEnum, Scene } from 'src/stores/useSceneStore';
|
||||
import { NODE_DEFAULTS, GRID_DEFAULTS } from 'src/utils/defaults';
|
||||
|
||||
export const clamp = (num: number, min: number, max: number) =>
|
||||
num <= min ? min : num >= max ? max : num; // eslint-disable-line no-nested-ternary
|
||||
@@ -61,6 +61,40 @@ export const nodeInputToNode = (nodeInput: NodeInput): Node => {
|
||||
return node;
|
||||
};
|
||||
|
||||
export const sceneInputtoScene = (sceneInput: SceneInput) => {
|
||||
const scene = {
|
||||
...sceneInput,
|
||||
nodes: sceneInput.nodes.map((nodeInput) => nodeInputToNode(nodeInput)),
|
||||
icons: sceneInput.icons,
|
||||
gridSize: sceneInput.gridSize
|
||||
? new Coords(sceneInput.gridSize.width, sceneInput.gridSize.height)
|
||||
: Coords.fromObject(GRID_DEFAULTS.size)
|
||||
};
|
||||
|
||||
return scene;
|
||||
};
|
||||
|
||||
export const sceneToSceneInput = (scene: Scene) => {
|
||||
const nodes: SceneInput['nodes'] = scene.nodes.map((node) => ({
|
||||
id: node.id,
|
||||
position: node.position.toObject(),
|
||||
label: node.label,
|
||||
labelHeight: node.labelHeight,
|
||||
color: node.color,
|
||||
iconId: node.iconId
|
||||
}));
|
||||
|
||||
const sceneInput: SceneInput = {
|
||||
nodes,
|
||||
connectors: [],
|
||||
groups: [],
|
||||
icons: scene.icons,
|
||||
gridSize: { width: scene.gridSize.x, height: scene.gridSize.y }
|
||||
};
|
||||
|
||||
return sceneInput;
|
||||
};
|
||||
|
||||
export const getColorVariant = (
|
||||
color: string,
|
||||
variant: 'light' | 'dark',
|
||||
|
||||
@@ -68,10 +68,12 @@ export const sceneInput = z
|
||||
nodes: z.array(nodeInput),
|
||||
connectors: z.array(connectorInput),
|
||||
groups: z.array(groupInput),
|
||||
gridSize: z.object({
|
||||
x: z.number(),
|
||||
y: z.number()
|
||||
})
|
||||
gridSize: z
|
||||
.object({
|
||||
width: z.number(),
|
||||
height: z.number()
|
||||
})
|
||||
.optional()
|
||||
})
|
||||
.superRefine((scene, ctx) => {
|
||||
const invalidNode = findInvalidNode(scene.nodes, scene.icons);
|
||||
|
||||
Reference in New Issue
Block a user