diff --git a/nerfstudio/viewer/app/package.json b/nerfstudio/viewer/app/package.json index b0768523b..6e2f1269f 100644 --- a/nerfstudio/viewer/app/package.json +++ b/nerfstudio/viewer/app/package.json @@ -1,7 +1,7 @@ { "name": "viewer", "homepage": ".", - "version": "22-10-13-0", + "version": "22-11-10-0", "private": true, "dependencies": { "@emotion/react": "^11.10.4", diff --git a/nerfstudio/viewer/app/src/index.scss b/nerfstudio/viewer/app/src/index.scss index 146947df4..194c5ac2e 100644 --- a/nerfstudio/viewer/app/src/index.scss +++ b/nerfstudio/viewer/app/src/index.scss @@ -369,3 +369,27 @@ $viewer_button_size: 50px; .ViewerWindow-iconbutton { margin-top: 5px; } + +// ------------------------------------ +// Export panel styling + +.ExportModal-code { + padding: 10px; + background-color: $base-color3; + overflow-x: scroll; + font-family: monospace; + white-space: pre; + margin-top: 16px; + font-size: 0.9em; +} + +.ExportModal-text { + text-align: left; + margin-top: 20px; + font-size: 0.9em; +} + +.ExportModal-button { + // margin: 15px; + padding: 20px; +} diff --git a/nerfstudio/viewer/app/src/modules/SidePanel/ExportPanel/ExportPanel.jsx b/nerfstudio/viewer/app/src/modules/SidePanel/ExportPanel/ExportPanel.jsx new file mode 100644 index 000000000..9703e2c14 --- /dev/null +++ b/nerfstudio/viewer/app/src/modules/SidePanel/ExportPanel/ExportPanel.jsx @@ -0,0 +1,103 @@ +/* eslint-disable react/jsx-props-no-spreading */ +import * as React from 'react'; + +import { Box, Typography, Tab, Tabs } from '@mui/material'; +import { LevaPanel, LevaStoreProvider, useCreateStore } from 'leva'; +import BlurOnIcon from '@mui/icons-material/BlurOn'; +import CategoryIcon from '@mui/icons-material/Category'; +import PointcloudSubPanel from './PointcloudSubPanel'; +import LevaTheme from '../../../themes/leva_theme.json'; + +interface TabPanelProps { + children: React.ReactNode; + index: number; + value: number; +} + +function TabPanel(props: TabPanelProps) { + const { children, value, index, ...other } = props; + + return ( + + ); +} + +function a11yProps(index: number) { + return { + id: `simple-tab-${index}`, + 'aria-controls': `simple-tabpanel-${index}`, + }; +} + +export default function ExportPanel(props) { + // unpack relevant information + const sceneTree = props.sceneTree; + const showExportBox = props.showExportBox; + + const [type_value, setTypeValue] = React.useState(0); + + const handleTypeChange = (event: React.SyntheticEvent, newValue: number) => { + setTypeValue(newValue); + }; + + const pointcloudStore = useCreateStore(); + + return ( +
+ + } + iconPosition="start" + label="Point Cloud" + disableRipple + {...a11yProps(0)} + /> + } + iconPosition="start" + label="Mesh" + disableRipple + {...a11yProps(1)} + /> + + + + + + + + + Coming Soon + +
+ ); +} diff --git a/nerfstudio/viewer/app/src/modules/SidePanel/ExportPanel/PointcloudSubPanel.jsx b/nerfstudio/viewer/app/src/modules/SidePanel/ExportPanel/PointcloudSubPanel.jsx new file mode 100644 index 000000000..8951c8051 --- /dev/null +++ b/nerfstudio/viewer/app/src/modules/SidePanel/ExportPanel/PointcloudSubPanel.jsx @@ -0,0 +1,163 @@ +import * as React from 'react'; +import * as THREE from 'three'; +import { useControls, useStoreContext } from 'leva'; +import { useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import ContentCopyRoundedIcon from '@mui/icons-material/ContentCopyRounded'; + +import { Button } from '@mui/material'; + +const PCD_CLIPPING_BOX_NAME = 'PCD Clipping Box'; + +export default function PointcloudSubPanel(props) { + const sceneTree = props.sceneTree; + const showExportBox = props.showExportBox; + const store = useStoreContext(); + + const dispatch = useDispatch(); + + // redux store state + const config_base_dir = useSelector( + (state) => state.renderingState.config_base_dir, + ); + const clippingEnabled = useSelector( + (state) => state.renderingState.clipping_enabled, + ); + const clippingCenter = useSelector( + (state) => state.renderingState.clipping_center, + ); + const clippingScale = useSelector( + (state) => state.renderingState.clipping_box_scale, + ); + + const config_filename = `${config_base_dir}/config.yml`; + + const geometry = new THREE.BoxGeometry(1, 1, 1); + const material = new THREE.MeshBasicMaterial({ + color: 0xffd369, + transparent: true, + opacity: 0.7, + side: THREE.DoubleSide, + }); + const cube = new THREE.Mesh(geometry, material); + sceneTree.set_object_from_path([PCD_CLIPPING_BOX_NAME], cube); + cube.visible = showExportBox && clippingEnabled; + + const update_box_center = (value) => { + const box = sceneTree.find_object_no_create([PCD_CLIPPING_BOX_NAME]); + box.position.set(value[0], value[1], value[2]); + }; + + const update_box_scale = (value) => { + const box = sceneTree.find_object_no_create([PCD_CLIPPING_BOX_NAME]); + box.scale.set(value[0], value[1], value[2]); + }; + + const controlValues = useControls( + { + numPoints: { label: 'Number of Points', value: 1000000, min: 1 }, + removeOutliers: { label: 'Remove Outliers', value: true }, + estimateNormals: { label: 'Estimate Normals', value: false }, + useBoundingBox: { + label: 'Crop', + value: clippingEnabled, + onChange: (value) => { + const box = sceneTree.find_object_no_create([PCD_CLIPPING_BOX_NAME]); + box.visible = value; + dispatch({ + type: 'write', + path: 'renderingState/clipping_enabled', + data: value, + }); + }, + }, + center: { + label: '| Center', + value: clippingCenter, + render: (get) => get('useBoundingBox'), + onChange: (value) => { + update_box_center(value); + }, + onEditEnd: (value) => { + dispatch({ + type: 'write', + path: 'renderingState/clipping_center', + data: value, + }); + }, + }, + scale: { + label: '| Scale', + value: clippingScale, + min: 0.01, + render: (get) => get('useBoundingBox'), + onChange: (value) => { + update_box_scale(value); + }, + onEditEnd: (value) => { + dispatch({ + type: 'write', + path: 'renderingState/clipping_box_scale', + data: value, + }); + }, + }, + outputDir: { label: 'Output Directory', value: 'pcd/' }, + }, + { store }, + ); + + const getPythonBool = (boolean) => { + return boolean ? 'True' : 'False'; + }; + + const bbox_min = [ + clippingCenter[0] - clippingScale[0] / 2, + clippingCenter[1] - clippingScale[1] / 2, + clippingCenter[2] - clippingScale[2] / 2, + ]; + const bbox_max = [ + clippingCenter[0] + clippingScale[0] / 2, + clippingCenter[1] + clippingScale[1] / 2, + clippingCenter[2] + clippingScale[2] / 2, + ]; + + const cmd = + `ns-export pointcloud` + + ` --load-config ${config_filename}` + + ` --output-dir ${controlValues.outputDir}` + + ` --num-points ${controlValues.numPoints}` + + ` --remove-outliers ${getPythonBool(controlValues.removeOutliers)}` + + ` --estimate-normals ${getPythonBool(controlValues.estimateNormals)}` + + ` --use-bounding-box ${getPythonBool(clippingEnabled)}` + + ` --bounding-box-min ${bbox_min[0]} ${bbox_min[1]} ${bbox_min[2]}` + + ` --bounding-box-max ${bbox_max[0]} ${bbox_max[1]} ${bbox_max[2]}`; + + const handleCopy = () => { + navigator.clipboard.writeText(cmd); + }; + + useEffect(() => { + update_box_center(clippingCenter); + update_box_scale(clippingScale); + }); + + return ( +
+
+ Run the following command in a terminal to export the pointcloud: +
+
{cmd}
+
+ +
+
+ ); +} diff --git a/nerfstudio/viewer/app/src/modules/SidePanel/ExportPanel/index.jsx b/nerfstudio/viewer/app/src/modules/SidePanel/ExportPanel/index.jsx new file mode 100644 index 000000000..41d179916 --- /dev/null +++ b/nerfstudio/viewer/app/src/modules/SidePanel/ExportPanel/index.jsx @@ -0,0 +1,3 @@ +import ExportPanel from './ExportPanel'; + +export default ExportPanel; diff --git a/nerfstudio/viewer/app/src/modules/SidePanel/ScenePanel/ScenePanel.jsx b/nerfstudio/viewer/app/src/modules/SidePanel/ScenePanel/ScenePanel.jsx index 69dc99416..b65d4d420 100644 --- a/nerfstudio/viewer/app/src/modules/SidePanel/ScenePanel/ScenePanel.jsx +++ b/nerfstudio/viewer/app/src/modules/SidePanel/ScenePanel/ScenePanel.jsx @@ -17,7 +17,7 @@ import { VisibilityOff, } from '@mui/icons-material/'; import SceneNode from '../../../SceneNode'; -import LevaTheme from '../../ConfigPanel/leva_theme.json'; +import LevaTheme from '../../../themes/leva_theme.json'; export const snap_to_camera = (sceneTree, camera, matrix) => { const mat = new THREE.Matrix4(); diff --git a/nerfstudio/viewer/app/src/modules/SidePanel/SidePanel.jsx b/nerfstudio/viewer/app/src/modules/SidePanel/SidePanel.jsx index 5f1da13cc..dff6e70b9 100644 --- a/nerfstudio/viewer/app/src/modules/SidePanel/SidePanel.jsx +++ b/nerfstudio/viewer/app/src/modules/SidePanel/SidePanel.jsx @@ -13,13 +13,14 @@ import { CameraAltRounded, TuneRounded, WidgetsRounded, + ImportExportRounded, } from '@mui/icons-material/'; import StatusPanel from './StatusPanel'; -import LevaTheme from '../ConfigPanel/leva_theme.json'; +import LevaTheme from '../../themes/leva_theme.json'; import CameraPanel from './CameraPanel'; import ScenePanel from './ScenePanel'; import { RenderControls } from '../ConfigPanel/ConfigPanel'; -import { LogPanel } from '../LogPanel/LogPanel'; +import ExportPanel from './ExportPanel'; export const snap_to_camera = (sceneTree, camera, matrix) => { const mat = new THREE.Matrix4(); @@ -77,9 +78,11 @@ export function BasicTabs(props: BasicTabsProps) { const sceneTree = props.sceneTree; const [value, setValue] = React.useState(0); + const [showExportBox, setShowExportBox] = React.useState(false); const handleChange = (event: React.SyntheticEvent, newValue: number) => { setValue(newValue); + setShowExportBox(newValue === 3); }; return ( @@ -97,6 +100,11 @@ export function BasicTabs(props: BasicTabsProps) { } label="Controls" {...a11yProps(0)} /> } label="Render" {...a11yProps(1)} /> } label="Scene" {...a11yProps(2)} /> + } + label="Export" + {...a11yProps(3)} + /> @@ -124,7 +132,7 @@ export function BasicTabs(props: BasicTabsProps) { - TODO: Something maybe? + diff --git a/nerfstudio/viewer/app/src/reducer.js b/nerfstudio/viewer/app/src/reducer.js index ba1d8262b..401804bba 100644 --- a/nerfstudio/viewer/app/src/reducer.js +++ b/nerfstudio/viewer/app/src/reducer.js @@ -4,7 +4,7 @@ const initialState = { // the websocket connection state websocketState: { isConnected: false, - websocket_url: "ws://localhost:7007", + websocket_url: 'ws://localhost:7007', }, // the webrtc connection state webrtcState: { @@ -34,6 +34,11 @@ const initialState = { train_eta: 'Paused', vis_train_ratio: 'Paused', log_errors: '', + + // export options + clipping_enabled: true, + clipping_center: [0.0, 0.0, 0.0], + clipping_box_scale: [2.0, 2.0, 2.0], }, // the scene state sceneState: { diff --git a/nerfstudio/viewer/app/src/modules/ConfigPanel/leva_theme.json b/nerfstudio/viewer/app/src/themes/leva_theme.json similarity index 100% rename from nerfstudio/viewer/app/src/modules/ConfigPanel/leva_theme.json rename to nerfstudio/viewer/app/src/themes/leva_theme.json