Three.js: put all resources into res/threejs/ and embed on export.
This means that our exported viewer is independent of internet connection as well as any CDNs that will eventually die.pull/10/head
parent
4b0dc5819b
commit
432e7680a4
|
@ -179,7 +179,10 @@ add_resources(
|
|||
fonts/private/5-stipple-dash-long.png
|
||||
fonts/private/6-stipple-dash.png
|
||||
fonts/private/7-stipple-zigzag.png
|
||||
fonts/unicode.lff.gz)
|
||||
fonts/unicode.lff.gz
|
||||
threejs/three-r76.js.gz
|
||||
threejs/hammer-2.0.8.js.gz
|
||||
threejs/SolveSpaceControls.js)
|
||||
|
||||
# Third, distribute the resources.
|
||||
add_custom_target(resources
|
||||
|
|
|
@ -0,0 +1,472 @@
|
|||
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, event.screenY);
|
||||
_rotatePrev.copy(_rotateCur);
|
||||
document.addEventListener('mousemove', mousemove, false);
|
||||
document.addEventListener('mouseup', mouseup, false);
|
||||
break;
|
||||
case 2:
|
||||
_offsetCur.set(event.screenX, event.screenY);
|
||||
_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, event.screenY);
|
||||
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, event.screenY);
|
||||
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;
|
||||
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;
|
||||
}
|
||||
|
||||
domElement = init();
|
||||
render();
|
||||
return domElement;
|
||||
|
||||
|
||||
function init() {
|
||||
scene = new THREE.Scene();
|
||||
edgeScene = new THREE.Scene();
|
||||
|
||||
camera = new SolvespaceCamera(width,
|
||||
height, 5, new THREE.Vector3(0, 1, 0),
|
||||
new THREE.Vector3(1, 0, 0), new THREE.Vector3(0, 0, 0));
|
||||
|
||||
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;
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
Binary file not shown.
Binary file not shown.
487
src/export.cpp
487
src/export.cpp
|
@ -843,494 +843,21 @@ void SolveSpaceUI::ExportMeshAsThreeJsTo(FILE *f, const std::string &filename,
|
|||
STriangle *tr;
|
||||
SEdge *e;
|
||||
Vector bndl, bndh;
|
||||
const char htmlbegin0[] = R"(
|
||||
const char htmlbegin[] = R"(
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"></meta>
|
||||
<title>Three.js Solvespace Mesh</title>
|
||||
<script src="http://threejs.org/build/three.js"></script>
|
||||
<script src="http://hammerjs.github.io/dist/hammer.js"></script>
|
||||
<script id="three-r76.js">%s</script>
|
||||
<script id="hammer-2.0.8.js">%s</script>
|
||||
<script id="SolveSpaceControls.js">%s</script>
|
||||
<style type="text/css">
|
||||
body { margin: 0; overflow: hidden; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
</script>
|
||||
<script>
|
||||
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, event.screenY);
|
||||
_rotatePrev.copy(_rotateCur);
|
||||
document.addEventListener('mousemove', mousemove, false);
|
||||
document.addEventListener('mouseup', mouseup, false);
|
||||
break;
|
||||
case 2:
|
||||
_offsetCur.set(event.screenX, event.screenY);
|
||||
_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, event.screenY);
|
||||
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, event.screenY);
|
||||
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;
|
||||
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;
|
||||
}
|
||||
|
||||
domElement = init();
|
||||
render();
|
||||
return domElement;
|
||||
|
||||
|
||||
function init() {
|
||||
scene = new THREE.Scene();
|
||||
edgeScene = new THREE.Scene();
|
||||
|
||||
camera = new SolvespaceCamera(width,
|
||||
height, 5, new THREE.Vector3(0, 1, 0),
|
||||
new THREE.Vector3(1, 0, 0), new THREE.Vector3(0, 0, 0));
|
||||
|
||||
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;
|
||||
|
||||
controls = new SolvespaceControls(camera, renderer.domElement);
|
||||
controls.addEventListener("change", render);
|
||||
controls.addEventListener("change", lightUpdate);
|
||||
|
||||
animate();
|
||||
return renderer.domElement;
|
||||
})";
|
||||
const char htmlbegin1[] = R"(
|
||||
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);
|
||||
}
|
||||
};
|
||||
)";
|
||||
const char htmlend[] = R"(
|
||||
document.body.appendChild(solvespace(solvespace_model_%s));
|
||||
|
@ -1378,8 +905,10 @@ SolvespaceCamera = function(renderWidth, renderHeight, scale, up, right, offset)
|
|||
}
|
||||
|
||||
if(extension == "html") {
|
||||
fputs(htmlbegin0, f);
|
||||
fputs(htmlbegin1, f);
|
||||
fprintf(f, htmlbegin,
|
||||
LoadStringFromGzip("threejs/three-r76.js.gz").c_str(),
|
||||
LoadStringFromGzip("threejs/hammer-2.0.8.js.gz").c_str(),
|
||||
LoadString("threejs/SolveSpaceControls.js").c_str());
|
||||
}
|
||||
|
||||
fprintf(f, "var solvespace_model_%s = {\n"
|
||||
|
|
Loading…
Reference in New Issue