Merge remote-tracking branch 'upstream/master' into containers_kickstart
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
.vscode
|
||||
coverage
|
||||
|
||||
node_modules
|
||||
**/node_modules
|
||||
npm-debug.log
|
||||
Dockerfile*
|
||||
@@ -18,6 +19,7 @@ test-queries
|
||||
Contributing.md
|
||||
ISSUE_TEMPLATE.md
|
||||
lerna.json
|
||||
.env
|
||||
.env.example
|
||||
.eslintrc.json
|
||||
.mocharc.js
|
||||
|
||||
+30
-25
@@ -1,45 +1,50 @@
|
||||
FROM node:14.15.4-alpine3.10@sha256:fe215d05cdde4b7f2a0f546c88a8ddc4f5fa280a204acdfc2383afe901fd6d84 as build
|
||||
|
||||
WORKDIR /home/node
|
||||
FROM node:14.15.4-alpine3.12@sha256:55bf28ea11b18fd914e1242835ea3299ec76f5a034e8c6e42b2ede70064e338c as node
|
||||
|
||||
FROM node as build
|
||||
# Having multiple steps in builder doesn't increase the final image size
|
||||
# So having verbose steps for readability and caching should be the target
|
||||
|
||||
# 1. Copy package defs first they are the least likely to change
|
||||
WORKDIR /opt
|
||||
|
||||
# Copy package defs first they are the least likely to change
|
||||
# Keeping this order will least likely trigger full rebuild
|
||||
COPY packages/frontend/package*.json frontend/
|
||||
COPY packages/server/package*.json server/
|
||||
|
||||
# 2. Install packages
|
||||
# Use dev environment to install dev packages for frontend build
|
||||
ENV NODE_ENV development
|
||||
RUN npm --prefix frontend ci frontend
|
||||
|
||||
# Switch to production env for server install
|
||||
COPY packages/server/package*.json server/
|
||||
ENV NODE_ENV production
|
||||
RUN npm --prefix server ci server
|
||||
|
||||
# 3. build frontend to static files
|
||||
# when testing container build, most propably not the frontend files are the ones
|
||||
# that are changing. So having a separate copy and build step speeds up the container
|
||||
# build.
|
||||
# Copy remaining files across for frontend. Changes to these files
|
||||
# will be more common than changes to the dependencies. This should
|
||||
# speed up rebuilds.
|
||||
COPY packages/frontend frontend
|
||||
RUN npm --prefix frontend run build
|
||||
|
||||
COPY packages/server server
|
||||
FROM node:14.15.4-alpine3.10@sha256:fe215d05cdde4b7f2a0f546c88a8ddc4f5fa280a204acdfc2383afe901fd6d84
|
||||
WORKDIR /opt/frontend
|
||||
RUN npm run build
|
||||
|
||||
FROM node as runtime
|
||||
|
||||
RUN apk add --no-cache tini=0.19.0-r0
|
||||
|
||||
# Use a non-root user for increased security.
|
||||
USER node
|
||||
|
||||
ENV NODE_ENV production
|
||||
|
||||
RUN mkdir -p frontend/dist server
|
||||
# Copy dependencies and static files from build layer
|
||||
COPY --from=build --chown=node /opt/frontend/dist /home/node/frontend/dist
|
||||
COPY --from=build --chown=node /opt/server /home/node/server
|
||||
|
||||
# only copy in the build artifacts for the frontend
|
||||
COPY --from=build --chown=node /home/node/frontend/dist frontend/dist
|
||||
# copy the server with installed modules
|
||||
COPY --from=build --chown=node /home/node/server server
|
||||
# Run the application from the non root users home directory
|
||||
WORKDIR /home/node/server
|
||||
|
||||
# change to no root user, node
|
||||
USER node
|
||||
# Copy remaining files across for the server. Changes to these
|
||||
# files will be more common than changes to the dependencies.
|
||||
# This should speed up rebuilds.
|
||||
COPY --chown=node packages/server /home/node/server
|
||||
|
||||
# Init for containers https://github.com/krallin/tini
|
||||
ENTRYPOINT [ "/sbin/tini", "--" ]
|
||||
|
||||
WORKDIR /server
|
||||
CMD ["node", "bin/www"]
|
||||
|
||||
@@ -32,7 +32,7 @@ npm run build
|
||||
|
||||
## Community
|
||||
|
||||
The Speckle Community hangs out on [the forum](https://discourse.speckle.works), do join and introduce yourself & feel free to ask us questions!
|
||||
If in trouble, the Speckle Community hangs out on [the forum](https://discourse.speckle.works). Do join and introduce yourself! We're happy to help.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -128,7 +128,6 @@ module.exports = {
|
||||
subscribe: withFilter( () => pubsub.asyncIterator( [ COMMIT_CREATED ] ),
|
||||
async ( payload, variables, context ) => {
|
||||
await authorizeResolver( context.userId, payload.streamId, 'stream:reviewer' )
|
||||
|
||||
return payload.streamId === variables.streamId
|
||||
} )
|
||||
},
|
||||
|
||||
@@ -12,18 +12,22 @@ We're working to stabilize the 2.0 API, and until then there will be breaking ch
|
||||
|
||||
## Introduction
|
||||
|
||||
The Speckle Server is a node application. To start it locally, simply:
|
||||
The Speckle Server is a node application. To start it locally:
|
||||
|
||||
- ensure you have a local instance of postgres & redis running
|
||||
First, ensure you have postgres and redis ready and running:
|
||||
|
||||
- ensure you have a local instance of postgres running
|
||||
- create a postgres db called `speckle2_dev`
|
||||
- then run `npm install`
|
||||
- finally `npm run dev` will start the server.
|
||||
- ensure you have an instance of redis running
|
||||
|
||||
Finally, in the `packages/server` folder:
|
||||
|
||||
- copy the `.env-example` file to `.env`,
|
||||
- open and edit the `.env` file, filling in the required variables,
|
||||
- run `npm install`,
|
||||
- finally `npm run dev`,
|
||||
- check `localhost:3000/graphql` out!
|
||||
|
||||
You can customise your local deployment by editing and filling in a `.env` file. To do so:
|
||||
|
||||
- copy the `.env-example` file to `.env`
|
||||
- open and edit the `.env` file.
|
||||
|
||||
## Developing
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"ignore": ["node_modules/**/*"],
|
||||
"presets": [
|
||||
["@babel/preset-typescript"],
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
"loose": true,
|
||||
"targets": {
|
||||
"browsers": "last 2 versions, > 0.5%, ie >= 11",
|
||||
"esmodules": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"@babel/preset-react"
|
||||
],
|
||||
"plugins": [
|
||||
"@babel/plugin-proposal-class-properties",
|
||||
"babel-plugin-add-module-exports",
|
||||
"@babel/plugin-transform-classes"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// eslint-disable-next-line no-undef
|
||||
module.exports = {
|
||||
'env': {
|
||||
'browser': true,
|
||||
'commonjs': true,
|
||||
'es2021': true
|
||||
},
|
||||
'extends': 'eslint:recommended',
|
||||
'parserOptions': {
|
||||
'ecmaVersion': 12,
|
||||
'sourceType': 'module'
|
||||
},
|
||||
'ignorePatterns': [ 'node_modules/*' ],
|
||||
'rules': {
|
||||
'indent': [
|
||||
'error',
|
||||
2
|
||||
],
|
||||
'linebreak-style': [
|
||||
'error',
|
||||
'unix'
|
||||
],
|
||||
'quotes': [
|
||||
'error',
|
||||
'single'
|
||||
],
|
||||
'semi': [
|
||||
'error',
|
||||
'never'
|
||||
],
|
||||
'arrow-spacing': [
|
||||
2,
|
||||
{
|
||||
'before': true,
|
||||
'after': true
|
||||
}
|
||||
],
|
||||
'array-bracket-spacing': [ 2, 'always' ],
|
||||
'object-curly-spacing': [ 1, 'always' ],
|
||||
'block-spacing': [ 2, 'always' ],
|
||||
'space-in-parens': [ 2, 'always' ],
|
||||
'keyword-spacing': 2,
|
||||
'space-unary-ops': [
|
||||
2,
|
||||
{
|
||||
'words': true,
|
||||
'nonwords': false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
Vendored
+56276
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,68 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Speckle Viewer</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Space+Mono:ital,wght@0,400;0,700;1,700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/skeleton/2.0.4/skeleton.min.css" integrity="sha512-EZLkOqwILORob+p0BXZc+Vm3RgJBOe1Iq/0fiI7r/wJgzOFZMlsqTa29UEl6v6U6gsV4uIpsNZoV32YZqrCRCQ==" crossorigin="anonymous" />
|
||||
<style type="text/css">
|
||||
body{
|
||||
font-family: 'Space Mono', monospace !important;
|
||||
}
|
||||
button {
|
||||
font-family: 'Space Mono', monospace !important;
|
||||
border-color: #0A66FF;
|
||||
}
|
||||
#renderer{
|
||||
height: 700px;
|
||||
width: 100%;
|
||||
border: 5px dashed;
|
||||
border-color: rgba(100,100,100, 0.1);
|
||||
}
|
||||
</style>
|
||||
<script defer src="demo.js"></script></head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="row" style="padding-top: 20px">
|
||||
<div class="twelve columns">
|
||||
<h3>Viewer</h3>
|
||||
<h5>Controls summary:</h5>
|
||||
<p>Click an object to select it. Double click it to focus on it. Press `esc` to clear the selection. Press `shift-s` to toggle a section plane. Press `s` while the section plane is active to toggle its control mode. Double click anywhere outside an object to zoom extents to the entire scene.</p>
|
||||
</div>
|
||||
<div class="twelve columns">
|
||||
<button onclick="v.sceneManager.zoomExtents()">Zoom Extents</button>
|
||||
<button onclick="v.postprocessing = !v.postprocessing">Postprocessing Toggle</button>
|
||||
<button onclick="v.sectionPlaneHelper.toggleSectionPlanes()">Show Section Plane</button>
|
||||
</div>
|
||||
<div class="twelve columns">
|
||||
<input id="objectUrlInput" type="text" name="objectId" placeholder="Object Url" style="width:49%" value="https://staging.speckle.dev/streams/a75ab4f10f/objects/a59617590b0bec4e9b5ee487ee75b1a7"/>
|
||||
<button class="" onclick="LoadData()" style="width:49%">Load Object URL</button>
|
||||
</div>
|
||||
<div class="twelve columns">
|
||||
<input id="objectIdInput" type="text" name="objectId" placeholder="Object Id" style="width:49%"/>
|
||||
<button class="" onclick="LoadDataOld()" style="width:49%">Load Object URL</button>
|
||||
</div>
|
||||
<div class="twelve columns">
|
||||
<button onclick="LoadDataOld('cd2d10cafb01a3e76954ae5906d00dfc')">Load Boat</button>
|
||||
<button onclick="LoadDataOld('9a00ad76438d06612968190546552c56')">Load Jet Fuselage</button>
|
||||
<button onclick="LoadDataOld('3242eb9db2199b83b7045507917689ae')">Load Strange Thing</button>
|
||||
<button onclick="LoadDataOld('eb7e163cfe9e8afdbc74bd7451f96096')">Load Lots of Strange Things</button>
|
||||
<button onclick="LoadDataOld('145432210c277bef7e02e0040f70f283')">Load Boring Stuff</button>
|
||||
<button onclick="LoadDataOld('9bdac9ee6c95a809ea7fe7218eb87b48')">Load Revit Thing</button>
|
||||
<button onclick="v.sceneManager.removeAllObjects()">Dispose Everything</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="twelve columns">
|
||||
<div id="renderer"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="twelve columns">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+11778
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "@speckle/sdk",
|
||||
"version": "2.0.0",
|
||||
"description": "Speckle js utilities.",
|
||||
"main": "dist/Speckle.js",
|
||||
"scripts": {
|
||||
"serve": "webpack serve --env dev --config webpack.config.example.js",
|
||||
"dev": "webpack --progress --watch --env dev",
|
||||
"build": "webpack --env dev && webpack --env build"
|
||||
},
|
||||
"author": "AEC Systems",
|
||||
"license": "SEE LICENSE NOTE IN readme.md",
|
||||
"devDependencies": {
|
||||
"@babel/cli": "7.12.10",
|
||||
"@babel/core": "7.12.10",
|
||||
"@babel/eslint-parser": "^7.12.1",
|
||||
"@babel/plugin-proposal-class-properties": "7.12.1",
|
||||
"@babel/preset-env": "7.12.11",
|
||||
"@babel/preset-react": "7.12.10",
|
||||
"@babel/preset-typescript": "7.12.7",
|
||||
"babel-jest": "26.6.3",
|
||||
"babel-loader": "^8.0.0-beta.4",
|
||||
"babel-plugin-add-module-exports": "1.0.4",
|
||||
"babel-plugin-transform-class-properties": "6.24.1",
|
||||
"clean-webpack-plugin": "^3.0.0",
|
||||
"cross-env": "7.0.3",
|
||||
"eslint": "^7.17.0",
|
||||
"html-webpack-plugin": "^5.0.0-beta.4",
|
||||
"jest": "26.6.3",
|
||||
"mocha": "^4.0.1",
|
||||
"webpack": "5.11.0",
|
||||
"webpack-cli": "^4.3.1",
|
||||
"webpack-dev-server": "^3.11.1",
|
||||
"yargs": "^10.0.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"three": "^0.124.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
# The Speckle Viewer
|
||||
|
||||
[](https://twitter.com/SpeckleSystems) [](https://discourse.speckle.works)
|
||||
[](https://speckle-works.slack.com/join/shared_invite/enQtNjY5Mzk2NTYxNTA4LTU4MWI5ZjdhMjFmMTIxZDIzOTAzMzRmMTZhY2QxMmM1ZjVmNzJmZGMzMDVlZmJjYWQxYWU0MWJkYmY3N2JjNGI) [](https://speckle.systems)
|
||||
|
||||
## Disclaimer
|
||||
We're working to stabilize the 2.0 API, and until then there will be breaking changes.
|
||||
|
||||
## Getting started
|
||||
|
||||
Note, these are WIP instructions. For development purposes, to start a webpack live reload server run:
|
||||
|
||||
```
|
||||
npm run serve
|
||||
```
|
||||
|
||||
To build the library, you should run:
|
||||
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
|
||||
## Community
|
||||
|
||||
If in trouble, the Speckle Community hangs out on [the forum](https://discourse.speckle.works). Do join and introduce yourself! We're happy to help.
|
||||
|
||||
## License
|
||||
Unless otherwise described, the code in this repository is licensed under the Apache-2.0 License. Please note that some modules, extensions or code herein might be otherwise licensed. This is indicated either in the root of the containing folder under a different license file, or in the respective file's header. If you have any questions, don't hesitate to get in touch with us via [email](mailto:hello@speckle.systems).
|
||||
@@ -0,0 +1,41 @@
|
||||
|
||||
import Viewer from './modules/Viewer'
|
||||
import ObjectLoader from './modules/ObjectLoader'
|
||||
import Converter from './modules/Converter'
|
||||
|
||||
let v = new Viewer( { container: document.getElementById( 'renderer' ) } )
|
||||
v.on( 'load-progress', args => console.log( args ) )
|
||||
|
||||
window.v = v
|
||||
|
||||
const token = 'e844747dc6f6b0b5c7d5fbd82d66de6e9529531d75'
|
||||
|
||||
window.LoadData = async function LoadData( url ) {
|
||||
url = url || document.getElementById( 'objectUrlInput' ).value
|
||||
await v.loadObject( url, token )
|
||||
}
|
||||
|
||||
window.LoadDataOld = async function LoadData( id ) {
|
||||
|
||||
// v.sceneManager.removeAllObjects()
|
||||
|
||||
id = id || document.getElementById( 'objectIdInput' ).value
|
||||
let loader = new ObjectLoader( {
|
||||
serverUrl: 'https://staging.speckle.dev',
|
||||
streamId: '5486aa9fc7',
|
||||
token,
|
||||
objectId: id
|
||||
} )
|
||||
|
||||
let converter = new Converter( loader )
|
||||
let first = true
|
||||
// Note: it's important the loop continues to load.
|
||||
for await ( let obj of loader.getObjectIterator() ) {
|
||||
if ( first ) {
|
||||
( async() => {
|
||||
await converter.traverseAndConvert( obj, ( o ) => v.sceneManager.addObject( o ) )
|
||||
} )()
|
||||
first = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Speckle Viewer</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Space+Mono:ital,wght@0,400;0,700;1,700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/skeleton/2.0.4/skeleton.min.css" integrity="sha512-EZLkOqwILORob+p0BXZc+Vm3RgJBOe1Iq/0fiI7r/wJgzOFZMlsqTa29UEl6v6U6gsV4uIpsNZoV32YZqrCRCQ==" crossorigin="anonymous" />
|
||||
<style type="text/css">
|
||||
body{
|
||||
font-family: 'Space Mono', monospace !important;
|
||||
}
|
||||
button {
|
||||
font-family: 'Space Mono', monospace !important;
|
||||
border-color: #0A66FF;
|
||||
}
|
||||
#renderer{
|
||||
height: 700px;
|
||||
width: 100%;
|
||||
border: 5px dashed;
|
||||
border-color: rgba(100,100,100, 0.1);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="row" style="padding-top: 20px">
|
||||
<div class="twelve columns">
|
||||
<h3>Viewer</h3>
|
||||
<h5>Controls summary:</h5>
|
||||
<p>Click an object to select it. Double click it to focus on it. Press `esc` to clear the selection. Press `shift-s` to toggle a section plane. Press `s` while the section plane is active to toggle its control mode. Double click anywhere outside an object to zoom extents to the entire scene.</p>
|
||||
</div>
|
||||
<div class="twelve columns">
|
||||
<button onclick="v.sceneManager.zoomExtents()">Zoom Extents</button>
|
||||
<button onclick="v.postprocessing = !v.postprocessing">Postprocessing Toggle</button>
|
||||
<button onclick="v.sectionPlaneHelper.toggleSectionPlanes()">Show Section Plane</button>
|
||||
</div>
|
||||
<div class="twelve columns">
|
||||
<input id="objectUrlInput" type="text" name="objectId" placeholder="Object Url" style="width:49%" value="https://staging.speckle.dev/streams/a75ab4f10f/objects/a59617590b0bec4e9b5ee487ee75b1a7"/>
|
||||
<button class="" onclick="LoadData()" style="width:49%">Load Object URL</button>
|
||||
</div>
|
||||
<div class="twelve columns">
|
||||
<input id="objectIdInput" type="text" name="objectId" placeholder="Object Id" style="width:49%"/>
|
||||
<button class="" onclick="LoadDataOld()" style="width:49%">Load Object URL</button>
|
||||
</div>
|
||||
<div class="twelve columns">
|
||||
<button onclick="LoadDataOld('cd2d10cafb01a3e76954ae5906d00dfc')">Load Boat</button>
|
||||
<button onclick="LoadDataOld('9a00ad76438d06612968190546552c56')">Load Jet Fuselage</button>
|
||||
<button onclick="LoadDataOld('3242eb9db2199b83b7045507917689ae')">Load Strange Thing</button>
|
||||
<button onclick="LoadDataOld('eb7e163cfe9e8afdbc74bd7451f96096')">Load Lots of Strange Things</button>
|
||||
<button onclick="LoadDataOld('145432210c277bef7e02e0040f70f283')">Load Boring Stuff</button>
|
||||
<button onclick="LoadDataOld('9bdac9ee6c95a809ea7fe7218eb87b48')">Load Revit Thing</button>
|
||||
<button onclick="v.sceneManager.removeAllObjects()">Dispose Everything</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="twelve columns">
|
||||
<div id="renderer"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="twelve columns">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,5 @@
|
||||
import Viewer from './modules/Viewer'
|
||||
import ObjectLoader from './modules/ObjectLoader'
|
||||
import Converter from './modules/Converter'
|
||||
|
||||
export { Viewer, ObjectLoader, Converter }
|
||||
@@ -0,0 +1,216 @@
|
||||
import * as THREE from 'three'
|
||||
import ObjectWrapper from './ObjectWrapper'
|
||||
import { getConversionFactor } from './Units'
|
||||
|
||||
/**
|
||||
* Utility class providing some top level conversion methods.
|
||||
*/
|
||||
export default class Coverter {
|
||||
|
||||
constructor( objectLoader ) {
|
||||
if ( !objectLoader ) {
|
||||
console.warn( 'Converter initialized without a corresponding object loader. Any objects that include references will throw errors.' )
|
||||
}
|
||||
|
||||
this.objectLoader = objectLoader
|
||||
}
|
||||
|
||||
/**
|
||||
* If the object is convertable (there is a direct conversion routine), it will invoke the callback with the conversion result.
|
||||
* If the object is not convertable, it will recursively iterate through it (arrays & objects) and invoke the callback on any postive conversion result.
|
||||
* @param {[type]} obj [description]
|
||||
* @param {Function} callback [description]
|
||||
* @return {[type]} [description]
|
||||
*/
|
||||
async traverseAndConvert( obj, callback ) {
|
||||
// Exit on primitives (string, ints, bools, bigints, etc.)
|
||||
if ( typeof obj !== 'object' ) return
|
||||
|
||||
if ( obj.referencedId ) obj = await this.resolveReference( obj )
|
||||
|
||||
// Traverse arrays, and exit early (we don't want to iterate through many numbers)
|
||||
if ( Array.isArray( obj ) ) {
|
||||
for ( let element of obj ) {
|
||||
if ( typeof element !== 'object' ) return // exit early for non-object based arrays
|
||||
( async() => await this.traverseAndConvert( element, callback ) )() //iife so we don't block
|
||||
}
|
||||
}
|
||||
|
||||
// If we can convert it, we should invoke the respective conversion routine.
|
||||
const type = this.getSpeckleType( obj )
|
||||
if ( this[`${type}ToBufferGeometry`] ) {
|
||||
try {
|
||||
callback( await this[`${type}ToBufferGeometry`]( obj.data || obj ) )
|
||||
return
|
||||
} catch ( e ) {
|
||||
console.warn( `(Traversing - direct) Failed to convert ${type} with id: ${obj.id}` )
|
||||
}
|
||||
}
|
||||
|
||||
let target = obj.data || obj
|
||||
|
||||
// Check if the object has a display value of sorts
|
||||
let displayValue = target['displayMesh'] || target['@displayMesh'] || target['displayValue']|| target['@displayValue']
|
||||
if ( displayValue ) {
|
||||
displayValue = await this.resolveReference( displayValue )
|
||||
if ( !displayValue.units ) displayValue.units = obj.units
|
||||
|
||||
try {
|
||||
let { bufferGeometry } = await this.convert( displayValue )
|
||||
callback( new ObjectWrapper( bufferGeometry, obj ) ) // use the parent's metadata!
|
||||
|
||||
// return // returning here is faster but excludes objects that have a display value and displayable children (ie, a wall with windows)
|
||||
} catch ( e ) {
|
||||
console.warn( `(Traversing) Failed to convert obj with id: ${obj.id}` )
|
||||
}
|
||||
}
|
||||
|
||||
// Last attempt: iterate through all object keys and see if we can display anything!
|
||||
// traverses the object in case there's any sub-objects we can convert.
|
||||
for ( let prop in target ) {
|
||||
if ( typeof target[prop] !== 'object' ) continue
|
||||
( async() => await this.traverseAndConvert( target[prop], callback ) )() //iife so we don't block
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Directly converts an object and invokes the callback with the the conversion result.
|
||||
* @param {[type]} obj [description]
|
||||
* @param {Function} callback [description]
|
||||
* @return {[type]} [description]
|
||||
*/
|
||||
async convert( obj ) {
|
||||
if ( obj.referencedId ) obj = await this.resolveReference( obj )
|
||||
try {
|
||||
let type = this.getSpeckleType( obj )
|
||||
if ( this[`${type}ToBufferGeometry`] ) {
|
||||
return await this[`${type}ToBufferGeometry`]( obj.data || obj )
|
||||
}
|
||||
else return null
|
||||
} catch ( e ) {
|
||||
console.warn( `(Direct convert) Failed to convert object with id: ${obj.id}` )
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes an array composed of chunked references and dechunks it.
|
||||
* @param {[type]} arr [description]
|
||||
* @return {[type]} [description]
|
||||
*/
|
||||
async dechunk( arr ) {
|
||||
if ( !arr ) return arr
|
||||
// Handles pre-chunking objects, or arrs that have not been chunked
|
||||
if ( !arr[0].referencedId ) return arr
|
||||
|
||||
let dechunked = []
|
||||
for ( let ref of arr ) {
|
||||
let real = await this.objectLoader.getObject( ref.referencedId )
|
||||
dechunked.push( ...real.data )
|
||||
}
|
||||
return dechunked
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves an object reference by waiting for the loader to load it up.
|
||||
* @param {[type]} obj [description]
|
||||
* @return {[type]} [description]
|
||||
*/
|
||||
async resolveReference( obj ) {
|
||||
if ( obj.referencedId )
|
||||
return await this.objectLoader.getObject( obj.referencedId )
|
||||
else return obj
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the speckle type of an object in various scenarios.
|
||||
* @param {[type]} obj [description]
|
||||
* @return {[type]} [description]
|
||||
*/
|
||||
getSpeckleType( obj ) {
|
||||
let type = 'Base'
|
||||
if ( obj.data )
|
||||
type = obj.data.speckle_type ? obj.data.speckle_type.split( '.' ).reverse()[0] : type
|
||||
else
|
||||
type = obj.speckle_type ? obj.speckle_type.split( '.' ).reverse()[0] : type
|
||||
return type
|
||||
}
|
||||
|
||||
async BrepToBufferGeometry( obj ) {
|
||||
try {
|
||||
if ( !obj ) return
|
||||
let { bufferGeometry } = await this.MeshToBufferGeometry( await this.resolveReference( obj.displayValue || obj.displayMesh ) )
|
||||
|
||||
// deletes known uneeded fields
|
||||
delete obj.displayMesh
|
||||
delete obj.displayValue
|
||||
delete obj.Edges
|
||||
delete obj.Faces
|
||||
delete obj.Loops
|
||||
delete obj.Trims
|
||||
delete obj.Curve2D
|
||||
delete obj.Curve3D
|
||||
delete obj.Surfaces
|
||||
delete obj.Vertices
|
||||
|
||||
return new ObjectWrapper( bufferGeometry, obj )
|
||||
} catch ( e ) {
|
||||
console.warn( `Failed to convert brep id: ${obj.id}` )
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
async MeshToBufferGeometry( obj ) {
|
||||
try {
|
||||
if ( !obj ) return
|
||||
|
||||
let conversionFactor = getConversionFactor( obj.units )
|
||||
// console.log( conversionFactor )
|
||||
let buffer = new THREE.BufferGeometry( )
|
||||
let indices = [ ]
|
||||
|
||||
let vertices = await this.dechunk( obj.vertices )
|
||||
let faces = await this.dechunk( obj.faces )
|
||||
|
||||
let k = 0
|
||||
while ( k < faces.length ) {
|
||||
if ( faces[ k ] === 1 ) { // QUAD FACE
|
||||
indices.push( faces[ k + 1 ], faces[ k + 2 ], faces[ k + 3 ] )
|
||||
indices.push( faces[ k + 1 ], faces[ k + 3 ], faces[ k + 4 ] )
|
||||
k += 5
|
||||
} else if ( faces[ k ] === 0 ) { // TRIANGLE FACE
|
||||
indices.push( faces[ k + 1 ], faces[ k + 2 ], faces[ k + 3 ] )
|
||||
k += 4
|
||||
} else throw new Error( `Mesh type not supported. Face topology indicator: ${faces[k]}` )
|
||||
}
|
||||
buffer.setIndex( indices )
|
||||
|
||||
buffer.setAttribute(
|
||||
'position',
|
||||
new THREE.Float32BufferAttribute( conversionFactor === 1 ? vertices : vertices.map( v => v * conversionFactor ), 3 ) )
|
||||
|
||||
buffer.computeVertexNormals( )
|
||||
buffer.computeFaceNormals( )
|
||||
buffer.computeBoundingSphere( )
|
||||
|
||||
delete obj.vertices
|
||||
delete obj.faces
|
||||
|
||||
return new ObjectWrapper( buffer, obj )
|
||||
} catch ( e ) {
|
||||
console.warn( `Failed to convert mesh with id: ${obj.id}` )
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
// TODOs:
|
||||
// async PointToBufferGeometry( obj ) {}
|
||||
// async LineToBufferGeometry( obj ) {}
|
||||
// async PolylineToBufferGeometry( obj ) {}
|
||||
// async PolycurveToBufferGeometry( obj ) {}
|
||||
// async CurveToBufferGeometry( obj ) {}
|
||||
// async CircleToBufferGeometry( obj ) {}
|
||||
// async ArcToBufferGeometry( obj ) {}
|
||||
// async EllipseToBufferGeometry( obj ) {}
|
||||
// async SurfaceToBufferGeometry( obj ) {}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
|
||||
export default class EventEmitter {
|
||||
constructor() {
|
||||
this._events = {}
|
||||
}
|
||||
|
||||
on( name, listener ) {
|
||||
if ( !this._events[name] ) {
|
||||
this._events[name] = []
|
||||
}
|
||||
|
||||
this._events[name].push( listener )
|
||||
}
|
||||
|
||||
removeListener( name, listenerToRemove ) {
|
||||
if ( !this._events[name] ) return
|
||||
|
||||
const filterListeners = ( listener ) => listener !== listenerToRemove
|
||||
|
||||
this._events[name] = this._events[name].filter( filterListeners )
|
||||
}
|
||||
|
||||
emit( name, data ) {
|
||||
if ( !this._events[name] ) return
|
||||
|
||||
const fireCallbacks = ( callback ) => {
|
||||
callback( data )
|
||||
}
|
||||
|
||||
this._events[name].forEach( fireCallbacks )
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* Simple client that streams object info from a Speckle Server.
|
||||
* TODO: This should be split from the viewer into its own package.
|
||||
*/
|
||||
export default class ObjectLoader {
|
||||
|
||||
constructor( { serverUrl, streamId, token, objectId } ) {
|
||||
this.INTERVAL_MS = 20
|
||||
this.TIMEOUT_MS = 180000 // three mins
|
||||
|
||||
this.serverUrl = serverUrl || window.location.origin
|
||||
this.streamId = streamId
|
||||
this.objectId = objectId
|
||||
this.token = token || localStorage.getItem( 'AuthToken' )
|
||||
this.headers = {
|
||||
'Authorization': `Bearer ${this.token}`,
|
||||
'Accept': 'text/plain'
|
||||
}
|
||||
this.requestUrl = `${this.serverUrl}/objects/${this.streamId}/${this.objectId}`
|
||||
this.promises = []
|
||||
this.intervals = {}
|
||||
this.buffer = []
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.buffer = []
|
||||
this.intervals.forEach( i => clearInterval( i.interval ) )
|
||||
}
|
||||
|
||||
async getObject( id ){
|
||||
if ( this.buffer[id] ) return this.buffer[id]
|
||||
|
||||
let promise = new Promise( ( resolve, reject ) => {
|
||||
this.promises.push( { id, resolve, reject } )
|
||||
// Only create a new interval checker if none is already present!
|
||||
if ( this.intervals[id] ) {
|
||||
this.intervals[id].elapsed = 0 // reset elapsed
|
||||
} else {
|
||||
let intervalId = setInterval( this.tryResolvePromise.bind( this ), this.INTERVAL_MS, id )
|
||||
this.intervals[id] = { interval: intervalId, elapsed: 0 }
|
||||
}
|
||||
} )
|
||||
return promise
|
||||
}
|
||||
|
||||
tryResolvePromise( id ) {
|
||||
this.intervals[id].elapsed += this.INTERVAL_MS
|
||||
if ( this.buffer[id] ) {
|
||||
for ( let p of this.promises.filter( p => p.id === id ) ) {
|
||||
p.resolve( this.buffer[id] )
|
||||
}
|
||||
|
||||
clearInterval( this.intervals[id].interval )
|
||||
delete this.intervals[id]
|
||||
// this.promises = this.promises.filter( p => p.id !== p.id ) // clearing out promises too early seems to nuke loading
|
||||
return
|
||||
}
|
||||
|
||||
if ( this.intervals[id].elapsed > this.TIMEOUT_MS ) {
|
||||
console.warn( `Timeout resolving ${id}. HIC SVNT DRACONES.` )
|
||||
clearInterval( this.intervals[id].interval )
|
||||
this.promises.filter( p => p.id === id ).forEach( p => p.reject() )
|
||||
this.promises = this.promises.filter( p => p.id !== p.id ) // clear out
|
||||
}
|
||||
}
|
||||
|
||||
async * getObjectIterator( ) {
|
||||
for await ( let line of this.getRawObjectIterator() ) {
|
||||
let { id, obj } = this.processLine( line )
|
||||
this.buffer[ id ] = obj
|
||||
yield obj
|
||||
}
|
||||
}
|
||||
|
||||
processLine( chunk ) {
|
||||
var pieces = chunk.split( '\t' )
|
||||
return { id: pieces[0], obj: JSON.parse( pieces[1] ) }
|
||||
}
|
||||
|
||||
async * getRawObjectIterator() {
|
||||
const decoder = new TextDecoder()
|
||||
const response = await fetch( this.requestUrl, { headers: this.headers } )
|
||||
const reader = response.body.getReader()
|
||||
let { value: chunk, done: readerDone } = await reader.read()
|
||||
chunk = chunk ? decoder.decode( chunk ) : ''
|
||||
|
||||
let re = /\r\n|\n|\r/gm
|
||||
let startIndex = 0
|
||||
|
||||
while ( true ) {
|
||||
let result = re.exec( chunk )
|
||||
if ( !result ) {
|
||||
if ( readerDone ) break
|
||||
let remainder = chunk.substr( startIndex );
|
||||
( { value: chunk, done: readerDone } = await reader.read() )
|
||||
chunk = remainder + ( chunk ? decoder.decode( chunk ) : '' )
|
||||
startIndex = re.lastIndex = 0
|
||||
continue
|
||||
}
|
||||
yield chunk.substring( startIndex, result.index )
|
||||
startIndex = re.lastIndex
|
||||
}
|
||||
|
||||
if ( startIndex < chunk.length ) {
|
||||
yield chunk.substr( startIndex )
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Class that wraps around a buffer geometry and any remaining speckle object
|
||||
* metadata. Used to match the two in the renderer.
|
||||
*/
|
||||
export default class ObjectWrapper {
|
||||
constructor( bufferGeometry, meta, geometryType ) {
|
||||
this.bufferGeometry = bufferGeometry
|
||||
this.meta = meta
|
||||
this.geometryType = geometryType || 'solid'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
import * as THREE from 'three'
|
||||
import debounce from 'lodash.debounce'
|
||||
|
||||
|
||||
/**
|
||||
* Manages objects and provides some convenience methods to focus on the entire scene, or one specific object.
|
||||
*/
|
||||
export default class SceneObjectManager {
|
||||
|
||||
constructor( viewer ) {
|
||||
this.viewer = viewer
|
||||
this.scene = viewer.scene
|
||||
this.userObjects = new THREE.Group()
|
||||
this.solidObjects = new THREE.Group()
|
||||
this.transparentObjects = new THREE.Group()
|
||||
|
||||
this.userObjects.add( this.solidObjects )
|
||||
this.userObjects.add( this.transparentObjects )
|
||||
this.scene.add( this.userObjects )
|
||||
|
||||
this.solidMaterial = new THREE.MeshStandardMaterial( {
|
||||
color: 0x8D9194,
|
||||
emissive: 0x0,
|
||||
roughness: 1,
|
||||
metalness: 0,
|
||||
side: THREE.DoubleSide,
|
||||
envMap: this.viewer.cubeCamera.renderTarget.texture
|
||||
} )
|
||||
|
||||
this.transparentMaterial = new THREE.MeshStandardMaterial( {
|
||||
color: 0xA0A4A8,
|
||||
emissive: 0x0,
|
||||
roughness: 0,
|
||||
metalness: 0.5,
|
||||
side: THREE.DoubleSide,
|
||||
transparent: true,
|
||||
opacity: 0.4,
|
||||
envMap: this.viewer.cubeCamera.renderTarget.texture
|
||||
} )
|
||||
|
||||
|
||||
this.objectIds = []
|
||||
this.postLoad = debounce( () => { this._postLoadFunction() }, 200 )
|
||||
|
||||
this.loaders = []
|
||||
}
|
||||
|
||||
get objects() {
|
||||
return [ ...this.solidObjects.children, ...this.transparentObjects.children ]
|
||||
}
|
||||
|
||||
// Note: we might switch later down the line from cloning materials to solely
|
||||
// using a few "default" ones and controlling color through vertex colors.
|
||||
// For now a small compromise to speed up dev; it is not the most memory
|
||||
// efficient approach.
|
||||
// To support big models we might need to merge everything in buffer geometries,
|
||||
// and control things separately to squeeze those sweet FPS (esp mobile); but
|
||||
// this conflicts a bit with the interactivity requirements of the viewer, esp.
|
||||
// the TODO ones (colour by property).
|
||||
addObject( wrapper ) {
|
||||
if ( !wrapper || !wrapper.bufferGeometry ) return
|
||||
|
||||
|
||||
switch ( wrapper.geometryType ) {
|
||||
case 'solid':
|
||||
// Do we have a defined material?
|
||||
if ( wrapper.meta.renderMaterial ) {
|
||||
|
||||
let renderMat = wrapper.meta.renderMaterial
|
||||
let color = new THREE.Color( this._argbToRGB( renderMat.diffuse ) )
|
||||
this._normaliseColor( color )
|
||||
// Is it a transparent material?
|
||||
if ( renderMat.opacity !== 1 ) {
|
||||
let material = this.transparentMaterial.clone()
|
||||
material.clippingPlanes = this.viewer.sectionPlaneHelper.planes
|
||||
material.color = color
|
||||
material.opacity = renderMat.opacity !== 0 ? renderMat.opacity : 0.2
|
||||
this.addTransparentSolid( wrapper, material )
|
||||
|
||||
// It's not a transparent material!
|
||||
} else {
|
||||
let material = this.solidMaterial.clone()
|
||||
material.clippingPlanes = this.viewer.sectionPlaneHelper.planes
|
||||
material.color = color
|
||||
material.metalness = renderMat.metalness
|
||||
if ( material.metalness !== 0 ) material.roughness = 0.1
|
||||
if ( material.metalness > 0.8 ) material.color = new THREE.Color( '#CDCDCD' ) // hack for rhino metal materials being black FFS
|
||||
this.addSolid( wrapper, material )
|
||||
}
|
||||
} else {
|
||||
// If we don't have defined material, just use the default
|
||||
let material = this.solidMaterial.clone()
|
||||
material.clippingPlanes = this.viewer.sectionPlaneHelper.planes
|
||||
this.addSolid( wrapper, material )
|
||||
}
|
||||
break
|
||||
case 'line':
|
||||
this.addLine( wrapper )
|
||||
break
|
||||
case 'point':
|
||||
this.addPoint( wrapper )
|
||||
break
|
||||
}
|
||||
|
||||
this.postLoad()
|
||||
}
|
||||
|
||||
addSolid( wrapper, material ) {
|
||||
const mesh = new THREE.Mesh( wrapper.bufferGeometry, material ? material : this.solidMaterial )
|
||||
mesh.userData = wrapper.meta
|
||||
mesh.uuid = wrapper.meta.id
|
||||
this.objectIds.push( mesh.uuid )
|
||||
this.solidObjects.add( mesh )
|
||||
}
|
||||
|
||||
addTransparentSolid( wrapper, material ) {
|
||||
const mesh = new THREE.Mesh( wrapper.bufferGeometry, material ? material : this.transparentMaterial )
|
||||
mesh.userData = wrapper.meta
|
||||
mesh.uuid = wrapper.meta.id
|
||||
this.objectIds.push( mesh.uuid )
|
||||
this.transparentObjects.add( mesh )
|
||||
}
|
||||
|
||||
addLine( wrapper ) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
addPoint( wrapper ){
|
||||
// TODO
|
||||
}
|
||||
|
||||
removeObject( id ) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
removeAllObjects() {
|
||||
for ( let obj of this.objects ) {
|
||||
if ( obj.geometry ){
|
||||
obj.geometry.dispose()
|
||||
}
|
||||
}
|
||||
this.solidObjects.clear()
|
||||
this.transparentObjects.clear()
|
||||
this.viewer.selectionHelper.unselect()
|
||||
this.objectIds = []
|
||||
|
||||
this._postLoadFunction()
|
||||
}
|
||||
|
||||
_postLoadFunction() {
|
||||
this.zoomExtents()
|
||||
this.viewer.reflectionsNeedUpdate = true
|
||||
this.viewer.sectionPlaneHelper._matchSceneSize()
|
||||
}
|
||||
|
||||
zoomToObject( target ) {
|
||||
const box = new THREE.Box3().setFromObject( target )
|
||||
this.zoomToBox( box )
|
||||
}
|
||||
|
||||
zoomExtents() {
|
||||
let bboxTarget = this.userObjects
|
||||
if ( this.objects.length === 0 ) {
|
||||
let box = new THREE.Box3( new THREE.Vector3( -1,-1,-1 ), new THREE.Vector3( 1,1,1 ) )
|
||||
this.zoomToBox( box )
|
||||
return
|
||||
}
|
||||
let box = new THREE.Box3().setFromObject( bboxTarget )
|
||||
this.zoomToBox( box )
|
||||
}
|
||||
|
||||
// see this discussion: https://github.com/mrdoob/three.js/pull/14526#issuecomment-497254491
|
||||
// Notes: seems that zooming in to a box 'rescales' the SSAO pass somehow and makes it
|
||||
// look better. Could we do the same thing somehow when controls stop moving?
|
||||
zoomToBox( box ) {
|
||||
const fitOffset = 1.2
|
||||
|
||||
const size = box.getSize( new THREE.Vector3() )
|
||||
const center = box.getCenter( new THREE.Vector3() )
|
||||
|
||||
const maxSize = Math.max( size.x, size.y, size.z )
|
||||
const fitHeightDistance = maxSize / ( 2 * Math.atan( Math.PI * this.viewer.camera.fov / 360 ) )
|
||||
const fitWidthDistance = fitHeightDistance / this.viewer.camera.aspect
|
||||
const distance = fitOffset * Math.max( fitHeightDistance, fitWidthDistance )
|
||||
|
||||
const direction = this.viewer.controls.target.clone()
|
||||
.sub( this.viewer.camera.position )
|
||||
.normalize()
|
||||
.multiplyScalar( distance )
|
||||
|
||||
// this.viewer.controls.maxDistance = distance * 20
|
||||
this.viewer.controls.target.copy( center )
|
||||
|
||||
this.viewer.camera.near = distance / 100
|
||||
this.viewer.camera.far = distance * 100
|
||||
this.viewer.camera.updateProjectionMatrix()
|
||||
|
||||
this.viewer.camera.position.copy( this.viewer.controls.target ).sub( direction )
|
||||
|
||||
this.viewer.controls.update()
|
||||
}
|
||||
|
||||
_argbToRGB( argb ) {
|
||||
return '#'+ ( '000000' + ( argb & 0xFFFFFF ).toString( 16 ) ).slice( -6 )
|
||||
}
|
||||
|
||||
_normaliseColor( color ) {
|
||||
// Note: full of **magic numbers** that will need changing once global scene
|
||||
// is properly set up; also to test with materials coming from other software too...
|
||||
let hsl = {}
|
||||
color.getHSL( hsl )
|
||||
|
||||
if ( hsl.s + hsl.l > 1 ) {
|
||||
while ( hsl.s + hsl.l > 1 ){
|
||||
hsl.s -= 0.05
|
||||
hsl.l -= 0.05
|
||||
}
|
||||
}
|
||||
|
||||
if ( hsl.l > 0.6 ) {
|
||||
hsl.l = 0.6
|
||||
}
|
||||
|
||||
if ( hsl.l < 0.3 ) {
|
||||
hsl.l = 0.3
|
||||
}
|
||||
|
||||
color.setHSL( hsl.h, hsl.s, hsl.l )
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
import * as THREE from 'three'
|
||||
import { TransformControls } from 'three/examples/jsm/controls/TransformControls.js'
|
||||
|
||||
/**
|
||||
* WIP: A utility class for adding section planes to the scene.
|
||||
* - 'S' shows/hides section planes
|
||||
* - 's' toggles controls from translate to rotate
|
||||
*/
|
||||
export default class SectionPlaneHelper {
|
||||
|
||||
constructor( parent ) {
|
||||
this.viewer = parent
|
||||
this.cutters = []
|
||||
this.visible = false
|
||||
|
||||
window.addEventListener( 'keydown', ( event ) => {
|
||||
if ( event.key === 's' ) {
|
||||
this.toggleTransformControls()
|
||||
}
|
||||
if ( event.key === 'S' ) {
|
||||
this.toggleSectionPlanes()
|
||||
}
|
||||
}, false )
|
||||
}
|
||||
|
||||
get planes() {
|
||||
return this.cutters.map( cutter => cutter.plane )
|
||||
}
|
||||
|
||||
get activePlanes() {
|
||||
return this.cutters.filter( cutter => cutter.visible ).map( cutter => cutter.plane )
|
||||
}
|
||||
|
||||
toggleTransformControls() {
|
||||
this.cutters.forEach( cutter => {
|
||||
if ( cutter.control.mode === 'rotate' ) {
|
||||
cutter.control.setMode( 'translate' )
|
||||
cutter.control.showX = false
|
||||
cutter.control.showY = false
|
||||
cutter.control.showZ = true
|
||||
return
|
||||
}
|
||||
cutter.control.setMode( 'rotate' )
|
||||
cutter.control.showX = true
|
||||
cutter.control.showY = true
|
||||
cutter.control.showZ = false
|
||||
} )
|
||||
}
|
||||
|
||||
createSectionPlane() {
|
||||
let cutter = { }
|
||||
|
||||
cutter.id = this.cutters.length
|
||||
cutter.visible = false
|
||||
cutter.plane = new THREE.Plane( new THREE.Vector3( 0, 0, -1 ), 1 )
|
||||
|
||||
cutter.helper = new THREE.Mesh( new THREE.PlaneGeometry( 1, 1, 1 ), new THREE.MeshBasicMaterial( { color: 0xAFAFAF, transparent: true, opacity: 0.1, side: THREE.DoubleSide } ) )
|
||||
cutter.helper.visible = false
|
||||
this.viewer.scene.add( cutter.helper )
|
||||
|
||||
cutter.control = new TransformControls( this.viewer.camera, this.viewer.renderer.domElement )
|
||||
cutter.control.setSize( 0.5 )
|
||||
cutter.control.space = 'local'
|
||||
cutter.control.showX = false
|
||||
cutter.control.showY = false
|
||||
cutter.control.setRotationSnap( THREE.MathUtils.degToRad( 15 ) )
|
||||
|
||||
cutter.control.addEventListener( 'change', () => this.viewer.render )
|
||||
cutter.control.addEventListener( 'dragging-changed', ( event ) => {
|
||||
if ( !cutter.visible ) return
|
||||
this.viewer.controls.enabled = !event.value
|
||||
|
||||
// Reference: https://stackoverflow.com/a/52124409
|
||||
let normal = new THREE.Vector3()
|
||||
let point = new THREE.Vector3()
|
||||
normal.set( 0, 0, -1 ).applyQuaternion( cutter.helper.quaternion )
|
||||
point.copy( cutter.helper.position )
|
||||
cutter.plane.setFromNormalAndCoplanarPoint( normal, point )
|
||||
} )
|
||||
|
||||
cutter.control.attach( cutter.helper )
|
||||
cutter.control.visible = false
|
||||
this.viewer.scene.add( cutter.control )
|
||||
|
||||
this.cutters.push( cutter )
|
||||
|
||||
// adds local clipping planes to all materials
|
||||
let objs = this.viewer.sceneManager.objects
|
||||
objs.forEach( obj => {
|
||||
obj.material.clippingPlanes = this.cutters.map( c => c.plane )
|
||||
} )
|
||||
}
|
||||
|
||||
toggleSectionPlanes() {
|
||||
if ( this.visible ) this.hideSectionPlanes()
|
||||
else this.showSectionPlanes()
|
||||
|
||||
this.visible = !this.visible
|
||||
}
|
||||
|
||||
showSectionPlanes() {
|
||||
this._matchSceneSize()
|
||||
|
||||
this.cutters.forEach( cutter => {
|
||||
cutter.visible = true
|
||||
cutter.helper.visible = true
|
||||
cutter.control.visible = true
|
||||
} )
|
||||
|
||||
this.viewer.renderer.localClippingEnabled = true
|
||||
}
|
||||
|
||||
hideSectionPlanes() {
|
||||
this.cutters.forEach( cutter => {
|
||||
cutter.visible = false
|
||||
cutter.helper.visible = false
|
||||
cutter.control.visible = false
|
||||
} )
|
||||
this.viewer.renderer.localClippingEnabled = false
|
||||
}
|
||||
|
||||
_matchSceneSize() {
|
||||
// Scales and translate helper to scene bbox center and origin
|
||||
const sceneBox = new THREE.Box3().setFromObject( this.viewer.sceneManager.userObjects )
|
||||
const sceneSize = new THREE.Vector3()
|
||||
sceneBox.getSize( sceneSize )
|
||||
const sceneCenter = new THREE.Vector3()
|
||||
sceneBox.getCenter( sceneCenter )
|
||||
|
||||
this.cutters.forEach( cutter => {
|
||||
cutter.helper.scale.set( sceneSize.x > 0 ? sceneSize.x : 1, sceneSize.y > 0 ? sceneSize.y : 1, sceneSize.z >0 ? sceneSize.z : 1 )
|
||||
cutter.helper.position.set( sceneCenter.x, sceneCenter.y, sceneCenter.z )
|
||||
|
||||
let normal = new THREE.Vector3()
|
||||
let point = new THREE.Vector3()
|
||||
normal.set( 0, 0, -1 ).applyQuaternion( cutter.helper.quaternion )
|
||||
point.copy( cutter.helper.position )
|
||||
cutter.plane.setFromNormalAndCoplanarPoint( normal, point )
|
||||
} )
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
import * as THREE from 'three'
|
||||
import debounce from 'lodash.debounce'
|
||||
import EventEmitter from './EventEmitter'
|
||||
|
||||
/**
|
||||
* Selects and deselects user added objects in the scene. Emits the array of all intersected objects on click.
|
||||
* Behaviours:
|
||||
* - Clicking on one object will select it.
|
||||
* - Double clicking on one object will focus on it.
|
||||
* - Double clicking anywhere else will focus the scene.
|
||||
* - Pressing escape will clear any selection present.
|
||||
* TODOs:
|
||||
* - Ensure clipped geometry is not selected.
|
||||
* - When objects are disposed, ensure selection is reset.
|
||||
*/
|
||||
export default class SelectionHelper extends EventEmitter {
|
||||
|
||||
constructor( parent ) {
|
||||
super()
|
||||
this.viewer = parent
|
||||
this.raycaster = new THREE.Raycaster()
|
||||
|
||||
// Handle clicks during camera moves
|
||||
this.orbiting = false
|
||||
this.viewer.controls.addEventListener( 'change', debounce( () => { this.orbiting = false }, 100 ) )
|
||||
this.viewer.controls.addEventListener( 'start', debounce( () => { this.orbiting = true }, 200 ) )
|
||||
this.viewer.controls.addEventListener( 'end', debounce( () => { this.orbiting = false }, 200 ) )
|
||||
|
||||
// Handle mouseclicks
|
||||
this.viewer.renderer.domElement.addEventListener( 'pointerup', ( e ) => {
|
||||
if ( this.orbiting ) return
|
||||
|
||||
let selectionObjects = this.getClickedObjects( e )
|
||||
this.handleSelection( selectionObjects )
|
||||
} )
|
||||
|
||||
// Doubleclicks on touch devices
|
||||
// http://jsfiddle.net/brettwp/J4djY/
|
||||
this.tapTimeout
|
||||
this.lastTap = 0
|
||||
this.touchLocation
|
||||
this.viewer.renderer.domElement.addEventListener( 'touchstart', ( e ) => { this.touchLocation = e.targetTouches[0] } )
|
||||
this.viewer.renderer.domElement.addEventListener( 'touchend', ( event ) => {
|
||||
var currentTime = new Date().getTime()
|
||||
var tapLength = currentTime - this.lastTap
|
||||
clearTimeout( this.tapTimeout )
|
||||
if ( tapLength < 500 && tapLength > 0 ) {
|
||||
let selectionObjects = this.getClickedObjects( this.touchLocation )
|
||||
this.emit( 'object-doubleclicked', selectionObjects )
|
||||
if ( !this.orbiting )
|
||||
this.handleDoubleClick( selectionObjects )
|
||||
event.preventDefault()
|
||||
} else {
|
||||
this.tapTimeout = setTimeout( function() {
|
||||
clearTimeout( this.tapTimeout )
|
||||
}, 500 )
|
||||
}
|
||||
this.lastTap = currentTime
|
||||
} )
|
||||
|
||||
this.viewer.renderer.domElement.addEventListener( 'dblclick', ( e ) => {
|
||||
// if ( this.orbiting ) return // not needed for zoom to thing?
|
||||
|
||||
let selectionObjects = this.getClickedObjects( e )
|
||||
|
||||
this.emit( 'object-doubleclicked', selectionObjects )
|
||||
this.handleDoubleClick( selectionObjects )
|
||||
} )
|
||||
|
||||
// Handle multiple object selection
|
||||
this.multiSelect = false
|
||||
document.addEventListener( 'keydown', ( e ) => {
|
||||
if ( e.isComposing || e.keyCode === 229 ) return
|
||||
if ( e.key === 'Shift' ) this.multiSelect = true
|
||||
if ( e.key === 'Escape' ) this.unselect( )
|
||||
} )
|
||||
document.addEventListener( 'keyup', ( e ) => {
|
||||
if ( e.isComposing || e.keyCode === 229 ) return
|
||||
if ( e.key === 'Shift' ) this.multiSelect = false
|
||||
} )
|
||||
|
||||
this.selectionMaterial = new THREE.MeshLambertMaterial( { color: 0x0B55D2, emissive: 0x0B55D2, side: THREE.DoubleSide } )
|
||||
this.selectedObjects = new THREE.Group()
|
||||
this.selectedObjects.renderOrder = 1000
|
||||
this.viewer.scene.add( this.selectedObjects )
|
||||
|
||||
this.originalSelectionObjects = []
|
||||
}
|
||||
|
||||
handleSelection( objects ) {
|
||||
this.select( objects[0] )
|
||||
}
|
||||
|
||||
handleDoubleClick( objects ) {
|
||||
if ( !objects || objects.length === 0 ) this.viewer.sceneManager.zoomExtents()
|
||||
else this.viewer.sceneManager.zoomToObject( objects[0].object )
|
||||
}
|
||||
|
||||
select( obj ) {
|
||||
if ( !this.multiSelect ) this.unselect()
|
||||
if ( !obj ) {
|
||||
this.emit( 'object-clicked', this.originalSelectionObjects )
|
||||
return
|
||||
}
|
||||
|
||||
let mesh = new THREE.Mesh( obj.object.geometry, this.selectionMaterial )
|
||||
this.selectedObjects.add( mesh )
|
||||
this.originalSelectionObjects.push( obj )
|
||||
this.emit( 'object-clicked', this.originalSelectionObjects )
|
||||
}
|
||||
|
||||
unselect() {
|
||||
this.selectedObjects.clear()
|
||||
this.originalSelectionObjects = []
|
||||
}
|
||||
|
||||
getClickedObjects( e ) {
|
||||
const normalizedPosition = this._getNormalisedClickPosition( e )
|
||||
this.raycaster.setFromCamera( normalizedPosition, this.viewer.camera )
|
||||
|
||||
let intersectedObjects = this.raycaster.intersectObjects( this.viewer.sceneManager.objects )
|
||||
intersectedObjects = intersectedObjects.filter( obj => this.viewer.sectionPlaneHelper.activePlanes.every( pl => pl.distanceToPoint( obj.point ) > 0 ) )
|
||||
|
||||
return intersectedObjects
|
||||
}
|
||||
|
||||
_getNormalisedClickPosition( e ) {
|
||||
// Reference: https://threejsfundamentals.org/threejs/lessons/threejs-picking.html
|
||||
const canvas = this.viewer.renderer.domElement
|
||||
const rect = this.viewer.renderer.domElement.getBoundingClientRect()
|
||||
|
||||
const pos = {
|
||||
x: ( e.clientX - rect.left ) * canvas.width / rect.width,
|
||||
y: ( e.clientY - rect.top ) * canvas.height / rect.height
|
||||
}
|
||||
return {
|
||||
x: ( pos.x / canvas.width ) * 2 - 1,
|
||||
y: ( pos.y / canvas.height ) * -2 + 1
|
||||
}
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.viewer.scene.remove( this.selectedObjects )
|
||||
this.unselect()
|
||||
this.originalSelectionObjects = null
|
||||
this.selectionMaterial = null
|
||||
this.selectedObjects = null
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
export const Units = {
|
||||
Millimeters: 'mm',
|
||||
Centimeters: 'cm',
|
||||
Meters: 'm',
|
||||
Kilometers: 'km',
|
||||
Inches: 'in',
|
||||
Feet: 'ft',
|
||||
Yards: 'yd',
|
||||
Miles: 'mi'
|
||||
}
|
||||
|
||||
export function getConversionFactor( from, to = Units.Meters ){
|
||||
from = normaliseName( from )
|
||||
to = normaliseName( to )
|
||||
switch ( from )
|
||||
{
|
||||
// METRIC
|
||||
case Units.Millimeters:
|
||||
switch ( to )
|
||||
{
|
||||
case Units.Centimeters: return 0.1
|
||||
case Units.Meters: return 0.001
|
||||
case Units.Kilometers: return 1e-6
|
||||
case Units.Inches: return 0.0393701
|
||||
case Units.Feet: return 0.00328084
|
||||
case Units.Yards: return 0.00109361
|
||||
case Units.Miles: return 6.21371e-7
|
||||
}
|
||||
break
|
||||
case Units.Centimeters:
|
||||
switch ( to )
|
||||
{
|
||||
case Units.Millimeters: return 10
|
||||
case Units.Meters: return 0.01
|
||||
case Units.Kilometers: return 1e-5
|
||||
case Units.Inches: return 0.393701
|
||||
case Units.Feet: return 0.0328084
|
||||
case Units.Yards: return 0.0109361
|
||||
case Units.Miles: return 6.21371e-6
|
||||
}
|
||||
break
|
||||
case Units.Meters:
|
||||
switch ( to )
|
||||
{
|
||||
case Units.Millimeters: return 1000
|
||||
case Units.Centimeters: return 100
|
||||
case Units.Kilometers: return 1000
|
||||
case Units.Inches: return 39.3701
|
||||
case Units.Feet: return 3.28084
|
||||
case Units.Yards: return 1.09361
|
||||
case Units.Miles: return 0.000621371
|
||||
}
|
||||
break
|
||||
case Units.Kilometers:
|
||||
switch ( to )
|
||||
{
|
||||
case Units.Millimeters: return 1000000
|
||||
case Units.Centimeters: return 100000
|
||||
case Units.Meters: return 1000
|
||||
case Units.Inches: return 39370.1
|
||||
case Units.Feet: return 3280.84
|
||||
case Units.Yards: return 1093.61
|
||||
case Units.Miles: return 0.621371
|
||||
}
|
||||
break
|
||||
|
||||
// IMPERIAL
|
||||
case Units.Inches:
|
||||
switch ( to )
|
||||
{
|
||||
case Units.Millimeters: return 25.4
|
||||
case Units.Centimeters: return 2.54
|
||||
case Units.Meters: return 0.0254
|
||||
case Units.Kilometers: return 2.54e-5
|
||||
case Units.Feet: return 0.0833333
|
||||
case Units.Yards: return 0.027777694
|
||||
case Units.Miles: return 1.57828e-5
|
||||
}
|
||||
break
|
||||
case Units.Feet:
|
||||
switch ( to )
|
||||
{
|
||||
case Units.Millimeters: return 304.8
|
||||
case Units.Centimeters: return 30.48
|
||||
case Units.Meters: return 0.3048
|
||||
case Units.Kilometers: return 0.0003048
|
||||
case Units.Inches: return 12
|
||||
case Units.Yards: return 0.333332328
|
||||
case Units.Miles: return 0.000189394
|
||||
}
|
||||
break
|
||||
case Units.Miles:
|
||||
switch ( to )
|
||||
{
|
||||
case Units.Millimeters: return 1.609e+6
|
||||
case Units.Centimeters: return 160934
|
||||
case Units.Meters: return 1609.34
|
||||
case Units.Kilometers: return 1.60934
|
||||
case Units.Inches: return 63360
|
||||
case Units.Feet: return 5280
|
||||
case Units.Yards: return 1759.99469184
|
||||
}
|
||||
break
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
export function normaliseName( unit ) {
|
||||
if ( !unit ) return Units.Meters
|
||||
switch ( unit.toLowerCase() )
|
||||
{
|
||||
case 'mm':
|
||||
case 'mil':
|
||||
case 'millimeters':
|
||||
case 'millimetres':
|
||||
return Units.Millimeters
|
||||
case 'cm':
|
||||
case 'centimetre':
|
||||
case 'centimeter':
|
||||
case 'centimetres':
|
||||
case 'centimeters':
|
||||
return Units.Centimeters
|
||||
case 'm':
|
||||
case 'meter':
|
||||
case 'metre':
|
||||
case 'meters':
|
||||
case 'metres':
|
||||
return Units.Meters
|
||||
case 'inches':
|
||||
case 'inch':
|
||||
case 'in':
|
||||
return Units.Inches
|
||||
case 'feet':
|
||||
case 'foot':
|
||||
case 'ft':
|
||||
return Units.Feet
|
||||
case 'yard':
|
||||
case 'yards':
|
||||
case 'yd':
|
||||
return Units.Yards
|
||||
case 'miles':
|
||||
case 'mile':
|
||||
case 'mi':
|
||||
return Units.Miles
|
||||
default:
|
||||
return Units.Meters
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
import * as THREE from 'three'
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
|
||||
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js'
|
||||
import { SSAOPass } from 'three/examples/jsm/postprocessing/SSAOPass.js'
|
||||
import Stats from 'three/examples/jsm/libs/stats.module.js'
|
||||
|
||||
import ObjectManager from './SceneObjectManager'
|
||||
import SelectionHelper from './SelectionHelper'
|
||||
import SectionPlaneHelper from './SectionPlaneHelper'
|
||||
import ViewerObjectLoader from './ViewerObjectLoader'
|
||||
import EventEmitter from './EventEmitter'
|
||||
|
||||
export default class Viewer extends EventEmitter {
|
||||
|
||||
constructor( { container, postprocessing = true, reflections = true } ) {
|
||||
super()
|
||||
this.container = container || document.getElementById( 'renderer' )
|
||||
this.postprocessing = postprocessing
|
||||
this.scene = new THREE.Scene()
|
||||
|
||||
this.camera = new THREE.PerspectiveCamera( 60, window.innerWidth / window.innerHeight )
|
||||
this.camera.up.set( 0, 0, 1 )
|
||||
this.camera.position.set( 1, 1, 1 )
|
||||
|
||||
this.renderer = new THREE.WebGLRenderer( { antialias: true, alpha: true } )
|
||||
this.renderer.setClearColor( 0xcccccc, 0 )
|
||||
this.renderer.setPixelRatio( window.devicePixelRatio )
|
||||
this.renderer.setSize( this.container.offsetWidth, this.container.offsetHeight )
|
||||
this.container.appendChild( this.renderer.domElement )
|
||||
|
||||
// commented out because the ssao flash is annoying
|
||||
// this.renderer.gammaFactor = 2.2
|
||||
// this.renderer.outputEncoding = THREE.sRGBEncoding
|
||||
|
||||
this.reflections = reflections
|
||||
this.reflectionsNeedUpdate = true
|
||||
const cubeRenderTarget = new THREE.WebGLCubeRenderTarget( 512, { format: THREE.RGBFormat, generateMipmaps: true, minFilter: THREE.LinearMipmapLinearFilter } )
|
||||
this.cubeCamera = new THREE.CubeCamera( 0.1, 10_000, cubeRenderTarget )
|
||||
this.scene.add( this.cubeCamera )
|
||||
|
||||
this.controls = new OrbitControls( this.camera, this.renderer.domElement )
|
||||
this.controls.enableDamping = true
|
||||
this.controls.dampingFactor = 0.1
|
||||
this.controls.screenSpacePanning = true
|
||||
this.controls.maxPolarAngle = Math.PI / 2
|
||||
this.controls.panSpeed = 0.8
|
||||
this.controls.rotateSpeed = 0.8
|
||||
|
||||
this.composer = new EffectComposer( this.renderer )
|
||||
|
||||
this.ssaoPass = new SSAOPass( this.scene, this.camera, this.container.offsetWidth, this.container.offsetHeight )
|
||||
this.ssaoPass.kernelRadius = 0.03
|
||||
this.ssaoPass.kernelSize = 16
|
||||
this.ssaoPass.minDistance = 0.0002
|
||||
this.ssaoPass.maxDistance = 10
|
||||
this.ssaoPass.output = SSAOPass.OUTPUT.Default
|
||||
this.composer.addPass( this.ssaoPass )
|
||||
|
||||
this.pauseSSAO = false
|
||||
this.controls.addEventListener( 'start', () => { this.pauseSSAO = true } )
|
||||
this.controls.addEventListener( 'end', () => { this.pauseSSAO = false } )
|
||||
|
||||
this.stats = new Stats()
|
||||
this.container.appendChild( this.stats.dom )
|
||||
|
||||
window.addEventListener( 'resize', this.onWindowResize.bind( this ), false )
|
||||
|
||||
this.sectionPlaneHelper = new SectionPlaneHelper( this )
|
||||
this.sceneManager = new ObjectManager( this )
|
||||
this.selectionHelper = new SelectionHelper( this )
|
||||
|
||||
this.sectionPlaneHelper.createSectionPlane()
|
||||
|
||||
this.sceneLights()
|
||||
this.animate()
|
||||
|
||||
this.loaders = []
|
||||
}
|
||||
|
||||
sceneLights() {
|
||||
let ambientLight = new THREE.AmbientLight( 0xffffff )
|
||||
this.scene.add( ambientLight )
|
||||
|
||||
const lights = []
|
||||
lights[ 0 ] = new THREE.PointLight( 0xffffff, 0.21, 0 )
|
||||
lights[ 1 ] = new THREE.PointLight( 0xffffff, 0.21, 0 )
|
||||
lights[ 2 ] = new THREE.PointLight( 0xffffff, 0.21, 0 )
|
||||
lights[ 3 ] = new THREE.PointLight( 0xffffff, 0.21, 0 )
|
||||
|
||||
let factor = 1000
|
||||
lights[ 0 ].position.set( 1 * factor, 1 * factor, 1 * factor )
|
||||
lights[ 1 ].position.set( 1 * factor, -1 * factor, 1 * factor )
|
||||
lights[ 2 ].position.set( -1 * factor, -1 * factor, 1 * factor )
|
||||
lights[ 3 ].position.set( -1 * factor, 1 * factor, 1 * factor )
|
||||
|
||||
this.scene.add( lights[ 0 ] )
|
||||
this.scene.add( lights[ 1 ] )
|
||||
this.scene.add( lights[ 2 ] )
|
||||
this.scene.add( lights[ 3 ] )
|
||||
|
||||
// let sphereSize = 0.2
|
||||
// this.scene.add( new THREE.PointLightHelper( lights[ 0 ], sphereSize ) )
|
||||
// this.scene.add( new THREE.PointLightHelper( lights[ 1 ], sphereSize ) )
|
||||
// this.scene.add( new THREE.PointLightHelper( lights[ 2 ], sphereSize ) )
|
||||
// this.scene.add( new THREE.PointLightHelper( lights[ 3 ], sphereSize ) )
|
||||
|
||||
|
||||
const hemiLight = new THREE.HemisphereLight( 0xffffff, 0x0, 0.2 )
|
||||
hemiLight.color.setHSL( 1, 1, 1 )
|
||||
hemiLight.groundColor.setHSL( 0.095, 1, 0.75 )
|
||||
hemiLight.up.set( 0, 0, 1 )
|
||||
this.scene.add( hemiLight )
|
||||
|
||||
let axesHelper = new THREE.AxesHelper( 1 )
|
||||
this.scene.add( axesHelper )
|
||||
|
||||
let group = new THREE.Group()
|
||||
this.scene.add( group )
|
||||
}
|
||||
|
||||
onWindowResize() {
|
||||
this.camera.aspect = this.container.offsetWidth / this.container.offsetHeight
|
||||
this.camera.updateProjectionMatrix()
|
||||
this.renderer.setSize( this.container.offsetWidth, this.container.offsetHeight )
|
||||
this.composer.setSize( this.container.offsetWidth, this.container.offsetHeight )
|
||||
}
|
||||
|
||||
animate() {
|
||||
requestAnimationFrame( this.animate.bind( this ) )
|
||||
this.controls.update()
|
||||
this.stats.begin()
|
||||
this.render()
|
||||
this.stats.end()
|
||||
}
|
||||
|
||||
render() {
|
||||
if ( this.reflections && this.reflectionsNeedUpdate ) {
|
||||
// Note: scene based "dynamic" reflections need to be handled a bit more carefully, or else:
|
||||
// GL ERROR :GL_INVALID_OPERATION : glDrawElements: Source and destination textures of the draw are the same.
|
||||
// First remove the env map from all materials
|
||||
for ( let obj of this.sceneManager.objects ) {
|
||||
obj.material.envMap = null
|
||||
}
|
||||
|
||||
// Second, set a scene background color (renderer is transparent by default)
|
||||
// and then finally update the cubemap camera.
|
||||
this.scene.background = new THREE.Color( '#F0F3F8' )
|
||||
this.cubeCamera.update( this.renderer, this.scene )
|
||||
this.scene.background = null
|
||||
|
||||
// Finally, re-set the env maps of all materials
|
||||
for ( let obj of this.sceneManager.objects ) {
|
||||
obj.material.envMap = this.cubeCamera.renderTarget.texture
|
||||
}
|
||||
this.reflectionsNeedUpdate = false
|
||||
}
|
||||
|
||||
|
||||
// Render as usual
|
||||
// TODO: post processing SSAO sucks so much currently it's off by default
|
||||
if ( this.postprocessing && !this.pauseSSAO && !this.renderer.localClippingEnabled ){
|
||||
this.composer.render( this.scene, this.camera )
|
||||
}
|
||||
else {
|
||||
this.renderer.render( this.scene, this.camera )
|
||||
}
|
||||
}
|
||||
|
||||
async loadObject( url, token ) {
|
||||
let loader = new ViewerObjectLoader( this, url, token )
|
||||
this.loaders.push( loader )
|
||||
await loader.load()
|
||||
}
|
||||
|
||||
dispose() {
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import ObjectLoader from './ObjectLoader'
|
||||
import Converter from './Converter'
|
||||
|
||||
/**
|
||||
* Helper wrapper around the ObjectLoader class, with some built in assumptions.
|
||||
*/
|
||||
|
||||
export default class ViewerObjectLoader {
|
||||
|
||||
|
||||
constructor( parent, objectUrl, authToken ) {
|
||||
this.viewer = parent
|
||||
this.token = authToken || localStorage.getItem( 'AuthToken' )
|
||||
|
||||
if ( !this.token ) {
|
||||
throw new Error( 'No suitable authorization token found.' )
|
||||
}
|
||||
|
||||
// example url: `https://staging.speckle.dev/streams/a75ab4f10f/objects/f33645dc9a702de8af0af16bd5f655b0`
|
||||
let url = new URL( objectUrl )
|
||||
|
||||
let segments = url.pathname.split( '/' )
|
||||
if ( segments.length < 5 || url.pathname.indexOf( 'streams' ) === -1 || url.pathname.indexOf( 'objects' ) === -1 ) {
|
||||
throw new Error( 'Unexpected object url format.' )
|
||||
}
|
||||
|
||||
this.serverUrl = url.origin
|
||||
this.streamId = segments[2]
|
||||
this.objectId = segments[4]
|
||||
|
||||
this.loader = new ObjectLoader( {
|
||||
serverUrl: this.serverUrl,
|
||||
token: this.token,
|
||||
streamId: this.streamId,
|
||||
objectId: this.objectId,
|
||||
} )
|
||||
|
||||
this.converter = new Converter( this.loader )
|
||||
}
|
||||
|
||||
async load( ) {
|
||||
let first = true
|
||||
let current = 0
|
||||
let total = 0
|
||||
for await ( let obj of this.loader.getObjectIterator() ) {
|
||||
if ( first ) {
|
||||
( async() => {
|
||||
await this.converter.traverseAndConvert( obj, ( o ) => this.viewer.sceneManager.addObject( o ) )
|
||||
} )()
|
||||
first = false
|
||||
total = obj.totalChildrenCount
|
||||
}
|
||||
current++
|
||||
this.viewer.emit( 'load-progress', { progress: current/total, id: this.objectId } )
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/* global __dirname, require, module*/
|
||||
const HtmlWebpackPlugin = require( 'html-webpack-plugin' )
|
||||
const { CleanWebpackPlugin } = require( 'clean-webpack-plugin' )
|
||||
const path = require( 'path' )
|
||||
const yargs = require( 'yargs' )
|
||||
const env = yargs.argv.env
|
||||
|
||||
let filename = 'demo'
|
||||
|
||||
let outputFile, mode
|
||||
|
||||
if ( env === 'build' ) {
|
||||
mode = 'production'
|
||||
outputFile = filename + '.min.js'
|
||||
} else {
|
||||
mode = 'development'
|
||||
outputFile = filename + '.js'
|
||||
}
|
||||
|
||||
const config = {
|
||||
mode: mode,
|
||||
entry: path.resolve( __dirname + '/src/app.js' ),
|
||||
target: 'web',
|
||||
devtool: 'source-map',
|
||||
output: {
|
||||
path: path.resolve( __dirname + '/example' ) ,
|
||||
filename: outputFile,
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /(\.jsx|\.js|\.ts|\.tsx)$/,
|
||||
use: {
|
||||
loader: 'babel-loader',
|
||||
},
|
||||
exclude: /(node_modules|bower_components)/,
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
new CleanWebpackPlugin( { cleanStaleWebpackAssets: false } ),
|
||||
new HtmlWebpackPlugin( { title: 'Speckle Viewer Example', template: 'src/assets/example.html', filename: 'example.html' } )
|
||||
],
|
||||
resolve: {
|
||||
modules: [ path.resolve( './node_modules' ), path.resolve( './src' ) ],
|
||||
extensions: [ '.json', '.js' ],
|
||||
},
|
||||
devServer: {
|
||||
contentBase: path.join( __dirname, 'example' ),
|
||||
compress: false,
|
||||
port: 9000,
|
||||
serveIndex: true,
|
||||
writeToDisk: true
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = config
|
||||
@@ -0,0 +1,51 @@
|
||||
/* global __dirname */
|
||||
const { CleanWebpackPlugin } = require( 'clean-webpack-plugin' )
|
||||
const path = require( 'path' )
|
||||
const yargs = require( 'yargs' )
|
||||
const env = yargs.argv.env
|
||||
|
||||
let libraryName = 'Speckle'
|
||||
|
||||
let outputFile, mode
|
||||
|
||||
if ( env === 'build' ) {
|
||||
mode = 'production'
|
||||
outputFile = libraryName + '.min.js'
|
||||
} else {
|
||||
mode = 'development'
|
||||
outputFile = libraryName + '.js'
|
||||
}
|
||||
|
||||
const config = {
|
||||
mode: mode,
|
||||
entry: path.resolve( __dirname + '/src/index.js' ),
|
||||
target: 'web',
|
||||
devtool: 'source-map',
|
||||
output: {
|
||||
path: path.resolve( __dirname + '/dist' ) ,
|
||||
filename: outputFile,
|
||||
library: libraryName,
|
||||
libraryTarget: 'umd',
|
||||
globalObject: 'this',
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /(\.jsx|\.js|\.ts|\.tsx)$/,
|
||||
use: {
|
||||
loader: 'babel-loader',
|
||||
},
|
||||
exclude: /(node_modules|bower_components)/,
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
new CleanWebpackPlugin( { cleanStaleWebpackAssets: false } ),
|
||||
],
|
||||
resolve: {
|
||||
modules: [ path.resolve( './node_modules' ), path.resolve( './src' ) ],
|
||||
extensions: [ '.json', '.js' ],
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = config
|
||||
@@ -26,9 +26,7 @@ The frontend is a static Vue app.
|
||||
|
||||
## Developing and Debugging
|
||||
|
||||
To get started, first clone this repo & run `npm install`. Next, you'll need to run `lerna boostrap` to initialize the dependencies of all packages (server & frontend).
|
||||
|
||||
After these steps are complete, run `lerna run dev --stream`. Alternatively, you can `npm run dev` independently in each separate package (this will make for less spammy output).
|
||||
To get started, first clone this repo. Check out the detailed instructions for each module in their respective folder (see links above).
|
||||
|
||||
## Contributing
|
||||
|
||||
|
||||
Reference in New Issue
Block a user