Merge remote-tracking branch 'upstream/master' into containers_kickstart

This commit is contained in:
Gergő Jedlicska
2021-01-22 15:34:36 +01:00
29 changed files with 139058 additions and 38 deletions
+2
View File
@@ -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
View File
@@ -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"]
+1 -1
View File
@@ -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 -8
View File
@@ -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
+22
View File
@@ -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"
]
}
+51
View File
@@ -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
}
]
}
}
+56276
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+68
View File
@@ -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>
+11778
View File
File diff suppressed because it is too large Load Diff
+40
View File
@@ -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"
}
}
+28
View File
@@ -0,0 +1,28 @@
# The Speckle Viewer
[![Twitter Follow](https://img.shields.io/twitter/follow/SpeckleSystems?style=social)](https://twitter.com/SpeckleSystems) [![Discourse users](https://img.shields.io/discourse/users?server=https%3A%2F%2Fdiscourse.speckle.works&style=flat-square)](https://discourse.speckle.works)
[![Slack Invite](https://img.shields.io/badge/-slack-grey?style=flat-square&logo=slack)](https://speckle-works.slack.com/join/shared_invite/enQtNjY5Mzk2NTYxNTA4LTU4MWI5ZjdhMjFmMTIxZDIzOTAzMzRmMTZhY2QxMmM1ZjVmNzJmZGMzMDVlZmJjYWQxYWU0MWJkYmY3N2JjNGI) [![website](https://img.shields.io/badge/www-speckle.systems-royalblue?style=flat-square)](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).
+41
View File
@@ -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
}
}
}
+68
View File
@@ -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>
+5
View File
@@ -0,0 +1,5 @@
import Viewer from './modules/Viewer'
import ObjectLoader from './modules/ObjectLoader'
import Converter from './modules/Converter'
export { Viewer, ObjectLoader, Converter }
+216
View File
@@ -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 )
}
}
+108
View File
@@ -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
}
}
+148
View File
@@ -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
}
}
+178
View File
@@ -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 } )
}
}
}
+57
View File
@@ -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
+51
View File
@@ -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
+1 -3
View File
@@ -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