react-jsc3d
Version:
React port of JSC3D software 3D mesh renderer
1,383 lines (1,208 loc) • 206 kB
JavaScript
/**
* @preserve Copyright (c) 2011~2014 Humu <humu2009@gmail.com>
* This file is part of jsc3d project, which is freely distributable under the
* terms of the MIT license.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
////////////////////////////////////////
// Currently, only 1 of 2 changes by michaelb (other is on line 1237)
/* eslint-disable */
var JSC3D = JSC3D || {};
if (module) module.exports = JSC3D;
var maxY, minY, maxZ, minZ, maxZ, VBArray;
////////////////////////////////////////
/**
@class Viewer
Viewer is the main class of JSC3D. It provides presentation of and interaction with a simple static 3D scene
which can either be given as the url of the scene file, or be manually constructed and passed in. It
also provides some settings to adjust the mode and quality of the rendering.<br /><br />
Viewer should be constructed with an existing canvas object where to perform the rendering.<br /><br />
Viewer provides 3 way to specify the scene:<br />
1. Use setParameter() method before initilization and set 'SceneUrl' parameter with a valid url
that describes where to load the scene. <br />
2. Use replaceSceneFromUrl() method, passing in a valid url to load/replace scene at runtime.<br />
3. Use replaceScene() method, passing in a manually constructed scene object to replace the current one
at runtime.<br />
*/
JSC3D.Viewer = function (canvas, parameters) {
if (parameters) this.params = {
SceneUrl: parameters.SceneUrl || '',
InitRotationX: parameters.InitRotationX || 0,
InitRotationY: parameters.InitRotationY || 0,
InitRotationZ: parameters.InitRotationZ || 0,
ModelColor: parameters.ModelColor || '#caa618',
BackgroundColor1: parameters.BackgroundColor1 || '#ffffff',
BackgroundColor2: parameters.BackgroundColor2 || '#383840',
BackgroundImageUrl: parameters.BackgroundImageUrl || '',
Background: parameters.Background || 'on',
RenderMode: parameters.RenderMode || 'flat',
Definition: parameters.Definition || 'standard',
FaceCulling: parameters.FaceCulling || 'on',
MipMapping: parameters.MipMapping || 'off',
CreaseAngle: parameters.CreaseAngle || -180,
SphereMapUrl: parameters.SphereMapUrl || '',
ProgressBar: parameters.ProgressBar || 'on',
Renderer: parameters.Renderer || '',
LocalBuffers: parameters.LocalBuffers || 'retain'
};else this.params = {
SceneUrl: '',
InitRotationX: 0,
InitRotationY: 0,
InitRotationZ: 0,
ModelColor: '#caa618',
BackgroundColor1: '#ffffff',
BackgroundColor2: '#383840',
BackgroundImageUrl: '',
Background: 'on',
RenderMode: 'flat',
Definition: 'standard',
FaceCulling: 'on',
MipMapping: 'off',
CreaseAngle: -180,
SphereMapUrl: '',
ProgressBar: 'on',
Renderer: '',
LocalBuffers: 'retain'
};
this.canvas = canvas;
this.ctx2d = null;
this.canvasData = null;
this.bkgColorBuffer = null;
this.colorBuffer = null;
this.zBuffer = null;
this.selectionBuffer = null;
this.frameWidth = canvas.width;
this.frameHeight = canvas.height;
this.scene = null;
this.defaultMaterial = null;
this.sphereMap = null;
this.isLoaded = false;
this.isFailed = false;
this.abortUnfinishedLoadingFn = null;
this.needUpdate = false;
this.needRepaint = false;
this.initRotX = 0;
this.initRotY = 0;
this.initRotZ = 0;
this.zoomFactor = 1;
this.panning = [0, 0];
this.rotMatrix = new JSC3D.Matrix3x4();
this.transformMatrix = new JSC3D.Matrix3x4();
this.sceneUrl = '';
this.modelColor = 0xcaa618;
this.bkgColor1 = 0xffffff;
this.bkgColor2 = 0x383840;
this.bkgImageUrl = '';
this.bkgImage = null;
this.isBackgroundOn = true;
this.renderMode = 'flat';
this.definition = 'standard';
this.isCullingDisabled = false;
this.isMipMappingOn = false;
this.creaseAngle = -180;
this.sphereMapUrl = '';
this.showProgressBar = true;
this.buttonStates = {};
this.keyStates = {};
this.mouseX = 0;
this.mouseY = 0;
this.mouseDownX = -1;
this.mouseDownY = -1;
this.isTouchHeld = false;
this.baseZoomFactor = 1;
this.suppressDraggingRotation = false;
this.onloadingstarted = null;
this.onloadingcomplete = null;
this.onloadingprogress = null;
this.onloadingaborted = null;
this.onloadingerror = null;
this.onmousedown = null;
this.onmouseup = null;
this.onmousemove = null;
this.onmousewheel = null;
this.onmouseclick = null;
this.beforeupdate = null;
this.afterupdate = null;
this.mouseUsage = 'default';
this.isDefaultInputHandlerEnabled = true;
this.progressFrame = null;
this.progressRectangle = null;
this.messagePanel = null;
this.webglBackend = null;
// setup input handlers.
// compatibility for touch devices is taken into account
var self = this;
if (!JSC3D.PlatformInfo.isTouchDevice) {
this.canvas.addEventListener('mousedown', function (e) {
self.mouseDownHandler(e);
}, false);
this.canvas.addEventListener('mouseup', function (e) {
self.mouseUpHandler(e);
}, false);
this.canvas.addEventListener('mousemove', function (e) {
self.mouseMoveHandler(e);
}, false);
this.canvas.addEventListener(JSC3D.PlatformInfo.browser == 'firefox' ? 'DOMMouseScroll' : 'mousewheel', function (e) {
self.mouseWheelHandler(e);
}, false);
document.addEventListener('keydown', function (e) {
self.keyDownHandler(e);
}, false);
document.addEventListener('keyup', function (e) {
self.keyUpHandler(e);
}, false);
} else if (JSC3D.Hammer) {
JSC3D.Hammer(this.canvas).on('touch release hold drag pinch transformend', function (e) {
self.gestureHandler(e);
});
} else {
this.canvas.addEventListener('touchstart', function (e) {
self.touchStartHandler(e);
}, false);
this.canvas.addEventListener('touchend', function (e) {
self.touchEndHandler(e);
}, false);
this.canvas.addEventListener('touchmove', function (e) {
self.touchMoveHandler(e);
}, false);
}
};
/**
Set the initial value for a parameter to parameterize the viewer.<br />
Available parameters are:<br />
'<b>SceneUrl</b>': URL string that describes where to load the scene, default to '';<br />
'<b>InitRotationX</b>': initial rotation angle around x-axis for the whole scene, default to 0;<br />
'<b>InitRotationY</b>': initial rotation angle around y-axis for the whole scene, default to 0;<br />
'<b>InitRotationZ</b>': initial rotation angle around z-axis for the whole scene, default to 0;<br />
'<b>CreaseAngle</b>': an angle to control the shading smoothness between faces. Two adjacent faces will be shaded with discontinuity at the edge if the angle between their normals exceeds this value. Not used by default;<br />
'<b>ModelColor</b>': fallback color for all meshes, default to '#caa618';<br />
'<b>BackgroundColor1</b>': color at the top of the background, default to '#ffffff';<br />
'<b>BackgroundColor2</b>': color at the bottom of the background, default to '#383840';<br />
'<b>BackgroundImageUrl</b>': URL string that describes where to load the image used for background, default to '';<br />
'<b>Background</b>': turn on/off rendering of background. If this is set to 'off', the background area will be transparent. Default to 'on';<br />
'<b>RenderMode</b>': render mode, default to 'flat';<br />
'<b>FaceCulling</b>': turn on/off back-face culling for all meshes, default to 'on';<br />
'<b>Definition</b>': quality level of rendering, default to 'standard';<br />
'<b>MipMapping</b>': turn on/off mip-mapping, default to 'off';<br />
'<b>SphereMapUrl</b>': URL string that describes where to load the image used for sphere mapping, default to '';<br />
'<b>ProgressBar</b>': turn on/off the progress bar when loading, default to 'on'. By turning off the default progress bar, a user defined loading indicator can be used instead;<br />
'<b>Renderer</b>': set to 'webgl' to enable WebGL for rendering, default to ''.
@param {String} name name of the parameter to set.
@param value new value for the parameter.
*/
JSC3D.Viewer.prototype.setParameter = function (name, value) {
this.params[name] = value;
};
/**
Initialize viewer for rendering and interactions.
*/
JSC3D.Viewer.prototype.init = function () {
this.sceneUrl = this.params['SceneUrl'];
this.initRotX = parseFloat(this.params['InitRotationX']);
this.initRotY = parseFloat(this.params['InitRotationY']);
this.initRotZ = parseFloat(this.params['InitRotationZ']);
this.modelColor = parseInt('0x' + this.params['ModelColor'].substring(1));
this.bkgColor1 = parseInt('0x' + this.params['BackgroundColor1'].substring(1));
this.bkgColor2 = parseInt('0x' + this.params['BackgroundColor2'].substring(1));
this.bkgImageUrl = this.params['BackgroundImageUrl'];
this.isBackgroundOn = this.params['Background'].toLowerCase() == 'on';
this.renderMode = this.params['RenderMode'].toLowerCase();
this.definition = this.params['Definition'].toLowerCase();
this.isCullingDisabled = this.params['FaceCulling'].toLowerCase() == 'off';
this.creaseAngle = parseFloat(this.params['CreaseAngle']);
this.isMipMappingOn = this.params['MipMapping'].toLowerCase() == 'on';
this.sphereMapUrl = this.params['SphereMapUrl'];
this.showProgressBar = this.params['ProgressBar'].toLowerCase() == 'on';
this.useWebGL = this.params['Renderer'].toLowerCase() == 'webgl';
this.releaseLocalBuffers = this.params['LocalBuffers'].toLowerCase() == 'release';
// Create WebGL render back-end if it is assigned to.
if (this.useWebGL && JSC3D.PlatformInfo.supportWebGL && JSC3D.WebGLRenderBackend) {
try {
this.webglBackend = new JSC3D.WebGLRenderBackend(this.canvas, this.releaseLocalBuffers);
} catch (e) {}
}
// Fall back to software rendering when WebGL is not assigned or unavailable.
if (!this.webglBackend) {
if (this.useWebGL) {
if (JSC3D.console) JSC3D.console.logWarning('WebGL is not available. Software rendering is enabled instead.');
}
try {
this.ctx2d = this.canvas.getContext('2d');
this.canvasData = this.ctx2d.getImageData(0, 0, this.canvas.width, this.canvas.height);
} catch (e) {
this.ctx2d = null;
this.canvasData = null;
}
}
if (this.canvas.width <= 2 || this.canvas.height <= 2) this.definition = 'standard';
// calculate dimensions of frame buffers
switch (this.definition) {
case 'low':
this.frameWidth = ~~((this.canvas.width + 1) / 2);
this.frameHeight = ~~((this.canvas.height + 1) / 2);
break;
case 'high':
this.frameWidth = this.canvas.width * 2;
this.frameHeight = this.canvas.height * 2;
break;
case 'standard':
default:
this.frameWidth = this.canvas.width;
this.frameHeight = this.canvas.height;
break;
}
// initialize states
this.zoomFactor = 1;
this.panning = [0, 0];
this.rotMatrix.identity();
this.transformMatrix.identity();
this.isLoaded = false;
this.isFailed = false;
this.needUpdate = false;
this.needRepaint = false;
this.scene = null;
// create a default material to render meshes that don't have one
this.defaultMaterial = new JSC3D.Material('default', undefined, this.modelColor, 0, true);
// allocate memory storage for frame buffers
if (!this.webglBackend) {
this.colorBuffer = new Array(this.frameWidth * this.frameHeight);
this.zBuffer = new Array(this.frameWidth * this.frameHeight);
this.selectionBuffer = new Array(this.frameWidth * this.frameHeight);
this.bkgColorBuffer = new Array(this.frameWidth * this.frameHeight);
}
// apply background
this.generateBackground();
this.drawBackground();
// wake up update routine per 30 milliseconds
var self = this;
(function tick() {
self.doUpdate();
setTimeout(tick, 30);
})();
// load background image if any
this.setBackgroudImageFromUrl(this.bkgImageUrl);
// load scene if any
this.loadScene();
// load sphere mapping image if any
this.setSphereMapFromUrl(this.sphereMapUrl);
};
/**
Ask viewer to render a new frame or just repaint last frame.
@param {Boolean} repaintOnly true to repaint last frame; false(default) to render a new frame.
*/
JSC3D.Viewer.prototype.update = function (repaintOnly) {
if (this.isFailed) return;
if (repaintOnly) this.needRepaint = true;else this.needUpdate = true;
};
/**
Rotate the scene with given angles around Cardinal axes.
@param {Number} rotX rotation angle around X-axis in degrees.
@param {Number} rotY rotation angle around Y-axis in degrees.
@param {Number} rotZ rotation angle around Z-axis in degrees.
*/
JSC3D.Viewer.prototype.rotate = function (rotX, rotY, rotZ) {
this.rotMatrix.rotateAboutXAxis(rotX);
this.rotMatrix.rotateAboutYAxis(rotY);
this.rotMatrix.rotateAboutZAxis(rotZ);
};
/**
Set render mode.<br />
Available render modes are:<br />
'<b>point</b>': render meshes as point clouds;<br />
'<b>wireframe</b>': render meshes as wireframe;<br />
'<b>flat</b>': render meshes as solid objects using flat shading;<br />
'<b>smooth</b>': render meshes as solid objects using smooth shading;<br />
'<b>texture</b>': render meshes as solid textured objects, no lighting will be apllied;<br />
'<b>textureflat</b>': render meshes as solid textured objects, lighting will be calculated per face;<br />
'<b>texturesmooth</b>': render meshes as solid textured objects, lighting will be calculated per vertex and interpolated.<br />
@param {String} mode new render mode.
*/
JSC3D.Viewer.prototype.setRenderMode = function (mode) {
this.params['RenderMode'] = mode;
this.renderMode = mode;
};
/**
Set quality level of rendering.<br />
Available quality levels are:<br />
'<b>low</b>': low-quality rendering will be applied, with highest performance;<br />
'<b>standard</b>': normal-quality rendering will be applied, with modest performace;<br />
'<b>high</b>': high-quality rendering will be applied, with lowest performace.<br />
@params {String} definition new quality level.
*/
JSC3D.Viewer.prototype.setDefinition = function (definition) {
if (this.canvas.width <= 2 || this.canvas.height <= 2) definition = 'standard';
if (definition == this.definition) return;
this.params['Definition'] = definition;
this.definition = definition;
var oldFrameWidth = this.frameWidth;
switch (this.definition) {
case 'low':
this.frameWidth = ~~((this.canvas.width + 1) / 2);
this.frameHeight = ~~((this.canvas.height + 1) / 2);
break;
case 'high':
this.frameWidth = this.canvas.width * 2;
this.frameHeight = this.canvas.height * 2;
break;
case 'standard':
default:
this.frameWidth = this.canvas.width;
this.frameHeight = this.canvas.height;
break;
}
var ratio = this.frameWidth / oldFrameWidth;
// zoom factor should be adjusted, otherwise there would be an abrupt zoom-in or zoom-out on next frame
this.zoomFactor *= ratio;
// likewise, panning should also be adjusted to avoid abrupt jump on next frame
this.panning[0] *= ratio;
this.panning[1] *= ratio;
if (this.webglBackend) return;
/*
Reallocate frame buffers using the dimensions of current definition.
*/
var newSize = this.frameWidth * this.frameHeight;
if (this.colorBuffer.length < newSize) this.colorBuffer = new Array(newSize);
if (this.zBuffer.length < newSize) this.zBuffer = new Array(newSize);
if (this.selectionBuffer.length < newSize) this.selectionBuffer = new Array(newSize);
if (this.bkgColorBuffer.length < newSize) this.bkgColorBuffer = new Array(newSize);
this.generateBackground();
};
/**
Specify the url for the background image.
@param {String} backgroundImageUrl url string for the background image.
*/
JSC3D.Viewer.prototype.setBackgroudImageFromUrl = function (backgroundImageUrl) {
this.params['BackgroundImageUrl'] = backgroundImageUrl;
this.bkgImageUrl = backgroundImageUrl;
if (backgroundImageUrl == '') {
this.bkgImage = null;
return;
}
var self = this;
var img = new Image();
img.onload = function () {
self.bkgImage = this;
self.generateBackground();
};
img.crossOrigin = 'anonymous'; // explicitly enable cross-domain image
img.src = encodeURI(backgroundImageUrl);
};
/**
Specify a new image from the given url which will be used for applying sphere mapping.
@param {String} sphereMapUrl url string that describes where to load the image.
*/
JSC3D.Viewer.prototype.setSphereMapFromUrl = function (sphereMapUrl) {
this.params['SphereMapUrl'] = sphereMapUrl;
this.sphereMapUrl = sphereMapUrl;
if (sphereMapUrl == '') {
this.sphereMap = null;
return;
}
var self = this;
var newMap = new JSC3D.Texture();
newMap.onready = function () {
self.sphereMap = newMap;
self.update();
};
newMap.createFromUrl(this.sphereMapUrl);
};
/**
Enable/Disable the default mouse and key event handling routines.
@param {Boolean} enabled true to enable the default handler; false to disable them.
*/
JSC3D.Viewer.prototype.enableDefaultInputHandler = function (enabled) {
this.isDefaultInputHandlerEnabled = enabled;
};
/**
Set control of mouse pointer.
Available options are:<br />
'<b>default</b>': default mouse control will be used;<br />
'<b>free</b>': this tells {JSC3D.Viewer} a user-defined mouse control will be adopted.
This is often used together with viewer.enableDefaultInputHandler(false)
and viewer.onmousedown, viewer.onmouseup and/or viewer.onmousemove overridden.<br />
'<b>rotate</b>': mouse will be used to rotate the scene;<br />
'<b>zoom</b>': mouse will be used to do zooming.<br />
'<b>pan</b>': mouse will be used to do panning.<br />
@param {String} usage control of mouse pointer to be set.
@deprecated This method is obsolete since version 1.5.0 and may be removed in the future.
*/
JSC3D.Viewer.prototype.setMouseUsage = function (usage) {
this.mouseUsage = usage;
};
/**
Check if WebGL is enabled for rendering.
@returns {Boolean} true if WebGL is enabled; false if WebGL is not enabled or unavailable.
*/
JSC3D.Viewer.prototype.isWebGLEnabled = function () {
return this.webglBackend != null;
};
/**
Load a new scene from the given url to replace the current scene.
@param {String} sceneUrl url string that describes where to load the new scene.
*/
JSC3D.Viewer.prototype.replaceSceneFromUrl = function (sceneUrl) {
this.params['SceneUrl'] = sceneUrl;
this.sceneUrl = sceneUrl;
this.isFailed = this.isLoaded = false;
this.loadScene();
};
/**
Replace the current scene with a given scene.
@param {JSC3D.Scene} scene the given scene.
*/
JSC3D.Viewer.prototype.replaceScene = function (scene) {
this.params['SceneUrl'] = '';
this.sceneUrl = '';
this.isFailed = false;
this.isLoaded = true;
this.setupScene(scene);
};
/**
Reset the current scene to its initial state.
*/
JSC3D.Viewer.prototype.resetScene = function () {
var d = !this.scene || this.scene.isEmpty() ? 0 : this.scene.aabb.lengthOfDiagonal();
this.zoomFactor = d == 0 ? 1 : (this.frameWidth < this.frameHeight ? this.frameWidth : this.frameHeight) / d;
this.panning = [0, 0];
this.rotMatrix.identity();
this.rotMatrix.rotateAboutXAxis(this.initRotX);
this.rotMatrix.rotateAboutYAxis(this.initRotY);
this.rotMatrix.rotateAboutZAxis(this.initRotZ);
};
/**
Get the current scene.
@returns {JSC3D.Scene} the current scene.
*/
JSC3D.Viewer.prototype.getScene = function () {
return this.scene;
};
/**
Query information at a given position on the canvas.
@param {Number} clientX client x coordinate on the current page.
@param {Number} clientY client y coordinate on the current page.
@returns {JSC3D.PickInfo} a PickInfo object which holds the result.
*/
JSC3D.Viewer.prototype.pick = function (clientX, clientY) {
var pickInfo = new JSC3D.PickInfo();
var canvasRect = this.canvas.getBoundingClientRect();
var canvasX = clientX - canvasRect.left;
var canvasY = clientY - canvasRect.top;
pickInfo.canvasX = canvasX;
pickInfo.canvasY = canvasY;
var pickedId = 0;
if (this.webglBackend) {
pickedId = this.webglBackend.pick(canvasX, canvasY);
} else {
var frameX = canvasX;
var frameY = canvasY;
if (this.selectionBuffer != null && canvasX >= 0 && canvasX < this.canvas.width && canvasY >= 0 && canvasY < this.canvas.height) {
switch (this.definition) {
case 'low':
frameX = ~~(frameX / 2);
frameY = ~~(frameY / 2);
break;
case 'high':
frameX *= 2;
frameY *= 2;
break;
case 'standard':
default:
break;
}
pickedId = this.selectionBuffer[frameY * this.frameWidth + frameX];
if (pickedId > 0) pickInfo.depth = this.zBuffer[frameY * this.frameWidth + frameX];
}
}
if (pickedId > 0) {
var meshes = this.scene.getChildren();
for (var i = 0; i < meshes.length; i++) {
if (meshes[i].internalId == pickedId) {
pickInfo.mesh = meshes[i];
break;
}
}
}
return pickInfo;
};
/**
Render a new frame or repaint last frame.
@private
*/
JSC3D.Viewer.prototype.doUpdate = function () {
if (this.needUpdate || this.needRepaint) {
if (this.beforeupdate != null && typeof this.beforeupdate == 'function') this.beforeupdate();
if (this.scene) {
/*
* Render a new frame or just redraw last frame.
*/
if (this.needUpdate) {
this.beginScene();
this.render();
this.endScene();
}
this.paint();
} else {
// Only need to redraw the background since there is nothing to render.
this.drawBackground();
}
// clear dirty flags
this.needRepaint = false;
this.needUpdate = false;
if (this.afterupdate != null && typeof this.afterupdate == 'function') this.afterupdate();
}
};
/**
Paint onto canvas.
@private
*/
JSC3D.Viewer.prototype.paint = function () {
if (this.webglBackend || !this.ctx2d) return;
this.ctx2d.putImageData(this.canvasData, 0, 0);
};
/**
The mouseDown event handling routine.
@private
*/
JSC3D.Viewer.prototype.mouseDownHandler = function (e) {
if (!this.isLoaded) return;
if (this.onmousedown) {
var info = this.pick(e.clientX, e.clientY);
this.onmousedown(info.canvasX, info.canvasY, e.button, info.depth, info.mesh);
}
e.preventDefault();
e.stopPropagation();
if (!this.isDefaultInputHandlerEnabled) return;
this.buttonStates[e.button] = true;
this.mouseX = e.clientX;
this.mouseY = e.clientY;
this.mouseDownX = e.clientX;
this.mouseDownY = e.clientY;
};
/**
The mouseUp event handling routine.
@private
*/
JSC3D.Viewer.prototype.mouseUpHandler = function (e) {
if (!this.isLoaded) return;
var info;
if (this.onmouseup || this.onmouseclick) {
info = this.pick(e.clientX, e.clientY);
}
if (this.onmouseup) {
this.onmouseup(info.canvasX, info.canvasY, e.button, info.depth, info.mesh);
}
if (this.onmouseclick && this.mouseDownX == e.clientX && this.mouseDownY == e.clientY) {
this.onmouseclick(info.canvasX, info.canvasY, e.button, info.depth, info.mesh);
this.mouseDownX = -1;
this.mouseDownY = -1;
}
e.preventDefault();
e.stopPropagation();
if (!this.isDefaultInputHandlerEnabled) return;
this.buttonStates[e.button] = false;
};
/**
The mouseMove event handling routine.
@private
*/
JSC3D.Viewer.prototype.mouseMoveHandler = function (e) {
if (!this.isLoaded) return;
if (this.onmousemove) {
var info = this.pick(e.clientX, e.clientY);
this.onmousemove(info.canvasX, info.canvasY, e.button, info.depth, info.mesh);
}
e.preventDefault();
e.stopPropagation();
if (!this.isDefaultInputHandlerEnabled) return;
var isDragging = this.buttonStates[0] == true;
var isShiftDown = this.keyStates[0x10] == true;
var isCtrlDown = this.keyStates[0x11] == true;
if (isDragging) {
if (isShiftDown && this.mouseUsage == 'default' || this.mouseUsage == 'zoom') {
this.zoomFactor *= this.mouseY <= e.clientY ? 1.04 : 0.96;
} else if (isCtrlDown && this.mouseUsage == 'default' || this.mouseUsage == 'pan') {
var ratio = this.definition == 'low' ? 0.5 : this.definition == 'high' ? 2 : 1;
this.panning[0] += ratio * (e.clientX - this.mouseX);
this.panning[1] += ratio * (e.clientY - this.mouseY);
} else if (this.mouseUsage == 'default' || this.mouseUsage == 'rotate') {
var rotX = (e.clientY - this.mouseY) * 360 / this.canvas.width;
var rotY = (e.clientX - this.mouseX) * 360 / this.canvas.height;
this.rotMatrix.rotateAboutXAxis(rotX);
this.rotMatrix.rotateAboutYAxis(rotY);
}
this.mouseX = e.clientX;
this.mouseY = e.clientY;
this.mouseDownX = -1;
this.mouseDownY = -1;
this.update();
}
};
JSC3D.Viewer.prototype.mouseWheelHandler = function (e) {
if (!this.isLoaded) return;
if (this.onmousewheel) {
var info = this.pick(e.clientX, e.clientY);
this.onmousewheel(info.canvasX, info.canvasY, e.button, info.depth, info.mesh);
}
e.preventDefault();
e.stopPropagation();
if (!this.isDefaultInputHandlerEnabled) return;
this.mouseDownX = -1;
this.mouseDownY = -1;
this.zoomFactor *= (JSC3D.PlatformInfo.browser == 'firefox' ? -e.detail : e.wheelDelta) < 0 ? 1.1 : 0.91;
this.update();
};
/**
The touchStart event handling routine. This is for compatibility for touch devices.
@private
*/
JSC3D.Viewer.prototype.touchStartHandler = function (e) {
if (!this.isLoaded) return;
if (e.touches.length > 0) {
var clientX = e.touches[0].clientX;
var clientY = e.touches[0].clientY;
if (this.onmousedown) {
var info = this.pick(clientX, clientY);
this.onmousedown(info.canvasX, info.canvasY, 0, info.depth, info.mesh);
}
e.preventDefault();
e.stopPropagation();
if (!this.isDefaultInputHandlerEnabled) return;
this.buttonStates[0] = true;
this.mouseX = clientX;
this.mouseY = clientY;
this.mouseDownX = clientX;
this.mouseDownY = clientY;
}
};
/**
The touchEnd event handling routine. This is for compatibility for touch devices.
@private
*/
JSC3D.Viewer.prototype.touchEndHandler = function (e) {
if (!this.isLoaded) return;
var info;
if (this.onmouseup || this.onmouseclick) {
info = this.pick(this.mouseX, this.mouseY);
}
if (this.onmouseup) {
this.onmouseup(info.canvasX, info.canvasY, 0, info.depth, info.mesh);
}
if (this.onmouseclick && this.mouseDownX == e.touches[0].clientX && this.mouseDownY == e.touches[0].clientY) {
this.onmouseclick(info.canvasX, info.canvasY, 0, info.depth, info.mesh);
this.mouseDownX = -1;
this.mouseDownY = -1;
}
e.preventDefault();
e.stopPropagation();
if (!this.isDefaultInputHandlerEnabled) return;
this.buttonStates[0] = false;
};
/**
The touchMove event handling routine. This is for compatibility for touch devices.
@private
*/
JSC3D.Viewer.prototype.touchMoveHandler = function (e) {
if (!this.isLoaded) return;
if (e.touches.length > 0) {
var clientX = e.touches[0].clientX;
var clientY = e.touches[0].clientY;
if (this.onmousemove) {
var info = this.pick(clientX, clientY);
this.onmousemove(info.canvasX, info.canvasY, 0, info.depth, info.mesh);
}
e.preventDefault();
e.stopPropagation();
if (!this.isDefaultInputHandlerEnabled) return;
if (this.mouseUsage == 'zoom') {
this.zoomFactor *= this.mouseY <= clientY ? 1.04 : 0.96;
} else if (this.mouseUsage == 'pan') {
var ratio = this.definition == 'low' ? 0.5 : this.definition == 'high' ? 2 : 1;
this.panning[0] += ratio * (clientX - this.mouseX);
this.panning[1] += ratio * (clientY - this.mouseY);
} else if (this.mouseUsage == 'default' || this.mouseUsage == 'rotate') {
var rotX = (clientY - this.mouseY) * 360 / this.canvas.width;
var rotY = (clientX - this.mouseX) * 360 / this.canvas.height;
this.rotMatrix.rotateAboutXAxis(rotX);
this.rotMatrix.rotateAboutYAxis(rotY);
}
this.mouseX = clientX;
this.mouseY = clientY;
this.mouseDownX = -1;
this.mouseDownY = -1;
this.update();
}
};
/**
The keyDown event handling routine.
@private
*/
JSC3D.Viewer.prototype.keyDownHandler = function (e) {
if (!this.isDefaultInputHandlerEnabled) return;
this.keyStates[e.keyCode] = true;
};
/**
The keyUp event handling routine.
@private
*/
JSC3D.Viewer.prototype.keyUpHandler = function (e) {
if (!this.isDefaultInputHandlerEnabled) return;
this.keyStates[e.keyCode] = false;
};
/**
The gesture event handling routine which implements gesture-based control on touch devices.
This is based on Hammer.js gesture event implementation.
@private
*/
JSC3D.Viewer.prototype.gestureHandler = function (e) {
if (!this.isLoaded) return;
var clientX = e.gesture.center.pageX - document.body.scrollLeft;
var clientY = e.gesture.center.pageY - document.body.scrollTop;
var info = this.pick(clientX, clientY);
switch (e.type) {
case 'touch':
if (this.onmousedown) this.onmousedown(info.canvasX, info.canvasY, 0, info.depth, info.mesh);
this.baseZoomFactor = this.zoomFactor;
this.mouseX = clientX;
this.mouseY = clientY;
this.mouseDownX = clientX;
this.mouseDownY = clientY;
break;
case 'release':
if (this.onmouseup) this.onmouseup(info.canvasX, info.canvasY, 0, info.depth, info.mesh);
if (this.onmouseclick && this.mouseDownX == clientX && this.mouseDownY == clientY) this.onmouseclick(info.canvasX, info.canvasY, 0, info.depth, info.mesh);
this.mouseDownX = -1;
this.mouseDownY = -1;
this.isTouchHeld = false;
break;
case 'hold':
this.isTouchHeld = true;
this.mouseDownX = -1;
this.mouseDownY = -1;
break;
case 'drag':
if (this.onmousemove) this.onmousemove(info.canvasX, info.canvasY, 0, info.depth, info.mesh);
if (!this.isDefaultInputHandlerEnabled) break;
if (this.isTouchHeld) {
// pan
var ratio = this.definition == 'low' ? 0.5 : this.definition == 'high' ? 2 : 1;
this.panning[0] += ratio * (clientX - this.mouseX);
this.panning[1] += ratio * (clientY - this.mouseY);
} else if (!this.suppressDraggingRotation) {
// rotate
var rotX = (clientY - this.mouseY) * 360 / this.canvas.width;
var rotY = (clientX - this.mouseX) * 360 / this.canvas.height;
this.rotMatrix.rotateAboutXAxis(rotX);
this.rotMatrix.rotateAboutYAxis(rotY);
}
this.mouseX = clientX;
this.mouseY = clientY;
this.mouseDownX = -1;
this.mouseDownY = -1;
this.update();
break;
case 'pinch':
// zoom
if (this.onmousewheel) this.onmousewheel(info.canvasX, info.canvasY, 0, info.depth, info.mesh);
if (!this.isDefaultInputHandlerEnabled) break;
this.suppressDraggingRotation = true;
this.zoomFactor = this.baseZoomFactor * e.gesture.scale;
this.mouseDownX = -1;
this.mouseDownY = -1;
this.update();
break;
case 'transformend':
/*
* Reset the flag to enable dragging rotation again after a delay of 0.25s after the end of a zooming.
* This fixed unnecessary rotation at the end of a zooming when one finger has leaved the touch device
* while the other still stays on it sliding.
* By Jeremy Ellis <jeremy.ellis@mpsd.ca>
*/
var self = this;
setTimeout(function () {
self.suppressDraggingRotation = false;
}, 250);
break;
default:
break;
}
e.gesture.preventDefault();
e.gesture.stopPropagation();
};
/**
Internally load a scene.
@private
*/
JSC3D.Viewer.prototype.loadScene = function () {
// terminate current loading if it is not finished yet
if (this.abortUnfinishedLoadingFn) this.abortUnfinishedLoadingFn();
this.scene = null;
this.isLoaded = false;
this.update();
if (this.sceneUrl == '') return false;
/*
* Discard the query part of the URL string, if any, to get the correct file name.
* By negatif@gmail.com
*/
var questionMarkAt = this.sceneUrl.indexOf('?');
var sceneUrlNoQuery = questionMarkAt == -1 ? this.sceneUrl : this.sceneUrl.substring(0, questionMarkAt);
var lastSlashAt = sceneUrlNoQuery.lastIndexOf('/');
if (lastSlashAt == -1) lastSlashAt = sceneUrlNoQuery.lastIndexOf('\\');
var fileName = sceneUrlNoQuery.substring(lastSlashAt + 1);
var lastDotAt = fileName.lastIndexOf('.');
if (lastDotAt == -1) {
if (JSC3D.console) JSC3D.console.logError('Cannot get file format for the lack of file extension.');
return false;
}
var fileExtName = fileName.substring(lastDotAt + 1);
var loader = JSC3D.LoaderSelector.getLoader(fileExtName);
if (!loader) {
if (JSC3D.console) JSC3D.console.logError('Unsupported file format: "' + fileExtName + '".');
return false;
}
var self = this;
loader.onload = function (scene) {
self.abortUnfinishedLoadingFn = null;
self.setupScene(scene);
if (self.onloadingcomplete && typeof self.onloadingcomplete == 'function') self.onloadingcomplete();
};
loader.onerror = function (errorMsg) {
self.scene = null;
self.isLoaded = false;
self.isFailed = true;
self.abortUnfinishedLoadingFn = null;
self.update();
self.reportError(errorMsg);
if (self.onloadingerror && typeof self.onloadingerror == 'function') self.onloadingerror(errorMsg);
};
loader.onprogress = function (task, prog) {
if (self.showProgressBar) self.reportProgress(task, prog);
if (self.onloadingprogress && typeof self.onloadingprogress == 'function') self.onloadingprogress(task, prog);
};
loader.onresource = function (resource) {
if (resource instanceof JSC3D.Texture && self.isMipMappingOn && !resource.hasMipmap()) resource.generateMipmaps();
self.update();
};
this.abortUnfinishedLoadingFn = function () {
loader.abort();
self.abortUnfinishedLoadingFn = null;
self.hideProgress();
if (self.onloadingaborted && typeof self.onloadingaborted == 'function') self.onloadingaborted();
};
loader.loadFromUrl(this.sceneUrl);
if (this.onloadingstarted && typeof this.onloadingstarted == 'function') this.onloadingstarted();
return true;
};
/**
Prepare for rendering of a new scene.
@private
*/
JSC3D.Viewer.prototype.setupScene = function (scene) {
// crease-angle should be applied onto each mesh before their initialization
if (this.creaseAngle >= 0) {
var cAngle = this.creaseAngle;
scene.forEachChild(function (mesh) {
mesh.creaseAngle = cAngle;
});
}
scene.init();
if (!scene.isEmpty()) {
var d = scene.aabb.lengthOfDiagonal();
var w = this.frameWidth;
var h = this.frameHeight;
this.zoomFactor = d == 0 ? 1 : (w < h ? w : h) / d;
this.panning = [0, 0];
}
this.rotMatrix.identity();
this.rotMatrix.rotateAboutXAxis(this.initRotX);
this.rotMatrix.rotateAboutYAxis(this.initRotY);
this.rotMatrix.rotateAboutZAxis(this.initRotZ);
this.scene = scene;
this.isLoaded = true;
this.isFailed = false;
this.needUpdate = false;
this.needRepaint = false;
this.update();
this.hideProgress();
this.hideError();
};
/**
Show progress with information on current time-cosuming task.
@param {String} task text information about current task.
@param {Number} progress progress of current task. this should be a number between 0 and 1.
*/
JSC3D.Viewer.prototype.reportProgress = function (task, progress) {
if (!this.progressFrame) {
var canvasRect = this.canvas.getBoundingClientRect();
var r = 255 - ((this.bkgColor1 & 0xff0000) >> 16);
var g = 255 - ((this.bkgColor1 & 0xff00) >> 8);
var b = 255 - (this.bkgColor1 & 0xff);
var color = 'rgb(' + r + ',' + g + ',' + b + ')';
var barX = window.pageXOffset + canvasRect.left + 40;
var barY = window.pageYOffset + canvasRect.top + canvasRect.height * 0.38;
var barWidth = canvasRect.width - (barX - canvasRect.left) * 2;
var barHeight = 20;
this.progressFrame = document.createElement('div');
this.progressFrame.style.position = 'absolute';
this.progressFrame.style.left = barX + 'px';
this.progressFrame.style.top = barY + 'px';
this.progressFrame.style.width = barWidth + 'px';
this.progressFrame.style.height = barHeight + 'px';
this.progressFrame.style.border = '1px solid ' + color;
this.progressFrame.style.pointerEvents = 'none';
document.body.appendChild(this.progressFrame);
this.progressRectangle = document.createElement('div');
this.progressRectangle.style.position = 'absolute';
this.progressRectangle.style.left = barX + 3 + 'px';
this.progressRectangle.style.top = barY + 3 + 'px';
this.progressRectangle.style.width = '0px';
this.progressRectangle.style.height = barHeight - 4 + 'px';
this.progressRectangle.style.background = color;
this.progressRectangle.style.pointerEvents = 'none';
document.body.appendChild(this.progressRectangle);
if (!this.messagePanel) {
this.messagePanel = document.createElement('div');
this.messagePanel.style.position = 'absolute';
this.messagePanel.style.left = barX + 'px';
this.messagePanel.style.top = barY - 16 + 'px';
this.messagePanel.style.width = barWidth + 'px';
this.messagePanel.style.height = '14px';
this.messagePanel.style.font = 'bold 14px Courier New';
this.messagePanel.style.color = color;
this.messagePanel.style.pointerEvents = 'none';
document.body.appendChild(this.messagePanel);
}
}
if (this.progressFrame.style.display != 'block') {
this.progressFrame.style.display = 'block';
this.progressRectangle.style.display = 'block';
}
if (task && this.messagePanel.style.display != 'block') this.messagePanel.style.display = 'block';
this.progressRectangle.style.width = (parseFloat(this.progressFrame.style.width) - 4) * progress + 'px';
this.messagePanel.innerHTML = task;
};
/**
Hide the progress bar.
@private
*/
JSC3D.Viewer.prototype.hideProgress = function () {
if (this.progressFrame) {
this.messagePanel.style.display = 'none';
this.progressFrame.style.display = 'none';
this.progressRectangle.style.display = 'none';
}
};
/**
Show information about a fatal error.
@param {String} message text information about this error.
*/
JSC3D.Viewer.prototype.reportError = function (message) {
if (!this.messagePanel) {
var canvasRect = this.canvas.getBoundingClientRect();
var r = 255 - ((this.bkgColor1 & 0xff0000) >> 16);
var g = 255 - ((this.bkgColor1 & 0xff00) >> 8);
var b = 255 - (this.bkgColor1 & 0xff);
var color = 'rgb(' + r + ',' + g + ',' + b + ')';
var panelX = window.pageXOffset + canvasRect.left + 40;
var panelY = window.pageYOffset + canvasRect.top + canvasRect.height * 0.38;
var panelWidth = canvasRect.width - (panelX - canvasRect.left) * 2;
var panelHeight = 14;
this.messagePanel = document.createElement('div');
this.messagePanel.style.position = 'absolute';
this.messagePanel.style.left = panelX + 'px';
this.messagePanel.style.top = panelY - 16 + 'px';
this.messagePanel.style.width = panelWidth + 'px';
this.messagePanel.style.height = panelHeight + 'px';
this.messagePanel.style.font = 'bold 14px Courier New';
this.messagePanel.style.color = color;
this.messagePanel.style.pointerEvents = 'none';
document.body.appendChild(this.messagePanel);
}
// hide the progress bar if it is visible
// michaelb change #2
if (this.progressFrame && this.progressFrame.style.display != 'none') {
this.progressFrame.style.display = 'none';
this.progressRectangle.style.display = 'none';
}
if (message && this.messagePanel.style.display != 'block') this.messagePanel.style.display = 'block';
this.messagePanel.innerHTML = message;
};
/**
Hide the error message.
@private
*/
JSC3D.Viewer.prototype.hideError = function () {
if (this.messagePanel) this.messagePanel.style.display = 'none';
};
/**
Fill the background color buffer.
@private
*/
JSC3D.Viewer.prototype.generateBackground = function () {
if (this.webglBackend) {
if (this.bkgImage) this.webglBackend.setBackgroundImage(this.bkgImage);else this.webglBackend.setBackgroundColors(this.bkgColor1, this.bkgColor2);
return;
}
if (this.bkgImage) this.fillBackgroundWithImage();else this.fillGradientBackground();
};
/**
Do fill the background color buffer with gradient colors.
@private
*/
JSC3D.Viewer.prototype.fillGradientBackground = function () {
var w = this.frameWidth;
var h = this.frameHeight;
var pixels = this.bkgColorBuffer;
var r1 = (this.bkgColor1 & 0xff0000) >> 16;
var g1 = (this.bkgColor1 & 0xff00) >> 8;
var b1 = this.bkgColor1 & 0xff;
var r2 = (this.bkgColor2 & 0xff0000) >> 16;
var g2 = (this.bkgColor2 & 0xff00) >> 8;
var b2 = this.bkgColor2 & 0xff;
var alpha = this.isBackgroundOn ? 0xff000000 : 0;
var pix = 0;
for (var i = 0; i < h; i++) {
var r = r1 + i * (r2 - r1) / h & 0xff;
var g = g1 + i * (g2 - g1) / h & 0xff;
var b = b1 + i * (b2 - b1) / h & 0xff;
for (var j = 0; j < w; j++) {
pixels[pix++] = alpha | r << 16 | g << 8 | b;
}
}
};
/**
Do fill the background color buffer with a loaded image.
@private
*/
JSC3D.Viewer.prototype.fillBackgroundWithImage = function () {
var w = this.frameWidth;
var h = this.frameHeight;
if (this.bkgImage.width <= 0 || this.bkgImage.height <= 0) return;
var isCanvasClean = false;
var canvas = JSC3D.Texture.cv;
if (!canvas) {
try {
canvas = document.createElement('canvas');
JSC3D.Texture.cv = canvas;
isCanvasClean = true;
} catch (e) {
return;
}
}
if (canvas.width != w || canvas.height != h) {
canvas.width = w;
canvas.height = h;
isCanvasClean = true;
}
var data = null;
try {
var ctx = canvas.getContext('2d');
if (!isCanvasClean) ctx.clearRect(0, 0, w, h);
ctx.drawImage(this.bkgImage, 0, 0, w, h);
var imgData = ctx.getImageData(0, 0, w, h);
data = imgData.data;
} catch (e) {
return;
}
var pixels = this.bkgColorBuffer;
var size = w * h;
var alpha = this.isBackgroundOn ? 0xff000000 : 0;
for (var i = 0, j = 0; i < size; i++, j += 4) {
pixels[i] = alpha | data[j] << 16 | data[j + 1] << 8 | data[j + 2];
}
};
/**
Draw background onto canvas.
@private
*/
JSC3D.Viewer.prototype.drawBackground = function () {
if (!this.webglBackend && !this.ctx2d) return;
this.beginScene();
this.endScene();
this.paint();
};
/**
Begin to render a new frame.
@private
*/
JSC3D.Viewer.prototype.beginScene = function () {
if (this.webglBackend) {
this.webglBackend.beginFrame(this.definition, this.isBackgroundOn);
return;
}
var cbuf = this.colorBuffer;
var zbuf = this.zBuffer;
var sbuf = this.selectionBuffer;
var bbuf = this.bkgColorBuffer;
var size = this.frameWidth * this.frameHeight;
var MIN_Z = -Infinity;
for (var i = 0; i < size; i++) {
cbuf[i] = bbuf[i];
zbuf[i] = MIN_Z;
sbuf[i] = 0;
}
};
/**
End for rendering of a frame.
@private
*/
JSC3D.Viewer.prototype.endScene = function () {
if (this.webglBackend) {
this.webglBackend.endFrame();
return;
}
var data = this.canvasData.data;
var width = this.canvas.width;
var height = this.canvas.height;
var cbuf = this.colorBuffer;
var cwidth = this.frameWidth;
var cheight = this.frameHeight;
var csize = cwidth * cheight;
switch (this.definition) {
case 'low':
var halfWidth = width >> 1;
var surplus = cwidth - halfWidth;
var src = 0,
dest = 0;
for (var i = 0; i < height; i++) {
for (var j = 0; j < width; j++) {
var color = cbuf[src];
data[dest] = (color & 0xff0000) >> 16;
data[dest + 1] = (color & 0xff00) >> 8;
data[dest + 2] = color & 0xff;
data[dest + 3] = color >>> 24;
src += j & 1;
dest += 4;
}
src += i & 1 ? surplus : -halfWidth;
}
break;
case 'high':
var src = 0,
dest = 0;
for (var i = 0; i < height; i++) {
for (var j = 0; j < width; j++) {
var color0 = cbuf[src];
var color1 = cbuf[src + 1];
var color2 = cbuf[src + cwidth];
var color3 = cbuf[src + cwidth + 1];
data[dest] = (color0 & 0xff0000) + (color1 & 0xff0000) + (color2 & 0xff0000) + (color3 & 0xff0000) >> 18;
data[dest + 1] = (color0 & 0xff00) + (color1 & 0xff00) + (color2 & 0xff00) + (color3 & 0xff00) >> 10;
data[dest + 2] = (color0 & 0xff) + (color1 & 0xff) + (color2 & 0xff) + (color3 & 0xff) >> 2;
data[dest + 3] = color0 >>> 24;