feat: implements node labels

This commit is contained in:
Mark Mankarious
2023-07-21 21:29:55 +01:00
parent dc41805f82
commit f93f901521
25 changed files with 425 additions and 171 deletions

View File

@@ -2,7 +2,7 @@ import React, { useEffect } from 'react';
import { ThemeProvider } from '@mui/material/styles';
import { Box } from '@mui/material';
import { theme } from 'src/styles/theme';
import { ToolMenu } from 'src/components/ToolMenu';
import { ToolMenu } from 'src/components/ToolMenu/ToolMenu';
import { SceneInput } from 'src/validation/SceneSchema';
import { useSceneStore, Scene } from 'src/stores/useSceneStore';
import { GlobalStyles } from 'src/styles/GlobalStyles';

View File

@@ -1,9 +1,9 @@
import React, { useMemo } from 'react';
import React from 'react';
import {
ArrowRightAlt as ConnectIcon,
Delete as DeleteIcon
} from '@mui/icons-material';
import { useSceneStore } from 'src/stores/useSceneStore';
import { useNodeHooks } from 'src/stores/useSceneStore';
import { ContextMenu } from './components/ContextMenu';
import { ContextMenuItem } from './components/ContextMenuItem';
@@ -12,12 +12,8 @@ interface Props {
}
export const NodeContextMenu = ({ nodeId }: Props) => {
const sceneItems = useSceneStore(({ nodes }) => ({ nodes }));
const getNodeById = useSceneStore((state) => state.actions.getNodeById);
const node = useMemo(
() => getNodeById(nodeId),
[getNodeById, nodeId, sceneItems]
);
const { useGetNodeById } = useNodeHooks();
const node = useGetNodeById(nodeId);
if (!node) return null;

View File

@@ -1,50 +1,36 @@
import React, { useMemo, useCallback } from 'react';
import { useTheme } from '@mui/material';
import Card from '@mui/material/Card';
import Slide from '@mui/material/Slide';
import React, { useMemo } from 'react';
import { Card, useTheme } from '@mui/material';
import { useUiStateStore } from 'src/stores/useUiStateStore';
import { NodeControls } from './NodeControls/NodeControls';
import { ProjectControls } from './ProjectControls/ProjectControls';
export const ItemControlsManager = () => {
const theme = useTheme();
const itemControls = useUiStateStore((state) => state.itemControls);
const uiStateActions = useUiStateStore((state) => state.actions);
const onClose = useCallback(() => {
uiStateActions.setSidebar(null);
}, [uiStateActions]);
const theme = useTheme();
const Controls = useMemo(() => {
switch (itemControls?.type) {
case 'SINGLE_NODE':
return <NodeControls onClose={onClose} />;
return <NodeControls nodeId={itemControls.nodeId} />;
case 'PROJECT_SETTINGS':
return <ProjectControls onClose={onClose} />;
return <ProjectControls />;
default:
return null;
}
}, [itemControls, onClose]);
}, [itemControls]);
return (
<Slide
direction="right"
in={itemControls !== null}
mountOnEnter
unmountOnExit
<Card
sx={{
position: 'absolute',
width: '325px',
maxHeight: `calc(100% - ${theme.customVars.appPadding.y * 2}px)`,
left: theme.customVars.appPadding.x,
top: theme.customVars.appPadding.y,
borderRadius: 2
}}
>
<Card
sx={{
position: 'absolute',
width: '400px',
height: '100%',
top: 0,
left: theme.customVars.appPadding.x,
borderRadius: 0
}}
>
{Controls}
</Card>
</Slide>
{Controls}
</Card>
);
};

View File

