'use strict';
var glPlot3d = require('../../../stackgl_modules').gl_plot3d;
var createCamera = glPlot3d.createCamera;
var createPlot = glPlot3d.createScene;
var getContext = require('webgl-context');
var passiveSupported = require('has-passive-events');
var Registry = require('../../registry');
var Lib = require('../../lib');
var preserveDrawingBuffer = Lib.preserveDrawingBuffer();
var Axes = require('../../plots/cartesian/axes');
var Fx = require('../../components/fx');
var str2RGBAarray = require('../../lib/str2rgbarray');
var showNoWebGlMsg = require('../../lib/show_no_webgl_msg');
var project = require('./project');
var createAxesOptions = require('./layout/convert');
var createSpikeOptions = require('./layout/spikes');
var computeTickMarks = require('./layout/tick_marks');
var applyAutorangeOptions = require('../cartesian/autorange').applyAutorangeOptions;
var STATIC_CANVAS, STATIC_CONTEXT;
var tabletmode = false;
function Scene(options, fullLayout) {
// create sub container for plot
var sceneContainer = document.createElement('div');
var plotContainer = options.container;
// keep a ref to the graph div to fire hover+click events
this.graphDiv = options.graphDiv;
// create SVG container for hover text
var svgContainer = document.createElementNS(
'http://www.w3.org/2000/svg',
'svg');
svgContainer.style.position = 'absolute';
svgContainer.style.top = svgContainer.style.left = '0px';
svgContainer.style.width = svgContainer.style.height = '100%';
svgContainer.style['z-index'] = 20;
svgContainer.style['pointer-events'] = 'none';
sceneContainer.appendChild(svgContainer);
this.svgContainer = svgContainer;
// Tag the container with the sceneID
sceneContainer.id = options.id;
sceneContainer.style.position = 'absolute';
sceneContainer.style.top = sceneContainer.style.left = '0px';
sceneContainer.style.width = sceneContainer.style.height = '100%';
plotContainer.appendChild(sceneContainer);
this.fullLayout = fullLayout;
this.id = options.id || 'scene';
this.fullSceneLayout = fullLayout[this.id];
// Saved from last call to plot()
this.plotArgs = [ [], {}, {} ];
/*
* Move this to calc step? Why does it work here?
*/
this.axesOptions = createAxesOptions(fullLayout, fullLayout[this.id]);
this.spikeOptions = createSpikeOptions(fullLayout[this.id]);
this.container = sceneContainer;
this.staticMode = !!options.staticPlot;
this.pixelRatio = this.pixelRatio || options.plotGlPixelRatio || 2;
// Coordinate rescaling
this.dataScale = [1, 1, 1];
this.contourLevels = [ [], [], [] ];
this.convertAnnotations = Registry.getComponentMethod('annotations3d', 'convert');
this.drawAnnotations = Registry.getComponentMethod('annotations3d', 'draw');
this.initializeGLPlot();
}
var proto = Scene.prototype;
proto.prepareOptions = function() {
var scene = this;
var opts = {
canvas: scene.canvas,
gl: scene.gl,
glOptions: {
preserveDrawingBuffer: preserveDrawingBuffer,
premultipliedAlpha: true,
antialias: true
},
container: scene.container,
axes: scene.axesOptions,
spikes: scene.spikeOptions,
pickRadius: 10,
snapToData: true,
autoScale: true,
autoBounds: false,
cameraObject: scene.camera,
pixelRatio: scene.pixelRatio
};
// for static plots, we reuse the WebGL context
// as WebKit doesn't collect them reliably
if(scene.staticMode) {
if(!STATIC_CONTEXT) {
STATIC_CANVAS = document.createElement('canvas');
STATIC_CONTEXT = getContext({
canvas: STATIC_CANVAS,
preserveDrawingBuffer: true,
premultipliedAlpha: true,
antialias: true
});
if(!STATIC_CONTEXT) {
throw new Error('error creating static canvas/context for image server');
}
}
opts.gl = STATIC_CONTEXT;
opts.canvas = STATIC_CANVAS;
}
return opts;
};
var firstInit = true;
proto.tryCreatePlot = function() {
var scene = this;
var opts = scene.prepareOptions();
var success = true;
try {
scene.glplot = createPlot(opts);
} catch(e) {
if(scene.staticMode || !firstInit || preserveDrawingBuffer) {
success = false;
} else { // try second time
// enable preserveDrawingBuffer setup
// in case is-mobile not detecting the right device
Lib.warn([
'webgl setup failed possibly due to',
'false preserveDrawingBuffer config.',
'The mobile/tablet device may not be detected by is-mobile module.',
'Enabling preserveDrawingBuffer in second attempt to create webgl scene...'
].join(' '));
try {
// invert preserveDrawingBuffer
preserveDrawingBuffer = opts.glOptions.preserveDrawingBuffer = true;
scene.glplot = createPlot(opts);
} catch(e) {
// revert changes to preserveDrawingBuffer
preserveDrawingBuffer = opts.glOptions.preserveDrawingBuffer = false;
success = false;
}
}
}
firstInit = false;
return success;
};
proto.initializeGLCamera = function() {
var scene = this;
var cameraData = scene.fullSceneLayout.camera;
var isOrtho = (cameraData.projection.type === 'orthographic');
scene.camera = createCamera(scene.container, {
center: [cameraData.center.x, cameraData.center.y, cameraData.center.z],
eye: [cameraData.eye.x, cameraData.eye.y, cameraData.eye.z],
up: [cameraData.up.x, cameraData.up.y, cameraData.up.z],
_ortho: isOrtho,
zoomMin: 0.01,
zoomMax: 100,
mode: 'orbit'
});
};
proto.initializeGLPlot = function() {
var scene = this;
scene.initializeGLCamera();
var success = scene.tryCreatePlot();
/*
* createPlot will throw when webgl is not enabled in the client.
* Lets return an instance of the module with all functions noop'd.
* The destroy method - which will remove the container from the DOM
* is overridden with a function that removes the container only.
*/
if(!success) return showNoWebGlMsg(scene);
// List of scene objects
scene.traces = {};
scene.make4thDimension();
var gd = scene.graphDiv;
var layout = gd.layout;
var makeUpdate = function() {
var update = {};
if(scene.isCameraChanged(layout)) {
// camera updates
update[scene.id + '.camera'] = scene.getCamera();
}
if(scene.isAspectChanged(layout)) {
// scene updates
update[scene.id + '.aspectratio'] = scene.glplot.getAspectratio();
if(layout[scene.id].aspectmode !== 'manual') {
scene.fullSceneLayout.aspectmode =
layout[scene.id].aspectmode =
update[scene.id + '.aspectmode'] = 'manual';
}
}
return update;
};
var relayoutCallback = function(scene) {
if(scene.fullSceneLayout.dragmode === false) return;
var update = makeUpdate();
scene.saveLayout(layout);
scene.graphDiv.emit('plotly_relayout', update);
};
if(scene.glplot.canvas) {
scene.glplot.canvas.addEventListener('mouseup', function() {
relayoutCallback(scene);
});
scene.glplot.canvas.addEventListener('touchstart', function() {
tabletmode = true;
});
scene.glplot.canvas.addEventListener('wheel', function(e) {
if(gd._context._scrollZoom.gl3d) {
if(scene.camera._ortho) {
var s = (e.deltaX > e.deltaY) ? 1.1 : 1.0 / 1.1;
var o = scene.glplot.getAspectratio();
scene.glplot.setAspectratio({
x: s * o.x,
y: s * o.y,
z: s * o.z
});
}
relayoutCallback(scene);
}
}, passiveSupported ? {passive: false} : false);
scene.glplot.canvas.addEventListener('mousemove', function() {
if(scene.fullSceneLayout.dragmode === false) return;
if(scene.camera.mouseListener.buttons === 0) return;
var update = makeUpdate();
scene.graphDiv.emit('plotly_relayouting', update);
});
if(!scene.staticMode) {
scene.glplot.canvas.addEventListener('webglcontextlost', function(event) {
if(gd && gd.emit) {
gd.emit('plotly_webglcontextlost', {
event: event,
layer: scene.id
});
}
}, false);
}
}
scene.glplot.oncontextloss = function() {
scene.recoverContext();
};
scene.glplot.onrender = function() {
scene.render();
};
return true;
};
proto.render = function() {
var scene = this;
var gd = scene.graphDiv;
var trace;
// update size of svg container
var svgContainer = scene.svgContainer;
var clientRect = scene.container.getBoundingClientRect();
gd._fullLayout._calcInverseTransform(gd);
var scaleX = gd._fullLayout._invScaleX;
var scaleY = gd._fullLayout._invScaleY;
var width = clientRect.width * scaleX;
var height = clientRect.height * scaleY;
svgContainer.setAttributeNS(null, 'viewBox', '0 0 ' + width + ' ' + height);
svgContainer.setAttributeNS(null, 'width', width);
svgContainer.setAttributeNS(null, 'height', height);
computeTickMarks(scene);
scene.glplot.axes.update(scene.axesOptions);
// check if pick has changed
var keys = Object.keys(scene.traces);
var lastPicked = null;
var selection = scene.glplot.selection;
for(var i = 0; i < keys.length; ++i) {
trace = scene.traces[keys[i]];
if(trace.data.hoverinfo !== 'skip' && trace.handlePick(selection)) {
lastPicked = trace;
}
if(trace.setContourLevels) trace.setContourLevels();
}
function formatter(axLetter, val, hoverformat) {
var ax = scene.fullSceneLayout[axLetter + 'axis'];
if(ax.type !== 'log') {
val = ax.d2l(val);
}
return Axes.hoverLabelText(ax, val, hoverformat);
}
if(lastPicked !== null) {
var pdata = project(scene.glplot.cameraParams, selection.dataCoordinate);
trace = lastPicked.data;
var traceNow = gd._fullData[trace.index];
var ptNumber = selection.index;
var labels = {
xLabel: formatter('x', selection.traceCoordinate[0], trace.xhoverformat),
yLabel: formatter('y', selection.traceCoordinate[1], trace.yhoverformat),
zLabel: formatter('z', selection.traceCoordinate[2], trace.zhoverformat)
};
var hoverinfo = Fx.castHoverinfo(traceNow, scene.fullLayout, ptNumber);
var hoverinfoParts = (hoverinfo || '').split('+');
var isHoverinfoAll = hoverinfo && hoverinfo === 'all';
if(!traceNow.hovertemplate && !isHoverinfoAll) {
if(hoverinfoParts.indexOf('x') === -1) labels.xLabel = undefined;
if(hoverinfoParts.indexOf('y') === -1) labels.yLabel = undefined;
if(hoverinfoParts.indexOf('z') === -1) labels.zLabel = undefined;
if(hoverinfoParts.indexOf('text') === -1) selection.textLabel = undefined;
if(hoverinfoParts.indexOf('name') === -1) lastPicked.name = undefined;
}
var tx;
var vectorTx = [];
if(trace.type === 'cone' || trace.type === 'streamtube') {
labels.uLabel = formatter('x', selection.traceCoordinate[3], trace.uhoverformat);
if(isHoverinfoAll || hoverinfoParts.indexOf('u') !== -1) {
vectorTx.push('u: ' + labels.uLabel);
}
labels.vLabel = formatter('y', selection.traceCoordinate[4], trace.vhoverformat);
if(isHoverinfoAll || hoverinfoParts.indexOf('v') !== -1) {
vectorTx.push('v: ' + labels.vLabel);
}
labels.wLabel = formatter('z', selection.traceCoordinate[5], trace.whoverformat);
if(isHoverinfoAll || hoverinfoParts.indexOf('w') !== -1) {
vectorTx.push('w: ' + labels.wLabel);
}
labels.normLabel = selection.traceCoordinate[6].toPrecision(3);
if(isHoverinfoAll || hoverinfoParts.indexOf('norm') !== -1) {
vectorTx.push('norm: ' + labels.normLabel);
}
if(trace.type === 'streamtube') {
labels.divergenceLabel = selection.traceCoordinate[7].toPrecision(3);
if(isHoverinfoAll || hoverinfoParts.indexOf('divergence') !== -1) {
vectorTx.push('divergence: ' + labels.divergenceLabel);
}
}
if(selection.textLabel) {
vectorTx.push(selection.textLabel);
}
tx = vectorTx.join('
');
} else if(trace.type === 'isosurface' || trace.type === 'volume') {
labels.valueLabel = Axes.hoverLabelText(scene._mockAxis, scene._mockAxis.d2l(selection.traceCoordinate[3]), trace.valuehoverformat);
vectorTx.push('value: ' + labels.valueLabel);
if(selection.textLabel) {
vectorTx.push(selection.textLabel);
}
tx = vectorTx.join('
');
} else {
tx = selection.textLabel;
}
var pointData = {
x: selection.traceCoordinate[0],
y: selection.traceCoordinate[1],
z: selection.traceCoordinate[2],
data: traceNow._input,
fullData: traceNow,
curveNumber: traceNow.index,
pointNumber: ptNumber
};
Fx.appendArrayPointValue(pointData, traceNow, ptNumber);
if(trace._module.eventData) {
pointData = traceNow._module.eventData(pointData, selection, traceNow, {}, ptNumber);
}
var eventData = {points: [pointData]};
if(scene.fullSceneLayout.hovermode) {
var bbox = [];
Fx.loneHover({
trace: traceNow,
x: (0.5 + 0.5 * pdata[0] / pdata[3]) * width,
y: (0.5 - 0.5 * pdata[1] / pdata[3]) * height,
xLabel: labels.xLabel,
yLabel: labels.yLabel,
zLabel: labels.zLabel,
text: tx,
name: lastPicked.name,
color: Fx.castHoverOption(traceNow, ptNumber, 'bgcolor') || lastPicked.color,
borderColor: Fx.castHoverOption(traceNow, ptNumber, 'bordercolor'),
fontFamily: Fx.castHoverOption(traceNow, ptNumber, 'font.family'),
fontSize: Fx.castHoverOption(traceNow, ptNumber, 'font.size'),
fontColor: Fx.castHoverOption(traceNow, ptNumber, 'font.color'),
nameLength: Fx.castHoverOption(traceNow, ptNumber, 'namelength'),
textAlign: Fx.castHoverOption(traceNow, ptNumber, 'align'),
hovertemplate: Lib.castOption(traceNow, ptNumber, 'hovertemplate'),
hovertemplateLabels: Lib.extendFlat({}, pointData, labels),
eventData: [pointData]
}, {
container: svgContainer,
gd: gd,
inOut_bbox: bbox
});
pointData.bbox = bbox[0];
}
if(selection.distance < 5 && (selection.buttons || tabletmode)) {
gd.emit('plotly_click', eventData);
} else {
gd.emit('plotly_hover', eventData);
}
this.oldEventData = eventData;
} else {
Fx.loneUnhover(svgContainer);
if(this.oldEventData) gd.emit('plotly_unhover', this.oldEventData);
this.oldEventData = undefined;
}
scene.drawAnnotations(scene);
};
proto.recoverContext = function() {
var scene = this;
scene.glplot.dispose();
var tryRecover = function() {
if(scene.glplot.gl.isContextLost()) {
requestAnimationFrame(tryRecover);
return;
}
if(!scene.initializeGLPlot()) {
Lib.error('Catastrophic and unrecoverable WebGL error. Context lost.');
return;
}
scene.plot.apply(scene, scene.plotArgs);
};
requestAnimationFrame(tryRecover);
};
var axisProperties = [ 'xaxis', 'yaxis', 'zaxis' ];
function computeTraceBounds(scene, trace, bounds) {
var fullSceneLayout = scene.fullSceneLayout;
for(var d = 0; d < 3; d++) {
var axisName = axisProperties[d];
var axLetter = axisName.charAt(0);
var ax = fullSceneLayout[axisName];
var coords = trace[axLetter];
var calendar = trace[axLetter + 'calendar'];
var len = trace['_' + axLetter + 'length'];
if(!Lib.isArrayOrTypedArray(coords)) {
bounds[0][d] = Math.min(bounds[0][d], 0);
bounds[1][d] = Math.max(bounds[1][d], len - 1);
} else {
var v;
for(var i = 0; i < (len || coords.length); i++) {
if(Lib.isArrayOrTypedArray(coords[i])) {
for(var j = 0; j < coords[i].length; ++j) {
v = ax.d2l(coords[i][j], 0, calendar);
if(!isNaN(v) && isFinite(v)) {
bounds[0][d] = Math.min(bounds[0][d], v);
bounds[1][d] = Math.max(bounds[1][d], v);
}
}
} else {
v = ax.d2l(coords[i], 0, calendar);
if(!isNaN(v) && isFinite(v)) {
bounds[0][d] = Math.min(bounds[0][d], v);
bounds[1][d] = Math.max(bounds[1][d], v);
}
}
}
}
}
}
function computeAnnotationBounds(scene, bounds) {
var fullSceneLayout = scene.fullSceneLayout;
var annotations = fullSceneLayout.annotations || [];
for(var d = 0; d < 3; d++) {
var axisName = axisProperties[d];
var axLetter = axisName.charAt(0);
var ax = fullSceneLayout[axisName];
for(var j = 0; j < annotations.length; j++) {
var ann = annotations[j];
if(ann.visible) {
var pos = ax.r2l(ann[axLetter]);
if(!isNaN(pos) && isFinite(pos)) {
bounds[0][d] = Math.min(bounds[0][d], pos);
bounds[1][d] = Math.max(bounds[1][d], pos);
}
}
}
}
}
proto.plot = function(sceneData, fullLayout, layout) {
var scene = this;
// Save parameters
scene.plotArgs = [sceneData, fullLayout, layout];
if(scene.glplot.contextLost) return;
var data, trace;
var i, j, axis, axisType;
var fullSceneLayout = fullLayout[scene.id];
var sceneLayout = layout[scene.id];
// Update layout
scene.fullLayout = fullLayout;
scene.fullSceneLayout = fullSceneLayout;
scene.axesOptions.merge(fullLayout, fullSceneLayout);
scene.spikeOptions.merge(fullSceneLayout);
// Update camera and camera mode
scene.setViewport(fullSceneLayout);
scene.updateFx(fullSceneLayout.dragmode, fullSceneLayout.hovermode);
scene.camera.enableWheel = scene.graphDiv._context._scrollZoom.gl3d;
// Update scene background
scene.glplot.setClearColor(str2RGBAarray(fullSceneLayout.bgcolor));
// Update axes functions BEFORE updating traces
scene.setConvert(axis);
// Convert scene data
if(!sceneData) sceneData = [];
else if(!Array.isArray(sceneData)) sceneData = [sceneData];
// Compute trace bounding box
var dataBounds = [
[Infinity, Infinity, Infinity],
[-Infinity, -Infinity, -Infinity]
];
for(i = 0; i < sceneData.length; ++i) {
data = sceneData[i];
if(data.visible !== true || data._length === 0) continue;
computeTraceBounds(this, data, dataBounds);
}
computeAnnotationBounds(this, dataBounds);
var dataScale = [1, 1, 1];
for(j = 0; j < 3; ++j) {
if(dataBounds[1][j] === dataBounds[0][j]) {
dataScale[j] = 1.0;
} else {
dataScale[j] = 1.0 / (dataBounds[1][j] - dataBounds[0][j]);
}
}
// Save scale
scene.dataScale = dataScale;
// after computeTraceBounds where ax._categories are filled in
scene.convertAnnotations(this);
// Update traces
for(i = 0; i < sceneData.length; ++i) {
data = sceneData[i];
if(data.visible !== true || data._length === 0) {
continue;
}
trace = scene.traces[data.uid];
if(trace) {
if(trace.data.type === data.type) {
trace.update(data);
} else {
trace.dispose();
trace = data._module.plot(this, data);
scene.traces[data.uid] = trace;
}
} else {
trace = data._module.plot(this, data);
scene.traces[data.uid] = trace;
}
trace.name = data.name;
}
// Remove empty traces
var traceIds = Object.keys(scene.traces);
traceIdLoop:
for(i = 0; i < traceIds.length; ++i) {
for(j = 0; j < sceneData.length; ++j) {
if(sceneData[j].uid === traceIds[i] &&
(sceneData[j].visible === true && sceneData[j]._length !== 0)) {
continue traceIdLoop;
}
}
trace = scene.traces[traceIds[i]];
trace.dispose();
delete scene.traces[traceIds[i]];
}
// order object per trace index
scene.glplot.objects.sort(function(a, b) {
return a._trace.data.index - b._trace.data.index;
});
// Update ranges (needs to be called *after* objects are added due to updates)
var sceneBounds = [[0, 0, 0], [0, 0, 0]];
var axisDataRange = [];
var axisTypeRatios = {};
for(i = 0; i < 3; ++i) {
axis = fullSceneLayout[axisProperties[i]];
axisType = axis.type;
if(axisType in axisTypeRatios) {
axisTypeRatios[axisType].acc *= dataScale[i];
axisTypeRatios[axisType].count += 1;
} else {
axisTypeRatios[axisType] = {
acc: dataScale[i],
count: 1
};
}
var range;
if(axis.autorange) {
sceneBounds[0][i] = Infinity;
sceneBounds[1][i] = -Infinity;
var objects = scene.glplot.objects;
var annotations = scene.fullSceneLayout.annotations || [];
var axLetter = axis._name.charAt(0);
for(j = 0; j < objects.length; j++) {
var obj = objects[j];
var objBounds = obj.bounds;
var pad = obj._trace.data._pad || 0;
if(obj.constructor.name === 'ErrorBars' && axis._lowerLogErrorBound) {
sceneBounds[0][i] = Math.min(sceneBounds[0][i], axis._lowerLogErrorBound);
} else {
sceneBounds[0][i] = Math.min(sceneBounds[0][i], objBounds[0][i] / dataScale[i] - pad);
}
sceneBounds[1][i] = Math.max(sceneBounds[1][i], objBounds[1][i] / dataScale[i] + pad);
}
for(j = 0; j < annotations.length; j++) {
var ann = annotations[j];
// N.B. not taking into consideration the arrowhead
if(ann.visible) {
var pos = axis.r2l(ann[axLetter]);
sceneBounds[0][i] = Math.min(sceneBounds[0][i], pos);
sceneBounds[1][i] = Math.max(sceneBounds[1][i], pos);
}
}
if('rangemode' in axis && axis.rangemode === 'tozero') {
sceneBounds[0][i] = Math.min(sceneBounds[0][i], 0);
sceneBounds[1][i] = Math.max(sceneBounds[1][i], 0);
}
if(sceneBounds[0][i] > sceneBounds[1][i]) {
sceneBounds[0][i] = -1;
sceneBounds[1][i] = 1;
} else {
var d = sceneBounds[1][i] - sceneBounds[0][i];
sceneBounds[0][i] -= d / 32.0;
sceneBounds[1][i] += d / 32.0;
}
range = [
sceneBounds[0][i],
sceneBounds[1][i]
];
range = applyAutorangeOptions(range, axis);
sceneBounds[0][i] = range[0];
sceneBounds[1][i] = range[1];
if(axis.isReversed()) {
// swap bounds:
var tmp = sceneBounds[0][i];
sceneBounds[0][i] = sceneBounds[1][i];
sceneBounds[1][i] = tmp;
}
} else {
range = axis.range;
sceneBounds[0][i] = axis.r2l(range[0]);
sceneBounds[1][i] = axis.r2l(range[1]);
}
if(sceneBounds[0][i] === sceneBounds[1][i]) {
sceneBounds[0][i] -= 1;
sceneBounds[1][i] += 1;
}
axisDataRange[i] = sceneBounds[1][i] - sceneBounds[0][i];
axis.range = [
sceneBounds[0][i],
sceneBounds[1][i]
];
axis.limitRange();
// Update plot bounds
scene.glplot.setBounds(i, {
min: axis.range[0] * dataScale[i],
max: axis.range[1] * dataScale[i]
});
}
/*
* Dynamically set the aspect ratio depending on the users aspect settings
*/
var aspectRatio;
var aspectmode = fullSceneLayout.aspectmode;
if(aspectmode === 'cube') {
aspectRatio = [1, 1, 1];
} else if(aspectmode === 'manual') {
var userRatio = fullSceneLayout.aspectratio;
aspectRatio = [userRatio.x, userRatio.y, userRatio.z];
} else if(aspectmode === 'auto' || aspectmode === 'data') {
var axesScaleRatio = [1, 1, 1];
// Compute axis scale per category
for(i = 0; i < 3; ++i) {
axis = fullSceneLayout[axisProperties[i]];
axisType = axis.type;
var axisRatio = axisTypeRatios[axisType];
axesScaleRatio[i] = Math.pow(axisRatio.acc, 1.0 / axisRatio.count) / dataScale[i];
}
if(aspectmode === 'data') {
aspectRatio = axesScaleRatio;
} else { // i.e. 'auto' option
if(
Math.max.apply(null, axesScaleRatio) /
Math.min.apply(null, axesScaleRatio) <= 4
) {
// USE DATA MODE WHEN AXIS RANGE DIMENSIONS ARE RELATIVELY EQUAL
aspectRatio = axesScaleRatio;
} else {
// USE EQUAL MODE WHEN AXIS RANGE DIMENSIONS ARE HIGHLY UNEQUAL
aspectRatio = [1, 1, 1];
}
}
} else {
throw new Error('scene.js aspectRatio was not one of the enumerated types');
}
/*
* Write aspect Ratio back to user data and fullLayout so that it is modifies as user
* manipulates the aspectmode settings and the fullLayout is up-to-date.
*/
fullSceneLayout.aspectratio.x = sceneLayout.aspectratio.x = aspectRatio[0];
fullSceneLayout.aspectratio.y = sceneLayout.aspectratio.y = aspectRatio[1];
fullSceneLayout.aspectratio.z = sceneLayout.aspectratio.z = aspectRatio[2];
/*
* Finally assign the computed aspecratio to the glplot module. This will have an effect
* on the next render cycle.
*/
scene.glplot.setAspectratio(fullSceneLayout.aspectratio);
// save 'initial' aspectratio & aspectmode view settings for modebar buttons
if(!scene.viewInitial.aspectratio) {
scene.viewInitial.aspectratio = {
x: fullSceneLayout.aspectratio.x,
y: fullSceneLayout.aspectratio.y,
z: fullSceneLayout.aspectratio.z
};
}
if(!scene.viewInitial.aspectmode) {
scene.viewInitial.aspectmode = fullSceneLayout.aspectmode;
}
// Update frame position for multi plots
var domain = fullSceneLayout.domain || null;
var size = fullLayout._size || null;
if(domain && size) {
var containerStyle = scene.container.style;
containerStyle.position = 'absolute';
containerStyle.left = (size.l + domain.x[0] * size.w) + 'px';
containerStyle.top = (size.t + (1 - domain.y[1]) * size.h) + 'px';
containerStyle.width = (size.w * (domain.x[1] - domain.x[0])) + 'px';
containerStyle.height = (size.h * (domain.y[1] - domain.y[0])) + 'px';
}
// force redraw so that promise is returned when rendering is completed
scene.glplot.redraw();
};
proto.destroy = function() {
var scene = this;
if(!scene.glplot) return;
scene.camera.mouseListener.enabled = false;
scene.container.removeEventListener('wheel', scene.camera.wheelListener);
scene.camera = null;
scene.glplot.dispose();
scene.container.parentNode.removeChild(scene.container);
scene.glplot = null;
};
// getCameraArrays :: plotly_coords -> gl-plot3d_coords
// inverse of getLayoutCamera
function getCameraArrays(camera) {
return [
[camera.eye.x, camera.eye.y, camera.eye.z],
[camera.center.x, camera.center.y, camera.center.z],
[camera.up.x, camera.up.y, camera.up.z]
];
}
// getLayoutCamera :: gl-plot3d_coords -> plotly_coords
// inverse of getCameraArrays
function getLayoutCamera(camera) {
return {
up: {x: camera.up[0], y: camera.up[1], z: camera.up[2]},
center: {x: camera.center[0], y: camera.center[1], z: camera.center[2]},
eye: {x: camera.eye[0], y: camera.eye[1], z: camera.eye[2]},
projection: {type: (camera._ortho === true) ? 'orthographic' : 'perspective'}
};
}
// get camera position in plotly coords from 'gl-plot3d' coords
proto.getCamera = function() {
var scene = this;
scene.camera.view.recalcMatrix(scene.camera.view.lastT());
return getLayoutCamera(scene.camera);
};
// set gl-plot3d camera position and scene aspects with a set of plotly coords
proto.setViewport = function(sceneLayout) {
var scene = this;
var cameraData = sceneLayout.camera;
scene.camera.lookAt.apply(this, getCameraArrays(cameraData));
scene.glplot.setAspectratio(sceneLayout.aspectratio);
var newOrtho = (cameraData.projection.type === 'orthographic');
var oldOrtho = scene.camera._ortho;
if(newOrtho !== oldOrtho) {
scene.glplot.redraw(); // TODO: figure out why we need to redraw here?
scene.glplot.clearRGBA();
scene.glplot.dispose();
scene.initializeGLPlot();
}
};
proto.isCameraChanged = function(layout) {
var scene = this;
var cameraData = scene.getCamera();
var cameraNestedProp = Lib.nestedProperty(layout, scene.id + '.camera');
var cameraDataLastSave = cameraNestedProp.get();
function same(x, y, i, j) {
var vectors = ['up', 'center', 'eye'];
var components = ['x', 'y', 'z'];
return y[vectors[i]] && (x[vectors[i]][components[j]] === y[vectors[i]][components[j]]);
}
var changed = false;
if(cameraDataLastSave === undefined) {
changed = true;
} else {
for(var i = 0; i < 3; i++) {
for(var j = 0; j < 3; j++) {
if(!same(cameraData, cameraDataLastSave, i, j)) {
changed = true;
break;
}
}
}
if(!cameraDataLastSave.projection || (
cameraData.projection &&
cameraData.projection.type !== cameraDataLastSave.projection.type)) {
changed = true;
}
}
return changed;
};
proto.isAspectChanged = function(layout) {
var scene = this;
var aspectData = scene.glplot.getAspectratio();
var aspectNestedProp = Lib.nestedProperty(layout, scene.id + '.aspectratio');
var aspectDataLastSave = aspectNestedProp.get();
return (
aspectDataLastSave === undefined || (
aspectDataLastSave.x !== aspectData.x ||
aspectDataLastSave.y !== aspectData.y ||
aspectDataLastSave.z !== aspectData.z
));
};
// save camera to user layout (i.e. gd.layout)
proto.saveLayout = function(layout) {
var scene = this;
var fullLayout = scene.fullLayout;
var cameraData;
var cameraNestedProp;
var cameraDataLastSave;
var aspectData;
var aspectNestedProp;
var aspectDataLastSave;
var cameraChanged = scene.isCameraChanged(layout);
var aspectChanged = scene.isAspectChanged(layout);
var hasChanged = cameraChanged || aspectChanged;
if(hasChanged) {
var preGUI = {};
if(cameraChanged) {
cameraData = scene.getCamera();
cameraNestedProp = Lib.nestedProperty(layout, scene.id + '.camera');
cameraDataLastSave = cameraNestedProp.get();
preGUI[scene.id + '.camera'] = cameraDataLastSave;
}
if(aspectChanged) {
aspectData = scene.glplot.getAspectratio();
aspectNestedProp = Lib.nestedProperty(layout, scene.id + '.aspectratio');
aspectDataLastSave = aspectNestedProp.get();
preGUI[scene.id + '.aspectratio'] = aspectDataLastSave;
}
Registry.call('_storeDirectGUIEdit', layout, fullLayout._preGUI, preGUI);
if(cameraChanged) {
cameraNestedProp.set(cameraData);
var cameraFullNP = Lib.nestedProperty(fullLayout, scene.id + '.camera');
cameraFullNP.set(cameraData);
}
if(aspectChanged) {
aspectNestedProp.set(aspectData);
var aspectFullNP = Lib.nestedProperty(fullLayout, scene.id + '.aspectratio');
aspectFullNP.set(aspectData);
scene.glplot.redraw();
}
}
return hasChanged;
};
proto.updateFx = function(dragmode, hovermode) {
var scene = this;
var camera = scene.camera;
if(camera) {
// rotate and orbital are synonymous
if(dragmode === 'orbit') {
camera.mode = 'orbit';
camera.keyBindingMode = 'rotate';
} else if(dragmode === 'turntable') {
camera.up = [0, 0, 1];
camera.mode = 'turntable';
camera.keyBindingMode = 'rotate';
// The setter for camera.mode animates the transition to z-up,
// but only if we *don't* explicitly set z-up earlier via the
// relayout. So push `up` back to layout & fullLayout manually now.
var gd = scene.graphDiv;
var fullLayout = gd._fullLayout;
var fullCamera = scene.fullSceneLayout.camera;
var x = fullCamera.up.x;
var y = fullCamera.up.y;
var z = fullCamera.up.z;
// only push `up` back to (full)layout if it's going to change
if(z / Math.sqrt(x * x + y * y + z * z) < 0.999) {
var attr = scene.id + '.camera.up';
var zUp = {x: 0, y: 0, z: 1};
var edits = {};
edits[attr] = zUp;
var layout = gd.layout;
Registry.call('_storeDirectGUIEdit', layout, fullLayout._preGUI, edits);
fullCamera.up = zUp;
Lib.nestedProperty(layout, attr).set(zUp);
}
} else {
// none rotation modes [pan or zoom]
camera.keyBindingMode = dragmode;
}
}
// to put dragmode and hovermode on the same grounds from relayout
scene.fullSceneLayout.hovermode = hovermode;
};
function flipPixels(pixels, w, h) {
for(var i = 0, q = h - 1; i < q; ++i, --q) {
for(var j = 0; j < w; ++j) {
for(var k = 0; k < 4; ++k) {
var a = 4 * (w * i + j) + k;
var b = 4 * (w * q + j) + k;
var tmp = pixels[a];
pixels[a] = pixels[b];
pixels[b] = tmp;
}
}
}
}
function correctRGB(pixels, w, h) {
for(var i = 0; i < h; ++i) {
for(var j = 0; j < w; ++j) {
var k = 4 * (w * i + j);
var a = pixels[k + 3]; // alpha
if(a > 0) {
var q = 255 / a;
for(var l = 0; l < 3; ++l) { // RGB
pixels[k + l] = Math.min(q * pixels[k + l], 255);
}
}
}
}
}
proto.toImage = function(format) {
var scene = this;
if(!format) format = 'png';
if(scene.staticMode) scene.container.appendChild(STATIC_CANVAS);
// Force redraw
scene.glplot.redraw();
// Grab context and yank out pixels
var gl = scene.glplot.gl;
var w = gl.drawingBufferWidth;
var h = gl.drawingBufferHeight;
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
var pixels = new Uint8Array(w * h * 4);
gl.readPixels(0, 0, w, h, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
flipPixels(pixels, w, h);
correctRGB(pixels, w, h);
var canvas = document.createElement('canvas');
canvas.width = w;
canvas.height = h;
var context = canvas.getContext('2d', {willReadFrequently: true});
var imageData = context.createImageData(w, h);
imageData.data.set(pixels);
context.putImageData(imageData, 0, 0);
var dataURL;
switch(format) {
case 'jpeg':
dataURL = canvas.toDataURL('image/jpeg');
break;
case 'webp':
dataURL = canvas.toDataURL('image/webp');
break;
default:
dataURL = canvas.toDataURL('image/png');
}
if(scene.staticMode) scene.container.removeChild(STATIC_CANVAS);
return dataURL;
};
proto.setConvert = function() {
var scene = this;
for(var i = 0; i < 3; i++) {
var ax = scene.fullSceneLayout[axisProperties[i]];
Axes.setConvert(ax, scene.fullLayout);
ax.setScale = Lib.noop;
}
};
proto.make4thDimension = function() {
var scene = this;
var gd = scene.graphDiv;
var fullLayout = gd._fullLayout;
// mock axis for hover formatting
scene._mockAxis = {
type: 'linear',
showexponent: 'all',
exponentformat: 'B'
};
Axes.setConvert(scene._mockAxis, fullLayout);
};
module.exports = Scene;