diff --git a/.flowconfig b/.flowconfig index 1eaa4969e..c2bb89028 100644 --- a/.flowconfig +++ b/.flowconfig @@ -21,5 +21,6 @@ module.name_mapper='^rewards\(.*\)$' -> '/src/renderer/rewards\1' module.name_mapper='^modal\(.*\)$' -> '/src/renderer/modal\1' module.name_mapper='^app\(.*\)$' -> '/src/renderer/app\1' module.name_mapper='^native\(.*\)$' -> '/src/renderer/native\1' +module.name_mapper='^analytics\(.*\)$' -> '/src/renderer/analytics\1' [strict] diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ed4a49eb..8b35537de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,6 @@ 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 diff --git a/package.json b/package.json index a7d9e7bfe..a8c5a22bb 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,6 @@ "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", diff --git a/src/renderer/component/fileDownloadLink/index.js b/src/renderer/component/fileDownloadLink/index.js index 22f4f0a3f..23ed6b3f7 100644 --- a/src/renderer/component/fileDownloadLink/index.js +++ b/src/renderer/component/fileDownloadLink/index.js @@ -4,6 +4,7 @@ import { makeSelectDownloadingForUri, makeSelectLoadingForUri, makeSelectCostInfoForUri, + makeSelectClaimForUri, } from 'lbry-redux'; import { doOpenFileInShell } from 'redux/actions/file'; import { doPurchaseUri, doStartDownload } from 'redux/actions/content'; @@ -16,6 +17,7 @@ const select = (state, props) => ({ downloading: makeSelectDownloadingForUri(props.uri)(state), costInfo: makeSelectCostInfoForUri(props.uri)(state), loading: makeSelectLoadingForUri(props.uri)(state), + claim: makeSelectClaimForUri(props.uri)(state), }); const perform = dispatch => ({ @@ -25,4 +27,7 @@ const perform = dispatch => ({ doPause: () => dispatch(doPause()), }); -export default connect(select, perform)(FileDownloadLink); +export default connect( + select, + perform +)(FileDownloadLink); diff --git a/src/renderer/component/fileDownloadLink/view.jsx b/src/renderer/component/fileDownloadLink/view.jsx index ccc0f2ba7..bcb352eb2 100644 --- a/src/renderer/component/fileDownloadLink/view.jsx +++ b/src/renderer/component/fileDownloadLink/view.jsx @@ -3,8 +3,11 @@ import React from 'react'; import Button from 'component/button'; import * as icons from 'constants/icons'; import ToolTip from 'component/common/tooltip'; +import analytics from 'analytics'; +import type { Claim } from 'types/claim'; type Props = { + claim: Claim, uri: string, downloading: boolean, fileInfo: ?{ @@ -48,6 +51,7 @@ class FileDownloadLink extends React.PureComponent { costInfo, loading, doPause, + claim, } = this.props; const openFile = () => { @@ -80,6 +84,12 @@ class FileDownloadLink extends React.PureComponent { iconColor="green" onClick={() => { purchaseUri(uri); + + const { name, claim_id: claimId, nout, txid } = claim; + // // ideally outpoint would exist inside of claim information + // // we can use it after https://github.com/lbryio/lbry/issues/1306 is addressed + const outpoint = `${txid}:${nout}`; + analytics.apiLogView(`${name}#${claimId}`, outpoint, claimId); }} /> diff --git a/src/renderer/component/fileViewer/internal/player.jsx b/src/renderer/component/fileViewer/internal/player.jsx index 8d3bf07b9..d76d0bcaf 100644 --- a/src/renderer/component/fileViewer/internal/player.jsx +++ b/src/renderer/component/fileViewer/internal/player.jsx @@ -46,6 +46,7 @@ class MediaPlayer extends React.PureComponent { const loadedMetadata = () => { this.setState({ hasMetadata: true, startedPlaying: true }); + if (startedPlayingCb) { startedPlayingCb(); } diff --git a/src/renderer/component/fileViewer/view.jsx b/src/renderer/component/fileViewer/view.jsx index 57cab3463..aed32f07c 100644 --- a/src/renderer/component/fileViewer/view.jsx +++ b/src/renderer/component/fileViewer/view.jsx @@ -146,7 +146,12 @@ class FileViewer extends React.PureComponent { } playContent() { - const { play, uri } = this.props; + const { play, uri, fileInfo, isDownloading, isLoading } = this.props; + + if (fileInfo || isDownloading || isLoading) { + // User may have pressed download before clicking play + this.startedPlayingCb = null; + } if (this.startedPlayingCb) { this.startTime = Date.now(); diff --git a/src/renderer/component/viewers/threeViewer/index.jsx b/src/renderer/component/viewers/threeViewer/index.jsx index 0f4afb433..751c84dd2 100644 --- a/src/renderer/component/viewers/threeViewer/index.jsx +++ b/src/renderer/component/viewers/threeViewer/index.jsx @@ -1,9 +1,6 @@ // @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'; @@ -14,39 +11,123 @@ import ThreeRenderer from './internal/renderer'; type Props = { theme: string, + autoRotate: boolean, source: { fileType: string, downloadPath: string, }, }; -type State = { - error: ?string, - isReady: boolean, - isLoading: boolean, -}; +class ThreeViewer extends React.PureComponent { + constructor(props: Props) { + super(props); -class ThreeViewer extends React.PureComponent { - static testWebgl = new Promise((resolve, reject) => { - if (detectWebGL()) resolve(); - else reject(); - }); + const { theme } = this.props; - static createOrbitControls(camera, canvas) { + // 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; const controls = new THREE.OrbitControls(camera, canvas); // Controls configuration controls.enableDamping = true; controls.dampingFactor = 0.75; controls.enableZoom = true; - controls.minDistance = 5; - controls.maxDistance = 14; - controls.autoRotate = false; + controls.minDistance = 1; + controls.maxDistance = 50; + controls.autoRotate = autoRotate; controls.enablePan = false; - controls.saveState(); return controls; } - static fitMeshToCamera(group) { + 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) { const max = { x: 0, y: 0, z: 0 }; const min = { x: 0, y: 0, z: 0 }; @@ -66,214 +147,20 @@ class ThreeViewer extends React.PureComponent { 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; @@ -292,8 +179,6 @@ class ThreeViewer extends React.PureComponent { handleReady = () => { this.setState({ isReady: true, isLoading: false }); - // GUI - this.createInterfaceControls(); }; handleError = () => { @@ -308,29 +193,32 @@ class ThreeViewer extends React.PureComponent { 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) { - 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); + 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; } renderObj(event) { const mesh = event.detail.loaderRootNode; - this.mesh = new THREE.Group(); - this.mesh.name = 'model'; + const group = new THREE.Group(); + group.name = 'objGroup'; // Assign new material mesh.traverse(child => { @@ -338,18 +226,15 @@ class ThreeViewer extends React.PureComponent { // 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); - this.mesh.add(innerObj); - // Clean up geometry - geometry.dispose(); - child.geometry.dispose(); + group.add(innerObj); } }); - this.scene.add(this.mesh); - this.transformGroup(this.mesh); + + this.scene.add(group); + this.transformGroup(group); + this.mesh = group; } renderModel(fileType, parsedData) { @@ -369,7 +254,6 @@ class ThreeViewer extends React.PureComponent { this.renderer = ThreeRenderer({ antialias: true, shadowMap: true, - gammaCorrection: true, }); this.scene = ThreeScene({ @@ -384,56 +268,54 @@ class ThreeViewer extends React.PureComponent { // 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 = ThreeViewer.createOrbitControls(this.camera, canvas); + this.controls = this.createOrbitControls(this.camera, canvas); // Set viewer size this.renderer.setSize(width, height); // Create model material this.material = new THREE.MeshPhongMaterial({ - depthWrite: true, - flatShading: true, + opacity: 1, + transparent: true, + // depthWrite: true, vertexColors: THREE.FaceColors, + // Positive value pushes polygon further away + // polygonOffsetFactor: 1, + // polygonOffsetUnits: 1, }); - // Set material color - this.material.color.set(this.materialColor); + this.material.color.set(this.materialColors.green); // Load file and render mesh this.startLoader(); - // Append canvas - viewer.appendChild(canvas); - const updateScene = () => { - this.frameID = requestAnimationFrame(updateScene); - if (this.controls.autoRotate) this.controls.update(); + requestAnimationFrame(updateScene); + 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 ( {error && } {showLoading && } -
{ 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; }; diff --git a/src/renderer/component/viewers/threeViewer/internal/renderer.js b/src/renderer/component/viewers/threeViewer/internal/renderer.js index 7ddefd48a..11b6c0545 100644 --- a/src/renderer/component/viewers/threeViewer/internal/renderer.js +++ b/src/renderer/component/viewers/threeViewer/internal/renderer.js @@ -1,13 +1,14 @@ import { WebGLRenderer } from './three'; -const ThreeRenderer = ({ antialias, shadowMap, gammaCorrection }) => { - const renderer = new WebGLRenderer({ antialias }); +const ThreeRenderer = ({ antialias, shadowMap }) => { + const renderer = new WebGLRenderer({ + antialias, + }); // Renderer configuration renderer.setPixelRatio(window.devicePixelRatio); - renderer.gammaInput = gammaCorrection || false; - renderer.gammaOutput = gammaCorrection || false; - renderer.shadowMap.enabled = shadowMap || false; - renderer.shadowMap.autoUpdate = false; + renderer.gammaInput = true; + renderer.gammaOutput = true; + renderer.shadowMap.enabled = shadowMap; return renderer; }; diff --git a/src/renderer/scss/all.scss b/src/renderer/scss/all.scss index 4b9bae942..b8db82c0b 100644 --- a/src/renderer/scss/all.scss +++ b/src/renderer/scss/all.scss @@ -28,4 +28,3 @@ @import 'component/_search.scss'; @import 'component/_toggle.scss'; @import 'component/_search.scss'; -@import 'component/_dat-gui.scss'; diff --git a/src/renderer/scss/component/_dat-gui.scss b/src/renderer/scss/component/_dat-gui.scss deleted file mode 100644 index 4cc69cdf3..000000000 --- a/src/renderer/scss/component/_dat-gui.scss +++ /dev/null @@ -1,89 +0,0 @@ -/* -* 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; - } -} diff --git a/yarn.lock b/yarn.lock index 83104ad53..d52cf43b2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2514,10 +2514,6 @@ 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"