@@ -1,37 +1,60 @@
import React, { useState } from 'react';
import React, { useState, useCallback } from 'react';
import { Tabs, Tab, Box } from '@mui/material';
import { useSceneStore } from 'src/stores/useSceneStore';
import { useSceneStore, useNodeHooks, Node } from 'src/stores/useSceneStore';
import { ControlsContainer } from '../components/ControlsContainer';
import { Icons } from './IconSelection/IconSelection';
import { Header } from '../components/Header';
import { NodeSettings } from './NodeSettings/NodeSettings';
interface Props {
onClose: () => void;
nodeId: string;
}
export const NodeControls = ({ onClose }: Props) => {
export const NodeControls = ({ nodeId }: Props) => {
const [tab, setTab] = useState(0);
const icons = useSceneStore((state) => state.icons);
const sceneActions = useSceneStore((state) => state.actions);
const { useGetNodeById } = useNodeHooks();
const node = useGetNodeById(nodeId);
const onTabChanged = (event: React.SyntheticEvent, newValue: number) => {
setTab(newValue);
};
const onNodeUpdated = useCallback(
(updates: Partial<Node>) => {
sceneActions.updateNode(nodeId, updates);
},
[sceneActions, nodeId]
);
if (!node) return null;
return (
<ControlsContainer
header={
<Box>
<Header title="Node" onClose={onClose} />{' '}
<Tabs value={tab} onChange={onTabChanged}>
<Header title="Node settings" />
<Tabs sx={{ px: 2 }} value={tab} onChange={onTabChanged}>
<Tab label="Settings" />
<Tab label="Icons" />
</Tabs>
</Box>
}
>
{tab === 0 && <NodeSettings />}
{tab === 1 && <Icons icons={icons} onClick={() => {}} />}
{tab === 0 && (
<NodeSettings
label={node.label}
labelHeight={node.labelHeight}
onUpdate={onNodeUpdated}
/>
)}
{tab === 1 && (
<Icons
icons={icons}
onClick={(icon) => onNodeUpdated({ iconId: icon.id })}
/>
)}
</ControlsContainer>
);
};

View File

@@ -1,13 +1,34 @@
import React, { useState } from 'react';
import { MarkdownEditor } from '../../../MarkdownEditor';
import React from 'react';
import { Slider } from '@mui/material';
import { Node } from 'src/stores/useSceneStore';
import { MarkdownEditor } from '../../../MarkdownEditor/MarkdownEditor';
import { Section } from '../../components/Section';
export const NodeSettings = () => {
const [label, setLabel] = useState('');
interface Props {
label: string;
labelHeight: number;
onUpdate: (updates: Partial<Node>) => void;
}
return (
<Section>
<MarkdownEditor value={label} onChange={setLabel} />
export const NodeSettings = ({ label, labelHeight, onUpdate }: Props) => (
<>
<Section title="Label">
<MarkdownEditor
value={label}
onChange={(text) => onUpdate({ label: text })}
/>
</Section>
);
};
<Section title="Label height">
<Slider
marks
step={20}
min={0}
max={200}
value={labelHeight}
onChange={(e, newHeight) =>
onUpdate({ labelHeight: newHeight as number })
}
/>
</Section>
</>
);

View File

@@ -6,16 +6,12 @@ import { Header } from '../components/Header';
import { Section } from '../components/Section';
import { ControlsContainer } from '../components/ControlsContainer';
interface Props {
onClose: () => void;
}
interface Values {
name?: string;
notes?: string;
}
export const ProjectControls = ({ onClose }: Props) => {
export const ProjectControls = () => {
const { register, handleSubmit } = useForm<Values>({
defaultValues: {
name: '',
@@ -28,9 +24,7 @@ export const ProjectControls = ({ onClose }: Props) => {
}, []);
return (
<ControlsContainer
header={<Header title="Project settings" onClose={onClose} />}
>
<ControlsContainer header={<Header title="Project settings" />}>
<Section>
<form onSubmit={handleSubmit(onSubmit)}>
<Grid container spacing={4}>

View File

@@ -1,5 +1,5 @@
import React from 'react';
import Box from '@mui/material/Box';
import { Box } from '@mui/material';
interface Props {
header: React.ReactNode;
@@ -9,6 +9,7 @@ interface Props {
export const ControlsContainer = ({ header, children }: Props) => (
<Box
sx={{
position: 'relative',
height: '100%',
width: '100%',
display: 'flex',
@@ -20,16 +21,7 @@ export const ControlsContainer = ({ header, children }: Props) => (
sx={{
width: '100%',
overflowY: 'scroll',
flexGrow: 1,
'*::-webkit-scrollbar': {
width: '0.4em'
},
'*::-webkit-scrollbar-track': {
'-webkit-box-shadow': 'inset 0 0 6px rgba(0,0,0,0.00)'
},
'*::-webkit-scrollbar-thumb': {
backgroundColor: 'rgba(0,0,0,.1)'
}
flexGrow: 1
}}
>
<Box sx={{ width: '100%', pb: 6 }}>{children}</Box>

View File

@@ -2,28 +2,20 @@ import React from 'react';
import Typography from '@mui/material/Typography';
import Box from '@mui/material/Box';
import Grid from '@mui/material/Grid';
import Button from '@mui/material/Button';
import CloseIcon from '@mui/icons-material/Close';
import { Section } from './Section';
interface Props {
title: string;
onClose: () => void;
}
export const Header = ({ title, onClose }: Props) => (
export const Header = ({ title }: Props) => (
<Section py={2}>
<Grid container spacing={2}>
<Grid item xs={10}>
<Box sx={{ display: 'flex', alignItems: 'center', height: '100%' }}>
<Typography variant="h5">{title}</Typography>
</Box>
</Grid>
<Grid item xs={2}>
<Box>
<Button variant="text" onClick={onClose}>
<CloseIcon />
</Button>
<Typography variant="body2" color="text.secondary">
{title}
</Typography>
</Box>
</Grid>
</Grid>

View File

@@ -11,9 +11,13 @@ interface Props {
}
export const Section = ({ children, py, px, title }: Props) => (
<Box py={py ?? 3} px={px ?? 2}>
<Box py={py ?? 3} px={px ?? 3}>
<Stack>
{title && <Typography fontWeight={600}>{title}</Typography>}
{title && (
<Typography variant="body2" color="text.secondary" pb={0.5}>
{title}
</Typography>
)}
{children}
</Stack>
</Box>

View File

@@ -0,0 +1,56 @@
import React, { useMemo } from 'react';
import ReactQuill from 'react-quill';
import { Box } from '@mui/material';
interface Props {
value: string;
onChange?: (value: string) => void;
readOnly?: boolean;
}
const tools = ['bold', 'italic', 'underline', 'strike', 'bullet', 'link'];
export const MarkdownEditor = ({ value, onChange, readOnly }: Props) => {
const modules = useMemo(() => {
if (!readOnly)
return {
toolbar: tools
};
return { toolbar: false };
}, [readOnly]);
return (
<Box
sx={{
'.ql-toolbar.ql-snow': {
border: 'none',
pt: 0,
px: 0
},
'.ql-toolbar.ql-snow + .ql-container.ql-snow': {
border: '1px solid',
borderColor: 'grey.800',
borderTop: 'auto',
borderRadius: 1.5,
height: 200
},
'.ql-container.ql-snow': {
...(readOnly ? { border: 'none' } : {})
},
'.ql-editor': {
...(readOnly ? { p: 0 } : {})
}
}}
>
<ReactQuill
theme="snow"
value={value}
readOnly={readOnly}
onChange={onChange}
formats={tools}
modules={modules}
/>
</Box>
);
};

View File

@@ -1,41 +0,0 @@
import React from "react";
import ReactQuill from "react-quill";
import { Box } from "@mui/material";
interface Props {
value: string;
onChange: (value: string) => void;
}
const style = {
".ql-toolbar.ql-snow": {
border: "none",
pt: 0,
px: 0,
},
".ql-toolbar.ql-snow + .ql-container.ql-snow": {
border: "1px solid",
borderColor: "grey.800",
borderTop: "auto",
borderRadius: 1.5,
height: 200,
},
};
const tools = ["bold", "italic", "underline", "strike", "bullet", "link"];
export const MarkdownEditor = ({ value, onChange }: Props) => {
return (
<Box sx={style}>
<ReactQuill
theme="snow"
value={value}
onChange={onChange}
formats={tools}
modules={{
toolbar: tools,
}}
/>
</Box>
);
};

View File

@@ -23,8 +23,8 @@ export const ToolMenu = () => {
<Card
sx={{
position: 'absolute',
top: theme.spacing(4),
right: theme.spacing(4),
top: theme.customVars.appPadding.y,
right: theme.customVars.appPadding.x,
height: theme.customVars.toolMenu.height,
borderRadius: 2
}}

View File

@@ -24,6 +24,7 @@ export const Cursor: InteractionReducer = {
type: 'EMPTY_TILE',
position: draftState.mouse.tile
};
draftState.itemControls = null;
}
},
mouseup: () => {}

View File

@@ -1,4 +1,4 @@
import React, { useEffect } from 'react';
import React, { useEffect, useState } from 'react';
import Paper from 'paper';
import gsap from 'gsap';
import { Coords } from 'src/utils/Coords';
@@ -13,6 +13,7 @@ import { ContextMenuLayer } from './components/ContextMenuLayer/ContextMenuLayer
const InitialisedRenderer = () => {
const renderer = useRenderer();
const [isReady, setIsReady] = useState(false);
const scene = useSceneStore(({ nodes }) => ({ nodes }));
const gridSize = useSceneStore((state) => state.gridSize);
const mode = useUiStateStore((state) => state.mode);
@@ -32,6 +33,7 @@ const InitialisedRenderer = () => {
useEffect(() => {
initRenderer();
setIsReady(true);
return () => {
if (activeLayer) gsap.killTweensOf(activeLayer.view);
@@ -84,12 +86,14 @@ const InitialisedRenderer = () => {
renderer.cursor.setVisible(isCursorVisible);
}, [mode.type, mouse.position, renderer.cursor]);
if (!isReady) return null;
return (
<>
{scene.nodes.map((node) => (
<Node
key={node.id}
{...node}
node={node}
parentContainer={renderer.nodeManager.container as paper.Group}
/>
))}

View File

@@ -1,55 +1,128 @@
import { useEffect, useRef, useState } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import { Group } from 'paper';
import { Box } from '@mui/material';
import gsap from 'gsap';
import { Coords } from 'src/utils/Coords';
import { useUiStateStore } from 'src/stores/useUiStateStore';
import { Node as NodeInterface } from 'src/stores/useSceneStore';
import { useNodeIcon } from './useNodeIcon';
import { getTilePosition } from '../../utils/gridHelpers';
import { NodeLabel } from './NodeLabel';
import { useNodeTile } from './useNodeTile';
import {
getTilePosition,
getTileScreenPosition
} from '../../utils/gridHelpers';
import { useLabelConnector } from './useLabelConnector';
export interface NodeProps {
position: Coords;
iconId: string;
node: NodeInterface;
parentContainer: paper.Group;
}
export const Node = ({ position, iconId, parentContainer }: NodeProps) => {
const isEmptyLabel = (label: string) => label === '<p><br></p>' || label === '';
export const Node = ({ node, parentContainer }: NodeProps) => {
const [isFirstDisplay, setIsFirstDisplay] = useState(true);
const container = useRef(new Group());
const groupRef = useRef(new Group());
const labelRef = useRef<HTMLDivElement>();
const nodeIcon = useNodeIcon();
const labelConnector = useLabelConnector();
const nodeTile = useNodeTile();
const scroll = useUiStateStore((state) => state.scroll);
const zoom = useUiStateStore((state) => state.zoom);
const mode = useUiStateStore((state) => state.mode);
const [labelSize, setLabelSize] = useState({ width: 0, height: 0 });
const {
init: initNodeIcon,
update: updateNodeIcon,
isLoaded: isIconLoaded
} = nodeIcon;
const {
init: initLabelConnector,
updateHeight: updateLabelHeight,
setVisible: setLabelConnectorVisible
} = labelConnector;
const { init: initNodeTile } = nodeTile;
useEffect(() => {
const nodeIconContainer = initNodeIcon();
const labelConnectorContainer = initLabelConnector();
const nodeColorContainer = initNodeTile();
container.current.removeChildren();
container.current.addChild(nodeIconContainer);
parentContainer.addChild(container.current);
}, [initNodeIcon, parentContainer]);
groupRef.current.removeChildren();
groupRef.current.addChild(nodeColorContainer);
groupRef.current.addChild(labelConnectorContainer);
groupRef.current.addChild(nodeIconContainer);
groupRef.current.pivot = nodeIconContainer.bounds.bottomCenter;
parentContainer.addChild(groupRef.current);
}, [initNodeIcon, parentContainer, initLabelConnector, initNodeTile]);
useEffect(() => {
updateNodeIcon(iconId);
}, [iconId, updateNodeIcon]);
updateNodeIcon(node.iconId);
}, [node.iconId, updateNodeIcon]);
useEffect(() => {
if (!isIconLoaded) return;
const tweenValues = Coords.fromObject(container.current.position);
const endState = getTilePosition(position);
const tweenValues = Coords.fromObject(groupRef.current.position);
const endState = getTilePosition(node.position);
gsap.to(tweenValues, {
duration: isFirstDisplay ? 0 : 0.1,
...endState,
onUpdate: () => {
container.current.position.set(tweenValues);
groupRef.current.position.set(tweenValues);
}
});
if (isFirstDisplay) setIsFirstDisplay(false);
}, [position, isFirstDisplay, isIconLoaded]);
}, [node.position, isFirstDisplay, isIconLoaded]);
return null;
useEffect(() => {
if (!labelRef.current) return;
const screenPosition = getTileScreenPosition({
position: node.position,
scrollPosition: scroll.position,
zoom,
origin: 'top'
});
gsap.to(labelRef.current, {
duration: mode.type === 'PAN' ? 0 : 0.1,
left: screenPosition.x - labelSize.width * 0.5,
top: screenPosition.y - labelSize.height - node.labelHeight * zoom,
scale: zoom
});
}, [node.position, node.labelHeight, zoom, scroll.position, mode, labelSize]);
useEffect(() => {
setLabelConnectorVisible(!isEmptyLabel(node.label));
if (!labelRef.current) return;
setLabelSize({
width: labelRef.current.clientWidth ?? 0,
height: labelRef.current.clientHeight ?? 0
});
}, [node.label, setLabelConnectorVisible]);
useEffect(() => {
updateLabelHeight(node.labelHeight);
}, [node.labelHeight, updateLabelHeight]);
if (!node.label) return null;
return (
<Box
ref={labelRef}
sx={{
position: 'absolute',
transformOrigin: 'bottom center'
}}
>
<NodeLabel label={node.label} />
</Box>
);
};

View File

@@ -0,0 +1,24 @@
import React from 'react';
import { Box } from '@mui/material';
import { MarkdownEditor } from 'src/components/MarkdownEditor/MarkdownEditor';
interface Props {
label: string;
}
export const NodeLabel = ({ label }: Props) => (
<Box
sx={{
bgcolor: 'common.white',
border: '1px solid',
borderColor: 'grey.500',
maxWidth: 200,
maxHeight: 150,
borderRadius: 2,
overflow: 'hidden',
p: 1.5
}}
>
<MarkdownEditor readOnly value={label} />
</Box>
);

View File

@@ -0,0 +1,51 @@
import { useRef, useCallback } from 'react';
import { Group, Path, Point } from 'paper';
import { useTheme } from '@mui/material';
import {
PIXEL_UNIT,
PROJECTED_TILE_DIMENSIONS
} from 'src/renderer/utils/constants';
export const useLabelConnector = () => {
const theme = useTheme();
const containerRef = useRef(new Group());
const pathRef = useRef<paper.Path.Line>();
const updateHeight = useCallback((labelHeight: number) => {
if (!pathRef.current) return;
pathRef.current.segments[1].point.y = -(
labelHeight +
PROJECTED_TILE_DIMENSIONS.y * 0.5
);
}, []);
const setVisible = useCallback((state: boolean) => {
containerRef.current.visible = state;
}, []);
const init = useCallback(() => {
containerRef.current.removeChildren();
pathRef.current = new Path.Line({
strokeColor: theme.palette.grey[800],
strokeWidth: PIXEL_UNIT * 2.5,
dashArray: [0, PIXEL_UNIT * 6],
strokeJoin: 'round',
strokeCap: 'round',
dashOffset: PIXEL_UNIT * 4,
from: new Point(0, 0)
});
containerRef.current.addChild(pathRef.current);
return containerRef.current;
}, [theme.palette.grey]);
return {
containerRef,
init,
updateHeight,
setVisible
};
};

View File

@@ -1,5 +1,5 @@
import { useRef, useCallback, useState } from 'react';
import { Group, Raster } from 'paper';
import { Group, Raster, Point } from 'paper';
import { useSceneStore } from 'src/stores/useSceneStore';
import { PROJECTED_TILE_DIMENSIONS } from '../../utils/constants';
@@ -13,6 +13,7 @@ export const useNodeIcon = () => {
const update = useCallback(
async (iconId: string) => {
setIsLoaded(false);
container.current.removeChildren();
const icon = icons.find((_icon) => _icon.id === iconId);
@@ -31,9 +32,12 @@ export const useNodeIcon = () => {
const raster = iconRaster.rasterize();
container.current.removeChildren();
container.current.addChild(raster);
container.current.pivot = iconRaster.bounds.bottomCenter;
container.current.position = new Point(
0,
PROJECTED_TILE_DIMENSIONS.y * 0.5
);
resolve(null);
};

View File

@@ -0,0 +1,38 @@
import { useCallback, useRef } from 'react';
import { Group, Shape } from 'paper';
import { TILE_SIZE, PIXEL_UNIT } from 'src/renderer/utils/constants';
import { applyProjectionMatrix } from 'src/renderer/utils/projection';
export const useNodeTile = () => {
const containerRef = useRef(new Group());
const init = useCallback(() => {
containerRef.current.removeChildren();
containerRef.current.set({ pivot: [0, 0] });
const rectangle = new Shape.Rectangle({
strokeCap: 'round',
fillColor: 'blue',
size: [TILE_SIZE, TILE_SIZE],
radius: PIXEL_UNIT * 8,
strokeWidth: 0,
strokeColor: 'transparent',
pivot: [0, 0],
position: [0, 0],
dashArray: null
});
containerRef.current.addChild(rectangle);
applyProjectionMatrix(containerRef.current);
rectangle.position.set(0, 0);
rectangle.scaling.set(1.2);
rectangle.applyMatrix = true;
return containerRef.current;
}, []);
return {
init
};
};

View File

@@ -75,25 +75,24 @@ export const getItemsByTile = ({ tile, sceneItems }: GetItemsByTile) => {
return { nodes };
};
interface GetTileScreenPosition {
interface CanvasCoordsToScreenCoords {
position: Coords;
scrollPosition: Coords;
zoom: number;
}
export const getTileScreenPosition = ({
export const canvasCoordsToScreenCoords = ({
position,
scrollPosition,
zoom
}: GetTileScreenPosition) => {
}: CanvasCoordsToScreenCoords) => {
const { width: viewW, height: viewH } = Paper.view.bounds;
const { offsetLeft: offsetX, offsetTop: offsetY } =
Paper.project.view.element;
const tilePosition = getTileBounds(position).center;
const container = Paper.project.activeLayer.children[0];
const globalItemsGroupPosition = container.globalToLocal([0, 0]);
const screenPosition = new Coords(
(tilePosition.x +
const onScreenPosition = new Coords(
(position.x +
scrollPosition.x +
globalItemsGroupPosition.x +
container.position.x +
@@ -101,7 +100,7 @@ export const getTileScreenPosition = ({
zoom +
offsetX,
(tilePosition.y +
(position.y +
scrollPosition.y +
globalItemsGroupPosition.y +
container.position.y +
@@ -110,5 +109,25 @@ export const getTileScreenPosition = ({
offsetY
);
return screenPosition;
return onScreenPosition;
};
type GetTileScreenPosition = CanvasCoordsToScreenCoords & {
origin?: 'center' | 'top' | 'bottom' | 'left' | 'right';
};
export const getTileScreenPosition = ({
position,
scrollPosition,
zoom,
origin = 'center'
}: GetTileScreenPosition) => {
const tilePosition = getTileBounds(position)[origin];
const onScreenPosition = canvasCoordsToScreenCoords({
position: tilePosition,
scrollPosition,
zoom
});
return onScreenPosition;
};

View File

@@ -1,6 +1,8 @@
import { useCallback } from 'react';
import { create } from 'zustand';
import { v4 as uuid } from 'uuid';
import { produce } from 'immer';
import { NODE_DEFAULTS } from 'src/utils/defaults';
import { IconInput } from '../validation/SceneSchema';
import { Coords } from '../utils/Coords';
@@ -17,7 +19,8 @@ export interface Node {
type: SceneItemTypeEnum.NODE;
id: string;
iconId: string;
label?: string;
label: string;
labelHeight: number;
position: Coords;
isSelected: boolean;
}
@@ -37,7 +40,6 @@ export interface SceneActions {
set: (scene: Scene) => void;
setItems: (elements: SceneItems) => void;
updateNode: (id: string, updates: Partial<Node>) => void;
getNodeById: (id: string) => Node | undefined;
createNode: (position: Coords) => void;
}
@@ -56,10 +58,6 @@ export const useSceneStore = create<UseSceneStore>((set, get) => ({
setItems: (items: SceneItems) => {
set({ nodes: items.nodes });
},
getNodeById: (id: string) => {
const { nodes } = get();
return nodes.find((node) => node.id === id);
},
updateNode: (id, updates) => {
const { nodes } = get();
const nodeIndex = nodes.findIndex((node) => node.id === id);
@@ -77,6 +75,7 @@ export const useSceneStore = create<UseSceneStore>((set, get) => ({
createNode: (position) => {
const { nodes, icons } = get();
const newNode: Node = {
...NODE_DEFAULTS,
id: uuid(),
type: SceneItemTypeEnum.NODE,
iconId: icons[0].id,
@@ -88,3 +87,14 @@ export const useSceneStore = create<UseSceneStore>((set, get) => ({
}
}
}));
export const useNodeHooks = () => {
const nodes = useSceneStore((state) => state.nodes);
const useGetNodeById = useCallback(
(id: string) => nodes.find((node) => node.id === id),
[nodes]
);
return { useGetNodeById };
};

View File

@@ -25,8 +25,8 @@ declare module '@mui/material/styles' {
const customVars: CustomThemeVars = {
appPadding: {
x: 60,
y: 60
x: 40,
y: 40
},
toolMenu: {
height: 55

4
src/utils/defaults.ts Normal file
View File

@@ -0,0 +1,4 @@
export const NODE_DEFAULTS = {
label: '',
labelHeight: 100
};

View File

@@ -2,6 +2,7 @@ import gsap from 'gsap';
import { Coords } from 'src/utils/Coords';
import type { NodeInput } from 'src/validation/SceneSchema';
import { Node, SceneItemTypeEnum } from 'src/stores/useSceneStore';
import { NODE_DEFAULTS } from 'src/utils/defaults';
export const clamp = (num: number, min: number, max: number) =>
num <= min ? min : num >= max ? max : num;
@@ -47,7 +48,8 @@ export const roundToOneDecimalPlace = (num: number) =>
export const nodeInputToNode = (nodeInput: NodeInput): Node => {
const node: Node = {
id: nodeInput.id,
label: nodeInput.label,
label: nodeInput.label ?? NODE_DEFAULTS.label,
labelHeight: nodeInput.labelHeight ?? NODE_DEFAULTS.labelHeight,
iconId: nodeInput.iconId,
position: Coords.fromObject(nodeInput.position),
isSelected: false,

View File

@@ -10,6 +10,7 @@ export const iconInput = z.object({
export const nodeInput = z.object({
id: z.string(),
label: z.string().optional(),
labelHeight: z.number().optional(),
iconId: z.string(),
position: z.object({
x: z.number(),