window.devicePixelRatio = window.devicePixelRatio || 1; SolvespaceCamera = function(renderWidth, renderHeight, scale, up, right, offset) { THREE.Camera.call(this); this.type = 'SolvespaceCamera'; this.renderWidth = renderWidth; this.renderHeight = renderHeight; this.zoomScale = scale; /* Avoid namespace collision w/ THREE.Object.scale */ this.up = up; this.right = right; this.offset = offset; this.depthBias = 0; this.updateProjectionMatrix(); }; SolvespaceCamera.prototype = Object.create(THREE.Camera.prototype); SolvespaceCamera.prototype.constructor = SolvespaceCamera; SolvespaceCamera.prototype.updateProjectionMatrix = function() { var temp = new THREE.Matrix4(); var offset = new THREE.Matrix4().makeTranslation(this.offset.x, this.offset.y, this.offset.z); // Convert to right handed- do up cross right instead. var n = new THREE.Vector3().crossVectors(this.up, this.right); var rotate = new THREE.Matrix4().makeBasis(this.right, this.up, n); rotate.transpose(); /* FIXME: At some point we ended up using row-major. THREE.js wants column major. Scale/depth correct unaffected b/c diagonal matrices remain the same when transposed. makeTranslation also makes a column-major matrix. */ /* TODO: If we want perspective, we need an additional matrix here which will modify w for perspective divide. */ var scale = new THREE.Matrix4().makeScale(2 * this.zoomScale / this.renderWidth, 2 * this.zoomScale / this.renderHeight, this.zoomScale / 30000.0); temp.multiply(scale); temp.multiply(rotate); temp.multiply(offset); this.projectionMatrix.copy(temp); }; SolvespaceCamera.prototype.NormalizeProjectionVectors = function() { /* After rotating, up and right may no longer be orthogonal. However, their cross product will produce the correct rotated plane, and we can recover an orthogonal basis. */ var n = new THREE.Vector3().crossVectors(this.right, this.up); this.up = new THREE.Vector3().crossVectors(n, this.right); this.right.normalize(); this.up.normalize(); }; SolvespaceCamera.prototype.rotate = function(right, up) { var oldRight = new THREE.Vector3().copy(this.right).normalize(); var oldUp = new THREE.Vector3().copy(this.up).normalize(); this.up.applyAxisAngle(oldRight, up); this.right.applyAxisAngle(oldUp, right); this.NormalizeProjectionVectors(); } SolvespaceCamera.prototype.offsetProj = function(right, up) { var shift = new THREE.Vector3(right * this.right.x + up * this.up.x, right * this.right.y + up * this.up.y, right * this.right.z + up * this.up.z); this.offset.add(shift); } /* Calculate the offset in terms of up and right projection vectors that will preserve the world coordinates of the current mouse position after the zoom. */ SolvespaceCamera.prototype.zoomTo = function(x, y, delta) { // Get offset components in world coordinates, in terms of up/right. var projOffsetX = this.offset.dot(this.right); var projOffsetY = this.offset.dot(this.up); /* Remove offset before scaling so, that mouse position changes proportionally to the model and independent of current offset. */ var centerRightI = x/this.zoomScale - projOffsetX; var centerUpI = y/this.zoomScale - projOffsetY; var zoomFactor; /* Zoom 20% every 100 delta. */ if(delta < 0) { zoomFactor = (-delta * 0.002 + 1); } else if(delta > 0) { zoomFactor = (delta * (-1.0/600.0) + 1) } else { return; } this.zoomScale = this.zoomScale * zoomFactor; var centerRightF = x/this.zoomScale - projOffsetX; var centerUpF = y/this.zoomScale - projOffsetY; this.offset.addScaledVector(this.right, centerRightF - centerRightI); this.offset.addScaledVector(this.up, centerUpF - centerUpI); } SolvespaceControls = function(object, domElement) { var _this = this; this.object = object; this.domElement = ( domElement !== undefined ) ? domElement : document; var threePan = new Hammer.Pan({event : 'threepan', pointers : 3, enable : false}); var panAfterTap = new Hammer.Pan({event : 'panaftertap', enable : false}); this.touchControls = new Hammer.Manager(domElement, { recognizers: [ [Hammer.Pinch, { enable: true }], [Hammer.Pan], [Hammer.Tap], ] }); this.touchControls.add(threePan); this.touchControls.add(panAfterTap); var changeEvent = { type: 'change' }; var startEvent = { type: 'start' }; var endEvent = { type: 'end' }; var _changed = false; var _mouseMoved = false; //var _touchPoints = new Array(); var _offsetPrev = new THREE.Vector2(0, 0); var _offsetCur = new THREE.Vector2(0, 0); var _rotatePrev = new THREE.Vector2(0, 0); var _rotateCur = new THREE.Vector2(0, 0); // Used during touch events. var _rotateOrig = new THREE.Vector2(0, 0); var _offsetOrig = new THREE.Vector2(0, 0); var _prevScale = 1.0; this.handleEvent = function(event) { if (typeof this[event.type] == 'function') { this[event.type](event); } } function mousedown(event) { event.preventDefault(); event.stopPropagation(); switch (event.button) { case 0: _rotateCur.set(event.screenX/window.devicePixelRatio, event.screenY/window.devicePixelRatio); _rotatePrev.copy(_rotateCur); document.addEventListener('mousemove', mousemove, false); document.addEventListener('mouseup', mouseup, false); break; case 2: _offsetCur.set(event.screenX/window.devicePixelRatio, event.screenY/window.devicePixelRatio); _offsetPrev.copy(_offsetCur); document.addEventListener('mousemove', mousemove, false); document.addEventListener('mouseup', mouseup, false); break; default: break; } } function wheel( event ) { event.preventDefault(); /* FIXME: Width and height might not be supported universally, but can be calculated? */ var box = _this.domElement.getBoundingClientRect(); object.zoomTo(event.clientX - box.width/2 - box.left, -(event.clientY - box.height/2 - box.top), event.deltaY); _changed = true; } function mousemove(event) { switch (event.button) { case 0: _rotateCur.set(event.screenX/window.devicePixelRatio, event.screenY/window.devicePixelRatio); var diff = new THREE.Vector2().subVectors(_rotateCur, _rotatePrev) .multiplyScalar(1 / object.zoomScale); object.rotate(-0.3 * Math.PI / 180 * diff.x * object.zoomScale, -0.3 * Math.PI / 180 * diff.y * object.zoomScale); _changed = true; _rotatePrev.copy(_rotateCur); break; case 2: _mouseMoved = true; _offsetCur.set(event.screenX/window.devicePixelRatio, event.screenY/window.devicePixelRatio); var diff = new THREE.Vector2().subVectors(_offsetCur, _offsetPrev) .multiplyScalar(1 / object.zoomScale); object.offsetProj(diff.x, -diff.y); _changed = true; _offsetPrev.copy(_offsetCur) break; } } function mouseup(event) { /* TODO: Opera mouse gestures will intercept this event, making it possible to have multiple mousedown events consecutively without a corresponding mouseup (so multiple viewports can be rotated/panned simultaneously). Disable mouse gestures for now. */ event.preventDefault(); event.stopPropagation(); document.removeEventListener('mousemove', mousemove); document.removeEventListener('mouseup', mouseup); _this.dispatchEvent(endEvent); } function pan(event) { /* neWcur - prev does not necessarily equal (cur + diff) - prev. Floating point is not associative. */ touchDiff = new THREE.Vector2(event.deltaX, event.deltaY); _rotateCur.addVectors(_rotateOrig, touchDiff); incDiff = new THREE.Vector2().subVectors(_rotateCur, _rotatePrev) .multiplyScalar(1 / object.zoomScale); object.rotate(-0.3 * Math.PI / 180 * incDiff.x * object.zoomScale, -0.3 * Math.PI / 180 * incDiff.y * object.zoomScale); _changed = true; _rotatePrev.copy(_rotateCur); } function panstart(event) { /* TODO: Dynamically enable pan function? */ _rotateOrig.copy(_rotateCur); } function pinchstart(event) { _prevScale = event.scale; } function pinch(event) { /* FIXME: Width and height might not be supported universally, but can be calculated? */ var box = _this.domElement.getBoundingClientRect(); /* 16.6... pixels chosen heuristically... matches my touchpad. */ if (event.scale < _prevScale) { object.zoomTo(event.center.x - box.width/2 - box.left, -(event.center.y - box.height/2 - box.top), 100/6.0); _changed = true; } else if (event.scale > _prevScale) { object.zoomTo(event.center.x - box.width/2 - box.left, -(event.center.y - box.height/2 - box.top), -100/6.0); _changed = true; } _prevScale = event.scale; } /* A tap will enable panning/disable rotate. */ function tap(event) { panAfterTap.set({enable : true}); _this.touchControls.get('pan').set({enable : false}); } function panaftertap(event) { touchDiff = new THREE.Vector2(event.deltaX, event.deltaY); _offsetCur.addVectors(_offsetOrig, touchDiff); incDiff = new THREE.Vector2().subVectors(_offsetCur, _offsetPrev) .multiplyScalar(1 / object.zoomScale); object.offsetProj(incDiff.x, -incDiff.y); _changed = true; _offsetPrev.copy(_offsetCur); } function panaftertapstart(event) { _offsetOrig.copy(_offsetCur); } function panaftertapend(event) { panAfterTap.set({enable : false}); _this.touchControls.get('pan').set({enable : true}); } function contextmenu(event) { event.preventDefault(); } this.update = function() { if (_changed) { _this.dispatchEvent(changeEvent); _changed = false; } } this.domElement.addEventListener('mousedown', mousedown, false); this.domElement.addEventListener('wheel', wheel, false); this.domElement.addEventListener('contextmenu', contextmenu, false); /* Hammer.on wraps addEventListener */ // Rotate this.touchControls.on('pan', pan); this.touchControls.on('panstart', panstart); // Zoom this.touchControls.on('pinch', pinch); this.touchControls.on('pinchstart', pinchstart); //Pan this.touchControls.on('tap', tap); this.touchControls.on('panaftertapstart', panaftertapstart); this.touchControls.on('panaftertap', panaftertap); this.touchControls.on('panaftertapend', panaftertapend); } SolvespaceControls.prototype = Object.create(THREE.EventDispatcher.prototype); SolvespaceControls.prototype.constructor = SolvespaceControls; solvespace = function(obj, params) { var scene, edgeScene, camera, edgeCamera, renderer; var geometry, controls, material, mesh, edges; var width, height, scale, offset; var directionalLightArray = []; if (typeof params === "undefined" || !("width" in params)) { width = window.innerWidth; } else { width = params.width; } if (typeof params === "undefined" || !("height" in params)) { height = window.innerHeight; } else { height = params.height; } if (typeof params === "undefined" || !("scale" in params)) { scale = 5; } else { scale = params.scale; } if (typeof params === "undefined" || !("offset" in params)) { offset = new THREE.Vector3(0, 0, 0); } else { offset = params.offset; } width *= window.devicePixelRatio; height *= window.devicePixelRatio; domElement = init(); render(); return domElement; function init() { scene = new THREE.Scene(); edgeScene = new THREE.Scene(); camera = new SolvespaceCamera(width/window.devicePixelRatio, height/window.devicePixelRatio, scale, new THREE.Vector3(0, 1, 0), new THREE.Vector3(0.5, 0, -0.5).normalize(), offset); mesh = createMesh(obj); scene.add(mesh); edges = createEdges(obj); edgeScene.add(edges); for (var i = 0; i < obj.lights.d.length; i++) { var lightColor = new THREE.Color(obj.lights.d[i].intensity, obj.lights.d[i].intensity, obj.lights.d[i].intensity); var directionalLight = new THREE.DirectionalLight(lightColor, 1); directionalLight.position.set(obj.lights.d[i].direction[0], obj.lights.d[i].direction[1], obj.lights.d[i].direction[2]); directionalLightArray.push(directionalLight); scene.add(directionalLight); } var lightColor = new THREE.Color(obj.lights.a, obj.lights.a, obj.lights.a); var ambientLight = new THREE.AmbientLight(lightColor.getHex()); scene.add(ambientLight); renderer = new THREE.WebGLRenderer({ antialias: true}); renderer.setSize(width, height); renderer.autoClear = false; renderer.domElement.style = "width:"+width/window.devicePixelRatio+"px;height:"+height/window.devicePixelRatio+"px;"; controls = new SolvespaceControls(camera, renderer.domElement); controls.addEventListener("change", render); controls.addEventListener("change", lightUpdate); animate(); return renderer.domElement; } function animate() { requestAnimationFrame(animate); controls.update(); } function render() { var context = renderer.getContext(); camera.updateProjectionMatrix(); renderer.clear(); context.depthRange(0.1, 1); renderer.render(scene, camera); context.depthRange(0.1-(2/60000.0), 1-(2/60000.0)); renderer.render(edgeScene, camera); } function lightUpdate() { var changeBasis = new THREE.Matrix4(); // The original light positions were in camera space. // Project them into standard space using camera's basis // vectors (up, target, and their cross product). n = new THREE.Vector3().crossVectors(camera.up, camera.right); changeBasis.makeBasis(camera.right, camera.up, n); for (var i = 0; i < 2; i++) { var newLightPos = changeBasis.applyToVector3Array( [obj.lights.d[i].direction[0], obj.lights.d[i].direction[1], obj.lights.d[i].direction[2]]); directionalLightArray[i].position.set(newLightPos[0], newLightPos[1], newLightPos[2]); } } function createMesh(meshObj) { var geometry = new THREE.Geometry(); var materialIndex = 0; var materialList = []; var opacitiesSeen = {}; for (var i = 0; i < meshObj.points.length; i++) { geometry.vertices.push(new THREE.Vector3(meshObj.points[i][0], meshObj.points[i][1], meshObj.points[i][2])); } for (var i = 0; i < meshObj.faces.length; i++) { var currOpacity = ((meshObj.colors[i] & 0xFF000000) >>> 24) / 255.0; if (opacitiesSeen[currOpacity] === undefined) { opacitiesSeen[currOpacity] = materialIndex; materialIndex++; materialList.push(new THREE.MeshLambertMaterial({ vertexColors: THREE.FaceColors, opacity: currOpacity, transparent: true, side: THREE.DoubleSide })); } geometry.faces.push(new THREE.Face3(meshObj.faces[i][0], meshObj.faces[i][1], meshObj.faces[i][2], [new THREE.Vector3(meshObj.normals[i][0][0], meshObj.normals[i][0][1], meshObj.normals[i][0][2]), new THREE.Vector3(meshObj.normals[i][1][0], meshObj.normals[i][1][1], meshObj.normals[i][1][2]), new THREE.Vector3(meshObj.normals[i][2][0], meshObj.normals[i][2][1], meshObj.normals[i][2][2])], new THREE.Color(meshObj.colors[i] & 0x00FFFFFF), opacitiesSeen[currOpacity])); } geometry.computeBoundingSphere(); return new THREE.Mesh(geometry, new THREE.MultiMaterial(materialList)); } function createEdges(meshObj) { var geometry = new THREE.Geometry(); var material = new THREE.LineBasicMaterial(); for (var i = 0; i < meshObj.edges.length; i++) { geometry.vertices.push(new THREE.Vector3(meshObj.edges[i][0][0], meshObj.edges[i][0][1], meshObj.edges[i][0][2]), new THREE.Vector3(meshObj.edges[i][1][0], meshObj.edges[i][1][1], meshObj.edges[i][1][2])); } geometry.computeBoundingSphere(); return new THREE.LineSegments(geometry, material); } }; function findBootstrapEnvironment() { let envs = ['xs', 'sm', 'md', 'lg', 'xl']; let el = document.createElement('div'); document.body.appendChild(el); let curEnv = envs.shift(); for (let env of envs.reverse()) { el.classList.add(`d-${env}-none`); if (window.getComputedStyle(el).display === 'none') { curEnv = env; break; } } document.body.removeChild(el); return curEnv; } function load_treejs_model(model_id, model_solvespace, model_param) { let node = document.getElementById(model_id); node.parentNode.replaceChild( solvespace(model_solvespace, model_param), node ); } var bootstrap_env = findBootstrapEnvironment();