3D-viewer v2 #1870
8 changed files with 358 additions and 147 deletions
|
@ -13,6 +13,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/).
|
|||
* New viewer for human-readable text files ([#1826](https://github.com/lbryio/lbry-desktop/pull/1826))
|
||||
* CSV and JSON viewer ([#1410](https://github.com/lbryio/lbry-desktop/pull/1410))
|
||||
* Recommended content on file viewer page ([#1845](https://github.com/lbryio/lbry-desktop/pull/1845))
|
||||
* 3D File viewer improvements ([#1870](https://github.com/lbryio/lbry-desktop/pull/1870))
|
||||
* Desktop notification when publish is completed ([#1892](https://github.com/lbryio/lbry-desktop/pull/1892))
|
||||
|
||||
### Changed
|
||||
|
|
|
@ -39,6 +39,7 @@
|
|||
"classnames": "^2.2.5",
|
||||
"codemirror": "^5.39.2",
|
||||
"country-data": "^0.0.31",
|
||||
"dat.gui": "^0.7.2",
|
||||
"dom-scroll-into-view": "^1.2.1",
|
||||
"electron-dl": "^1.11.0",
|
||||
"electron-is-dev": "^0.3.0",
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
// @flow
|
||||
import * as React from 'react';
|
||||
import * as dat from 'dat.gui';
|
||||
import classNames from 'classnames';
|
||||
import LoadingScreen from 'component/common/loading-screen';
|
||||
|
||||
// ThreeJS
|
||||
import * as THREE from './internal/three';
|
||||
import detectWebGL from './internal/detector';
|
||||
|
@ -11,123 +14,39 @@ import ThreeRenderer from './internal/renderer';
|
|||
|
||||
type Props = {
|
||||
theme: string,
|
||||
autoRotate: boolean,
|
||||
source: {
|
||||
fileType: string,
|
||||
downloadPath: string,
|
||||
![]() Also, some of the operations might be faster if you can do them before you convert the Also, some of the operations might be faster if you can do them before you convert the `GeometryBuffer` into a `Geometry`
![]() We don't really support textures ATM so that won't be a problem... We don't really support textures ATM so that won't be a problem...
|
||||
},
|
||||
};
|
||||
|
||||
class ThreeViewer extends React.PureComponent<Props> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
type State = {
|
||||
error: ?string,
|
||||
isReady: boolean,
|
||||
isLoading: boolean,
|
||||
};
|
||||
|
||||
const { theme } = this.props;
|
||||
class ThreeViewer extends React.PureComponent<Props, State> {
|
||||
static testWebgl = new Promise((resolve, reject) => {
|
||||
if (detectWebGL()) resolve();
|
||||
else reject();
|
||||
});
|
||||
|
||||
// Main container
|
||||
this.viewer = React.createRef();
|
||||
|
||||
// Object colors
|
||||
this.materialColors = {
|
||||
red: '#e74c3c',
|
||||
blue: '#3498db',
|
||||
green: '#44b098',
|
||||
orange: '#f39c12',
|
||||
};
|
||||
|
||||
// Viewer themes
|
||||
this.themes = {
|
||||
dark: {
|
||||
gridColor: '#414e5c',
|
||||
groundColor: '#13233C',
|
||||
backgroundColor: '#13233C',
|
||||
centerLineColor: '#7f8c8d',
|
||||
},
|
||||
light: {
|
||||
gridColor: '#7f8c8d',
|
||||
groundColor: '#DDD',
|
||||
backgroundColor: '#EEE',
|
||||
centerLineColor: '#2F2F2F',
|
||||
},
|
||||
};
|
||||
|
||||
// Select current theme
|
||||
this.theme = this.themes[theme] || this.themes.light;
|
||||
|
||||
// State
|
||||
this.state = {
|
||||
error: null,
|
||||
isReady: false,
|
||||
isLoading: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (detectWebGL()) {
|
||||
this.renderScene();
|
||||
// Update render on resize window
|
||||
window.addEventListener('resize', this.handleResize, false);
|
||||
} else {
|
||||
// No webgl support, handle Error...
|
||||
// TODO: Use a better error message
|
||||
this.setState({ error: "Sorry, your computer doesn't support WebGL." });
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('resize', this.handleResize, false);
|
||||
}
|
||||
|
||||
transformGroup(group) {
|
||||
this.fitMeshToCamera(group);
|
||||
this.createWireFrame(group);
|
||||
this.updateControlsTarget(group.position);
|
||||
}
|
||||
|
||||
createOrbitControls(camera, canvas) {
|
||||
const { autoRotate } = this.props;
|
||||
static createOrbitControls(camera, canvas) {
|
||||
const controls = new THREE.OrbitControls(camera, canvas);
|
||||
// Controls configuration
|
||||
controls.enableDamping = true;
|
||||
controls.dampingFactor = 0.75;
|
||||
controls.enableZoom = true;
|
||||
controls.minDistance = 1;
|
||||
controls.maxDistance = 50;
|
||||
controls.autoRotate = autoRotate;
|
||||
controls.minDistance = 5;
|
||||
controls.maxDistance = 14;
|
||||
controls.autoRotate = false;
|
||||
controls.enablePan = false;
|
||||
controls.saveState();
|
||||
return controls;
|
||||
}
|
||||
|
||||
createGeometry(data) {
|
||||
const geometry = new THREE.Geometry();
|
||||
geometry.fromBufferGeometry(data);
|
||||
geometry.computeBoundingBox();
|
||||
geometry.computeVertexNormals();
|
||||
geometry.center();
|
||||
geometry.rotateX(-Math.PI / 2);
|
||||
geometry.lookAt(new THREE.Vector3(0, 0, 1));
|
||||
return geometry;
|
||||
}
|
||||
|
||||
createWireFrame(group) {
|
||||
const wireframeGeometry = new THREE.WireframeGeometry(group.geometry);
|
||||
const wireframeMaterial = new THREE.LineBasicMaterial({
|
||||
opacity: 0,
|
||||
transparent: true,
|
||||
linewidth: 1,
|
||||
});
|
||||
// Set material color
|
||||
wireframeMaterial.color.set(this.materialColors.green);
|
||||
this.wireframe = new THREE.LineSegments(wireframeGeometry, wireframeMaterial);
|
||||
group.add(this.wireframe);
|
||||
}
|
||||
|
||||
toggleWireFrame(show = false) {
|
||||
this.wireframe.opacity = show ? 1 : 0;
|
||||
this.mesh.material.opacity = show ? 0 : 1;
|
||||
}
|
||||
|
||||
fitMeshToCamera(group) {
|
||||
static fitMeshToCamera(group) {
|
||||
const max = { x: 0, y: 0, z: 0 };
|
||||
const min = { x: 0, y: 0, z: 0 };
|
||||
|
||||
|
@ -147,20 +66,214 @@ class ThreeViewer extends React.PureComponent<Props> {
|
|||
|
||||
const meshY = Math.abs(max.y - min.y);
|
||||
const meshX = Math.abs(max.x - min.x);
|
||||
|
||||
const scaleFactor = 10 / Math.max(meshX, meshY);
|
||||
|
||||
group.scale.set(scaleFactor, scaleFactor, scaleFactor);
|
||||
group.position.setY((meshY / 2) * scaleFactor);
|
||||
|
||||
// Reset object position
|
||||
const box = new THREE.Box3().setFromObject(group);
|
||||
// Update position
|
||||
box.getCenter(group.position);
|
||||
|
||||
group.position.multiplyScalar(-1);
|
||||
group.position.setY(group.position.y + meshY * scaleFactor);
|
||||
}
|
||||
|
||||
/*
|
||||
See: https://github.com/mrdoob/three.js/blob/dev/docs/scenes/js/material.js#L195
|
||||
*/
|
||||
|
||||
static updateMaterial(material, geometry) {
|
||||
material.vertexColors = +material.vertexColors; // Ensure number
|
||||
material.side = +material.side; // Ensure number
|
||||
material.needsUpdate = true;
|
||||
// If Geometry needs update
|
||||
if (geometry) {
|
||||
geometry.verticesNeedUpdate = true;
|
||||
geometry.normalsNeedUpdate = true;
|
||||
geometry.colorsNeedUpdate = true;
|
||||
}
|
||||
}
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
const { theme } = this.props;
|
||||
this.viewer = React.createRef();
|
||||
this.guiContainer = React.createRef();
|
||||
// Object defualt color
|
||||
this.materialColor = '#44b098';
|
||||
// Viewer themes
|
||||
this.themes = {
|
||||
dark: {
|
||||
gridColor: '#414e5c',
|
||||
groundColor: '#13233C',
|
||||
backgroundColor: '#13233C',
|
||||
centerLineColor: '#7f8c8d',
|
||||
},
|
||||
light: {
|
||||
gridColor: '#7f8c8d',
|
||||
groundColor: '#DDD',
|
||||
backgroundColor: '#EEE',
|
||||
centerLineColor: '#2F2F2F',
|
||||
},
|
||||
};
|
||||
// Select current theme
|
||||
this.theme = this.themes[theme] || this.themes.light;
|
||||
// State
|
||||
this.state = {
|
||||
error: null,
|
||||
isReady: false,
|
||||
isLoading: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
ThreeViewer.testWebgl
|
||||
.then(() => {
|
||||
this.renderScene();
|
||||
// Update render on resize window
|
||||
window.addEventListener('resize', this.handleResize, false);
|
||||
})
|
||||
.catch(() => {
|
||||
// No webgl support, handle Error...
|
||||
// TODO: Use a better error message
|
||||
this.setState({ error: "Sorry, your computer doesn't support WebGL." });
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
// Remove event listeners
|
||||
window.removeEventListener('resize', this.handleResize, false);
|
||||
// Free memory
|
||||
if (this.renderer && this.mesh) {
|
||||
// Clean up group
|
||||
this.scene.remove(this.mesh);
|
||||
if (this.mesh.geometry) this.mesh.geometry.dispose();
|
||||
if (this.mesh.material) this.mesh.material.dispose();
|
||||
// Cleanup shared geometry
|
||||
if (this.geometry) this.geometry.dispose();
|
||||
if (this.bufferGeometry) this.bufferGeometry.dispose();
|
||||
// Clean up shared material
|
||||
if (this.material) this.material.dispose();
|
||||
// Clean up grid
|
||||
if (this.grid) {
|
||||
this.grid.material.dispose();
|
||||
this.grid.geometry.dispose();
|
||||
}
|
||||
// Clean up group items
|
||||
this.mesh.traverse(child => {
|
||||
if (child instanceof THREE.Mesh) {
|
||||
if (child.geometry) child.geometry.dispose();
|
||||
if (child.material) child.material.dispose();
|
||||
}
|
||||
});
|
||||
// Clean up controls
|
||||
if (this.controls) this.controls.dispose();
|
||||
// It's unclear if we need this:
|
||||
if (this.renderer) {
|
||||
this.renderer.renderLists.dispose();
|
||||
this.renderer.dispose();
|
||||
}
|
||||
// Stop animation
|
||||
cancelAnimationFrame(this.frameID);
|
||||
// Destroy GUI Controls
|
||||
if (this.gui) this.gui.destroy();
|
||||
// Empty objects
|
||||
this.grid = null;
|
||||
this.mesh = null;
|
||||
this.renderer = null;
|
||||
this.material = null;
|
||||
this.geometry = null;
|
||||
this.bufferGeometry = null;
|
||||
}
|
||||
}
|
||||
|
||||
transformGroup(group) {
|
||||
ThreeViewer.fitMeshToCamera(group);
|
||||
|
||||
if (!this.targetCenter) {
|
||||
const box = new THREE.Box3();
|
||||
this.targetCenter = box.setFromObject(this.mesh).getCenter();
|
||||
}
|
||||
this.updateControlsTarget(this.targetCenter);
|
||||
}
|
||||
|
||||
createInterfaceControls() {
|
||||
if (this.guiContainer && this.mesh) {
|
||||
this.gui = new dat.GUI({ autoPlace: false, name: 'controls' });
|
||||
|
||||
const config = {
|
||||
color: this.materialColor,
|
||||
};
|
||||
|
||||
config.reset = () => {
|
||||
// Reset material color
|
||||
config.color = this.materialColor;
|
||||
// Reset material
|
||||
this.material.color.set(config.color);
|
||||
this.material.flatShading = true;
|
||||
this.material.shininess = 30;
|
||||
this.material.wireframe = false;
|
||||
// Reset autoRotate
|
||||
this.controls.autoRotate = false;
|
||||
// Reset camera
|
||||
this.restoreCamera();
|
||||
};
|
||||
|
||||
const materialFolder = this.gui.addFolder('Material');
|
||||
|
||||
// Color picker
|
||||
const colorPicker = materialFolder
|
||||
.addColor(config, 'color')
|
||||
.name('Color')
|
||||
.listen();
|
||||
|
||||
colorPicker.onChange(color => {
|
||||
this.material.color.set(color);
|
||||
});
|
||||
|
||||
materialFolder
|
||||
.add(this.material, 'shininess', 0, 100)
|
||||
.name('Shininess')
|
||||
.listen();
|
||||
|
||||
materialFolder
|
||||
.add(this.material, 'flatShading')
|
||||
.name('FlatShading')
|
||||
.onChange(() => {
|
||||
ThreeViewer.updateMaterial(this.material);
|
||||
})
|
||||
.listen();
|
||||
|
||||
materialFolder
|
||||
.add(this.material, 'wireframe')
|
||||
.name('Wireframe')
|
||||
.listen();
|
||||
|
||||
const sceneFolder = this.gui.addFolder('Scene');
|
||||
|
||||
sceneFolder
|
||||
.add(this.controls, 'autoRotate')
|
||||
.name('Auto-Rotate')
|
||||
.listen();
|
||||
|
||||
sceneFolder.add(config, 'reset').name('Reset');
|
||||
|
||||
this.guiContainer.current.appendChild(this.gui.domElement);
|
||||
}
|
||||
}
|
||||
|
||||
createGeometry(data) {
|
||||
this.bufferGeometry = data;
|
||||
this.bufferGeometry.computeBoundingBox();
|
||||
this.bufferGeometry.center();
|
||||
this.bufferGeometry.rotateX(-Math.PI / 2);
|
||||
this.bufferGeometry.lookAt(new THREE.Vector3(0, 0, 1));
|
||||
// Get geometry from bufferGeometry
|
||||
this.geometry = new THREE.Geometry().fromBufferGeometry(this.bufferGeometry);
|
||||
this.geometry.mergeVertices();
|
||||
this.geometry.computeVertexNormals();
|
||||
}
|
||||
|
||||
startLoader() {
|
||||
const { source } = this.props;
|
||||
|
||||
|
@ -179,6 +292,8 @@ class ThreeViewer extends React.PureComponent<Props> {
|
|||
|
||||
handleReady = () => {
|
||||
this.setState({ isReady: true, isLoading: false });
|
||||
// GUI
|
||||
this.createInterfaceControls();
|
||||
};
|
||||
|
||||
handleError = () => {
|
||||
|
@ -193,32 +308,29 @@ class ThreeViewer extends React.PureComponent<Props> {
|
|||
this.renderer.setSize(width, height);
|
||||
};
|
||||
|
||||
handleColorChange(color) {
|
||||
if (!this.mesh) return;
|
||||
const pickColor = this.materialColors[color] || this.materialColors.green;
|
||||
this.mesh.material.color.set(pickColor);
|
||||
this.wireframe.material.color.set(pickColor);
|
||||
}
|
||||
|
||||
updateControlsTarget(point) {
|
||||
this.controls.target.fromArray([point.x, point.y, point.z]);
|
||||
this.controls.update();
|
||||
}
|
||||
|
||||
restoreCamera() {
|
||||
this.controls.reset();
|
||||
this.camera.position.set(-9.5, 14, 11);
|
||||
this.updateControlsTarget(this.targetCenter);
|
||||
}
|
||||
|
||||
renderStl(data) {
|
||||
const geometry = this.createGeometry(data);
|
||||
const group = new THREE.Mesh(geometry, this.material);
|
||||
// Assign name
|
||||
group.name = 'objectGroup';
|
||||
this.scene.add(group);
|
||||
this.transformGroup(group);
|
||||
this.mesh = group;
|
||||
this.createGeometry(data);
|
||||
this.mesh = new THREE.Mesh(this.geometry, this.material);
|
||||
this.mesh.name = 'model';
|
||||
this.scene.add(this.mesh);
|
||||
this.transformGroup(this.mesh);
|
||||
}
|
||||
|
||||
renderObj(event) {
|
||||
const mesh = event.detail.loaderRootNode;
|
||||
const group = new THREE.Group();
|
||||
group.name = 'objGroup';
|
||||
this.mesh = new THREE.Group();
|
||||
this.mesh.name = 'model';
|
||||
|
||||
// Assign new material
|
||||
mesh.traverse(child => {
|
||||
|
@ -226,15 +338,18 @@ class ThreeViewer extends React.PureComponent<Props> {
|
|||
// Get geometry from child
|
||||
const geometry = new THREE.Geometry();
|
||||
geometry.fromBufferGeometry(child.geometry);
|
||||
geometry.mergeVertices();
|
||||
geometry.computeVertexNormals();
|
||||
// Create and regroup inner objects
|
||||
const innerObj = new THREE.Mesh(geometry, this.material);
|
||||
group.add(innerObj);
|
||||
this.mesh.add(innerObj);
|
||||
// Clean up geometry
|
||||
geometry.dispose();
|
||||
child.geometry.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
this.scene.add(group);
|
||||
this.transformGroup(group);
|
||||
this.mesh = group;
|
||||
this.scene.add(this.mesh);
|
||||
this.transformGroup(this.mesh);
|
||||
}
|
||||
|
||||
renderModel(fileType, parsedData) {
|
||||
|
@ -254,6 +369,7 @@ class ThreeViewer extends React.PureComponent<Props> {
|
|||
this.renderer = ThreeRenderer({
|
||||
antialias: true,
|
||||
shadowMap: true,
|
||||
gammaCorrection: true,
|
||||
});
|
||||
|
||||
this.scene = ThreeScene({
|
||||
|
@ -268,54 +384,56 @@ class ThreeViewer extends React.PureComponent<Props> {
|
|||
// Grid
|
||||
this.grid = ThreeGrid({ size: 100, gridColor, centerLineColor });
|
||||
this.scene.add(this.grid);
|
||||
|
||||
// Camera
|
||||
this.camera = new THREE.PerspectiveCamera(80, width / height, 0.1, 1000);
|
||||
this.camera.position.set(-9.5, 14, 11);
|
||||
|
||||
// Controls
|
||||
this.controls = this.createOrbitControls(this.camera, canvas);
|
||||
this.controls = ThreeViewer.createOrbitControls(this.camera, canvas);
|
||||
|
||||
// Set viewer size
|
||||
this.renderer.setSize(width, height);
|
||||
|
||||
// Create model material
|
||||
this.material = new THREE.MeshPhongMaterial({
|
||||
opacity: 1,
|
||||
transparent: true,
|
||||
// depthWrite: true,
|
||||
depthWrite: true,
|
||||
flatShading: true,
|
||||
vertexColors: THREE.FaceColors,
|
||||
// Positive value pushes polygon further away
|
||||
// polygonOffsetFactor: 1,
|
||||
// polygonOffsetUnits: 1,
|
||||
});
|
||||
|
||||
// Set material color
|
||||
this.material.color.set(this.materialColors.green);
|
||||
this.material.color.set(this.materialColor);
|
||||
|
||||
// Load file and render mesh
|
||||
this.startLoader();
|
||||
|
||||
// Append canvas
|
||||
viewer.appendChild(canvas);
|
||||
|
||||
const updateScene = () => {
|
||||
requestAnimationFrame(updateScene);
|
||||
this.controls.update();
|
||||
this.frameID = requestAnimationFrame(updateScene);
|
||||
if (this.controls.autoRotate) this.controls.update();
|
||||
this.renderer.render(this.scene, this.camera);
|
||||
};
|
||||
|
||||
updateScene();
|
||||
// Append canvas
|
||||
viewer.appendChild(canvas);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { theme } = this.props;
|
||||
const { error, isReady, isLoading } = this.state;
|
||||
const loadingMessage = 'Loading 3D model.';
|
||||
const loadingMessage = __('Loading 3D model.');
|
||||
const showViewer = isReady && !error;
|
||||
const showLoading = isLoading && !error;
|
||||
|
||||
// Adaptive theme for gui controls
|
||||
const containerClass = classNames('gui-container', { light: theme === 'light' });
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{error && <LoadingScreen status={error} spinner={false} />}
|
||||
{showLoading && <LoadingScreen status={loadingMessage} spinner />}
|
||||
<div ref={this.guiContainer} className={containerClass} />
|
||||
<div
|
||||
style={{ opacity: showViewer ? 1 : 0 }}
|
||||
className="three-viewer file-render__viewer"
|
||||
|
|
|
@ -3,10 +3,8 @@ import { GridHelper, Color } from './three';
|
|||
const ThreeGrid = ({ size, gridColor, centerLineColor }) => {
|
||||
const divisions = size / 2;
|
||||
const grid = new GridHelper(size, divisions, new Color(centerLineColor), new Color(gridColor));
|
||||
|
||||
grid.material.opacity = 0.4;
|
||||
grid.material.transparent = true;
|
||||
|
||||
return grid;
|
||||
};
|
||||
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
import { WebGLRenderer } from './three';
|
||||
|
||||
const ThreeRenderer = ({ antialias, shadowMap }) => {
|
||||
const renderer = new WebGLRenderer({
|
||||
antialias,
|
||||
});
|
||||
const ThreeRenderer = ({ antialias, shadowMap, gammaCorrection }) => {
|
||||
const renderer = new WebGLRenderer({ antialias });
|
||||
// Renderer configuration
|
||||
renderer.setPixelRatio(window.devicePixelRatio);
|
||||
renderer.gammaInput = true;
|
||||
renderer.gammaOutput = true;
|
||||
renderer.shadowMap.enabled = shadowMap;
|
||||
renderer.gammaInput = gammaCorrection || false;
|
||||
renderer.gammaOutput = gammaCorrection || false;
|
||||
renderer.shadowMap.enabled = shadowMap || false;
|
||||
renderer.shadowMap.autoUpdate = false;
|
||||
return renderer;
|
||||
};
|
||||
|
||||
|
|
|
@ -28,3 +28,4 @@
|
|||
@import 'component/_search.scss';
|
||||
@import 'component/_toggle.scss';
|
||||
@import 'component/_search.scss';
|
||||
@import 'component/_dat-gui.scss';
|
||||
|
|
89
src/renderer/scss/component/_dat-gui.scss
Normal file
89
src/renderer/scss/component/_dat-gui.scss
Normal file
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* dat.gui component
|
||||
*/
|
||||
|
||||
.gui-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 2;
|
||||
|
||||
.dg.main {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
overflow: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Light theme:
|
||||
* https://github.com/liabru/dat-gui-light-theme
|
||||
*/
|
||||
|
||||
.gui-container.light {
|
||||
.dg.main.taller-than-window .close-button {
|
||||
border-top: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.dg.main .close-button {
|
||||
background-color: #e8e8e8;
|
||||
}
|
||||
|
||||
.dg.main .close-button:hover {
|
||||
background-color: #ddd;
|
||||
}
|
||||
|
||||
.dg {
|
||||
color: #555;
|
||||
text-shadow: none !important;
|
||||
}
|
||||
|
||||
.dg.main::-webkit-scrollbar {
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.dg.main::-webkit-scrollbar-thumb {
|
||||
background: #bbb;
|
||||
}
|
||||
|
||||
.dg li:not(.folder) {
|
||||
background: #fafafa;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.dg li.save-row .button {
|
||||
text-shadow: none !important;
|
||||
}
|
||||
|
||||
.dg li.title {
|
||||
background: #e8e8e8
|
||||
url(data:image/gif;base64,R0lGODlhBQAFAJEAAP////Pz8////////yH5BAEAAAIALAAAAAAFAAUAAAIIlI+hKgFxoCgAOw==)
|
||||
6px 10px no-repeat;
|
||||
}
|
||||
|
||||
.dg .cr.function:hover,
|
||||
.dg .cr.boolean:hover {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.dg .c input[type='text'] {
|
||||
background: #e9e9e9;
|
||||
}
|
||||
|
||||
.dg .c input[type='text']:hover {
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
.dg .c input[type='text']:focus {
|
||||
background: #eee;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.dg .c .slider {
|
||||
background: #e9e9e9;
|
||||
}
|
||||
|
||||
.dg .c .slider:hover {
|
||||
background: #eee;
|
||||
}
|
||||
}
|
|
@ -2514,6 +2514,10 @@ dashdash@^1.12.0:
|
|||
dependencies:
|
||||
assert-plus "^1.0.0"
|
||||
|
||||
dat.gui@^0.7.2:
|
||||
version "0.7.2"
|
||||
resolved "https://registry.yarnpkg.com/dat.gui/-/dat.gui-0.7.2.tgz#57056f286d0f989e83f5adec196f24fd69c01ae3"
|
||||
|
||||
date-fns@^1.27.2:
|
||||
version "1.29.0"
|
||||
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.29.0.tgz#12e609cdcb935127311d04d33334e2960a2a54e6"
|
||||
|
|
Loading…
Add table
Reference in a new issue
Might want to add
geometry.mergeVertices();
and if you end up having issues mapping textures, this might help: