feat: exposes api to update single node and hook into scene changes

This commit is contained in:
Mark Mankarious
2023-07-24 12:36:23 +01:00
committed by GitHub
parent 39afd84553
commit 37fd9ea16c
16 changed files with 197 additions and 103 deletions

24
package-lock.json generated
View File

@@ -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",

View File

@@ -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",

View File

@@ -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;

View File

@@ -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 />

View File

@@ -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 = {

View File

@@ -72,7 +72,7 @@ export const mockScene: SceneInput = {
connectors: [],
groups: [],
gridSize: {
x: 51,
y: 51
width: 51,
height: 51
}
};

View File

@@ -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);

View File

@@ -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,

View File

@@ -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 (

View File

@@ -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
};
};

View File

@@ -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;

View File

@@ -2,8 +2,8 @@ import { SceneInput } from 'src/validation/SceneSchema';
export const scene: SceneInput = {
gridSize: {
x: 10,
y: 10
width: 10,
height: 10
},
icons: [
{

View File

@@ -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);
}

View File

@@ -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,

View File

@@ -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',

View File

@@ -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);