├── .gitignore ├── .npmrc ├── README.md ├── example ├── demo.css └── demo.js ├── package.json └── src ├── bound-utils ├── boundingBox.js ├── boundingSphere.js ├── computeBounds.js ├── computeBounds.test.js └── index.js ├── cameraAndControls ├── camera.js ├── elementSizing.js ├── orbitControls.js ├── orthographicCamera.js └── perspectiveCamera.js ├── cameraControlsActions.js ├── cameraControlsReducers.js ├── csg-utils ├── areCAGsIdentical.js └── areCSGsIdentical.js ├── dataParamsActions.js ├── dataParamsReducers.js ├── entitiesFromSolids.js ├── geometry-utils ├── cagToGeometries.js └── csgToGeometries.js ├── index.js ├── observable-utils ├── limitFlow.js ├── most-subject │ ├── Subject.js │ ├── index.js │ ├── source │ │ ├── HoldSubjectSource.js │ │ └── SubjectSource.js │ └── utils.js └── rafStream.js ├── rendering ├── basic.vert ├── drawAxis.js ├── drawGrid │ ├── index.js │ ├── multi.js │ └── shaders │ │ └── grid.frag ├── drawMesh.js ├── drawMesh │ └── index.js ├── drawMeshNoNormals.js ├── drawNormals.js ├── drawNormals2.js ├── render.js └── renderWrapper.js ├── state.js └── utils.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .DS_Store 3 | .nyc_output 4 | coverage 5 | *.log 6 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # csg-viewer 2 | 3 | [![GitHub version](https://badge.fury.io/gh/jscad%2Fcsg-viewer.svg)](https://badge.fury.io/gh/jscad%2Fcsg-viewer) 4 | [![Build Status](https://travis-ci.org/jscad/csg-viewer.svg)](https://travis-ci.org/jscad/csg-viewer) 5 | 6 | > 3D viewer for Csg.js / Openjscad csg/cag data : small, fast, simple 7 | 8 | This is a very early version of this viewer ! Expect changes ! 9 | 10 | ## Overview 11 | 12 | 13 | ## Table of Contents 14 | 15 | - [Installation](#installation) 16 | - [Usage](#usage) 17 | - [Test](#test) 18 | - [API](#api) 19 | 20 | ## Installation 21 | 22 | ``` 23 | npm install jscad/csg-viewer 24 | ``` 25 | 26 | ## Usage 27 | 28 | ```javascript 29 | // With ES6/2015 + 30 | import makeViewer from 'csg-viewer' 31 | // with commonjs 32 | // import viewer creator function 33 | const makeViewer = require('csg-viewer') 34 | 35 | const viewerOptions = { 36 | background: [0.211, 0.2, 0.207, 1], // [1, 1, 1, 1],//54, 51, 53 37 | meshColor: [0.4, 0.6, 0.5, 1], 38 | grid: { 39 | display: true, 40 | color: [1, 1, 1, 0.1] 41 | }, 42 | camera: { 43 | position: [450, 550, 700] 44 | }, 45 | controls: { 46 | limits: { 47 | maxDistance: 1600, 48 | minDistance: 0.01 49 | } 50 | } 51 | } 52 | // create viewer 53 | const {csgViewer, viewerDefaults, viewerState$} = makeViewer(document.body, viewerOptions) 54 | // and just run it, providing csg/cag data 55 | let csg = CSG.cube() 56 | csgViewer(viewerOptions, {solids: csg}) 57 | 58 | //you can also just call the viewer function again with either/or new data or new settings 59 | csgViewer({camera: { position: [0, 100, 100] }}) 60 | 61 | csg = CSG.sphere() 62 | csgViewer({}, {solids: csg}) 63 | 64 | // and again, with different settings: it only overrides the given settings 65 | csgViewer({controls: {autoRotate: {enabled: true}}}) 66 | 67 | ``` 68 | 69 | ## Test 70 | 71 | There are no unit tests for the 3d viewer, however there is a small demo that is very practical to iterate fast and to see something visual without a complicated setup: 72 | 73 | type: 74 | 75 | ```npm run dev``` this will start the demo at `localhost:9966` 76 | 77 | ## API 78 | 79 | Work in progress! 80 | 81 | 82 | ## Sponsors 83 | 84 | * An earlier version of this viewer has been developped for and very kindly sponsored by [Copenhagen Fabrication / Stykka](https://www.stykka.com/) 85 | 86 | ## License 87 | 88 | [The MIT License (MIT)](./LICENSE) 89 | (unless specified otherwise) -------------------------------------------------------------------------------- /example/demo.css: -------------------------------------------------------------------------------- 1 | html{ 2 | height: 100%; 3 | } 4 | body { 5 | min-height: 100%; 6 | min-width: 100%; 7 | } 8 | 9 | document.body { 10 | top: 0px; 11 | bottom: 0px; 12 | left: 0px; 13 | right: 0px; 14 | position: absolute; 15 | } 16 | -------------------------------------------------------------------------------- /example/demo.js: -------------------------------------------------------------------------------- 1 | const makeCsgViewer = require('../src/index') 2 | const {cube} = require('@jscad/scad-api').primitives3d 3 | 4 | const initializeData = function () { 5 | return cube({size: 100 * Math.random()}) 6 | } 7 | // dark bg : [0.211, 0.2, 0.207, 1] 8 | // dark grid : [1, 1, 1, 0.1], 9 | 10 | const viewerOptions = { 11 | rendering: { 12 | background: [0.211, 0.2, 0.207, 1], // [1, 1, 1, 1],//54, 51, 53 13 | meshColor: [0.4, 0.6, 0.5, 1] 14 | }, 15 | grid: { 16 | show: true, 17 | color: [1, 1, 1, 1] 18 | }, 19 | camera: { 20 | position: [450, 550, 700] 21 | }, 22 | controls: { 23 | zoomToFit: { 24 | targets: 'all' 25 | }, 26 | limits: { 27 | maxDistance: 1600, 28 | minDistance: 0.01 29 | } 30 | } 31 | } 32 | 33 | const csg = initializeData() 34 | const {csgViewer, viewerDefaults, viewerState$} = makeCsgViewer(document.body, viewerOptions) 35 | 36 | // update / initialize the viewer with some data 37 | csgViewer(viewerOptions, {solids: csg}) 38 | 39 | // you also have access to the defaults 40 | console.log('viewerDefaults', viewerDefaults) 41 | 42 | // you can subscribe to the state of the viewer to react to it if you want to, 43 | // as the state is a most.js observable 44 | viewerState$ 45 | .throttle(5000) 46 | // .skipRepeats() 47 | .forEach(viewerState => console.log('viewerState', viewerState)) 48 | 49 | // you can change the state of the viewer at any time by just calling the viewer 50 | // function again with different params 51 | // NOTE: the params need to respect the SAME structure as the defaults 52 | setTimeout(function (t) { 53 | csgViewer({camera: { position: [0, 100, 100] }}) 54 | }, 5000) 55 | 56 | // or different params AND different data 57 | setTimeout(function (t) { 58 | const csg = initializeData() 59 | csgViewer({overrideOriginalColors: true, rendering: {meshColor: [0.8, 0, 0, 1], background: [0.2, 1, 1, 1]}}, {solids: csg}) 60 | }, 10000) 61 | 62 | // and again 63 | setTimeout(function (t) { 64 | csgViewer({controls: {autoRotate: {enabled: true}}}) 65 | }, 15000) 66 | 67 | /* setTimeout(function (t) { 68 | csgViewer({camera: {position: 'top'}}) 69 | }, 2000) 70 | 71 | setTimeout(function (t) { 72 | csgViewer({camera: {position: 'bottom'}}) 73 | }, 2500) 74 | 75 | setTimeout(function (t) { 76 | csgViewer({camera: {position: 'front'}}) 77 | }, 3000) 78 | 79 | setTimeout(function (t) { 80 | csgViewer({camera: {position: 'back'}}) 81 | }, 3500) 82 | 83 | setTimeout(function (t) { 84 | csgViewer({camera: {position: 'left'}}) 85 | }, 4000) 86 | 87 | setTimeout(function (t) { 88 | csgViewer({camera: {position: 'right'}}) 89 | }, 4500) 90 | 91 | setTimeout(function (t) { 92 | csgViewer({camera: {position: 'goomba'}}) 93 | }, 5500) */ 94 | 95 | setTimeout(function (t) { 96 | csgViewer({grid: {size: [90, 90], display: true}}) 97 | }, 2500) 98 | 99 | setTimeout(function (t) { 100 | csgViewer({grid: {size: [200, 20], display: true, fadeout: true}}) 101 | }, 5500) 102 | 103 | setTimeout(function (t) { 104 | csgViewer({grid: {size: [800, 800], display: true, fadeout: true}}) 105 | }, 6500) 106 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@jscad/csg-viewer", 3 | "version": "0.0.0", 4 | "description": "", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "test": "tape test.js", 8 | "dev": "budo example/demo.js --title csg-viewer --live --css example/demo.css -- -t glslify -t brfs", 9 | "build": "browserify -p common-shakeify --standalone makeCsgViewer src/index.js > dist/bundle.js", 10 | "minify": "uglifyjs dist/bundle.js --compress > dist/bundle.min.js", 11 | "compress": "gzipme dist/bundle.min.js", 12 | "build-min": "npm run build && npm run minify && npm run compress" 13 | }, 14 | "author": "Mark 'kaosat-dev' Moissette", 15 | "license": "MIT", 16 | "devDependencies": { 17 | "@jscad/scad-api": "^0.4.2", 18 | "browserify": "^14.5.0", 19 | "budo": "^10.0.4", 20 | "common-shakeify": "^0.4.5", 21 | "gzipme": "^0.1.1", 22 | "uglify-es": "^3.2.1" 23 | }, 24 | "dependencies": { 25 | "@most/create": "^2.0.1", 26 | "angle-normals": "^1.0.0", 27 | "camera-unproject": "^1.0.1", 28 | "gl-mat4": "^1.1.4", 29 | "gl-vec3": "^1.0.3", 30 | "glslify": "^6.1.0", 31 | "most": "^1.7.2", 32 | "most-gestures": "^0.4.1", 33 | "most-proxy": "^3.3.0", 34 | "regl": "^1.3.0", 35 | "vertex-ao": "^1.0.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/bound-utils/boundingBox.js: -------------------------------------------------------------------------------- 1 | // modified version of https://github.com/thibauts/vertices-bounding-box that also works with non nested positions 2 | function boundingBox (positions) { 3 | if (positions.length === 0) { 4 | return null 5 | } 6 | 7 | const nested = (Array.isArray(positions) && Array.isArray(positions[0])) 8 | 9 | const dimensions = nested ? positions[0].length : 3 10 | let min = new Array(dimensions) 11 | let max = new Array(dimensions) 12 | 13 | for (var i = 0; i < dimensions; i += 1) { 14 | min[i] = Infinity 15 | max[i] = -Infinity 16 | } 17 | 18 | if (nested) { 19 | positions.forEach(function (position) { 20 | for (var i = 0; i < dimensions; i += 1) { 21 | const _position = nested ? position[i] : position 22 | max[i] = _position > max[i] ? _position : max[i] // position[i] > max[i] ? position[i] : max[i] 23 | min[i] = _position < min[i] ? _position : min[i] // min[i] = position[i] < min[i] ? position[i] : min[i] 24 | } 25 | }) 26 | } else { 27 | for (let j = 0; j < positions.length; j += dimensions) { 28 | for (let i = 0; i < dimensions; i += 1) { 29 | const _position = positions[i + j] // nested ? positions[i] : position 30 | max[i] = _position > max[i] ? _position : max[i] // position[i] > max[i] ? position[i] : max[i] 31 | min[i] = _position < min[i] ? _position : min[i] // min[i] = position[i] < min[i] ? position[i] : min[i] 32 | } 33 | } 34 | } 35 | 36 | return [min, max] 37 | } 38 | module.exports = boundingBox 39 | -------------------------------------------------------------------------------- /src/bound-utils/boundingSphere.js: -------------------------------------------------------------------------------- 1 | const { squaredDistance, vec3 } = require('gl-vec3') 2 | const boundingBox = require('./boundingBox') 3 | /** 4 | * compute boundingSphere, given positions 5 | * @param {array} center the center to use (optional). 6 | * @param {array} positions the array/typed array of positions. 7 | * for now loosely based on three.js implementation 8 | */ 9 | function boundingSphere (center = [0, 0, 0], positions) { 10 | if (positions.length === 0) { 11 | return null 12 | } 13 | 14 | if (!center) { 15 | let box = boundingBox(positions) 16 | // min & max are the box's min & max 17 | let result = vec3.create() 18 | center = vec3.scale(result, vec3.add(result, box.min, box.max), 0.5) 19 | } 20 | const nested = (Array.isArray(positions) && Array.isArray(positions[0])) 21 | 22 | let maxRadiusSq = 0 23 | const increment = nested ? 1 : 3 24 | const max = positions.length 25 | for (let i = 0; i < max; i += increment) { 26 | if (nested) { 27 | maxRadiusSq = Math.max(maxRadiusSq, squaredDistance(center, positions[ i ])) 28 | } else { 29 | const position = [positions[i], positions[i + 1], positions[i + 2]] 30 | maxRadiusSq = Math.max(maxRadiusSq, squaredDistance(center, position)) 31 | } 32 | } 33 | return Math.sqrt(maxRadiusSq) 34 | } 35 | 36 | /* compute boundingSphere from boundingBox 37 | for now more or less based on three.js implementation 38 | */ 39 | function boundingSphereFromBoundingBox (center = [0, 0, 0], positions, boundingBox) { 40 | if (positions.length === 0) { 41 | return null 42 | } 43 | 44 | if (!center) { 45 | // min & max are the box's min & max 46 | let result = vec3.create() 47 | center = vec3.scale(result, vec3.add(result, boundingBox[0], boundingBox[1]), 0.5) 48 | } 49 | 50 | let maxRadiusSq = 0 51 | for (let i = 0, il = positions.length; i < il; i++) { 52 | maxRadiusSq = Math.max(maxRadiusSq, squaredDistance(center, positions[ i ])) 53 | } 54 | return Math.sqrt(maxRadiusSq) 55 | } 56 | 57 | module.exports = boundingSphere//{boundingSphere, boundingSphereFromBoundingBox} 58 | -------------------------------------------------------------------------------- /src/bound-utils/computeBounds.js: -------------------------------------------------------------------------------- 1 | const vec3 = require('gl-vec3') 2 | 3 | const boundingBox = require('./boundingBox') 4 | const boundingSphere = require('./boundingSphere') 5 | 6 | /* converts input data to array if it is not already an array */ 7 | function toArray (data) { 8 | if (data === undefined || data === null) return [] 9 | if (data.constructor !== Array) return [data] 10 | return data 11 | } 12 | 13 | /** 14 | * compute all bounding data given geometry data + position 15 | * @param {Object} transforms the initial transforms ie {pos:[x, x, x], rot:[x, x, x], sca:[x, x, x]}. 16 | * @param {String} bounds the current bounds of the entity 17 | * @param {String} axes on which axes to apply the transformation (default: [0, 0, 1]) 18 | * @return {Object} a new transforms object, with offset position 19 | * returns an object in this form: 20 | * bounds: { 21 | * dia: 40, 22 | * center: [0,20,8], 23 | * min: [9, -10, 0], 24 | * max: [15, 10, 4] 25 | * size: [6,20,4] 26 | *} 27 | */ 28 | function computeBounds (object) { 29 | let scale 30 | let dia 31 | let center 32 | let size 33 | let bbox 34 | if (Array.isArray(object) && object.length > 1) { 35 | const objects = object 36 | let positions = [] 37 | objects.forEach(function (object) { 38 | scale = object.transforms && object.transforms.sca ? object.transforms.sca : 1 39 | // TODO deal with nested/ non nested data 40 | let geomPositions = object.geometry.positions 41 | const isNested = geomPositions.length > 1 && Array.isArray(geomPositions[0]) 42 | geomPositions = scale === 1 ? geomPositions 43 | : ( 44 | isNested ? geomPositions.map(pos => pos.map(position => position * scale)) : geomPositions.map(position => position * scale) 45 | ) 46 | 47 | positions = positions.concat(geomPositions)// object.geometry.positions.map(position => position * scale)) 48 | }) 49 | bbox = boundingBox(positions) 50 | center = vec3.scale(vec3.create(), vec3.add(vec3.create(), bbox[0], bbox[1]), 0.5) 51 | const bsph = boundingSphere(center, positions) 52 | size = [bbox[1][0] - bbox[0][0], bbox[1][1] - bbox[0][1], bbox[1][2] - bbox[0][2]] 53 | dia = scale !== 1 ? bsph * Math.max(...scale) : bsph 54 | } else { 55 | scale = object.transforms && object.transforms.sca ? object.transforms.sca : undefined 56 | bbox = boundingBox(object.geometry.positions) 57 | if (scale) { 58 | bbox[0] = bbox[0].map((x, i) => x * scale[i]) 59 | bbox[1] = bbox[1].map((x, i) => x * scale[i]) 60 | } 61 | 62 | center = vec3.scale(vec3.create(), vec3.add(vec3.create(), bbox[0], bbox[1]), 0.5) 63 | const bsph = boundingSphere(center, object.geometry.positions) 64 | size = [bbox[1][0] - bbox[0][0], bbox[1][1] - bbox[0][1], bbox[1][2] - bbox[0][2]] 65 | dia = scale ? bsph * Math.max(...scale) : bsph 66 | } 67 | 68 | return { 69 | dia, 70 | center: [...center], 71 | min: bbox[0], 72 | max: bbox[1], 73 | size 74 | } 75 | } 76 | module.exports = computeBounds 77 | -------------------------------------------------------------------------------- /src/bound-utils/computeBounds.test.js: -------------------------------------------------------------------------------- 1 | const test = require('tape') 2 | const computeBounds = require('./computeBounds') 3 | 4 | test('computeBounds (geometry only)', t => { 5 | t.plan(1) 6 | const input = { 7 | geometry: { 8 | positions: [0, 2, 1, -10, 2, 1, -2.4, -2.8, 4] 9 | } 10 | } 11 | 12 | const expBounds = { 13 | dia: 5.745432971379113, 14 | center: [-5, -0.4000000059604645, 2.5], 15 | min: [-10, -2.8, 1], 16 | max: [0, 2, 4], 17 | size: [10, 4.8, 3] 18 | } 19 | 20 | const bounds = computeBounds(input) 21 | 22 | t.equal(bounds, expBounds) 23 | }) 24 | 25 | test('computeBounds (with transforms)', t => { 26 | t.plan(1) 27 | const input = { 28 | geometry: { 29 | positions: [0, 2, 1, -10, 2, 1, -2.4, -2.8, 4] 30 | }, 31 | transforms: { 32 | sca: [1, 1, 1] 33 | } 34 | } 35 | 36 | const expBounds = { 37 | dia: 5.745432971379113, 38 | center: [-5, -0.4000000059604645, 2.5], 39 | min: [-10, -2.8, 1], 40 | max: [0, 2, 4], 41 | size: [10, 4.8, 3] 42 | } 43 | 44 | const bounds = computeBounds(input) 45 | 46 | t.equal(bounds, expBounds) 47 | }) 48 | 49 | test('computeBounds (non default scale)', t => { 50 | t.plan(1) 51 | const input = { 52 | geometry: { 53 | positions: [0, 2, 1, -10, 2, 1, -2.4, -2.8, 4] 54 | }, 55 | transforms: { 56 | sca: [1.2, 0.7, -1] 57 | } 58 | } 59 | 60 | const expBounds = { 61 | dia: 9.415252306303229, 62 | center: [-6, -0.2800000011920929, -2.5], 63 | min: [-12, -1.9599999999999997, -1], 64 | max: [0, 1.4, -4], 65 | size: [12, 3.3599999999999994, -3] 66 | } 67 | 68 | const bounds = computeBounds(input) 69 | 70 | t.equal(bounds, expBounds) 71 | }) 72 | -------------------------------------------------------------------------------- /src/bound-utils/index.js: -------------------------------------------------------------------------------- 1 | export {default as computeBounds} from './computeBounds' 2 | export {default as isObjectOutsideBounds} from './isObjectOutsideBounds' 3 | -------------------------------------------------------------------------------- /src/cameraAndControls/camera.js: -------------------------------------------------------------------------------- 1 | 2 | const vec3 = require('gl-vec3') 3 | const mat4 = require('gl-mat4') 4 | 5 | function fromOrthographicToPerspective (orthographicCamera) { 6 | const {near, far, fov, zoom} = orthographicCamera 7 | console.log('fov', fov, 'zoom', zoom) 8 | //: fov / zoom 9 | // recompute projection matrix to use perspective camera projection matrix 10 | const {viewport} = orthographicCamera 11 | const projection = require('./perspectiveCamera').setProjection(orthographicCamera, {width: viewport[2], height: viewport[3]}) 12 | const {projectionType} = require('./perspectiveCamera').cameraState 13 | return Object.assign({}, orthographicCamera, projection, {projectionType}, {near, far, fov}) 14 | } 15 | 16 | function fromPerspectiveToOrthographic (perspectiveCamera) { 17 | const {fov, aspect} = perspectiveCamera 18 | 19 | // set the orthographic view rectangle to 0,0,width,height 20 | // see here : http://stackoverflow.com/questions/13483775/set-zoomvalue-of-a-perspective-equal-to-perspective 21 | const target = perspectiveCamera.target === undefined ? vec3.create() : perspectiveCamera.target 22 | 23 | const distance = vec3.length(vec3.subtract([], perspectiveCamera.position, perspectiveCamera.target)) * 0.3 24 | const width = Math.tan(fov) * distance * aspect 25 | const height = Math.tan(fov) * distance 26 | 27 | const halfWidth = width 28 | const halfHeight = height 29 | 30 | const left = halfWidth 31 | const right = -halfWidth 32 | const top = -halfHeight 33 | const bottom = halfHeight 34 | 35 | // we need to compute zoom from distance ? or pass it from controls ? 36 | 37 | // we re-use near, far, & projection matrix of orthographicCamera 38 | const {near, far, viewport} = perspectiveCamera 39 | const fCam = {zoom: 1, near, far} 40 | const orthographicCamera = require('./orthographicCamera').cameraState 41 | const projection = require('./orthographicCamera').setProjection(fCam, {width, height}) 42 | return Object.assign({}, orthographicCamera, perspectiveCamera, projection, {projectionType: orthographicCamera.projectionType, viewport}) 43 | //return Object.assign({}, orthoCam, projection, {near, far, left, right, top, bottom, target}) 44 | } 45 | 46 | function toPerspectiveView ({camera}) { 47 | const offsetToTarget = vec3.distance(camera.position, camera.target) 48 | const distance = offsetToTarget 49 | const position = [distance, distance, distance] 50 | const view = mat4.lookAt(mat4.create(), position, camera.target, camera.up) 51 | 52 | return {view, position} 53 | } 54 | 55 | function toPresetView (viewName, {camera}) { 56 | const presets = { 57 | 'top': [0, 0, 1], 58 | 'bottom': [0, 0, -1], 59 | 'front': [0, 1, 0], 60 | 'back': [0, -1, 0], 61 | 'left': [1, 0, 0], 62 | 'right': [-1, 0, 0], 63 | undefined: [0, 0, 0] 64 | } 65 | 66 | const offsetToTarget = vec3.distance(camera.position, camera.target) 67 | const position = vec3.add([], presets[viewName].map(x => x * offsetToTarget), camera.target) 68 | const view = mat4.lookAt(mat4.create(), position, camera.target, camera.up) 69 | 70 | return {view, position} 71 | } 72 | 73 | module.exports = {toPerspectiveView, toPresetView, fromOrthographicToPerspective, fromPerspectiveToOrthographic} 74 | -------------------------------------------------------------------------------- /src/cameraAndControls/elementSizing.js: -------------------------------------------------------------------------------- 1 | const {fromEvent} = require('most') 2 | // element resize event stream, throttled by throttle amount (250ms default) 3 | module.exports = function elementSize (element, throttle = 25) { 4 | const pixelRatio = window.devicePixelRatio || 1 5 | function extractSize () { 6 | const width = Math.floor(element.clientWidth * pixelRatio) 7 | const height = Math.floor(element.clientHeight * pixelRatio) 8 | const bRect = element.getBoundingClientRect ? element.getBoundingClientRect() : { left: 0, top: 0, bottom: 0, right: 0, width: 0, height: 0 } 9 | return {width, height, aspect: width / height, bRect} 10 | } 11 | 12 | return fromEvent('resize', window)// only window fires resize events... 13 | .throttle(throttle /* ms */) 14 | .map(extractSize) 15 | .startWith(extractSize()) 16 | } 17 | -------------------------------------------------------------------------------- /src/cameraAndControls/orbitControls.js: -------------------------------------------------------------------------------- 1 | const vec3 = require('gl-vec3') 2 | const mat4 = require('gl-mat4') 3 | const {max, min, sqrt, PI, sin, cos, atan2} = Math 4 | 5 | // TODO: make it more data driven ? 6 | /* 7 | setFocus => modify the focusPoint input 8 | rotate => modify the angle input 9 | 10 | */ 11 | /* cameras are assumed to have: 12 | projection 13 | view 14 | target (focal point) 15 | eye/position 16 | up 17 | */ 18 | // TODO: multiple data, sometimes redundant, needs simplification 19 | /* 20 | - camera state 21 | - camera props 22 | 23 | - controls state 24 | - controls props 25 | 26 | - other 27 | 28 | */ 29 | 30 | const controlsProps = { 31 | limits: { 32 | minDistance: 0.01, 33 | maxDistance: 10000 34 | }, 35 | drag: 0.27, // Decrease the momentum by 1% each iteration 36 | EPS: 0.000001, 37 | zoomToFit: { 38 | auto: true, // always tried to apply zoomTofit 39 | targets: 'all', 40 | tightness: 1.5 // how close should the fit be: the lower the tigher : 1 means very close, but fitting most of the time 41 | }, 42 | // all these, not sure are needed in this shape 43 | userControl: { 44 | zoom: true, 45 | zoomSpeed: 1.0, 46 | rotate: true, 47 | rotateSpeed: 1.0, 48 | pan: true, 49 | panSpeed: 1.0 50 | }, 51 | autoRotate: { 52 | enabled: false, 53 | speed: 2.0 // 30 seconds per round when fps is 60 54 | }, 55 | autoAdjustPlanes: true // adjust near & far planes when zooming in &out 56 | } 57 | 58 | const controlsState = { 59 | // orbit controls state 60 | thetaDelta: 0, 61 | phiDelta: 0, 62 | scale: 1 63 | } 64 | 65 | const defaults = Object.assign({}, controlsState, controlsProps) 66 | 67 | function update ({controls, camera}) { 68 | // custom z up is settable, with inverted Y and Z (since we use camera[2] => up) 69 | const {EPS, drag} = controls 70 | let {position, target} = camera 71 | const up = controls.up ? controls.up : camera.up 72 | 73 | let curThetaDelta = controls.thetaDelta 74 | let curPhiDelta = controls.phiDelta 75 | let curScale = controls.scale 76 | 77 | let offset = vec3.subtract([], position, target) 78 | let theta 79 | let phi 80 | 81 | // console.log('target', target) 82 | // console.log(matrix) 83 | 84 | if (up[2] === 1) { 85 | // angle from z-axis around y-axis, upVector : z 86 | theta = atan2(offset[0], offset[1]) 87 | // angle from y-axis 88 | phi = atan2(sqrt(offset[0] * offset[0] + offset[1] * offset[1]), offset[2]) 89 | } else { 90 | // in case of y up 91 | theta = atan2(offset[0], offset[2]) 92 | phi = atan2(sqrt(offset[0] * offset[0] + offset[2] * offset[2]), offset[1]) 93 | // curThetaDelta = -(curThetaDelta) 94 | } 95 | 96 | if (controls.autoRotate.enabled && controls.userControl.rotate) { 97 | curThetaDelta += 2 * Math.PI / 60 / 60 * controls.autoRotate.speed 98 | } 99 | 100 | theta += curThetaDelta 101 | phi += curPhiDelta 102 | 103 | // restrict phi to be betwee EPS and PI-EPS 104 | phi = max(EPS, min(PI - EPS, phi)) 105 | // multiply by scaling effect and restrict radius to be between desired limits 106 | const radius = max(controls.limits.minDistance, min(controls.limits.maxDistance, vec3.length(offset) * curScale)) 107 | 108 | if (up[2] === 1) { 109 | offset[0] = radius * sin(phi) * sin(theta) 110 | offset[2] = radius * cos(phi) 111 | offset[1] = radius * sin(phi) * cos(theta) 112 | } else { 113 | offset[0] = radius * sin(phi) * sin(theta) 114 | offset[1] = radius * cos(phi) 115 | offset[2] = radius * sin(phi) * cos(theta) 116 | } 117 | 118 | let newPosition = vec3.add(vec3.create(), target, offset) 119 | let newView = mat4.lookAt(mat4.create(), newPosition, target, up) 120 | 121 | const dragEffect = 1 - max(min(drag, 1.0), 0.01) 122 | const positionChanged = vec3.distance(position, newPosition) > 0 // TODO optimise 123 | 124 | /* let newMatrix = mat4.create() 125 | newMatrix = mat4.lookAt(newMatrix, newPosition, target, up) 126 | newMatrix = mat4.translate(matrix, matrix, newPosition) */ 127 | 128 | // update camera matrix 129 | // let quaternion = quatFromRotationMatrix(mat4.lookAt(mat4.create(), [0, 0, 0], target, up)) 130 | // let newMatrix = composeMat4(mat4.create(), newPosition, quaternion, [1, 1, 1]) 131 | 132 | // view = newMatrix 133 | return { 134 | // controls state 135 | controls: { 136 | thetaDelta: curThetaDelta * dragEffect, 137 | phiDelta: curPhiDelta * dragEffect, 138 | scale: 1, 139 | changed: positionChanged 140 | }, 141 | // camera state 142 | camera: { 143 | position: newPosition, 144 | view: newView 145 | } 146 | // matrix: newMatrix 147 | } 148 | } 149 | 150 | /** 151 | * compute camera state to rotate the camera 152 | * @param {Object} controls the controls data/state 153 | * @param {Object} camera the camera data/state 154 | * @param {Float} angle value of the angle to rotate 155 | * @return {Object} the updated camera data/state 156 | */ 157 | function rotate ({controls, camera}, angle) { 158 | const reductionFactor = 500 159 | let { 160 | thetaDelta, 161 | phiDelta 162 | } = controls 163 | 164 | if (controls.userControl.rotate) { 165 | thetaDelta += (angle[0] / reductionFactor) 166 | phiDelta += (angle[1] / reductionFactor) 167 | } 168 | 169 | return { 170 | controls: { 171 | thetaDelta, 172 | phiDelta 173 | }, 174 | camera 175 | } 176 | } 177 | 178 | /** 179 | * compute camera state to zoom the camera 180 | * @param {Object} controls the controls data/state 181 | * @param {Object} camera the camera data/state 182 | * @param {Float} zoomDelta value of the zoom 183 | * @return {Object} the updated camera data/state 184 | */ 185 | function zoom ({controls, camera}, zoomDelta = 0) { 186 | let {scale} = controls 187 | 188 | if (controls.userControl.zoom && camera && zoomDelta !== undefined && zoomDelta !== 0 && !isNaN(zoomDelta)) { 189 | const sign = Math.sign(zoomDelta) === 0 ? 1 : Math.sign(zoomDelta) 190 | zoomDelta = (zoomDelta / zoomDelta) * sign * 0.04// controls.userControl.zoomSpeed 191 | // adjust zoom scaling based on distance : the closer to the target, the lesser zoom scaling we apply 192 | // zoomDelta *= Math.exp(Math.max(camera.scale * 0.05, 1)) 193 | // updated scale after we will apply the new zoomDelta to the current scale 194 | const newScale = (zoomDelta + controls.scale) 195 | // updated distance after the scale has been updated, used to prevent going outside limits 196 | const newDistance = vec3.distance(camera.position, camera.target) * newScale 197 | 198 | if (newDistance > controls.limits.minDistance && newDistance < controls.limits.maxDistance) { 199 | scale += zoomDelta 200 | } 201 | // for ortho cameras 202 | if (camera.projectionType === 'orthographic') { 203 | const distance = vec3.length(vec3.subtract([], camera.position, camera.target)) * 0.3 204 | const width = Math.tan(camera.fov) * distance * camera.aspect 205 | const height = Math.tan(camera.fov) * distance 206 | 207 | const projection = require('./orthographicCamera').setProjection(camera, {width, height}) 208 | camera = projection 209 | } 210 | 211 | /* if (controls.autoAdjustPlanes) { 212 | // these are empirical values , after a LOT of testing 213 | const distance = vec3.squaredDistance(camera.target, camera.position) 214 | camera.near = Math.min(Math.max(5, distance * 0.0015), 100) 215 | } */ 216 | } 217 | return {controls: {scale}, camera} 218 | } 219 | 220 | /** 221 | * compute camera state to pan the camera 222 | * @param {Object} controls the controls data/state 223 | * @param {Object} camera the camera data/state 224 | * @param {Float} delta value of the raw pan delta 225 | * @return {Object} the updated camera data/state 226 | */ 227 | function pan ({controls, camera}, delta) { 228 | const unproject = require('camera-unproject') 229 | const {projection, view, viewport} = camera 230 | const combinedProjView = mat4.multiply([], projection, view) 231 | const invProjView = mat4.invert([], combinedProjView) 232 | 233 | const panStart = [ 234 | viewport[2], 235 | viewport[3], 236 | 0 237 | ] 238 | const panEnd = [ 239 | viewport[2] - delta[0], 240 | viewport[3] + delta[1], 241 | 0 242 | ] 243 | const unPanStart = unproject([], panStart, viewport, invProjView) 244 | const unPanEnd = unproject([], panEnd, viewport, invProjView) 245 | // TODO scale by the correct near/far value instead of 1000 ? 246 | // const planesDiff = camera.far - camera.near 247 | const offset = vec3.subtract([], unPanStart, unPanEnd).map(x => x * 1000 * controls.userControl.panSpeed * controls.scale) 248 | 249 | return { 250 | controls, 251 | camera: { 252 | position: vec3.add(vec3.create(), camera.position, offset), 253 | target: vec3.add(vec3.create(), camera.target, offset) 254 | }} 255 | } 256 | 257 | /** 258 | * compute camera state to 'fit' an object on screen 259 | * Note1: this is a non optimal but fast & easy implementation 260 | * @param {Object} controls the controls data/state 261 | * @param {Object} camera the camera data/state 262 | * @param {Object} entity an object containing a 'bounds' property for bounds information 263 | * @return {Object} the updated camera data/state 264 | */ 265 | function zoomToFit ({controls, camera, entities}) { 266 | // our camera.fov is already in radian, no need to convert 267 | const {zoomToFit} = controls 268 | if (entities.length < 1 || zoomToFit.targets !== 'all') { //! == 'all' || entity === undefined || !entity.bounds) { 269 | return {controls, camera} 270 | } 271 | 272 | let bounds 273 | // more than one entity, targeted, need to compute the overall bounds 274 | if (entities.length > 1) { 275 | const computeBounds = require('../bound-utils/computeBounds') 276 | bounds = computeBounds(entities) 277 | } else { 278 | bounds = entities[0].bounds 279 | } 280 | 281 | // fixme: for now , we only use the first item 282 | const {fov, target, position} = camera 283 | const {tightness} = Object.assign({}, zoomToFit, controlsProps.zoomToFit) 284 | /* 285 | - x is scaleForIdealDistance 286 | - currentDistance is fixed 287 | - how many times currentDistance * x = idealDistance 288 | So 289 | x = idealDistance / currentDistance 290 | */ 291 | const idealDistanceFromCamera = (bounds.dia * tightness) / Math.tan(fov / 2.0) 292 | const currentDistance = vec3.distance(target, position) 293 | const scaleForIdealDistance = idealDistanceFromCamera / currentDistance 294 | 295 | return { 296 | camera: {target: bounds.center}, 297 | controls: {scale: scaleForIdealDistance} 298 | } 299 | } 300 | 301 | /** 302 | * compute controls state to 'reset it' to the given state 303 | * Note1: this is a non optimal but fast & easy implementation 304 | * @param {Object} controls the controls data/state 305 | * @param {Object} camera the camera data/state 306 | * @param {Object} desiredState the state to reset the camera to: defaults to default values 307 | * @return {Object} the updated camera data/state 308 | */ 309 | function reset ({controls, camera}, desiredState) { 310 | /* camera = Object.assign({}, camera, desiredState.camera) 311 | camera.projection = mat4.perspective([], camera.fov, camera.aspect, camera.near, camera.far) 312 | controls = Object.assign({}, controls, desiredState.controls) 313 | return { 314 | camera, 315 | controls 316 | } */ 317 | return { 318 | camera: { 319 | position: desiredState.camera.position, 320 | target: desiredState.camera.target, 321 | projection: mat4.perspective([], camera.fov, camera.aspect, camera.near, camera.far), 322 | view: desiredState.camera.view 323 | }, 324 | controls: { 325 | thetaDelta: desiredState.controls.thetaDelta, 326 | phiDelta: desiredState.controls.phiDelta, 327 | scale: desiredState.controls.scale 328 | } 329 | } 330 | } 331 | 332 | // FIXME: upgrade or obsolete 333 | function setFocus ({controls, camera}, focusPoint) { 334 | const sub = (a, b) => a.map((a1, i) => a1 - b[i]) 335 | const add = (a, b) => a.map((a1, i) => a1 + b[i]) // NOTE: NO typedArray.map support on old browsers, polyfilled 336 | const camTarget = camera.target 337 | const diff = sub(focusPoint, camTarget) // [ focusPoint[0] - camTarget[0], 338 | const zOffset = [0, 0, diff[2] * 0.5] 339 | camera.target = add(camTarget, zOffset) 340 | camera.position = add(camera.position, zOffset) 341 | return camera 342 | 343 | // old 'zoom to fit' update code 344 | /* if (targetTgt && positionTgt) { 345 | const posDiff = vec3.subtract([], positionTgt, newPosition) 346 | const tgtDiff = vec3.subtract([], targetTgt, newTarget) 347 | // console.log('posDiff', newPosition, positionTgt, newTarget, targetTgt) 348 | if (vec3.length(posDiff) > 0.1 && vec3.length(tgtDiff) > 0.1) { 349 | newPosition = vec3.scaleAndAdd(newPosition, newPosition, posDiff, 0.1) 350 | newTarget = vec3.scaleAndAdd(newTarget, newTarget, tgtDiff, 0.1) 351 | } 352 | 353 | if (settings.autoAdjustPlanes) { 354 | var distance = vec3.squaredDistance(newTarget, newPosition) 355 | near = Math.min(Math.max(5, distance * 0.0015), 100) // these are empirical values , after a LOT of testing 356 | projection = mat4.perspective([], camera.fov, camera.aspect, camera.near, camera.far) 357 | } 358 | } */ 359 | } 360 | module.exports = { 361 | controlsProps, 362 | controlsState, 363 | defaults, 364 | update, 365 | rotate, 366 | zoom, 367 | pan, 368 | zoomToFit, 369 | reset 370 | } 371 | -------------------------------------------------------------------------------- /src/cameraAndControls/orthographicCamera.js: -------------------------------------------------------------------------------- 1 | const mat4 = require('gl-mat4') 2 | 3 | const cameraState = { 4 | view: mat4.identity(new Float32Array(16)), 5 | projection: mat4.identity(new Float32Array(16)), 6 | matrix: mat4.identity(new Float32Array(16)), // not sure if needed 7 | near: 1, // 0.01, 8 | far: 1300, 9 | up: [0, 0, 1], 10 | // distance: 10.0, // not sure if needed 11 | eye: new Float32Array(3), // same as position 12 | position: [150, 250, 200], 13 | target: [0, 0, 0], 14 | fov: Math.PI / 4, 15 | aspect: 1, 16 | viewport: [0, 0, 0, 0], 17 | zoom: 1, 18 | projectionType: 'orthographic' 19 | } 20 | 21 | const cameraProps = { 22 | 23 | } 24 | 25 | function setProjection (camera, input) { 26 | const {width, height} = input 27 | // context.viewportWidth / context.viewportHeight, 28 | const aspect = width / height 29 | const viewport = [0, 0, width, height] 30 | const multiplier = camera.zoom 31 | console.log('zoom', multiplier) 32 | 33 | const left = -width * multiplier 34 | const right = width * multiplier 35 | const bottom = -height * multiplier 36 | const top = height * multiplier 37 | 38 | const projection = mat4.ortho([], left, right, bottom, top, camera.near, camera.far) 39 | return {projection, aspect, viewport} 40 | } 41 | 42 | module.exports = {cameraState, cameraProps, setProjection} 43 | -------------------------------------------------------------------------------- /src/cameraAndControls/perspectiveCamera.js: -------------------------------------------------------------------------------- 1 | const mat4 = require('gl-mat4') 2 | 3 | const cameraState = { 4 | view: mat4.identity(new Float32Array(16)), 5 | projection: mat4.identity(new Float32Array(16)), 6 | matrix: mat4.identity(new Float32Array(16)), // not sure if needed 7 | near: 1, // 0.01, 8 | far: 18000, 9 | up: [0, 0, 1], 10 | // distance: 10.0, // not sure if needed 11 | eye: new Float32Array(3), // same as position 12 | position: [450, 550, 700], 13 | target: [0, 0, 0], 14 | fov: Math.PI / 4, 15 | aspect: 1, 16 | viewport: [0, 0, 0, 0], 17 | projectionType: 'perspective' 18 | } 19 | 20 | const cameraProps = { 21 | } 22 | 23 | const defaults = Object.assign({}, cameraState, cameraProps) 24 | 25 | function setProjection (camera, input) { 26 | // context.viewportWidth / context.viewportHeight, 27 | const aspect = input.width / input.height 28 | 29 | const projection = mat4.perspective(camera.projection, camera.fov, aspect, 30 | camera.near, 31 | camera.far) 32 | const viewport = [0, 0, input.width, input.height] 33 | 34 | return {projection, aspect, viewport} 35 | } 36 | 37 | module.exports = {cameraState, cameraProps, defaults, setProjection} 38 | -------------------------------------------------------------------------------- /src/cameraControlsActions.js: -------------------------------------------------------------------------------- 1 | const most = require('most') 2 | const limitFlow = require('./observable-utils/limitFlow') 3 | 4 | function actions (sources) { 5 | const {gestures, heartBeat$, params$, data$, state$} = sources 6 | 7 | const resizes$ = sources.resizes$ 8 | // .startWith({width: 600, height:400, aspect:1, brect:{}}) 9 | .map(data => ({type: 'resize', data})) 10 | .multicast() 11 | // .tap(x=>console.log('resizes',x)) 12 | 13 | let rotations$ = gestures.drags 14 | .filter(x => x !== undefined) // TODO: add this at gestures.drags level 15 | .map(function (data) { 16 | let delta = [data.delta.x, data.delta.y] 17 | const {shiftKey} = data.originalEvents[0] 18 | if (!shiftKey) { 19 | return delta 20 | } 21 | return undefined 22 | }) 23 | .filter(x => x !== undefined) 24 | .map(delta => delta.map(d => d * -Math.PI)) 25 | .map(data => ({type: 'rotate', data})) 26 | .multicast() 27 | 28 | let pan$ = gestures.drags 29 | .filter(x => x !== undefined) // TODO: add this at gestures.drags level 30 | .map(function (data) { 31 | const delta = [data.delta.x, data.delta.y] 32 | const {shiftKey} = data.originalEvents[0] 33 | if (shiftKey) { 34 | return delta 35 | } 36 | return undefined 37 | }) 38 | .filter(x => x !== undefined) 39 | .map(data => ({type: 'pan', data})) 40 | .multicast() 41 | 42 | let zoom$ = gestures.zooms 43 | .startWith(0) // TODO: add this at gestures.zooms level 44 | .map(x => -x) // we invert zoom direction 45 | .filter(x => !isNaN(x)) // TODO: add this at gestures.zooms level 46 | .skip(1) 47 | .map(data => ({type: 'zoom', data})) 48 | .multicast() 49 | 50 | const setProjectionType$ = params$ 51 | .filter(params => { 52 | return params.camera && params.camera.projectionType 53 | }) 54 | .map(data => ({type: 'setProjectionType', data: data.camera.projectionType})) 55 | 56 | // Reset view with a double tap/ when data changed 57 | let reset$ = most.mergeArray([ 58 | gestures.taps 59 | .filter(taps => taps.nb === 2) 60 | .multicast(), 61 | state$ 62 | .filter(state => state.behaviours.resetViewOn.includes('new-entities')) 63 | .map(state => state.entities).skipRepeatsWith(areEntitiesIdentical) 64 | .map(_ => ({origin: 'new-entities'})), 65 | params$ 66 | .filter(params => { 67 | return params.camera && params.camera === 'reset' 68 | // params === {camera: 'reset'}) 69 | }) 70 | .map(x => ({origin: 'request'})) 71 | ]) 72 | .map(data => ({type: 'reset', data})) 73 | .multicast() 74 | 75 | function areEntitiesIdentical (previous, current) { 76 | // console.log('areEntitiesIdentical', previous, current) 77 | if (current.length !== previous.length) { 78 | return false 79 | } 80 | for (let i = 0; i < current.length; i++) { 81 | if (current[i].geometry.positions.length !== previous[i].geometry.positions.length) { 82 | return false 83 | } 84 | } 85 | 86 | return true 87 | } 88 | // zoomToFit main mesh bounds 89 | const zoomToFit$ = most.mergeArray([ 90 | gestures.taps.filter(taps => taps.nb === 3) 91 | .map(_ => ({type: 'zoomToFit', data: {origin: 'demand'}})), 92 | state$ 93 | .filter(state => state.behaviours.zoomToFitOn.includes('new-entities')) 94 | .map(state => state.entities).skipRepeatsWith(areEntitiesIdentical) 95 | .map(_ => ({type: 'zoomToFit', data: {origin: 'new-entities'}})) 96 | // .multicast().tap(x => console.log('zoomToFit on new entities')) 97 | ]) 98 | .multicast() 99 | 100 | const update$ = heartBeat$.thru(limitFlow(33)) 101 | .map(_ => ({type: 'update', data: undefined})) 102 | 103 | return [ 104 | rotations$, 105 | pan$, 106 | zoom$, 107 | reset$, 108 | zoomToFit$, 109 | resizes$, 110 | update$ 111 | 112 | // toPresetView$, 113 | // setProjectionType$ 114 | ] 115 | } 116 | 117 | module.exports = actions 118 | -------------------------------------------------------------------------------- /src/cameraControlsReducers.js: -------------------------------------------------------------------------------- 1 | const {update, rotate, zoom, pan, zoomToFit, reset} = require('./cameraAndControls/orbitControls') 2 | const {setProjection} = require('./cameraAndControls/perspectiveCamera') 3 | const {merge} = require('./utils') 4 | const {toPresetView, fromPerspectiveToOrthographic, fromOrthographicToPerspective} = require('./cameraAndControls/camera') 5 | 6 | function makeReducers (initialState) { 7 | // make sure to actually save the initial state, as it might get mutated 8 | initialState = JSON.parse(JSON.stringify(initialState)) 9 | const reducers = { 10 | undefined: (state) => state, // no op 11 | update: (state) => { 12 | return merge({}, state, update(state)) 13 | }, 14 | resize: (state, sizes) => { 15 | return merge({}, state, {camera: setProjection(state.camera, sizes)}) 16 | }, 17 | rotate: (state, angles) => { 18 | return merge({}, state, rotate(state, angles)) 19 | }, 20 | zoom: (state, zooms) => { 21 | return merge({}, state, zoom(state, zooms)) 22 | }, 23 | pan: (state, delta) => { 24 | return merge({}, state, pan(state, delta)) 25 | }, 26 | zoomToFit: (state, when) => { 27 | return merge({}, state, zoomToFit(state)) 28 | }, 29 | reset: (state, params) => { 30 | let resetState = merge({}, state, reset(state, initialState)) 31 | // then apply zoomToFIt 32 | resetState = zoomToFit(resetState) 33 | return resetState 34 | }, 35 | toPresetView: (state, viewName) => { 36 | const newState = merge({}, state, {camera: toPresetView(viewName, state)}) 37 | return newState 38 | }, 39 | setProjectionType: (state, projectionType) => { 40 | // console.log('setProjectionType', projectionType) 41 | if (projectionType === 'orthographic' && state.camera.projectionType === 'perspective') { 42 | const camera = fromPerspectiveToOrthographic(state.camera) 43 | const newState = merge({}, state, {camera}) 44 | return newState 45 | } 46 | if (projectionType === 'perspective' && state.camera.projectionType === 'orthographic') { 47 | const camera = fromOrthographicToPerspective(state.camera) 48 | const newState = merge({}, state, {camera}) 49 | return newState 50 | } 51 | return state 52 | }, 53 | toOrthoView: (state, params) => { 54 | return merge({}, state, {}) 55 | } 56 | } 57 | return reducers 58 | } 59 | 60 | module.exports = makeReducers 61 | -------------------------------------------------------------------------------- /src/csg-utils/areCAGsIdentical.js: -------------------------------------------------------------------------------- 1 | const areCSGsIdentical = (cag, otherCag) => { 2 | if ((!cag & otherCag) || (cag && !otherCag)) { 3 | return false 4 | } 5 | if (!cag && !otherCag) { 6 | return true 7 | } 8 | if ('uid' in cag.properties && 'uid' in otherCag.properties) { 9 | if (cag.properties.uid === otherCag.properties.uid) { 10 | return true 11 | } 12 | } 13 | if (cag.isCanonicalized !== otherCag.isCanonicalized) { 14 | return false 15 | } 16 | if (cag.isRetesselated !== otherCag.isRetesselated) { 17 | return false 18 | } 19 | if (cag.sides.length !== otherCag.sides.length) { 20 | return false 21 | } 22 | 23 | let sides = cag.sides 24 | let otherSides = otherCag.sides 25 | 26 | const compareArrays = (a, b) => { 27 | if (a.length !== b.length) { 28 | return false 29 | } 30 | for (let i = 0; i < a.length; i++) { 31 | // nested array 32 | 33 | if (a[i].length) { 34 | if (!compareArrays(a[i], b[i])) { 35 | return false 36 | } 37 | } else { 38 | if (a[i] !== b[i]) { 39 | return false 40 | } 41 | } 42 | } 43 | return true 44 | } 45 | 46 | for (let i = 0; i < sides.length; i++) { 47 | const sideA = sides[i]// .toString() 48 | const sideB = otherSides[i]// .toString() 49 | 50 | const polyAPlane = sideA.plane 51 | const polyAPlaneData = [polyAPlane.normal._x, polyAPlane.normal._y, polyAPlane.normal._z, polyAPlane.w] 52 | const polyAPoints = sideA.vertices.map(x => [x.pos._x, x.pos._y]) 53 | 54 | const polyBPlane = sideB.plane 55 | const polyBPlaneData = [polyBPlane.normal._x, polyBPlane.normal._y, polyBPlane.normal._z, polyBPlane.w] 56 | const polyBPoints = sideB.vertices.map(x => [x.pos._x, x.pos._y]) 57 | 58 | if (compareArrays(polyAPlaneData, polyBPlaneData) === false) { 59 | return false 60 | } 61 | if (compareArrays(polyAPoints, polyBPoints) === false) { 62 | return false 63 | } 64 | 65 | // console.log('side', side, otherPolygon) 66 | } 67 | return true 68 | } 69 | 70 | module.exports = areCSGsIdentical 71 | -------------------------------------------------------------------------------- /src/csg-utils/areCSGsIdentical.js: -------------------------------------------------------------------------------- 1 | const areCSGsIdentical = (csg, otherCsg) => { 2 | if ((!csg & otherCsg) || (csg && !otherCsg)) { 3 | return false 4 | } 5 | if (!csg && !otherCsg) { 6 | return true 7 | } 8 | if ('uid' in csg.properties && 'uid' in otherCsg.properties) { 9 | if (csg.properties.uid === otherCsg.properties.uid) { 10 | return true 11 | } 12 | } 13 | if (csg.isCanonicalized !== otherCsg.isCanonicalized) { 14 | return false 15 | } 16 | if (csg.isRetesselated !== otherCsg.isRetesselated) { 17 | return false 18 | } 19 | if (csg.polygons.length !== otherCsg.polygons.length) { 20 | return false 21 | } 22 | 23 | let polygons = csg.polygons 24 | let otherPolygons = otherCsg.polygons 25 | 26 | const compareArrays = (a, b) => { 27 | if (a.length !== b.length) { 28 | return false 29 | } 30 | for (let i = 0; i < a.length; i++) { 31 | // nested array 32 | 33 | if (a[i].length) { 34 | if (!compareArrays(a[i], b[i])) { 35 | return false 36 | } 37 | } else { 38 | if (a[i] !== b[i]) { 39 | return false 40 | } 41 | } 42 | } 43 | return true 44 | } 45 | 46 | for (let i = 0; i < polygons.length; i++) { 47 | const polygonA = polygons[i]// .toString() 48 | const polygonB = otherPolygons[i]// .toString() 49 | 50 | const polyAPlane = polygonA.plane 51 | const polyAPlaneData = [polyAPlane.normal._x, polyAPlane.normal._y, polyAPlane.normal._z, polyAPlane.w] 52 | const polyAPoints = polygonA.vertices.map(x => [x.pos._x, x.pos._y]) 53 | 54 | const polyBPlane = polygonB.plane 55 | const polyBPlaneData = [polyBPlane.normal._x, polyBPlane.normal._y, polyBPlane.normal._z, polyBPlane.w] 56 | const polyBPoints = polygonB.vertices.map(x => [x.pos._x, x.pos._y]) 57 | 58 | if (compareArrays(polyAPlaneData, polyBPlaneData) === false) { 59 | return false 60 | } 61 | if (compareArrays(polyAPoints, polyBPoints) === false) { 62 | return false 63 | } 64 | 65 | // console.log('polygon', polygon, otherPolygon) 66 | } 67 | return true 68 | } 69 | 70 | module.exports = areCSGsIdentical 71 | -------------------------------------------------------------------------------- /src/dataParamsActions.js: -------------------------------------------------------------------------------- 1 | // const limitFlow = require('./observable-utils/limitFlow') 2 | 3 | function actions (sources) { 4 | const {data$, params$} = sources 5 | 6 | const setEntitiesFromSolids$ = data$ 7 | // .thru(limitFlow(800)) 8 | /* .take(4) 9 | .merge( 10 | data$.debounce(100) 11 | ) */ 12 | .filter(data => data !== undefined && data.solids) 13 | .multicast() 14 | .map(data => ({type: 'setEntitiesFromSolids', data: data.solids})) 15 | 16 | const updateParams$ = params$ 17 | .multicast() 18 | .map(data => ({type: 'updateParams', data: data})) 19 | 20 | return [ 21 | setEntitiesFromSolids$, 22 | updateParams$ 23 | ] 24 | } 25 | 26 | module.exports = actions 27 | -------------------------------------------------------------------------------- /src/dataParamsReducers.js: -------------------------------------------------------------------------------- 1 | const entitiesFromSolids = require('./entitiesFromSolids') 2 | 3 | function makeReducers (initialState, regl) { 4 | const reducers = { 5 | setEntitiesFromSolids: (state, data, initialState, regl) => { 6 | const entities = entitiesFromSolids(initialState, data) 7 | // we need to update the render function to provide the new geometry data from entities 8 | // const render = prepareRender(regl, Object.assign({}, state, {entities})) 9 | const makeDrawMesh = require('./rendering/drawMesh/index') 10 | const drawCSGs = entities 11 | .map(e => makeDrawMesh(state.regl, {geometry: e.geometry})) 12 | return { 13 | entities, 14 | drawCommands: { 15 | drawCSGs 16 | } 17 | } 18 | }, 19 | updateParams: (state, data) => { 20 | // console.log('updateParams', data) 21 | if (data.camera && data.camera === 'reset') { 22 | return state 23 | } 24 | // console.log('updateParams', data) 25 | if ('camera' in data) { 26 | // to enable camera position from preset names 27 | if (data.camera && data.camera.position && !Array.isArray(data.camera.position)) { 28 | const {toPresetView} = require('./cameraAndControls/camera') 29 | const viewPresets = ['top', 'bottom', 'front', 'back', 'left', 'right'] 30 | if (viewPresets.includes(data.camera.position)) { 31 | const {merge} = require('./utils') 32 | data.camera = merge({}, data.camera, toPresetView(data.camera.position, state)) 33 | } else { 34 | throw new Error(`Unhandled camera position "${data.camera.position}" passed to viewer`) 35 | } 36 | } 37 | if (data.camera && data.camera.projectionType) { 38 | const projectionType = data.camera.projectionType 39 | const validTypes = ['orthographic', 'perspective'] 40 | if (!validTypes.includes(data.camera.projectionType)) { 41 | throw new Error(`Unhandled camera projection type "${data.camera.projectionType}" passed to viewer`) 42 | } 43 | const {fromPerspectiveToOrthographic, fromOrthographicToPerspective} = require('./cameraAndControls/camera') 44 | 45 | if (projectionType === 'orthographic' && state.camera.projectionType === 'perspective') { 46 | const camera = fromPerspectiveToOrthographic(state.camera) 47 | data.camera = camera 48 | } else if (projectionType === 'perspective' && state.camera.projectionType === 'orthographic') { 49 | const camera = fromOrthographicToPerspective(state.camera) 50 | data.camera = camera 51 | } 52 | } 53 | } 54 | if ('grid' in data) { 55 | if (data.grid && data.grid.size) { 56 | const {merge} = require('./utils') 57 | const makeDrawGrid = require('./rendering/drawGrid/multi') 58 | const {ticks, size} = Object.assign([], state.grid, data.grid) 59 | const drawGrid = makeDrawGrid(state.regl, {size, ticks}) 60 | data.drawCommands = merge({}, state.drawCommands, {drawGrid}) 61 | } 62 | } 63 | return data 64 | } 65 | } 66 | return reducers 67 | } 68 | 69 | module.exports = makeReducers 70 | -------------------------------------------------------------------------------- /src/entitiesFromSolids.js: -------------------------------------------------------------------------------- 1 | const mat4 = require('gl-mat4') 2 | const {flatten, toArray} = require('./utils') 3 | const csgToGeometries = require('./geometry-utils/csgToGeometries') 4 | const cagToGeometries = require('./geometry-utils/cagToGeometries') 5 | const computeBounds = require('./bound-utils/computeBounds') 6 | const areCSGsIdentical = require('./csg-utils/areCSGsIdentical') 7 | 8 | function entitiesFromSolids (baseParams, solids) { 9 | const defaultColor = baseParams.rendering.meshColor 10 | 11 | solids = toArray(solids) 12 | // warning !!! fixTJunctions alters the csg and can result in visual issues ?? 13 | // .fixTJunctions() 14 | // if (!params.csgCheck) { // || !areCSGsIdentical(csg, cachedSolids)) { 15 | // cachedSolids = solids 16 | // const start = performance.now() 17 | const entities = solids.map(function (solid) { 18 | let geometry 19 | let type 20 | if ('sides' in solid) { 21 | type = '2d' 22 | geometry = cagToGeometries(solid, {color: defaultColor}) 23 | } else { 24 | type = '3d' 25 | geometry = csgToGeometries(solid, { 26 | smoothLighting: baseParams.smoothNormals, 27 | normalThreshold: 0.3, 28 | faceColor: defaultColor})//, normalThreshold: 0}) 29 | } 30 | // geometry = flatten(geometries)// FXIME : ACTUALLY deal with arrays since a single csg can 31 | // generate multiple geometries if positions count is >65535 32 | geometry = flatten(geometry)[0] 33 | // const time = (performance.now() - start) / 1000 34 | // console.log(`Total time for geometry conversion: ${time} s`) 35 | // console.log('geometry', geometry) 36 | 37 | // bounds 38 | const bounds = computeBounds({geometry})// FXIME : ACTUALLY deal with arrays as inputs see above 39 | 40 | // transforms: for now not used, since all transformed are stored in the geometry 41 | const matrix = mat4.identity([]) 42 | 43 | const transforms = { 44 | matrix 45 | /* const modelViewMatrix = mat4.multiply(mat4.create(), model, props.camera.view) 46 | const normalMatrix = mat4.create() 47 | mat4.invert(normalMatrix, modelViewMatrix) 48 | mat4.transpose(normalMatrix, normalMatrix) 49 | return normalMatrix */ 50 | } 51 | 52 | const entity = {geometry, transforms, bounds, type} 53 | return entity 54 | }) 55 | // } 56 | return entities 57 | } 58 | 59 | module.exports = entitiesFromSolids 60 | -------------------------------------------------------------------------------- /src/geometry-utils/cagToGeometries.js: -------------------------------------------------------------------------------- 1 | 2 | const {flatten, toArray} = require('../utils') 3 | 4 | function cagToGeometries (cags, options) { 5 | const defaults = { 6 | color: [1, 0.4, 0, 1]// default color 7 | } 8 | const {color} = Object.assign({}, defaults, options) 9 | 10 | let points = cagToPointsArray(cags).map(x => [x[0], x[1], 0]) 11 | let normals = points.map(x => [0, 0, -1]) 12 | let colors = points.map(x => color) 13 | let indices = points.map((x, i) => i) // FIXME: temporary, not really needed, need to change drawMesh 14 | 15 | return [ 16 | { 17 | // indices, 18 | positions: points, 19 | normals: normals, 20 | colors: colors, 21 | indices 22 | }] 23 | } 24 | 25 | // FIXME same as in scad-api helpers... 26 | const cagToPointsArray = input => { 27 | let points 28 | if ('sides' in input) { // this is a cag 29 | points = [] 30 | input.sides.forEach(side => { 31 | points.push([side.vertex0.pos.x, side.vertex0.pos.y]) 32 | points.push([side.vertex1.pos.x, side.vertex1.pos.y]) 33 | }) 34 | // cag.sides.map(side => [side.vertex0.pos.x, side.vertex0.pos.y]) 35 | //, side.vertex1.pos.x, side.vertex1.pos.y]) 36 | // due to the logic of CAG.fromPoints() 37 | // move the first point to the last 38 | /* if (points.length > 0) { 39 | points.push(points.shift()) 40 | } */ 41 | } else if ('points' in input) { 42 | points = input.points.map(p => ([p.x, p.y])) 43 | } 44 | 45 | return points 46 | } 47 | 48 | module.exports = cagToGeometries 49 | -------------------------------------------------------------------------------- /src/geometry-utils/csgToGeometries.js: -------------------------------------------------------------------------------- 1 | const vec3 = require('gl-vec3') 2 | 3 | /** 4 | * convert a CSG from csg.js to an array of geometries with positions, normals, colors & indices 5 | * typically used for example to display the csg data in a webgl wiever 6 | * @param {Array} csgs single or an array of CSG object 7 | * @param {Object} options options hash 8 | * @param {Boolean} options.smoothLighting=false set to true if we want to use interpolated vertex normals 9 | * this creates nice round spheres but does not represent the shape of the actual model 10 | * @param {Float} options.normalThreshold=0.349066 threshold beyond which to split normals // 20 deg 11 | * @param {String} options.faceColor='#FF000' hex color 12 | * @returns {Object} {indices, positions, normals, colors} 13 | */ 14 | function csgToGeometries (csgs, options) { 15 | const defaults = { 16 | smoothLighting: false, // set to true if we want to use interpolated vertex normals this creates nice round spheres but does not represent the shape of the actual model 17 | normalThreshold: 0.349066, // 20 deg 18 | faceColor: [1, 0.4, 0, 1]// default color 19 | } 20 | const {smoothLighting, normalThreshold, faceColor} = Object.assign({}, defaults, options) 21 | const faceColorRgb = faceColor === undefined ? undefined : normalizedColor(faceColor) // TODO : detect if hex or rgba 22 | 23 | csgs = toArray(csgs) 24 | const geometriesPerCsg = csgs.map(convert) 25 | 26 | function convert (csg) { 27 | let geometries = [] 28 | 29 | let positions = [] 30 | let colors = [] 31 | let normals = [] 32 | let indices = [] 33 | 34 | const polygons = csg.canonicalized().toPolygons() 35 | 36 | /* let positions = new Float32Array(faces * 3 * 3) 37 | let normals = new Float32Array(faces * 3 * 3) */ 38 | 39 | let normalPositionLookup = [] 40 | normalPositionLookup = {} 41 | let tupplesIndex = 0 42 | 43 | for (let i = 0; i < polygons.length; i++) { 44 | const polygon = polygons[i] 45 | 46 | const color = polygonColor(polygon, faceColorRgb) 47 | const rawNormal = polygon.plane.normal 48 | const normal = [rawNormal.x, rawNormal.y, rawNormal.z] 49 | 50 | const polygonIndices = [] 51 | // we need unique tupples of normal + position , that gives us a specific index (indices) 52 | // if the angle between a given normal and another normal is less than X they are considered the same 53 | for (let j = 0; j < polygon.vertices.length; j++) { 54 | let index 55 | 56 | const vertex = polygon.vertices[j] 57 | const position = [vertex.pos.x, vertex.pos.y, vertex.pos.z] 58 | 59 | if (smoothLighting) { 60 | const candidateTupple = {normal, position} 61 | const existingTupple = fuzyNormalAndPositionLookup(normalPositionLookup, candidateTupple, normalThreshold) 62 | if (!existingTupple) { 63 | const existingPositing = normalPositionLookup[candidateTupple.position] 64 | const itemToAdd = [{normal: candidateTupple.normal, index: tupplesIndex}] 65 | if (!existingPositing) { 66 | normalPositionLookup[candidateTupple.position] = itemToAdd 67 | } else { 68 | normalPositionLookup[candidateTupple.position] = normalPositionLookup[candidateTupple.position] 69 | .concat(itemToAdd) 70 | } 71 | index = tupplesIndex 72 | // normalPositionLookup.push(candidateTupple) 73 | // index = normalPositionLookup.length - 1 74 | if (faceColor !== undefined) { 75 | colors.push(color) 76 | } 77 | normals.push(normal) 78 | positions.push(position) 79 | tupplesIndex += 1 80 | } else { 81 | index = existingTupple.index 82 | } 83 | } else { 84 | if (faceColor !== undefined) { 85 | colors.push(color) 86 | } 87 | normals.push(normal) 88 | positions.push(position) 89 | index = positions.length - 1 90 | } 91 | 92 | // let prevcolor = colors[index] 93 | polygonIndices.push(index) 94 | } 95 | 96 | for (let j = 2; j < polygonIndices.length; j++) { 97 | indices.push([polygonIndices[0], polygonIndices[j - 1], polygonIndices[j]]) 98 | } 99 | 100 | // if too many vertices or we are at the end, start a new geometry 101 | if (positions.length > 65000 || i === polygons.length - 1) { 102 | // special case to deal with face color SPECICIALLY SET TO UNDEFINED 103 | if (faceColor === undefined) { 104 | geometries.push({ 105 | indices, 106 | positions, 107 | normals 108 | }) 109 | } else { 110 | geometries.push({ 111 | indices, 112 | positions, 113 | normals, 114 | colors 115 | }) 116 | } 117 | } 118 | } 119 | return geometries 120 | } 121 | 122 | return geometriesPerCsg 123 | } 124 | 125 | /** 126 | * converts input data to array if it is not already an array 127 | * @param {Any} input data: can be null or undefined, an array , an object etc 128 | * @returns {Array} if inital data was an array it returns it unmodified, 129 | * otherwise a 0 or one element array 130 | */ 131 | function toArray (data) { 132 | if (data === undefined || data === null) { return [] } 133 | if (data.constructor !== Array) { return [data] } 134 | return data 135 | } 136 | 137 | /** 138 | * convert color from rgba object to the array of bytes 139 | * @param {Object} color `{r: r, g: g, b: b, a: a}` 140 | * @returns {Array} `[r, g, b, a]` 141 | */ 142 | function colorBytes (colorRGBA) { 143 | let result = [colorRGBA.r, colorRGBA.g, colorRGBA.b] 144 | if (colorRGBA.a !== undefined) result.push(colorRGBA.a) 145 | return result 146 | } 147 | /** determine if input is a hex (color) or not 148 | * @param {Object} object a string, array, object , whatever 149 | * @returns {Boolean} wether the input is a hex string or not 150 | */ 151 | function isHexColor (object) { 152 | if (typeof sNum !== 'string') { 153 | return false 154 | } 155 | return /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(object) 156 | } 157 | 158 | // modified from https://stackoverflow.com/questions/21646738/convert-hex-to-rgba 159 | function hexToRgbNormalized (hex, alpha) { 160 | hex = hex.replace('#', '') 161 | const r = parseInt(hex.length === 3 ? hex.slice(0, 1).repeat(2) : hex.slice(0, 2), 16) 162 | const g = parseInt(hex.length === 3 ? hex.slice(1, 2).repeat(2) : hex.slice(2, 4), 16) 163 | const b = parseInt(hex.length === 3 ? hex.slice(2, 3).repeat(2) : hex.slice(4, 6), 16) 164 | return (alpha ? [r, g, b, alpha] : [r, g, b, 255]).map(x => x / 255) 165 | } 166 | 167 | /** outputs a normalized [0...1] range, 4 component array color 168 | * @param {} input 169 | */ 170 | function normalizedColor (input) { 171 | if (isHexColor(input)) { 172 | return hexToRgbNormalized(input) 173 | } else if (Array.isArray(input) && input.length >= 3) { 174 | input = input.length < 4 ? [input[0], input[1], input[2], 1] : input.slice(0, 4) 175 | if (input[0] > 1 || input[1] > 1 || input[2] > 1) { 176 | return input.map(x => x / 255) 177 | } 178 | return input 179 | } 180 | } 181 | 182 | /** 183 | * return the color information of a polygon 184 | * @param {Object} polygon a csg.js polygon 185 | * @param {Object} faceColor a hex color value to default to 186 | * @returns {Array} `[r, g, b, a]` 187 | */ 188 | function polygonColor (polygon, faceColor) { 189 | let color = faceColor 190 | 191 | if (polygon.shared && polygon.shared.color) { 192 | color = polygon.shared.color 193 | } else if (polygon.color) { 194 | color = polygon.color 195 | } 196 | // opaque is default 197 | if (color !== undefined && color.length < 4) { 198 | color.push(1.0) 199 | } 200 | return color 201 | } 202 | 203 | /** 204 | * determine if the two given normals are 'similar' ie if the distance/angle between the 205 | * two is less than the given threshold 206 | * @param {Array} normal a 3 component array normal 207 | * @param {Array} otherNormal another 3 component array normal 208 | * @returns {Boolean} true if the two normals are similar 209 | */ 210 | function areNormalsSimilar (normal, otherNormal, threshold) { 211 | return vec3.distance(normal, otherNormal) <= threshold 212 | // angle computation is too slow but actually precise 213 | // return vec3.angle(normal, otherNormal) <= threshold 214 | } 215 | 216 | function fuzyNormalAndPositionLookup (normalPositionLookup, toCompare, normalThreshold = 0.349066) { 217 | const normalsCandidates = normalPositionLookup[toCompare.position] 218 | if (normalsCandidates) { 219 | // normalPositionLookup[toCompare.position] = normalPositionLookup[toCompare.position].concat([toCompare.normal]) 220 | // get array of normals with same position 221 | for (let i = 0; i < normalsCandidates.length; i++) { 222 | const normal = normalsCandidates[i].normal 223 | const similarNormal = areNormalsSimilar(normal, toCompare.normal, normalThreshold) 224 | const similar = similarNormal 225 | if (similar) { 226 | return {tupple: {position: toCompare.position, normal}, index: normalsCandidates[i].index} 227 | } 228 | } 229 | } 230 | return undefined 231 | } 232 | 233 | module.exports = csgToGeometries 234 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // const {pointerGestures} = require('most-gestures') 2 | const most = require('most') 3 | const {proxy} = require('most-proxy') 4 | const {holdSubject} = require('./observable-utils/most-subject/index') 5 | // require('most-subject')github:briancavalier/most-subject : issues with webpack hence the above 6 | const makeCameraControlsActions = require('./cameraControlsActions') 7 | const makeDataParamsActions = require('./dataParamsActions') 8 | const makeState = require('./state') 9 | const {merge} = require('./utils') 10 | const prepareRender = require('./rendering/render') 11 | 12 | // FIXME : element needs to be either canvas, other htmlelement or gl context 13 | const makeCsgViewer = function (element, options = {}, inputs$ = most.never()) { 14 | const defaults = { 15 | glOptions: {// all lower level regl options passed directly through, webgl ones are under attributes 16 | attributes: { 17 | alpha: false 18 | } 19 | }, 20 | // after this , initial params of camera, controls & render 21 | camera: require('./cameraAndControls/perspectiveCamera').defaults, 22 | controls: require('./cameraAndControls/orbitControls').defaults, 23 | // 24 | grid: { 25 | show: false, 26 | size: [200, 200], 27 | ticks: [10, 1], 28 | color: [1, 1, 1, 1], 29 | fadeOut: true // when set to true, the grid fades out progressively in the distance 30 | }, 31 | axes: { 32 | show: true 33 | }, 34 | rendering: { 35 | background: [1, 1, 1, 1], 36 | meshColor: [1, 0.5, 0.5, 1], // use as default face color for csgs, color for cags 37 | lightColor: [1, 1, 1, 1], 38 | lightDirection: [0.2, 0.2, 1], 39 | lightPosition: [100, 200, 100], 40 | ambientLightAmount: 0.3, 41 | diffuseLightAmount: 0.89, 42 | specularLightAmount: 0.16, 43 | materialShininess: 8.0 44 | }, 45 | // 46 | behaviours: { 47 | resetViewOn: [], // ['new-entities'], 48 | zoomToFitOn: [] // ['new-entities'], 49 | }, 50 | // next few are for solids / csg/ cags specifically 51 | overrideOriginalColors: false, // for csg/cag conversion: do not use the original (csg) color, use meshColor instead 52 | smoothNormals: true, 53 | // 54 | entities: [], // inner representation of the CSG's geometry + meta (bounds etc) 55 | csgCheck: false, // not used currently 56 | // draw commands 57 | drawCommands: {}, 58 | 59 | useGestures: true // toggle if you want to use external inputs to control camera etc 60 | } 61 | let state = merge({}, defaults, options) 62 | 63 | // we use an observable of parameters, date etc to play nicely with the other observables 64 | // note: subjects are anti patterns, but they simplify things here so ok for now 65 | const params$ = holdSubject() 66 | const data$ = holdSubject() 67 | const errors$ = holdSubject() 68 | const { attach, stream } = proxy() 69 | const state$ = stream 70 | 71 | // initialize regl options 72 | const reglOptions = Object.assign( 73 | {}, { 74 | canvas: (element.nodeName.toLowerCase() === 'canvas') ? element : undefined, 75 | container: (element.nodeName.toLowerCase() !== 'canvas') ? element : undefined 76 | }, 77 | state.glOptions, 78 | { 79 | onDone: function (err, callback) { 80 | if (err) { 81 | errors$.next(err) 82 | } 83 | } 84 | } 85 | ) 86 | const regl = require('regl')(reglOptions) 87 | 88 | // note we keep the render function around, until we need to swap it out in case of new data 89 | state.render = prepareRender(regl, state) 90 | state.regl = regl 91 | state.drawCommands.drawGrid = () => {} 92 | state.drawCommands.drawCSGs = [] 93 | 94 | const sources$ = { 95 | // data streams 96 | params$: params$.filter(x => x !== undefined).multicast(), // we filter out pointless data from the get go 97 | data$: data$.filter(x => x !== undefined), // we filter out pointless data from the get go 98 | state$, // thanks to proxying, we also have access to the state observable/stream inside our actions 99 | // inputs$: inputs$.filter(x => x !== undefined), // custom user inputs 100 | // live ui elements only 101 | gestures: state.useGestures ? require('most-gestures').pointerGestures(element) : {drags: most.never(), zooms: most.never(), taps: most.never()}, 102 | resizes$: state.useGestures ? require('./cameraAndControls/elementSizing')(element) : most.never(), 103 | heartBeat$: require('./observable-utils/rafStream').rafStream() // state.useGestures ? require('./observable-utils/rafStream').rafStream() : most.never() // heartbeat provided by requestAnimationFrame 104 | } 105 | // create our action streams 106 | const cameraControlsActions = makeCameraControlsActions(sources$) 107 | const dataParamsActions = makeDataParamsActions(sources$) 108 | const actions = most.mergeArray(dataParamsActions.concat(cameraControlsActions)) 109 | // combine proxy state & real state 110 | attach(makeState(actions, state, regl)) 111 | 112 | // .startWith(state) 113 | // skipRepeatsWith 114 | // re-render whenever state changes, since visuals are a function of the state 115 | state$.forEach(state => { 116 | // console.log('sending data for render', state) 117 | state.render(state) 118 | }) 119 | // dispatch initial params/state 120 | params$.next(state) 121 | 122 | /** main viewer function : call this one with different parameters and/or data to update the viewer 123 | * @param {Object} options={} 124 | * @param {Object} data 125 | */ 126 | const csgViewer = function (params = {}, data) { 127 | // dispatch data & params 128 | data$.next(data) 129 | params$.next(params) 130 | } 131 | return {csgViewer, viewerDefaults: defaults, viewerState$: state$} 132 | } 133 | 134 | module.exports = makeCsgViewer 135 | -------------------------------------------------------------------------------- /src/observable-utils/limitFlow.js: -------------------------------------------------------------------------------- 1 | // LimtiFlow by Nathan Ridley(axefrog) from https://gist.github.com/axefrog/84ec77c5f620dab5cdb7dd61e6f1df0b 2 | function limitFlow (period) { 3 | return function limitFlow (stream) { 4 | const source = new RateLimitSource(stream.source, period) 5 | return new stream.constructor(source) 6 | } 7 | } 8 | 9 | class RateLimitSource { 10 | constructor (source, period) { 11 | this.source = source 12 | this.period = period 13 | } 14 | 15 | run (sink, scheduler) { 16 | return this.source.run(new RateLimitSink(this, sink, scheduler), scheduler) 17 | } 18 | } 19 | 20 | class RateLimitSink { 21 | constructor (source, sink, scheduler) { 22 | this.source = source 23 | this.sink = sink 24 | this.scheduler = scheduler 25 | this.nextTime = 0 26 | this.buffered = void 0 27 | } 28 | 29 | _run (t) { 30 | if (this.buffered === void 0) { 31 | return 32 | } 33 | const x = this.buffered 34 | const now = this.scheduler.now() 35 | const period = this.source.period 36 | const nextTime = this.nextTime 37 | this.buffered = void 0 38 | this.nextTime = (nextTime + period > now ? nextTime : now) + period 39 | this.sink.event(t, x) 40 | } 41 | 42 | event (t, x) { 43 | const nothingScheduled = this.buffered === void 0 44 | this.buffered = x 45 | const task = new RateLimitTask(this) 46 | const nextTime = this.nextTime 47 | if (t >= nextTime) { 48 | this.scheduler.asap(task) 49 | } 50 | else if (nothingScheduled) { 51 | const interval = this.nextTime - this.scheduler.now() 52 | this.scheduler.delay(interval, new RateLimitTask(this)) 53 | } 54 | } 55 | 56 | end (t, x) { 57 | this._run(t) 58 | this.sink.end(t, x) 59 | } 60 | 61 | error (t, e) { 62 | this.sink.error(t, e) 63 | } 64 | } 65 | 66 | class RateLimitTask { 67 | constructor (sink) { 68 | this.sink = sink 69 | } 70 | 71 | run (t) { 72 | if (this.disposed) { 73 | return 74 | } 75 | this.sink._run(t) 76 | } 77 | 78 | error (t, e) { 79 | if (this.disposed) { 80 | return 81 | } 82 | this.sink.error(t, e) 83 | } 84 | 85 | dispose () { 86 | this.disposed = true 87 | } 88 | } 89 | module.exports = limitFlow -------------------------------------------------------------------------------- /src/observable-utils/most-subject/Subject.js: -------------------------------------------------------------------------------- 1 | const {Stream} = require('most') 2 | 3 | /** The base Subject class, which is an extension of Stream 4 | * @typedef {Object} Subject 5 | */ 6 | class Subject extends Stream { 7 | /** 8 | * Push a new value to the stream 9 | * 10 | * @method next 11 | * 12 | * @param {any} value The value you want to push to the stream 13 | */ 14 | next (value) { 15 | this.source.next(value) 16 | } 17 | 18 | /** 19 | * Push an error and end the stream 20 | * 21 | * @method error 22 | * 23 | * @param {Error} err The error you would like to push to the stream 24 | */ 25 | error (err) { 26 | this.source.error(err) 27 | } 28 | 29 | /** 30 | * Ends the stream with an optional value 31 | * 32 | * @method complete 33 | * 34 | * @param {any} value The value you would like to end the stream on 35 | */ 36 | complete (value) { 37 | this.source.complete(value) 38 | } 39 | } 40 | 41 | module.exports = {Subject} 42 | -------------------------------------------------------------------------------- /src/observable-utils/most-subject/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | const {Subject} = require('./Subject') 3 | const {SubjectSource} = require('./source/SubjectSource') 4 | const {HoldSubjectSource} = require('./source/HoldSubjectSource') 5 | 6 | /** 7 | * Creates a new Subject 8 | * 9 | * @return {Subject} {@link Subject} 10 | * 11 | * @example 12 | * const {subject} = require( 'most-subject' 13 | * 14 | * const stream = subject() 15 | * 16 | * stream.map(fn).observe(x => console.log(x)) 17 | * // 1 18 | * // 2 19 | * 20 | * stream.next(1) 21 | * stream.next(2) 22 | * setTimeout(() => stream.complete(), 10) 23 | */ 24 | function subject () { 25 | return new Subject(new SubjectSource()) 26 | } 27 | 28 | /** 29 | * Create a subject with a buffer to keep = require( missing events. 30 | * 31 | * @param {number} bufferSize = 1 The maximum size of the 32 | * buffer to create. 33 | * 34 | * @return {Subject} {@link Subject} 35 | * 36 | * @example 37 | * const {holdSubject} = require( 'most-subject' 38 | * 39 | * const stream = holdSubject(3) 40 | * 41 | * stream.next(1) 42 | * stream.next(2) 43 | * stream.next(3) 44 | * 45 | * stream.map(fn).observe(x => console.log(x)) 46 | * // 1 47 | * // 2 48 | * // 3 49 | * 50 | * setTimeout(() => stream.complete(), 10) 51 | */ 52 | function holdSubject (bufferSize = 1) { 53 | if (bufferSize <= 0) { 54 | throw new Error('First and only argument to holdSubject `bufferSize` ' + 55 | 'must be an integer 1 or greater') 56 | } 57 | return new Subject(new HoldSubjectSource(bufferSize)) 58 | } 59 | module.exports = {subject, holdSubject} 60 | -------------------------------------------------------------------------------- /src/observable-utils/most-subject/source/HoldSubjectSource.js: -------------------------------------------------------------------------------- 1 | const {SubjectSource} = require('./SubjectSource') 2 | const {drop, append} = require('@most/prelude') 3 | 4 | class HoldSubjectSource extends SubjectSource { 5 | constructor (bufferSize) { 6 | super() 7 | this.bufferSize = bufferSize 8 | this.buffer = [] 9 | } 10 | 11 | add (sink) { 12 | const buffer = this.buffer 13 | if (buffer.length > 0) { 14 | pushEvents(buffer, sink) 15 | } 16 | super.add(sink) 17 | } 18 | 19 | next (value) { 20 | if (!this.active) { return } 21 | const time = this.scheduler.now() 22 | this.buffer = dropAndAppend({time, value}, this.buffer, this.bufferSize) 23 | this._next(time, value) 24 | } 25 | } 26 | 27 | function pushEvents (buffer, sink) { 28 | for (let i = 0; i < buffer.length; ++i) { 29 | const {time, value} = buffer[i] 30 | sink.event(time, value) 31 | } 32 | } 33 | 34 | function dropAndAppend (event, buffer, bufferSize) { 35 | if (buffer.length >= bufferSize) { 36 | return append(event, drop(1, buffer)) 37 | } 38 | return append(event, buffer) 39 | } 40 | 41 | module.exports = {HoldSubjectSource} 42 | -------------------------------------------------------------------------------- /src/observable-utils/most-subject/source/SubjectSource.js: -------------------------------------------------------------------------------- 1 | const defaultScheduler = require('most/lib/scheduler/defaultScheduler').default 2 | const {MulticastSource} = require('@most/multicast') 3 | 4 | function SubjectSource () { 5 | this.scheduler = defaultScheduler 6 | this.sinks = [] 7 | this.active = true 8 | } 9 | 10 | // Source methods 11 | SubjectSource.prototype.run = function (sink, scheduler) { 12 | const n = this.add(sink) 13 | if (n === 1) { this.scheduler = scheduler } 14 | return new SubjectDisposable(this, sink) 15 | } 16 | 17 | SubjectSource.prototype._dispose = function dispose () { 18 | this.active = false 19 | } 20 | 21 | // Subject methods 22 | SubjectSource.prototype.next = function next (value) { 23 | if (!this.active) { return } 24 | this._next(this.scheduler.now(), value) 25 | } 26 | 27 | SubjectSource.prototype.error = function error (err) { 28 | if (!this.active) { return } 29 | 30 | this.active = false 31 | this._error(this.scheduler.now(), err) 32 | } 33 | 34 | SubjectSource.prototype.complete = function complete (value) { 35 | if (!this.active) { return } 36 | 37 | this.active = false 38 | this._complete(this.scheduler.now(), value, this.sink) 39 | } 40 | 41 | // Multicasting methods 42 | SubjectSource.prototype.add = MulticastSource.prototype.add 43 | SubjectSource.prototype.remove = MulticastSource.prototype.remove 44 | SubjectSource.prototype._next = MulticastSource.prototype.event 45 | SubjectSource.prototype._complete = MulticastSource.prototype.end 46 | SubjectSource.prototype._error = MulticastSource.prototype.error 47 | 48 | // SubjectDisposable 49 | function SubjectDisposable (source, sink) { 50 | this.source = source 51 | this.sink = sink 52 | this.disposed = false 53 | } 54 | 55 | SubjectDisposable.prototype.dispose = function () { 56 | if (this.disposed) { return } 57 | this.disposed = true 58 | const remaining = this.source.remove(this.sink) 59 | return remaining === 0 && this.source._dispose() 60 | } 61 | module.exports = {SubjectSource} -------------------------------------------------------------------------------- /src/observable-utils/most-subject/utils.js: -------------------------------------------------------------------------------- 1 | function tryEvent (time, value, sink) { 2 | try { 3 | sink.event(time, value) 4 | } catch (err) { 5 | sink.error(time, err) 6 | } 7 | } 8 | 9 | function tryEnd (time, value, sink) { 10 | try { 11 | sink.end(time, value) 12 | } catch (err) { 13 | sink.error(time, err) 14 | } 15 | } 16 | module.exports = {tryEvent, tryEnd} -------------------------------------------------------------------------------- /src/observable-utils/rafStream.js: -------------------------------------------------------------------------------- 1 | // taken from https://github.com/briancavalier/most-behavior/blob/2888b2b69fe2c8e44617c611eb5fdaf512d52007/src/animationFrames.js 2 | const { Stream } = require('most') 3 | const {create} = require('@most/create') 4 | 5 | function animationFrames () { 6 | return new Stream(new AnimationFramesSource()) 7 | } 8 | 9 | const recurse = (cancel, schedule) => (sink, scheduler) => { 10 | let canceler = new Cancel(cancel) 11 | const onNext = x => { 12 | sink.event(scheduler.now(), x) 13 | cancel.key = schedule(onNext) 14 | } 15 | cancel.key = schedule(onNext) 16 | 17 | return canceler 18 | } 19 | 20 | const _animationFrames = recurse(cancelAnimationFrame, requestAnimationFrame) 21 | 22 | class AnimationFramesSource { 23 | run (sink, scheduler) { 24 | return _animationFrames(sink, scheduler) 25 | } 26 | } 27 | 28 | class Cancel { 29 | constructor (cancel) { 30 | this.cancel = cancel 31 | this.key = undefined 32 | } 33 | dispose () { 34 | this.cancel(this.key) 35 | } 36 | } 37 | 38 | /* alternative version */ 39 | function rafStream () { 40 | const stream = create((add, end, error) => { 41 | function step (timestamp) { 42 | add(null) 43 | window.requestAnimationFrame(step) 44 | } 45 | window.requestAnimationFrame(step) 46 | }) 47 | return stream 48 | } 49 | module.exports = {rafStream, animationFrames} -------------------------------------------------------------------------------- /src/rendering/basic.vert: -------------------------------------------------------------------------------- 1 | precision mediump float; 2 | 3 | uniform float camNear, camFar; 4 | uniform mat4 model, view, projection; 5 | 6 | attribute vec3 position; 7 | varying vec3 fragNormal, fragPosition; 8 | varying vec4 worldPosition; 9 | 10 | //#pragma glslify: zBufferAdjust = require('./zBufferAdjust') 11 | 12 | void main() { 13 | //fragNormal = normal; 14 | fragPosition = position; 15 | worldPosition = model * vec4(position, 1); 16 | vec4 glPosition = projection * view * worldPosition; 17 | gl_Position = glPosition; 18 | //gl_Position = zBufferAdjust(glPosition, camNear, camFar); 19 | } 20 | -------------------------------------------------------------------------------- /src/rendering/drawAxis.js: -------------------------------------------------------------------------------- 1 | const mat4 = require('gl-mat4') 2 | 3 | const drawAxis = function (regl, params) { 4 | const defaults = { 5 | xColor: [1, 0, 0, 1], 6 | yColor: [0, 1, 0, 1], 7 | zColor: [0, 0, 1, 1], 8 | size: 10, 9 | lineWidth: 3, // FIXME/ linewidth has been "deprecated" in multiple browsers etc, need a workaround, 10 | alwaysVisible: true // to have the widget alway visible 'on top' of the rest of the scene 11 | } 12 | let {size, xColor, yColor, zColor, lineWidth, alwaysVisible} = Object.assign({}, defaults, params) 13 | 14 | if (lineWidth > regl.limits.lineWidthDims[1]) { 15 | lineWidth = regl.limits.lineWidthDims[1] 16 | } 17 | const points = [ 18 | 0, 0, 0, 19 | size, 0, 0 20 | ] 21 | 22 | const commandParams = { 23 | frag: `precision mediump float; 24 | uniform vec4 color; 25 | void main() { 26 | gl_FragColor = color; 27 | }`, 28 | vert: ` 29 | precision mediump float; 30 | attribute vec3 position; 31 | uniform mat4 model, view, projection; 32 | void main() { 33 | gl_Position = projection * view * model * vec4(position, 1); 34 | }`, 35 | 36 | uniforms: { 37 | model: (context, props) => props && props.model ? props.model : mat4.identity([]), 38 | color: (context, props) => props.color, 39 | angle: (contet, props) => props.angle 40 | }, 41 | attributes: { 42 | position: points 43 | }, 44 | count: points.length / 3, 45 | primitive: 'line loop', 46 | lineWidth, 47 | depth: { 48 | enable: !alwaysVisible // disable depth testing to have the axis widget 'alway on top' of other items in the 3d viewer 49 | } 50 | } 51 | 52 | const xAxisModel = mat4.identity([]) 53 | const yAxisModel = mat4.rotateZ(mat4.create(), mat4.identity([]), Math.PI / 2) 54 | const zAxisModel = mat4.rotateY(mat4.create(), mat4.identity([]), -Math.PI / 2) 55 | let single = regl(commandParams) 56 | return () => single([ 57 | { color: xColor, model: xAxisModel }, // X 58 | { color: yColor, model: yAxisModel }, // Y 59 | { color: zColor, model: zAxisModel } // Z 60 | ]) 61 | } 62 | 63 | module.exports = drawAxis 64 | -------------------------------------------------------------------------------- /src/rendering/drawGrid/index.js: -------------------------------------------------------------------------------- 1 | const glslify = require('glslify')// -sync') // works in client & server 2 | const mat4 = require('gl-mat4') 3 | const path = require('path') 4 | 5 | module.exports = function prepareDrawGrid (regl, params = {}) { 6 | let positions = [] 7 | const defaults = { 8 | color: [1, 1, 1, 1], 9 | ticks: 1, 10 | size: [16, 16], 11 | fadeOut: false, 12 | centered: false, 13 | lineWidth: 2 14 | } 15 | 16 | let {size, ticks, fadeOut, centered, lineWidth, color} = Object.assign({}, defaults, params) 17 | 18 | const width = size[0] 19 | const length = size[1] 20 | 21 | if (false) { 22 | const halfWidth = width * 0.5 23 | const halfLength = length * 0.5 24 | // const gridLine = 25 | positions.push(-halfWidth, 0, 0) 26 | positions.push(halfWidth, 0, 0) 27 | } 28 | 29 | if (centered) { 30 | const halfWidth = width * 0.5 31 | const halfLength = length * 0.5 32 | 33 | const remWidth = halfWidth % ticks 34 | const widthStart = -halfWidth + remWidth 35 | const widthEnd = -widthStart 36 | 37 | const remLength = halfLength % ticks 38 | let lengthStart = -halfLength + remLength 39 | const lengthEnd = -lengthStart 40 | 41 | const skipEvery = 0 42 | 43 | for (let i = widthStart, j = 0; i <= widthEnd; i += ticks, j += 1) { 44 | if (j % skipEvery !== 0) { 45 | positions.push(lengthStart, i, 0) 46 | positions.push(lengthEnd, i, 0) 47 | positions.push(lengthStart, i, 0) 48 | } 49 | } 50 | for (let i = lengthStart, j = 0; i <= lengthEnd; i += ticks, j += 1) { 51 | if (j % skipEvery !== 0) { 52 | positions.push(i, widthStart, 0) 53 | positions.push(i, widthEnd, 0) 54 | positions.push(i, widthStart, 0) 55 | } 56 | } 57 | } else { 58 | for (let i = -width * 0.5; i <= width * 0.5; i += ticks) { 59 | positions.push(-length * 0.5, i, 0) 60 | positions.push(length * 0.5, i, 0) 61 | positions.push(-length * 0.5, i, 0) 62 | } 63 | 64 | for (let i = -length * 0.5; i <= length * 0.5; i += ticks) { 65 | positions.push(i, -width * 0.5, 0) 66 | positions.push(i, width * 0.5, 0) 67 | positions.push(i, -width * 0.5, 0) 68 | } 69 | } 70 | 71 | return regl({ 72 | vert: glslify(path.join(__dirname, '/../basic.vert')), 73 | frag: glslify(path.join(__dirname, '/shaders/grid.frag')), 74 | 75 | attributes: { 76 | position: regl.buffer(positions) 77 | }, 78 | count: positions.length / 3, 79 | uniforms: { 80 | model: (context, props) => props && props.model ? props.model : mat4.identity([]), 81 | color: (context, props) => props && props.color ? props.color : color, 82 | fogColor: (context, props) => props && props.color ? [props.color[0], props.color[1], props.color[2], 0] 83 | : [color[0], color[1], color[2], 0.0], 84 | fadeOut: (context, props) => props && props.fadeOut !== undefined ? props.fadeOut : fadeOut 85 | }, 86 | lineWidth: (context, props) => Math.min((props && props.lineWidth ? props.lineWidth : lineWidth), regl.limits.lineWidthDims[1]), 87 | primitive: 'lines', 88 | cull: { 89 | enable: true, 90 | face: 'front' 91 | }, 92 | polygonOffset: { 93 | enable: true, 94 | offset: { 95 | factor: 1, 96 | units: Math.random() * 10 97 | } 98 | }, 99 | blend: { 100 | enable: true, 101 | func: { 102 | src: 'src alpha', 103 | dst: 'one minus src alpha' 104 | } 105 | } 106 | 107 | }) 108 | } 109 | 110 | /* alternate rendering method 111 | 112 | let count = 80 113 | let offset = 10 114 | const datas = Array(80).fill(0) 115 | .map(function (v, i) { 116 | const model = mat4.translate(mat4.identity([]), mat4.identity([]), [0, i * offset - (count * 0.5 * offset), 0]) 117 | return { 118 | color: gridColor, fadeOut, model 119 | } 120 | }) 121 | const datas2 = Array(80).fill(0) 122 | .map(function (v, i) { 123 | let model 124 | model = mat4.rotateZ(mat4.identity([]), mat4.identity([]), 1.5708) 125 | model = mat4.translate(model, model, [0, i * offset - (count * 0.5 * offset), 0]) 126 | return { 127 | color: gridColor, fadeOut, model 128 | } 129 | }) 130 | 131 | count = 80 132 | offset = 1 133 | const datas3 = Array(80).fill(0) 134 | .map(function (v, i) { 135 | const model = mat4.translate(mat4.identity([]), mat4.identity([]), [0, i * offset - (count * 0.5 * offset), 0]) 136 | return { 137 | color: subGridColor, fadeOut, model 138 | } 139 | }) 140 | const datas4 = Array(80).fill(0) 141 | .map(function (v, i) { 142 | let model 143 | model = mat4.rotateZ(mat4.identity([]), mat4.identity([]), 1.5708) 144 | model = mat4.translate(model, model, [0, i * offset - (count * 0.5 * offset), 0]) 145 | return { 146 | color: subGridColor, fadeOut, model 147 | } 148 | }) 149 | // const model = mat4.translate(mat4.identity([]), mat4.identity([]), [0, 50, 0]) 150 | drawGrid(datas)// {color: gridColor, fadeOut, model}) 151 | drawGrid(datas2) 152 | 153 | drawGrid(datas3)// {color: gridColor, fadeOut, model}) 154 | drawGrid(datas4) */ 155 | -------------------------------------------------------------------------------- /src/rendering/drawGrid/multi.js: -------------------------------------------------------------------------------- 1 | function makeDrawMultiGrid (regl, params) { 2 | const {size, ticks} = params 3 | const drawMainGrid = require('./index')(regl, {size, ticks: ticks[0]}) 4 | const drawSubGrid = require('./index')(regl, {size, ticks: ticks[1]}) 5 | const drawGrid = (props) => { 6 | drawMainGrid(props) 7 | drawSubGrid({color: props.subColor, fadeOut: props.fadeOut}) 8 | } 9 | return drawGrid 10 | } 11 | 12 | module.exports = makeDrawMultiGrid 13 | -------------------------------------------------------------------------------- /src/rendering/drawGrid/shaders/grid.frag: -------------------------------------------------------------------------------- 1 | precision mediump float; 2 | uniform vec4 color; 3 | varying vec3 fragNormal, fragPosition; 4 | varying vec4 worldPosition; 5 | 6 | uniform vec4 fogColor; 7 | uniform bool fadeOut; 8 | void main() { 9 | float dist = .5; 10 | if(fadeOut){ 11 | dist = distance( vec2(0.,0.), vec2(worldPosition.x, worldPosition.y)); 12 | dist *= 0.0025; 13 | dist = sqrt(dist); 14 | //dist = clamp(dist, 0.0, 1.0); 15 | } 16 | 17 | gl_FragColor = mix(color, fogColor, dist); 18 | } 19 | -------------------------------------------------------------------------------- /src/rendering/drawMesh.js: -------------------------------------------------------------------------------- 1 | const mat4 = require('gl-mat4') 2 | 3 | function drawMesh (regl, params) { 4 | const mesh = params.geometry 5 | return regl({ 6 | vert: ` 7 | precision mediump float; 8 | uniform mat4 projection, view; 9 | attribute vec3 position, normal; 10 | varying vec3 vNormal; 11 | void main () { 12 | vNormal = normal; 13 | gl_Position = projection * view * vec4(position, 1.0); 14 | }`, 15 | 16 | frag: `precision mediump float; 17 | varying vec3 vNormal; 18 | void main () { 19 | gl_FragColor = vec4(vNormal, 1.0); 20 | }`, 21 | 22 | // this converts the vertices of the mesh into the position attribute 23 | attributes: { 24 | position: mesh.positions, 25 | normal: mesh.normals 26 | }, 27 | 28 | // and this converts the faces fo the mesh into elements 29 | elements: mesh.triangles || mesh.indices || mesh.cells, 30 | 31 | uniforms: { 32 | model: (context, props) => props && props.model ? props.model : mat4.identity([]), 33 | color: (context, props) => { return [1, 0, 0, 1] }, 34 | view: ({tick}) => { 35 | const t = 0.01 * tick 36 | return mat4.lookAt([], 37 | [30 * Math.cos(t), 2.5, 30 * Math.sin(t)], 38 | [0, 2.5, 0], 39 | [0, 1, 0]) 40 | }, 41 | projection: ({viewportWidth, viewportHeight}) => 42 | mat4.perspective([], 43 | Math.PI / 4, 44 | viewportWidth / viewportHeight, 45 | 0.01, 46 | 1000) 47 | }, 48 | cull: { 49 | enable: true, 50 | face: 'front' 51 | }, 52 | blend: { 53 | enable: false, 54 | func: { 55 | src: 'src alpha', 56 | dst: 'one minus src alpha' 57 | } 58 | } 59 | }) 60 | } 61 | 62 | module.exports = drawMesh 63 | -------------------------------------------------------------------------------- /src/rendering/drawMesh/index.js: -------------------------------------------------------------------------------- 1 | const mat4 = require('gl-mat4') 2 | const vao = require('vertex-ao') 3 | 4 | const vColorVert = ` 5 | precision mediump float; 6 | 7 | uniform float camNear, camFar; 8 | uniform mat4 model, view, projection, unormal; 9 | 10 | attribute vec3 position, normal; 11 | attribute vec4 color; 12 | 13 | attribute float ao; 14 | varying float ambientAo; 15 | 16 | varying vec3 surfaceNormal, surfacePosition; 17 | varying vec4 _worldSpacePosition; 18 | varying vec4 vColor; 19 | 20 | void main() { 21 | surfacePosition = position; 22 | surfaceNormal = (unormal * vec4(normal, 1.0)).xyz; //vec4 transformedNormal = uNormalMatrix * vec4(aVertexNormal, 1.0); 23 | vec4 worldSpacePosition = model * vec4(position, 1); 24 | _worldSpacePosition = worldSpacePosition; 25 | //gl_Position = projection * view * worldSpacePosition; 26 | 27 | vColor = color; 28 | 29 | //ambientAo = (1. - ao) * (0.5 * max(normal.x, 0.) + 0.5); 30 | 31 | vec4 glPosition = projection * view * model * vec4(position, 1); 32 | gl_Position = glPosition; 33 | //gl_Position = zBufferAdjust(glPosition, camNear, camFar); 34 | } 35 | ` 36 | // NOTE : HEEEERE 37 | const vColorFrag = ` 38 | precision mediump float; 39 | varying vec3 surfaceNormal, surfacePosition; 40 | 41 | uniform float ambientLightAmount; 42 | uniform float diffuseLightAmount; 43 | uniform float specularLightAmount; 44 | 45 | uniform vec3 lightDirection; 46 | uniform vec4 lightColor; 47 | uniform vec3 opacity; 48 | uniform float uMaterialShininess; 49 | 50 | varying vec4 vColor; 51 | uniform vec4 ucolor; 52 | uniform float vColorToggler; 53 | 54 | uniform vec2 printableArea; 55 | vec4 errorColor = vec4(0.15, 0.15, 0.15, 0.3);//vec4(0.15, 0.15, 0.15, 0.3); 56 | varying vec4 _worldSpacePosition; 57 | varying float ambientAo; 58 | 59 | void main () { 60 | vec4 depth = gl_FragCoord; 61 | vec4 endColor = vColor * vColorToggler + ucolor * (1.0 - vColorToggler); 62 | 63 | vec3 ambient = ambientLightAmount * endColor.rgb ; //ambientAo * 64 | 65 | float diffuseWeight = dot(surfaceNormal, lightDirection); 66 | vec3 diffuse = diffuseLightAmount * endColor.rgb * clamp(diffuseWeight , 0.0, 1.0 ); 67 | 68 | //specular 69 | 70 | vec4 specularColor = vec4(lightColor); 71 | vec3 eyeDirection = normalize(surfacePosition.xyz); 72 | vec3 reflectionDirection = reflect(-lightDirection, surfaceNormal); 73 | float specularLightWeight = pow(max(dot(reflectionDirection, eyeDirection), 0.0), uMaterialShininess); 74 | vec3 specular = specularColor.rgb * specularLightWeight * specularLightAmount; 75 | 76 | /*float light2Multiplier = 0.2; 77 | float diffuseWeight2 = dot(surfaceNormal, vec3(-lightDirection.x, lightDirection.y, lightDirection.z)); 78 | vec3 diffuse2 = diffuseLightAmount * endColor.rgb * clamp(diffuseWeight2 , 0.0, 1.0 ) * light2Multiplier; 79 | 80 | float light3Multiplier = 0.2; 81 | float diffuseWeight3 = dot(surfaceNormal, vec3(lightDirection.x, -lightDirection.y, lightDirection.z)); 82 | vec3 diffuse3 = diffuseLightAmount * endColor.rgb * clamp(diffuseWeight3 , 0.0, 1.0 ) * light3Multiplier; 83 | 84 | float light4Multiplier = 0.2; 85 | float diffuseWeight4 = dot(surfaceNormal, vec3(-lightDirection.x, -lightDirection.y, lightDirection.z)); 86 | vec3 diffuse4 = diffuseLightAmount * endColor.rgb * clamp(diffuseWeight4 , 0.0, 1.0 ) * light4Multiplier;*/ 87 | 88 | gl_FragColor = vec4((ambient + diffuse +specular), endColor.a); 89 | //gl_FragColor = vec4((ambient + diffuse + diffuse2 + diffuse3 + diffuse4), endColor.a); 90 | } 91 | ` 92 | 93 | const meshFrag = ` 94 | precision mediump float; 95 | varying vec3 surfaceNormal; 96 | uniform float ambientLightAmount; 97 | uniform float diffuseLightAmount; 98 | uniform vec4 ucolor; 99 | uniform vec3 lightDirection; 100 | uniform vec3 opacity; 101 | 102 | varying vec4 _worldSpacePosition; 103 | 104 | uniform vec2 printableArea; 105 | 106 | vec4 errorColor = vec4(0.15, 0.15, 0.15, 0.3); 107 | 108 | void main () { 109 | vec4 depth = gl_FragCoord; 110 | 111 | float v = 0.8; // shadow value 112 | vec4 endColor = ucolor; 113 | 114 | vec3 ambient = ambientLightAmount * endColor.rgb; 115 | float cosTheta = dot(surfaceNormal, lightDirection); 116 | vec3 diffuse = diffuseLightAmount * endColor.rgb * clamp(cosTheta , 0.0, 1.0 ); 117 | 118 | float cosTheta2 = dot(surfaceNormal, vec3(-lightDirection.x, -lightDirection.y, lightDirection.z)); 119 | vec3 diffuse2 = diffuseLightAmount * endColor.rgb * clamp(cosTheta2 , 0.0, 1.0 ); 120 | 121 | gl_FragColor = vec4((ambient + diffuse + diffuse2 * v), endColor.a); 122 | }` 123 | 124 | const meshVert = ` 125 | precision mediump float; 126 | 127 | uniform float camNear, camFar; 128 | uniform mat4 model, view, projection; 129 | 130 | attribute vec3 position, normal; 131 | 132 | varying vec3 surfaceNormal, surfacePosition; 133 | varying vec4 _worldSpacePosition; 134 | 135 | void main() { 136 | surfacePosition = position; 137 | surfaceNormal = normal; 138 | vec4 worldSpacePosition = model * vec4(position, 1); 139 | _worldSpacePosition = worldSpacePosition; 140 | 141 | vec4 glPosition = projection * view * model * vec4(position, 1); 142 | gl_Position = glPosition; 143 | } 144 | ` 145 | 146 | const drawMesh = function (regl, params = {extras: {}}) { 147 | const {buffer} = regl 148 | const defaults = { 149 | useVertexColors: true, 150 | dynamicCulling: false, 151 | geometry: undefined 152 | } 153 | const {geometry, dynamicCulling, useVertexColors} = Object.assign({}, defaults, params) 154 | 155 | let ambientOcclusion // = vao(geometry.indices, geometry.positions, 64, 64) 156 | ambientOcclusion = regl.buffer([]) 157 | 158 | // vertex colors or not ? 159 | const hasIndices = !!(geometry.indices && geometry.indices.length > 0) 160 | const hasNormals = !!(geometry.normals && geometry.normals.length > 0) 161 | const hasVertexColors = !!(useVertexColors && geometry.colors && geometry.colors.length > 0) 162 | const cullFace = dynamicCulling ? function (context, props) { 163 | const isOdd = ([props.model[0], props.model[5], props.model[10]].filter(x => x < 0).length) & 1 // count the number of negative components & deterine if that is odd or even 164 | return isOdd ? 'front' : 'back' 165 | } : 'back' 166 | 167 | const vert = hasVertexColors ? vColorVert : meshVert 168 | const frag = hasVertexColors ? vColorFrag : meshFrag 169 | 170 | let commandParams = { 171 | vert, 172 | frag, 173 | 174 | uniforms: { 175 | model: (context, props) => props && props.model ? props.model : mat4.identity([]), 176 | ucolor: (context, props) => props && props.color ? props.color : [1, 1, 1, 1], 177 | // semi hack, woraround to enable/disable vertex colors!!! 178 | vColorToggler: (context, props) => (props && props.useVertexColors && props.useVertexColors === true) ? 1.0 : 0.0, 179 | // experimental 180 | unormal: (context, props) => { 181 | const model = props.model 182 | const modelViewMatrix = mat4.multiply(mat4.create(), model, props.camera.view) 183 | const normalMatrix = mat4.create() 184 | mat4.invert(normalMatrix, modelViewMatrix) 185 | mat4.transpose(normalMatrix, normalMatrix) 186 | return normalMatrix 187 | } 188 | }, 189 | attributes: { 190 | position: buffer(geometry.positions), 191 | ao: ambientOcclusion 192 | }, 193 | cull: { 194 | enable: true, 195 | face: cullFace 196 | }, 197 | blend: { 198 | enable: true, 199 | func: { 200 | src: 'src alpha', 201 | dst: 'one minus src alpha' 202 | } 203 | }, 204 | primitive: (context, props) => props && props.primitive ? props.primitive : 'triangles' 205 | } 206 | 207 | if (geometry.cells) { 208 | commandParams.elements = geometry.cells 209 | } else if (hasIndices) { 210 | // FIXME: not entirely sure about all this 211 | const indices = geometry.indices 212 | /* let type 213 | if (indices instanceof Uint32Array && regl.hasExtension('oes_element_index_uint')) { 214 | type = 'uint32' 215 | }else if (indices instanceof Uint16Array) { 216 | type = 'uint16' 217 | } else { 218 | type = 'uint8' 219 | } */ 220 | 221 | commandParams.elements = regl.elements({ 222 | // type, 223 | data: indices 224 | }) 225 | } else if (geometry.triangles) { 226 | commandParams.elements = geometry.triangles 227 | } else { 228 | commandParams.count = geometry.positions.length / 3 229 | } 230 | 231 | if (hasNormals) { 232 | commandParams.attributes.normal = buffer(geometry.normals) 233 | } 234 | if (hasVertexColors) { 235 | commandParams.attributes.color = buffer(geometry.colors) 236 | } 237 | 238 | // Splice in any extra params 239 | commandParams = Object.assign({}, commandParams, params.extras) 240 | return regl(commandParams) 241 | } 242 | 243 | module.exports = drawMesh 244 | -------------------------------------------------------------------------------- /src/rendering/drawMeshNoNormals.js: -------------------------------------------------------------------------------- 1 | const mat4 = require('gl-mat4') 2 | 3 | const drawMesh = function (regl, params) { 4 | const defaults = { 5 | geometry: undefined 6 | } 7 | const {geometry} = Object.assign({}, defaults, params) 8 | 9 | const commandParams = { 10 | frag: `precision mediump float; 11 | uniform vec4 uColor; 12 | void main() { 13 | gl_FragColor = uColor; 14 | }`, 15 | vert: ` 16 | precision mediump float; 17 | attribute vec3 position; 18 | uniform mat4 model, view, projection; 19 | void main() { 20 | gl_Position = projection * view * model * vec4(position, 1); 21 | }`, 22 | 23 | uniforms: { 24 | model: (context, props) => props && props.model ? props.model : mat4.identity([]), 25 | uColor: (context, props) => props && props.color ? props.color : [1, 1, 1, 1] 26 | }, 27 | attributes: { 28 | position: geometry.positions 29 | }, 30 | elements: geometry.cells, 31 | } 32 | return regl(commandParams) 33 | } 34 | 35 | module.exports = drawMesh -------------------------------------------------------------------------------- /src/rendering/drawNormals.js: -------------------------------------------------------------------------------- 1 | const mat4 = require('gl-mat4') 2 | 3 | const drawNormals = function (regl, params) { 4 | const defaults = { 5 | size: 20, 6 | lineWidth: 5, // FIXME/ linewidth has been "deprecated" in multiple browsers etc, need a workaround, 7 | alwaysVisible: true, // to have the widget alway visible 'on top' of the rest of the scene 8 | geometry: undefined 9 | } 10 | let {size, lineWidth, alwaysVisible, geometry} = Object.assign({}, defaults, params) 11 | 12 | if (!geometry) { 13 | throw new Error('no geometry provided to drawNormals') 14 | } 15 | if (lineWidth > regl.limits.lineWidthDims[1]) { 16 | lineWidth = regl.limits.lineWidthDims[1] 17 | } 18 | const points = [ 19 | 0, 0, 0, 20 | size, 0, 0 21 | ] 22 | 23 | const commandParams = { 24 | frag: `precision mediump float; 25 | uniform vec4 color; 26 | varying vec3 vnormal; 27 | vec3 foo = vec3(.0, .0, 1.0); 28 | void main() { 29 | 30 | gl_FragColor = color; 31 | }`, 32 | vert: ` 33 | precision mediump float; 34 | attribute vec3 position, normal; 35 | uniform mat4 model, view, projection; 36 | void main() { 37 | gl_Position = projection * view * model * vec4(position, 1); 38 | }`, 39 | 40 | uniforms: { 41 | model: (context, props) => props && props.model ? props.model : mat4.identity([]), 42 | color: (context, props) => props.color, 43 | angle: (contet, props) => props.angle 44 | }, 45 | attributes: { 46 | position: points 47 | }, 48 | count: points.length / 3, 49 | primitive: 'line loop', 50 | lineWidth, 51 | depth: { 52 | enable: !alwaysVisible // disable depth testing to have the axis widget 'alway on top' of other items in the 3d viewer 53 | } 54 | } 55 | 56 | // const xAxisModel = mat4.identity([]) 57 | // const yAxisModel = mat4.rotateZ(mat4.create(), mat4.identity([]), Math.PI / 2) 58 | // const zAxisModel = mat4.rotateY(mat4.create(), mat4.identity([]), -Math.PI / 2) 59 | 60 | const normaLines = geometry.normals.map(function (normal, index) { 61 | const position = geometry.positions[index] 62 | let orientation = mat4.multiply( 63 | mat4.identity([]), 64 | mat4.translate(mat4.identity([]), mat4.identity([]), position), 65 | mat4.lookAt(mat4.identity([]), [0, 0, 0], normal, [0, 0, 1]) 66 | // mat4.lookAt(mat4.identity([]), position, vec3.add([], position, normal), [0, 0, 0]) 67 | ) 68 | const matrix = orientation 69 | const absNormal = normal.map(x => Math.abs(x)) 70 | return {color: [absNormal[0], absNormal[1], absNormal[2], 1.0], model: matrix} 71 | }) 72 | let singleNormal = regl(commandParams) 73 | return () => singleNormal(normaLines) 74 | } 75 | 76 | module.exports = drawNormals 77 | -------------------------------------------------------------------------------- /src/rendering/drawNormals2.js: -------------------------------------------------------------------------------- 1 | const mat4 = require('gl-mat4') 2 | 3 | const drawNormals = function (regl, params) { 4 | const defaults = { 5 | xColor: [1, 0, 0, 1], 6 | yColor: [0, 1, 0, 1], 7 | zColor: [0, 0, 1, 1], 8 | size: 10, 9 | lineWidth: 3, // FIXME/ linewidth has been "deprecated" in multiple browsers etc, need a workaround, 10 | alwaysVisible: true, // to have the widget alway visible 'on top' of the rest of the scene 11 | geometry: undefined 12 | } 13 | let {size, xColor, yColor, zColor, lineWidth, alwaysVisible, geometry} = Object.assign({}, defaults, params) 14 | 15 | if (!geometry) { 16 | throw new Error('no geometry provided to drawNormals') 17 | } 18 | if (lineWidth > regl.limits.lineWidthDims[1]) { 19 | lineWidth = regl.limits.lineWidthDims[1] 20 | } 21 | const points = [ 22 | 0, 0, 0, 23 | size, 0, 0 24 | ] 25 | 26 | console.log('geometry', geometry) 27 | 28 | const commandParams = { 29 | frag: `precision mediump float; 30 | uniform vec4 color; 31 | void main() { 32 | gl_FragColor = color; 33 | }`, 34 | vert: ` 35 | precision mediump float; 36 | attribute vec3 position; 37 | uniform mat4 model, view, projection; 38 | void main() { 39 | gl_Position = projection * view * model * vec4(position, 1); 40 | }`, 41 | 42 | uniforms: { 43 | model: (context, props) => props && props.model ? props.model : mat4.identity([]), 44 | color: (context, props) => props.color, 45 | angle: (contet, props) => props.angle 46 | }, 47 | attributes: { 48 | position: points 49 | }, 50 | count: points.length / 3, 51 | primitive: 'line loop', 52 | lineWidth, 53 | depth: { 54 | enable: !alwaysVisible // disable depth testing to have the axis widget 'alway on top' of other items in the 3d viewer 55 | } 56 | } 57 | 58 | // const xAxisModel = mat4.identity([]) 59 | // const yAxisModel = mat4.rotateZ(mat4.create(), mat4.identity([]), Math.PI / 2) 60 | // const zAxisModel = mat4.rotateY(mat4.create(), mat4.identity([]), -Math.PI / 2) 61 | 62 | const foo = geometry.normals.map(function (normal, index) { 63 | let orientation = mat4.identity([]) 64 | orientation = mat4.rotateX(orientation, orientation, normal[0]) 65 | orientation = mat4.rotateY(orientation, orientation, normal[1]) 66 | orientation = mat4.rotateZ(orientation, orientation, normal[2]) 67 | const position = mat4.translate([], orientation, geometry.positions[index]) 68 | 69 | return {color: xColor, model: position} 70 | }) 71 | let singleNormal = regl(commandParams) 72 | return () => singleNormal(foo) 73 | } 74 | 75 | module.exports = drawNormals 76 | -------------------------------------------------------------------------------- /src/rendering/render.js: -------------------------------------------------------------------------------- 1 | const renderWrapper = require('./renderWrapper') 2 | 3 | const makeDrawMeshNoNormals = require('./drawMeshNoNormals') 4 | const makeDrawAxis = require('./drawAxis') 5 | const makeDrawNormals = require('./drawNormals') 6 | 7 | const prepareRender = (regl, params) => { 8 | // const drawGrid = prepDrawGrid(regl, {fadeOut: true, ticks: 10, size: [1000, 1000]}) 9 | // const drawNormals = makeDrawNormals(regl, {geometry}) 10 | /* const vectorizeText = require('vectorize-text') 11 | const complex = vectorizeText('Hello world! 你好', { 12 | triangles: true, 13 | width: 500, 14 | textBaseline: 'hanging' 15 | }) 16 | 17 | complex.positions = complex.positions.map(point => [point[0], point[1], 0]) */ 18 | 19 | const cube = {positions: [ 20 | 0, 0, 0, 21 | 0, 100, 0, 22 | 0, 100, 100], 23 | 24 | cells: [0, 1, 2] 25 | } 26 | 27 | const drawTest = makeDrawMeshNoNormals(regl, {geometry: cube}) 28 | const drawAxis = makeDrawAxis(regl, {}) 29 | let command = (props) => { 30 | // console.log('params in render', props) 31 | const {camera, drawCommands} = props 32 | const {drawGrid, drawCSGs} = drawCommands 33 | const useVertexColors = !props.overrideOriginalColors 34 | 35 | renderWrapper(regl)(props, context => { 36 | regl.clear({ 37 | color: props.rendering.background, 38 | depth: 1 39 | }) 40 | drawCSGs.forEach((drawCSG, index) => { 41 | const entity = props.entities[index] 42 | const primitive = entity.type === '2d' ? 'lines' : 'triangles' 43 | const model = entity.transforms.matrix 44 | drawCSG({color: props.rendering.meshColor, primitive, useVertexColors, camera, model}) 45 | }) 46 | // drawTest({color: [1, 0, 0, 1], model: mat4.translate(mat4.create(), mat4.identity([]), [100, 0, 200])}) 47 | if (drawGrid && props.grid.show) { 48 | const gridColor = props.grid.color 49 | const subGridColor = [gridColor[0], gridColor[1], gridColor[2], gridColor[3] * 0.35] 50 | const fadeOut = props.grid.fadeOut 51 | drawGrid({color: gridColor, subColor: subGridColor, fadeOut}) 52 | } 53 | if (props.axes.show) { 54 | drawAxis() // needs to be last to be 'on top' of the scene 55 | } 56 | // drawNormals() 57 | }) 58 | } 59 | return function render (data) { 60 | // important for stats, correct resizing etc 61 | regl.poll() 62 | command(data) 63 | // tick += 0.01 64 | } 65 | } 66 | 67 | module.exports = prepareRender 68 | -------------------------------------------------------------------------------- /src/rendering/renderWrapper.js: -------------------------------------------------------------------------------- 1 | const mat4 = require('gl-mat4') 2 | 3 | function renderWrapper (regl, params = {}) { 4 | const {fbo} = params 5 | 6 | const commandParams = { 7 | cull: { 8 | enable: true 9 | }, 10 | context: { 11 | lightDirection: [0.2, 0.2, 1]// [0.19, 0.47, 0.29] 12 | }, 13 | uniforms: { 14 | view: (context, props) => props.camera.view, 15 | eye: (context, props) => props.camera.position, 16 | // projection: (context, props) => mat4.perspective([], props.camera.fov, context.viewportWidth/context.viewportHeight, props.camera.near, props.camera.far), //props.camera.projection,//context.viewportWidth also an alternative? 17 | projection: (context, props) => props.camera.projection, 18 | camNear: (context, props) => props.camera.near, 19 | camFar: (context, props) => props.camera.far, 20 | // accessories to the above 21 | invertedView: (context, props) => mat4.invert([], props.camera.view), 22 | // lighting stuff, needs cleanup 23 | lightPosition: (context, props) => props && props.rendering && props.rendering.lightPosition ? props.rendering.lightPosition : [100, 200, 100], 24 | lightDirection: (context, props) => props.rendering.lightDirection || context.lightDirection || [0, 0, 0], 25 | lightView: (context) => { 26 | return mat4.lookAt([], context.lightDirection, [0.0, 0.0, 0.0], [0.0, 0.0, 1.0]) 27 | }, 28 | lightProjection: mat4.ortho([], -25, -25, -20, 20, -25, 25), 29 | lightColor: (context, props) => props && props.rendering && props.rendering.lightColor ? props.rendering.lightColor : [1, 0.8, 0], 30 | ambientLightAmount: (context, props) => props && props.rendering && props.rendering.ambientLightAmount ? props.rendering.ambientLightAmount : 0.3, 31 | diffuseLightAmount: (context, props) => props && props.rendering && props.rendering.diffuseLightAmount ? props && props.rendering && props.rendering.diffuseLightAmount : 0.89, 32 | specularLightAmount: (context, props) => props && props.rendering && props.rendering.specularLightAmount ? props.rendering.specularLightAmount : 0.16, 33 | uMaterialShininess: (context, props) => props && props.rendering && props.rendering.materialShininess ? props.rendering.materialShininess : 8.0, 34 | materialAmbient: [0.5, 0.8, 0.3], 35 | materialDiffuse: [0.5, 0.8, 0.3], 36 | materialSpecular: [0.5, 0.8, 0.3] 37 | }, 38 | framebuffer: fbo 39 | } 40 | 41 | return regl(Object.assign({}, commandParams, params.extras)) 42 | } 43 | 44 | module.exports = renderWrapper 45 | -------------------------------------------------------------------------------- /src/state.js: -------------------------------------------------------------------------------- 1 | const {merge} = require('./utils') 2 | const makeCameraAndControlsReducers = require('./cameraControlsReducers') 3 | const makeDataAndParamsReducers = require('./dataParamsReducers') 4 | 5 | function makeState (actions, initialState, regl) { 6 | const cameraControlsReducers = makeCameraAndControlsReducers(initialState, regl) 7 | const dataParamsReducers = makeDataAndParamsReducers(initialState, regl) 8 | const reducers = Object.assign({}, dataParamsReducers, cameraControlsReducers) 9 | // console.log('actions', actions) 10 | // console.log('reducers', reducers) 11 | 12 | const state$ = actions 13 | .scan(function (state, action) { 14 | const reducer = reducers[action.type] ? reducers[action.type] : (state) => state 15 | try { 16 | const updatedData = reducer(state, action.data, initialState, regl) 17 | const newState = merge({}, state, updatedData) 18 | return newState 19 | } catch (error) { 20 | console.error('error', error) 21 | return merge({}, state, {error}) 22 | } 23 | 24 | // console.log('SCAAAN', action, newState) 25 | }, initialState) 26 | .filter(x => x !== undefined)// just in case ... 27 | .multicast() 28 | 29 | return state$ 30 | } 31 | 32 | module.exports = makeState 33 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | function flatten (array) { 2 | return [].concat(...array) 3 | } 4 | function toArray (data) { 5 | if (data === undefined || data === null) { return [] } 6 | if (data.constructor !== Array) { return [data] } 7 | return data 8 | } 9 | 10 | /** kinda, sorta like a nested object.assign, so that nested object values 11 | * do not get lost 12 | * note : this is NOT actually making anything immutable ! 13 | * @param {} output={} 14 | * @param {} currentState 15 | * @param {} options 16 | */ 17 | function merge (output = {}, currentState, options) { 18 | output = currentState // JSON.parse(JSON.stringify(currentState)) 19 | Object.keys(options).forEach(function (key) { 20 | const item = options[key] 21 | const isObject = typeof item === 'object' 22 | const isFunction = typeof item === 'function' 23 | const isArray = Array.isArray(item) 24 | 25 | if (isFunction) { 26 | output[key] = options[key] 27 | } else if (isArray) { 28 | const current = currentState[key] || [] 29 | output[key] = Object.assign([], ...current, options[key]) 30 | } else if (isObject) { 31 | const current = currentState[key] || {} 32 | output[key] = merge({}, current, item) 33 | } else { 34 | output[key] = options[key] 35 | } 36 | }) 37 | 38 | return output 39 | } 40 | 41 | module.exports = { 42 | flatten, 43 | toArray, 44 | merge 45 | } 46 | --------------------------------------------------------------------------------