| import React, {createContext, useContext, useState, useEffect, useRef} from 'react'; | |
| import {TreeView, TreeItem} from '@mui/lab'; | |
| import {styled} from '@mui/system'; | |
| import Box from '@mui/material/Box'; | |
| import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; | |
| import ChevronRightIcon from '@mui/icons-material/ChevronRight'; | |
| import { | |
| Accordion, | |
| AccordionSummary, | |
| AccordionDetails, | |
| Button, | |
| FormControl, | |
| Radio, | |
| RadioGroup, | |
| FormLabel, | |
| FormControlLabel, | |
| Typography | |
| } from '@mui/material'; | |
| import Checkbox from "@mui/material/Checkbox"; | |
| import Slider from "@mui/material/Slider"; | |
| import {SketchPicker} from "react-color"; | |
| import NumberInput from "./NumberInput.js"; | |
| const CustomTreeView = styled(TreeView)` | |
| height: 240px; | |
| overflow-y: auto; | |
| `; | |
| const transformControlObjNames = ["Hemisphere Light", "Directional Light"]; | |
| let transformControlValues = {"Hemisphere Light": "none", "Directional Light": "none"}; | |
| const treeItemObjNames = ["Scene", "mainObject", "Hemisphere Light", "Directional Light", "Ground", "Grid", "Axis", "Preview Camera"]; | |
| const visibleControlObjNames = ["Directional Light", "Ground", "Grid", "Axis"]; | |
| let visibleValues = {"Directional Light": true, "Ground": true, "Grid": true, "Axis": true}; | |
| const ObjectContext = createContext([]); | |
| function useObjectUpdate() { | |
| const context = useContext(ObjectContext); | |
| if (!context) { | |
| throw new Error('useObjectUpdate must be used within ObjectProvider'); | |
| } | |
| return context; | |
| } | |
| function ObjectProvider({children}) { | |
| const [objects, setObjects] = useState([]); | |
| const updateObjects = (newObjects) => { | |
| setObjects(newObjects); | |
| }; | |
| return (<ObjectContext.Provider value={{objects, updateObjects}}> | |
| {children} | |
| </ObjectContext.Provider>); | |
| } | |
| function processNode(node, handleSelectedObject, setSelectedObj, transformControlMap) { | |
| if (!node) { | |
| return null; | |
| } | |
| if (!(treeItemObjNames.includes(node.name) || node.name.startsWith("mainObject"))) { | |
| return null; | |
| } | |
| return (<TreeItem key={node.uuid} nodeId={node.uuid} label={node.name || node.type} onClick={(objEvent) => { | |
| const objName = objEvent.target.innerHTML; | |
| handleSelectedObject(objName, transformControlMap[objName]); | |
| setSelectedObj(objName); | |
| }}> | |
| {node.children && node.children.map((child) => processNode(child, handleSelectedObject, setSelectedObj, transformControlMap))} | |
| </TreeItem>); | |
| } | |
| function ScenePanel({ | |
| refreshSceneTree, | |
| handleSelectedObject, | |
| setVisible, | |
| setCameraNear, | |
| setCameraFar, | |
| setCameraFOV, | |
| setCanvasBgColor, | |
| removeObject | |
| }) { | |
| return (<ObjectProvider> | |
| <SceneTreeWrapper refreshSceneTree={refreshSceneTree} handleSelectedObject={handleSelectedObject} | |
| setVisible={setVisible} | |
| setCameraNear={setCameraNear} setCameraFar={setCameraFar} setCameraFOV={setCameraFOV} | |
| setCanvasBgColor={setCanvasBgColor} removeObject={removeObject}/> | |
| </ObjectProvider>) | |
| } | |
| function SceneTree({handleSelectedObject, setSelectedObj, transformControlMap}) { | |
| const {objects} = useObjectUpdate(); | |
| return (<TreeView | |
| defaultCollapseIcon={<ExpandMoreIcon/>} | |
| defaultExpandIcon={<ChevronRightIcon/>} | |
| defaultEndIcon={<div/>} | |
| > | |
| {processNode(objects.object, handleSelectedObject, setSelectedObj, transformControlMap)} | |
| </TreeView>); | |
| } | |
| function SceneTreeWrapper({ | |
| refreshSceneTree, | |
| handleSelectedObject, | |
| setVisible, | |
| setCameraNear, | |
| setCameraFar, | |
| setCameraFOV, | |
| setCanvasBgColor, | |
| removeObject | |
| }) { | |
| const [selectedObj, setSelectedObj] = useState(null); | |
| const [far, setFar] = useState(1000); | |
| const [near, setNear] = useState(0.1); | |
| const [fov, setFOV] = useState(45); | |
| const [visibleMap, setVisibleMap] = useState(visibleValues); | |
| const [transformControlMap, setTransformControlMap] = useState(transformControlValues); | |
| const [bgColor, setBgColor] = useState(); | |
| const {updateObjects} = useObjectUpdate(); | |
| const updateObjectsRef = useRef(); | |
| updateObjectsRef.current = updateObjects; | |
| useEffect(() => { | |
| window.updateObjects = (newObjects) => { | |
| if (updateObjectsRef.current) { | |
| updateObjectsRef.current(newObjects); | |
| } | |
| }; | |
| return () => { | |
| window.updateObjects = null; | |
| }; | |
| }, []); | |
| return (<div> | |
| <Box mb={1} mt={1}> | |
| <Accordion> | |
| <AccordionSummary expandIcon={<ExpandMoreIcon/>}> | |
| Scene | |
| </AccordionSummary> | |
| <AccordionDetails> | |
| <SceneTree handleSelectedObject={handleSelectedObject} setSelectedObj={setSelectedObj} | |
| transformControlMap={transformControlMap}/> | |
| <Button variant="contained" color="primary" fullWidth sx={{margin: '2px'}} | |
| onClick={refreshSceneTree}>Refresh Scene Tree</Button> | |
| {(transformControlObjNames.includes(selectedObj) || (selectedObj && selectedObj.startsWith("mainObject"))) && | |
| <FormControl> | |
| <FormLabel>Operate</FormLabel> | |
| <RadioGroup | |
| aria-labelledby="operate-radio-buttons-group-label" | |
| defaultValue="none" | |
| name="operate-radio-buttons-group" | |
| row={true} | |
| onChange={(event) => { | |
| handleSelectedObject(selectedObj, event.target.value); | |
| const updatedMap = {...transformControlMap}; | |
| updatedMap[selectedObj] = event.target.value; | |
| setTransformControlMap(updatedMap); | |
| }} | |
| > | |
| <FormControlLabel value="none" control={<Radio/>} label="None" | |
| checked={transformControlMap[selectedObj] === "none" || !transformControlMap[selectedObj]}/> | |
| <FormControlLabel value="translate" control={<Radio/>} label="Translate" | |
| checked={transformControlMap[selectedObj] === "translate"}/> | |
| <FormControlLabel value="rotate" control={<Radio/>} label="Rotate" | |
| checked={transformControlMap[selectedObj] === "rotate"}/> | |
| </RadioGroup> | |
| </FormControl>} | |
| {(visibleControlObjNames.includes(selectedObj) || (selectedObj && selectedObj.startsWith("mainObject"))) && | |
| <FormControlLabel | |
| control={ | |
| <Checkbox | |
| checked={visibleMap[selectedObj] || !(selectedObj in visibleMap)} | |
| onChange={(event) => { | |
| setVisible(selectedObj, event.target.checked); | |
| const updatedMap = {...visibleMap}; | |
| updatedMap[selectedObj] = event.target.checked; | |
| setVisibleMap(updatedMap); | |
| }} | |
| color="primary" | |
| /> | |
| } | |
| label='Visible' | |
| /> | |
| } | |
| { | |
| selectedObj === "Preview Camera" && <Box width="100%"> | |
| <Typography gutterBottom>Near</Typography> | |
| <Slider min={0.1} max={100} | |
| valueLabelDisplay="auto" | |
| step={0.1} | |
| value={near} | |
| onChange={(event, newValue) => { | |
| setNear(newValue); | |
| setCameraNear(newValue); | |
| }} | |
| aria-labelledby="continuous-slider" | |
| /> | |
| <Typography gutterBottom>Far</Typography> | |
| <Slider min={0.1} max={20000} | |
| valueLabelDisplay="auto" | |
| value={far} | |
| onChange={(event, newValue) => { | |
| setFar(newValue); | |
| setCameraFar(newValue) | |
| }} | |
| aria-labelledby="continuous-slider" | |
| /> | |
| <Typography gutterBottom>FOV</Typography> | |
| <Slider min={1} max={100} | |
| valueLabelDisplay="auto" | |
| value={fov} | |
| onChange={(event, newValue) => { | |
| setFOV(newValue); | |
| setCameraFOV(newValue); | |
| }} | |
| aria-labelledby="continuous-slider" | |
| /> | |
| </Box> | |
| } | |
| { | |
| selectedObj && selectedObj.startsWith("mainObject") && | |
| <Button variant="contained" color="primary" fullWidth sx={{margin: '2px'}} | |
| onClick={() => { | |
| removeObject(selectedObj); | |
| refreshSceneTree(); | |
| setSelectedObj(null); | |
| }}>Remove</Button> | |
| } | |
| { | |
| selectedObj === "Scene" && <SketchPicker | |
| color={bgColor} | |
| onChangeComplete={(color) => { | |
| setBgColor(color); | |
| setCanvasBgColor(color); | |
| }} | |
| disableAlpha={true} | |
| /> | |
| } | |
| </AccordionDetails> | |
| </Accordion> | |
| </Box> | |
| </div>); | |
| } | |
| export default ScenePanel; |