From 694f8fb42bb3ab897dd89c54a82b6f26e91d2adf Mon Sep 17 00:00:00 2001 From: salam dalloul Date: Tue, 8 Oct 2024 16:28:36 +0200 Subject: [PATCH] add an example of using the redux toolkit store --- .../vite-redux-toolkit-react-app/package.json | 15 +- .../vite-redux-toolkit-react-app/src/App.tsx | 1 + .../src/components/ColorPicker.tsx | 47 ++++ .../src/components/CustomFormControlLabel.tsx | 81 +++++++ .../src/components/LeftSidebar.tsx | 1 - .../src/components/PickerWrapper.tsx | 37 ++++ .../components/viewers/ThreeD/STLViewer.tsx | 3 +- .../viewers/ThreeD/SceneControls.tsx | 204 +++++++++++++++--- .../viewers/ThreeD/ThreeDViewer.tsx | 33 +-- .../src/layoutManager/componentsMap.tsx | 2 +- .../src/models/models.ts | 2 + .../src/redux/slices/instanceSlice.ts | 45 ++++ .../src/redux/store.ts | 5 +- 13 files changed, 410 insertions(+), 66 deletions(-) create mode 100644 examples/vite-redux-toolkit-react-app/src/components/ColorPicker.tsx create mode 100644 examples/vite-redux-toolkit-react-app/src/components/CustomFormControlLabel.tsx create mode 100644 examples/vite-redux-toolkit-react-app/src/components/PickerWrapper.tsx create mode 100644 examples/vite-redux-toolkit-react-app/src/redux/slices/instanceSlice.ts diff --git a/examples/vite-redux-toolkit-react-app/package.json b/examples/vite-redux-toolkit-react-app/package.json index c122d89a1..a5c619103 100644 --- a/examples/vite-redux-toolkit-react-app/package.json +++ b/examples/vite-redux-toolkit-react-app/package.json @@ -16,6 +16,9 @@ "@emotion/react": "^11.13.3", "@emotion/styled": "^11.13.0", "@fortawesome/fontawesome-free": "^6.6.0", + "@fortawesome/fontawesome-svg-core": "^6.6.0", + "@fortawesome/free-solid-svg-icons": "^6.6.0", + "@fortawesome/react-fontawesome": "^0.2.2", "@material-ui/core": "^4.12.4", "@metacell/geppetto-meta-client": "^2.0.0", "@metacell/geppetto-meta-core": "^2.0.0", @@ -26,10 +29,16 @@ "@react-three/fiber": "^8.17.9", "@reduxjs/toolkit": "^2.2.7", "@types/react-redux": "^7.1.34", + "ami.js": ">=0.32.0", + "file-saver": "^2.0.5", + "jszip": "^3.10.1", + "openseadragon": "^5.0.0", "react": "^18.3.1", + "react-color": "^2.19.3", "react-dom": "^18.3.1", "react-redux": "^9.1.2", - "three": "^0.169.0" + "three": "^0.169.0", + "three-render-objects": "^1.29.5" }, "devDependencies": { "@eslint/js": "^9.9.0", @@ -59,5 +68,9 @@ "last 1 firefox version", "last 1 safari version" ] + }, + "resolutions": { + "bezier-js": "4.0.3", + "three": "0.118.0" } } diff --git a/examples/vite-redux-toolkit-react-app/src/App.tsx b/examples/vite-redux-toolkit-react-app/src/App.tsx index 8475b2922..a889e8427 100644 --- a/examples/vite-redux-toolkit-react-app/src/App.tsx +++ b/examples/vite-redux-toolkit-react-app/src/App.tsx @@ -2,6 +2,7 @@ import HomePage from "./pages/HomePage"; import { ThemeProvider, createTheme } from '@mui/material/styles'; import CssBaseline from '@mui/material/CssBaseline'; import MainLayout from "./components/MainLayout.tsx"; +import '@metacell/geppetto-meta-ui/flex-layout/style/light.scss' const darkTheme = createTheme({ palette: { diff --git a/examples/vite-redux-toolkit-react-app/src/components/ColorPicker.tsx b/examples/vite-redux-toolkit-react-app/src/components/ColorPicker.tsx new file mode 100644 index 000000000..b2dbaf61e --- /dev/null +++ b/examples/vite-redux-toolkit-react-app/src/components/ColorPicker.tsx @@ -0,0 +1,47 @@ +import { Box } from "@mui/material"; +import ChromePicker from "react-color"; + +const ColorPicker = ({ selectedColor, onChange }) => { + return ( + div": { + width: "100% !important", + boxShadow: "none !important", + background: "transparent !important", + fontFamily: "'IBM Plex Sans',sans-serif !important", + + "& > div:last-of-type": { + "& > div:first-of-type": { + "& > div:first-of-type": { + "& > div": { + // border: `0.0625rem solid ${headerBorderLeftColor}`, + }, + }, + }, + }, + + "& svg": { + "&:hover": { + // background: `${headerBorderLeftColor} !important`, + }, + }, + + "& input": { + // backgroundColor: `${headerBorderLeftColor} !important`, + boxShadow: "none !important", + // color: `${headingColor} !important`, + "&:focus": { + boxShadow: "none !important", + outline: "none !important", + }, + }, + }, + }} + > + + + ); +}; + +export default ColorPicker; diff --git a/examples/vite-redux-toolkit-react-app/src/components/CustomFormControlLabel.tsx b/examples/vite-redux-toolkit-react-app/src/components/CustomFormControlLabel.tsx new file mode 100644 index 000000000..d6f268fd9 --- /dev/null +++ b/examples/vite-redux-toolkit-react-app/src/components/CustomFormControlLabel.tsx @@ -0,0 +1,81 @@ +import {Box, FormControlLabel, Stack, Typography} from "@mui/material"; +import { vars } from "../theme/variables.ts"; +import PickerWrapper from "./PickerWrapper.tsx"; +import { useState } from "react"; + +const { gray600, gray50 } = vars; + +const CustomFormControlLabel = ({ label, color, handleColorChange }) => { + const [selectedColor, setSelectedColor] = useState(color); + const [anchorEl, setAnchorEl] = useState(null); + const [open, setOpen] = useState(false); + + const handleChange = (color) => { + setSelectedColor(color.hex); + handleColorChange(color.hex); + }; + + const handleClick = (event) => { + setAnchorEl(event.currentTarget); + setOpen(true); + }; + + const handleClose = () => { + setAnchorEl(null); + setOpen(false); + }; + + return ( + <> + + } + sx={{ + width: "100%", + p: ".5rem .5rem .5rem .5rem", + margin: 0, + alignItems: "center", + "&:hover": { + background: gray50, + borderRadius: ".5rem", + }, + "& .MuiFormControlLabel-label": { + width: "100%", + }, + "& .MuiIconButton-root": { + borderRadius: ".25rem", + }, + }} + label={ + + + + {label} + + + + } + /> + + + ); +}; + +export default CustomFormControlLabel; diff --git a/examples/vite-redux-toolkit-react-app/src/components/LeftSidebar.tsx b/examples/vite-redux-toolkit-react-app/src/components/LeftSidebar.tsx index 92305d276..bd8750283 100644 --- a/examples/vite-redux-toolkit-react-app/src/components/LeftSidebar.tsx +++ b/examples/vite-redux-toolkit-react-app/src/components/LeftSidebar.tsx @@ -12,7 +12,6 @@ import { threeDViewerWidget, } from "../layoutManager/widgets.ts"; import { useDispatch } from "react-redux"; -import "@metacell/geppetto-meta-ui/flex-layout/style/dark.scss"; import { FormControlLabel, FormGroup } from "@mui/material"; import Typography from "@mui/material/Typography"; import CustomSwitch from "./CustomSwitch.tsx"; diff --git a/examples/vite-redux-toolkit-react-app/src/components/PickerWrapper.tsx b/examples/vite-redux-toolkit-react-app/src/components/PickerWrapper.tsx new file mode 100644 index 000000000..37f42464e --- /dev/null +++ b/examples/vite-redux-toolkit-react-app/src/components/PickerWrapper.tsx @@ -0,0 +1,37 @@ +import { Popover } from "@mui/material"; +import ColorPicker from "./ColorPicker"; + +const PickerWrapper = ({ open, anchorEl, onClose, handleColorChange, selectedColor }) => { + return ( + + + + ); +}; + +export default PickerWrapper; diff --git a/examples/vite-redux-toolkit-react-app/src/components/viewers/ThreeD/STLViewer.tsx b/examples/vite-redux-toolkit-react-app/src/components/viewers/ThreeD/STLViewer.tsx index 39184c035..ce40f4481 100644 --- a/examples/vite-redux-toolkit-react-app/src/components/viewers/ThreeD/STLViewer.tsx +++ b/examples/vite-redux-toolkit-react-app/src/components/viewers/ThreeD/STLViewer.tsx @@ -1,6 +1,7 @@ import { FC } from "react"; import { Center } from "@react-three/drei"; import { useLoader } from "@react-three/fiber"; +// @ts-ignore import { STLLoader } from "three/examples/jsm/loaders/STLLoader"; import { BufferGeometry } from 'three'; import { Instance } from "./ThreeDViewer.tsx"; @@ -15,7 +16,7 @@ const STLViewer: FC = ({ instances, isWireframe }) => { // TODO: Check if useLoader caches or do we need to do it ourselves // @ts-expect-error Argument type STLLoader is not assignable to parameter type LoaderProto const stlObjects = useLoader(STLLoader, instances.map(i => i.url)); - + return (
diff --git a/examples/vite-redux-toolkit-react-app/src/components/viewers/ThreeD/SceneControls.tsx b/examples/vite-redux-toolkit-react-app/src/components/viewers/ThreeD/SceneControls.tsx index 2a8e2ef51..4e8dc7638 100644 --- a/examples/vite-redux-toolkit-react-app/src/components/viewers/ThreeD/SceneControls.tsx +++ b/examples/vite-redux-toolkit-react-app/src/components/viewers/ThreeD/SceneControls.tsx @@ -1,39 +1,177 @@ -import GridOnIcon from '@mui/icons-material/GridOn'; -import GridOffIcon from '@mui/icons-material/GridOff'; -import ZoomInIcon from '@mui/icons-material/ZoomIn'; -import ZoomOutIcon from '@mui/icons-material/ZoomOut'; -import HouseIcon from '@mui/icons-material/House'; -import {Box, IconButton} from "@mui/material"; - -function SceneControls({cameraControlRef, isWireframe, setIsWireframe}) { - console.log(cameraControlRef) - +import { + HomeOutlined, + PlayArrowOutlined, + SettingsOutlined, + TonalityOutlined, +} from "@mui/icons-material"; +import ZoomInIcon from "@mui/icons-material/ZoomIn"; +import ZoomOutIcon from "@mui/icons-material/ZoomOut"; +import { Box, Divider, IconButton, Popover } from "@mui/material"; +import Tooltip from "@mui/material/Tooltip"; +import { useRef, useState } from "react"; +import CustomFormControlLabel from "../../CustomFormControlLabel.tsx"; +import {updateColor} from "../../../redux/slices/instanceSlice.ts"; +import {useDispatch} from "react-redux"; + +function SceneControls({ + cameraControlRef, + isWireframe, + setIsWireframe, + instances, +}) { + const [anchorEl, setAnchorEl] = useState(null); + const rotateAnimationRef = useRef(null); + const [isRotating, setIsRotating] = useState(false); + const dispatch = useDispatch(); + + const open = Boolean(anchorEl); + const id = open ? "settings-popover" : undefined; + + const handleColorChange = (color, id) => { + dispatch(updateColor({ id, color })); + }; + + const handleOpenSettings = (event) => { + setAnchorEl(event.currentTarget); + }; + + const handleCloseSettings = () => { + setAnchorEl(null); + }; + + const handleRotation = () => { + if (!cameraControlRef.current) return; + + const rotate = () => { + cameraControlRef.current.rotate(0.01, 0, true); + rotateAnimationRef.current = requestAnimationFrame(rotate); + }; + + if (isRotating) { + if (rotateAnimationRef.current) { + cancelAnimationFrame(rotateAnimationRef.current); + rotateAnimationRef.current = null; + } + } else { + rotate(); + } + + setIsRotating(!isRotating); + }; + return ( - - { - cameraControlRef.current?.reset(true); - }} title="Reset"> - - - { - cameraControlRef.current?.zoom(cameraControlRef.current?._camera.zoom / 2, true); - }} title="Zoom In"> - - - { - cameraControlRef.current?.zoom(-cameraControlRef.current?._camera.zoom / 2, true); - }} title="Zoom Out"> - - - setIsWireframe(!isWireframe)} - title="Toggle Wireframe" + + + + + + + + + {instances?.map((instance) => ( + handleColorChange(color, instance.id)} + /> + ))} + + + + + setIsWireframe(!isWireframe)}> + + + + + + { + cameraControlRef.current?.zoom( + cameraControlRef.current?._camera.zoom / 2, + true, + ); + }} + > + + + + - {isWireframe ? : } - + { + cameraControlRef.current?.reset(true); + }} + > + + + + + { + cameraControlRef.current?.zoom( + -cameraControlRef.current?._camera.zoom / 2, + true, + ); + }} + > + + + + + + + + + ); } -export default SceneControls; \ No newline at end of file +export default SceneControls; diff --git a/examples/vite-redux-toolkit-react-app/src/components/viewers/ThreeD/ThreeDViewer.tsx b/examples/vite-redux-toolkit-react-app/src/components/viewers/ThreeD/ThreeDViewer.tsx index 92026c178..c0690f13e 100644 --- a/examples/vite-redux-toolkit-react-app/src/components/viewers/ThreeD/ThreeDViewer.tsx +++ b/examples/vite-redux-toolkit-react-app/src/components/viewers/ThreeD/ThreeDViewer.tsx @@ -13,6 +13,8 @@ import Loader from "./Loader.tsx"; import Gizmo from "./Gizmo.tsx"; import { CameraControls, PerspectiveCamera } from "@react-three/drei"; import SceneControls from "./SceneControls.tsx"; +import {useSelector} from "react-redux"; +import {RootState} from "../../../redux/store.ts"; export interface Instance { id: string; url: string; @@ -22,36 +24,10 @@ export interface Instance { function ThreeDViewer() { - // @ts-expect-error 'setShowNeurons' is declared but its value is never read. - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const [showNeurons, setShowNeurons] = useState(true); - // @ts-expect-error 'setShowSynapses' is declared but its value is never read. - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const [showSynapses, setShowSynapses] = useState(true); - const [instances, setInstances] = useState([]) const [isWireframe, setIsWireframe] = useState(false) - + const instances = useSelector((state: RootState) => state.instances.instances); const cameraControlRef = useRef(null); - - useEffect(() => { - if (showNeurons) { - setInstances([ - { - id: 'nerve_ring', - url: 'nervering-SEM_adult.stl', - color: 'white', - opacity: 0.5 - }, - { - id: 'adal_sem', - url: 'ADAL-SEM_adult.stl', - color: 'blue', - opacity: 1 - } - ]) - } - }, [showNeurons, showSynapses]); - + return ( <> ); diff --git a/examples/vite-redux-toolkit-react-app/src/layoutManager/componentsMap.tsx b/examples/vite-redux-toolkit-react-app/src/layoutManager/componentsMap.tsx index a91c7888f..c86ce2c34 100644 --- a/examples/vite-redux-toolkit-react-app/src/layoutManager/componentsMap.tsx +++ b/examples/vite-redux-toolkit-react-app/src/layoutManager/componentsMap.tsx @@ -3,7 +3,7 @@ import ThreeDViewer from "../components/viewers/ThreeD/ThreeDViewer.tsx"; const componentMap = { MyComponent, - ThreeDViewer + ThreeDViewer, } export default componentMap diff --git a/examples/vite-redux-toolkit-react-app/src/models/models.ts b/examples/vite-redux-toolkit-react-app/src/models/models.ts index 662f112b9..d338e2c1e 100644 --- a/examples/vite-redux-toolkit-react-app/src/models/models.ts +++ b/examples/vite-redux-toolkit-react-app/src/models/models.ts @@ -1,4 +1,6 @@ export enum ViewerType { default = "Default", ThreeD = "3D", + imageViewer= 'Image Viewer', + dicomViewer= 'Dicom Viewer', } \ No newline at end of file diff --git a/examples/vite-redux-toolkit-react-app/src/redux/slices/instanceSlice.ts b/examples/vite-redux-toolkit-react-app/src/redux/slices/instanceSlice.ts new file mode 100644 index 000000000..9e7516411 --- /dev/null +++ b/examples/vite-redux-toolkit-react-app/src/redux/slices/instanceSlice.ts @@ -0,0 +1,45 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; + +export interface Instance { + id: string; + url: string; + color: string; + opacity: number; +} + +export interface InstancesState { + instances: Instance[]; +} + +export const initialState: InstancesState = { + instances: [ + { + id: 'nerve_ring', + url: 'nervering-SEM_adult.stl', + color: '#000', + opacity: 0.5 + }, + { + id: 'adal_sem', + url: 'ADAL-SEM_adult.stl', + color: '#000', + opacity: 1 + } + ], +}; + +const instancesSlice = createSlice({ + name: 'instances', + initialState, + reducers: { + updateColor: (state, action: PayloadAction<{ id: string; color: string }>) => { + const instance = state?.instances?.find((inst) => inst.id === action.payload.id); + if (instance) { + instance.color = action.payload.color; + } + }, + }, +}); + +export const { updateColor } = instancesSlice.actions; +export default instancesSlice.reducer; diff --git a/examples/vite-redux-toolkit-react-app/src/redux/store.ts b/examples/vite-redux-toolkit-react-app/src/redux/store.ts index df632697a..71f1699aa 100644 --- a/examples/vite-redux-toolkit-react-app/src/redux/store.ts +++ b/examples/vite-redux-toolkit-react-app/src/redux/store.ts @@ -5,7 +5,7 @@ import geppettoClientReducer, { clientInitialState, type ClientState } from "@me import { type LayoutState, layout, layoutInitialState, widgets } from "@metacell/geppetto-meta-client/common/reducer/geppettoLayout"; import { reducerDecorator } from "@metacell/geppetto-meta-client/common/reducer/reducerDecorator"; import { type Action, type Reducer, combineReducers, configureStore } from "@reduxjs/toolkit"; - +import instancesReducer, { InstancesState, initialState as initialInstancesState } from './slices/instanceSlice.ts'; import baseLayout from '../layoutManager/defaultLayout' import componentMap from "../layoutManager/componentsMap.tsx"; @@ -14,12 +14,14 @@ export interface RootState { client: ClientState; layout: LayoutState; widgets: WidgetMap; + instances: InstancesState } const initialState = { client: clientInitialState, layout: layoutInitialState, widgets: {}, + instances: initialInstancesState, }; const rootReducer: Reducer = reducerDecorator( @@ -27,6 +29,7 @@ const rootReducer: Reducer = reducerDecorator( client: geppettoClientReducer, layout, widgets, + instances: instancesReducer }), );