Add pointcloud export panel to UI (#938)

Add export panel to UI
This commit is contained in:
Matthew Tancik authored and GitHub committed 2022-11-10 22:49:32 -08:00
1 parent d4119d4086
commit 0a5331fc5c
9 files changed
+312 -6

No files matched your search

+1 -1
View File
@@ -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",
+24
View File
@@ -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;
}
@@ -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 (
<div
role="tabpanel"
hidden={value !== index}
id={`simple-tabpanel-${index}`}
aria-labelledby={`simple-tab-${index}`}
{...other}
>
{value === index && (
<Box sx={{ p: 3 }}>
<Typography>{children}</Typography>
</Box>
)}
</div>
);
}
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 (
<div className="ExportPanel">
<Tabs
value={type_value}
onChange={handleTypeChange}
aria-label="export type"
variant="fullWidth"
centered
sx={{ height: 55, minHeight: 55 }}
>
<Tab
icon={<BlurOnIcon />}
iconPosition="start"
label="Point Cloud"
disableRipple
{...a11yProps(0)}
/>
<Tab
icon={<CategoryIcon />}
iconPosition="start"
label="Mesh"
disableRipple
{...a11yProps(1)}
/>
</Tabs>
<TabPanel value={type_value} index={0}>
<LevaPanel
store={pointcloudStore}
className="Leva-panel"
theme={LevaTheme}
titleBar={false}
fill
flat
/>
<LevaStoreProvider store={pointcloudStore}>
<PointcloudSubPanel
sceneTree={sceneTree}
showExportBox={showExportBox}
/>
</LevaStoreProvider>
</TabPanel>
<TabPanel value={type_value} index={1}>
Coming Soon
</TabPanel>
</div>
);
}
@@ -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 (
<div className="PointcloudPanel">
<div className="ExportModal-text">
Run the following command in a terminal to export the pointcloud:
</div>
<div className="ExportModal-code">{cmd}</div>
<div className="ExportModal-button" style={{ textAlign: 'center' }}>
<Button
variant="outlined"
size="small"
startIcon={<ContentCopyRoundedIcon />}
onClick={handleCopy}
>
Copy Command
</Button>
</div>
</div>
);
}
@@ -0,0 +1,3 @@
import ExportPanel from './ExportPanel';
export default ExportPanel;
@@ -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();
@@ -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) {
<Tab icon={<TuneRounded />} label="Controls" {...a11yProps(0)} />
<Tab icon={<CameraAltRounded />} label="Render" {...a11yProps(1)} />
<Tab icon={<WidgetsRounded />} label="Scene" {...a11yProps(2)} />
<Tab
icon={<ImportExportRounded />}
label="Export"
{...a11yProps(3)}
/>
</Tabs>
</Box>
<TabPanel value={value} index={0}>
@@ -124,7 +132,7 @@ export function BasicTabs(props: BasicTabsProps) {
</TabPanel>
<TabPanel value={value} index={3}>
<LogPanel>TODO: Something maybe?</LogPanel>
<ExportPanel sceneTree={sceneTree} showExportBox={showExportBox} />
</TabPanel>
</Box>
</div>
+6 -1
View File
@@ -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: {