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 (
+
+ {value === index && (
+
+ {children}
+
+ )}
+
+ );
+}
+
+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}
+
+ }
+ onClick={handleCopy}
+ >
+ Copy Command
+
+
+
+ );
+}
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