<commit_before>/* jshint shadow: true */
'use strict';

var createPlot          = require('gl-plot3d'),
    m4FromQuat          = require('gl-mat4/fromQuat'),
    createAxesOptions   = require('./convert/axes'),
    createSpikeOptions  = require('./convert/spikes'),
    createScatterTrace  = require('./convert/scatter'),
    createSurfaceTrace  = require('./convert/surface'),
    computeTickMarks    = require('./lib/tick-marks'),
    createCamera        = require('./lib/camera'),
    str2RGBAarray       = require('./lib/str2rgbarray'),
    Plotly              = require('../plotly');

function render(scene) {
    computeTickMarks(scene);
    scene.glplot.axes.update(scene.axesOptions);

    var keys = Object.keys(scene.traces);
    for (var i = 0; i < keys.length; ++i) {
        var trace = scene.traces[keys[i]];
        trace.handlePick(scene.glplot.selection);

        if (trace.setContourLevels) trace.setContourLevels();
    }
}

function Scene(options, fullLayout) {

    //Create sub container for plot
    var sceneContainer = document.createElement('div');
    var plotContainer = options.container;

    /*
     * 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';
    /*
     * Move this to calc step? Why does it work here?
     */
    this.axesOptions      = createAxesOptions(fullLayout[this.id]);
    this.spikeOptions     = createSpikeOptions(fullLayout[this.id]);
    this.container        = sceneContainer;

    var glplotOptions = {
            container:  sceneContainer,
            axes:       this.axesOptions,
            spikes:     this.spikeOptions,
            pickRadius: 10,
            snapToData: true,
            autoScale:  true,
            autoBounds: false
    };

    if (options.staticPlot) {
        glplotOptions.pixelRatio = options.plot3dPixelRatio;
        glplotOptions.glOptions = {
            preserveDrawingBuffer: true,
            premultipliedAlpha:true
        };
    }

    try {
        this.glplot = createPlot(glplotOptions);
    } catch (e) {

        /*
         * 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.
         */
        var noop = function () {};
        for (var prop in this) if (typeof this[prop] === 'function') this[prop] = noop;
        this.destroy = function () {
            this.container.parentNode.removeChild(this.container);
        };

        var div = document.createElement('div');
        div.textContent = 'Webgl is not supported by your browser - visit http://get.webgl.org for more info';
        div.style.cursor = 'pointer';
        div.style.fontSize = '24px';
        div.style.color = Plotly.Color.defaults[0];

        this.container.appendChild(div);
        this.container.style.background = '#FFFFFF';
        this.container.onclick = function () {
            window.open('http://get.webgl.org');
        };

        /*
         * return before setting up camera and onrender methods
         */
        return;
    }

    var cameraPos = options.fullSceneLayout.cameraposition

    this.camera = createCamera(this.container, {
        center: [cameraPos.center.x, cameraPos.center.y, cameraPos.center.z],
        eye:    [cameraPos.eye.x, cameraPos.eye.y, cameraPos.eye.z],
        up:     [cameraPos.up.x, cameraPos.up.y, cameraPos.up.z],
        zoomMin: 0.1,
        zoomMax: 100,
        mode:   'orbit'
    });

    this.glplot.camera = this.camera;

    this.glplot.onrender = render.bind(null, this);

    //List of scene objects
    this.traces = {};

    this.contourLevels = [ [], [], [] ];
}

var proto = Scene.prototype;

var axisProperties = [ 'xaxis', 'yaxis', 'zaxis' ];


proto.plot = function(sceneData, fullLayout, layout) {

    var data, trace;
    var i, j;
    var fullSceneLayout = fullLayout[this.id];
    var sceneLayout = layout[this.id];

    if (fullSceneLayout.bgcolor) this.glplot.clearColor = str2RGBAarray(fullSceneLayout.bgcolor);
    else this.glplot.clearColor = [0, 0, 0, 0];

    //Update layout
    this.fullSceneLayout = fullSceneLayout;

    this.glplotLayout = fullSceneLayout;
    this.axesOptions.merge(fullSceneLayout);
    this.spikeOptions.merge(fullSceneLayout);

    //Update scene
    this.glplot.update({});

    // Update axes functions BEFORE updating traces
    for (i = 0; i < 3; ++i) {
        var axis = fullSceneLayout[axisProperties[i]];
        Plotly.Gl3dAxes.setConvert(axis);
    }

    //Update traces
    if (sceneData) {
        if(!Array.isArray(sceneData)) sceneData = [sceneData];

        for(i = 0; i < sceneData.length; ++i) {
            data = sceneData[i];
            if(data.visible!==true) {
                continue;
            }
            trace = this.traces[data.uid];
            if(trace) {
                trace.update(data);
            } else {
                switch(data.type) {
                    case 'scatter3d':
                        trace = createScatterTrace(this, data);
                    break;

                    case 'surface':
                        trace = createSurfaceTrace(this, data);
                    break;

                    default:
                }
                this.traces[data.uid] = trace;
            }
        }
    } else {
        sceneData = [];
    }

    var traceIds = Object.keys(this.traces);
trace_id_loop:
    for(i = 0; i<traceIds.length; ++i) {
        for(j = 0; j<sceneData.length; ++j) {
            if(sceneData[j].uid === traceIds[i] && sceneData[j].visible===true) {
                continue trace_id_loop;
            }
        }
        trace = this.traces[traceIds[i]];
        trace.dispose();
        delete this.traces[traceIds[i]];
    }

    //Update ranges (needs to be called *after* objects are added due to updates)
    var sceneBounds = this.glplot.bounds,
        axisDataRange = [];

    for(i = 0; i < 3; ++i) {
        var axis = fullSceneLayout[axisProperties[i]];

        if(axis.autorange) {
            sceneBounds[0][i] = Infinity;
            sceneBounds[1][i] = -Infinity;
            for(j = 0; j < this.glplot.objects.length; ++j) {
                var objBounds = this.glplot.objects[j].bounds;
                sceneBounds[0][i] = Math.min(sceneBounds[0][i], objBounds[0][i]);
                sceneBounds[1][i] = Math.max(sceneBounds[1][i], objBounds[1][i]);
            }
            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;
            }
        } else {
            var range = fullSceneLayout[axisProperties[i]].range;
            sceneBounds[0][i] = range[0];
            sceneBounds[1][i] = 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];
    }

    var axesScaleRatio = [],
        maxRange = Math.max.apply(null, axisDataRange);

    for (i = 0; i < 3; ++i) axesScaleRatio[i] = axisDataRange[i] / maxRange;

    /*
     * Dynamically set the aspect ratio depending on the users aspect settings
     */
    var axisAutoScaleFactor = 4;
    var aspectRatio;

    if (fullSceneLayout.aspectmode === 'auto') {
        if (Math.max.apply(null, axesScaleRatio)/Math.min.apply(null, axesScaleRatio) <= axisAutoScaleFactor) {

            /*
             * 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 if (fullSceneLayout.aspectmode === 'cube') {
        aspectRatio = [1, 1, 1];

    } else if (fullSceneLayout.aspectmode === 'data') {
        aspectRatio = axesScaleRatio;

    } else if (fullSceneLayout.aspectmode === 'manual') {
        var userRatio = fullSceneLayout.aspectratio;
        aspectRatio = [userRatio.x, userRatio.y, userRatio.z];

    } 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.
     */
    this.glplot.aspect = aspectRatio;


    //Update frame position for multi plots
    var domain = fullSceneLayout.domain || null,
        size = fullLayout._size || null;

    if (domain && size) {
        var containerStyle = this.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';
    }
};

proto.destroy = function() {
    this.glplot.dispose();
    this.container.parentNode.removeChild(this.container);
};


// for reset camera button in modebar
proto.setCameraToDefault = function setCameraToDefault () {
    this.glplot.camera.lookAt(
        [1.25, 1.25, 1.25],
        [0   , 0   , 0   ],
        [0   , 0   , 1   ]
    );
};

// get camera position in plotly coords from 'orbit-camera' coords
proto.getCameraPosition = function getCameraPosition () {
    this.glplot.camera.view.recalcMatrix(this.camera.view.lastT());

    var up     = this.glplot.camera.up;
    var center = this.glplot.camera.center;
    var eye    = this.glplot.camera.eye;

    return {
        up:     {x:up[0], y:up[1], z:up[2]},
        center: {x:center[0], y:center[1], z:center[2]},
        eye:    {x:eye[0], y:eye[1], z:eye[2]}
    };
};

// set camera position with a set of plotly coords
proto.setCameraPosition = function setCameraPosition (camera) {
    var up      = camera.up;
    var center  = camera.center;
    var eye     = camera.eye;
    this.glplot.camera.lookAt(
      [eye.x, eye.y, eye.z],
      [center.x, center.y, center.z],
      [up.x, up.y, up.z]
    );
};

// save camera position to user layout (i.e. gd.layout)
proto.saveCamera = function saveCamera(layout) {
    var cameraposition = this.getCameraPosition();
    Plotly.Lib.nestedProperty(layout, this.id + '.cameraposition')
        .set(cameraposition);
};

proto.toImage = function (format) {
    if (!format) format = 'png';

    var gl = this.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);

    //Flip pixels
    for(var j=0,k=h-1; j<k; ++j, --k) {
        for(var i=0; i<w; ++i) {
            for(var l=0; l<4; ++l) {
                var tmp = pixels[4*(w*j+i)+l];
                pixels[4*(w*j+i)+l] = pixels[4*(w*k+i)+l];
                pixels[4*(w*k+i)+l] = tmp;
            }
        }
    }

    var canvas = document.createElement('canvas');
    canvas.width = w;
    canvas.height = h;
    var context = canvas.getContext('2d');
    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');
    }

    return dataURL;
};

module.exports = Scene;
<commit_msg>add logic around camera / cameraposition:<commit_after>/* jshint shadow: true */
'use strict';

var createPlot          = require('gl-plot3d'),
    m4FromQuat          = require('gl-mat4/fromQuat'),
    createAxesOptions   = require('./convert/axes'),
    createSpikeOptions  = require('./convert/spikes'),
    createScatterTrace  = require('./convert/scatter'),
    createSurfaceTrace  = require('./convert/surface'),
    computeTickMarks    = require('./lib/tick-marks'),
    createCamera        = require('./lib/camera'),
    str2RGBAarray       = require('./lib/str2rgbarray'),
    Plotly              = require('../plotly');

function render(scene) {
    computeTickMarks(scene);
    scene.glplot.axes.update(scene.axesOptions);

    var keys = Object.keys(scene.traces);
    for (var i = 0; i < keys.length; ++i) {
        var trace = scene.traces[keys[i]];
        trace.handlePick(scene.glplot.selection);

        if (trace.setContourLevels) trace.setContourLevels();
    }
}

function Scene(options, fullLayout) {

    //Create sub container for plot
    var sceneContainer = document.createElement('div');
    var plotContainer = options.container;

    /*
     * 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';
    /*
     * Move this to calc step? Why does it work here?
     */
    this.axesOptions      = createAxesOptions(fullLayout[this.id]);
    this.spikeOptions     = createSpikeOptions(fullLayout[this.id]);
    this.container        = sceneContainer;

    var glplotOptions = {
            container:  sceneContainer,
            axes:       this.axesOptions,
            spikes:     this.spikeOptions,
            pickRadius: 10,
            snapToData: true,
            autoScale:  true,
            autoBounds: false
    };

    if (options.staticPlot) {
        glplotOptions.pixelRatio = options.plot3dPixelRatio;
        glplotOptions.glOptions = {
            preserveDrawingBuffer: true,
            premultipliedAlpha:true
        };
    }

    try {
        this.glplot = createPlot(glplotOptions);
    } catch (e) {

        /*
         * 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.
         */
        var noop = function () {};
        for (var prop in this) if (typeof this[prop] === 'function') this[prop] = noop;
        this.destroy = function () {
            this.container.parentNode.removeChild(this.container);
        };

        var div = document.createElement('div');
        div.textContent = 'Webgl is not supported by your browser - visit http://get.webgl.org for more info';
        div.style.cursor = 'pointer';
        div.style.fontSize = '24px';
        div.style.color = Plotly.Color.defaults[0];

        this.container.appendChild(div);
        this.container.style.background = '#FFFFFF';
        this.container.onclick = function () {
            window.open('http://get.webgl.org');
        };

        /*
         * return before setting up camera and onrender methods
         */
        return;
    }

    var cameraPos;

    if(fullLayout.scene.cameraposition !== undefined) {
        cameraPos = this.cleanCamera(fullLayout.scene.cameraposition);
    }
    else cameraPos = fullLayout.scene.camera;

    this.camera = createCamera(this.container, {
        center: [cameraPos.center.x, cameraPos.center.y, cameraPos.center.z],
        eye:    [cameraPos.eye.x, cameraPos.eye.y, cameraPos.eye.z],
        up:     [cameraPos.up.x, cameraPos.up.y, cameraPos.up.z],
        zoomMin: 0.1,
        zoomMax: 100,
        mode:   'orbit'
    });

    this.glplot.camera = this.camera;

    this.glplot.onrender = render.bind(null, this);

    //List of scene objects
    this.traces = {};

    this.contourLevels = [ [], [], [] ];
}

var proto = Scene.prototype;

var axisProperties = [ 'xaxis', 'yaxis', 'zaxis' ];


proto.plot = function(sceneData, fullLayout, layout) {

    var data, trace;
    var i, j;
    var fullSceneLayout = fullLayout[this.id];
    var sceneLayout = layout[this.id];

    if (fullSceneLayout.bgcolor) this.glplot.clearColor = str2RGBAarray(fullSceneLayout.bgcolor);
    else this.glplot.clearColor = [0, 0, 0, 0];

    //Update layout
    this.fullSceneLayout = fullSceneLayout;

    this.glplotLayout = fullSceneLayout;
    this.axesOptions.merge(fullSceneLayout);
    this.spikeOptions.merge(fullSceneLayout);

    //Update scene
    this.glplot.update({});

    // Update axes functions BEFORE updating traces
    for (i = 0; i < 3; ++i) {
        var axis = fullSceneLayout[axisProperties[i]];
        Plotly.Gl3dAxes.setConvert(axis);
    }

    //Update traces
    if (sceneData) {
        if(!Array.isArray(sceneData)) sceneData = [sceneData];

        for(i = 0; i < sceneData.length; ++i) {
            data = sceneData[i];
            if(data.visible!==true) {
                continue;
            }
            trace = this.traces[data.uid];
            if(trace) {
                trace.update(data);
            } else {
                switch(data.type) {
                    case 'scatter3d':
                        trace = createScatterTrace(this, data);
                    break;

                    case 'surface':
                        trace = createSurfaceTrace(this, data);
                    break;

                    default:
                }
                this.traces[data.uid] = trace;
            }
        }
    } else {
        sceneData = [];
    }

    var traceIds = Object.keys(this.traces);
trace_id_loop:
    for(i = 0; i<traceIds.length; ++i) {
        for(j = 0; j<sceneData.length; ++j) {
            if(sceneData[j].uid === traceIds[i] && sceneData[j].visible===true) {
                continue trace_id_loop;
            }
        }
        trace = this.traces[traceIds[i]];
        trace.dispose();
        delete this.traces[traceIds[i]];
    }

    //Update ranges (needs to be called *after* objects are added due to updates)
    var sceneBounds = this.glplot.bounds,
        axisDataRange = [];

    for(i = 0; i < 3; ++i) {
        var axis = fullSceneLayout[axisProperties[i]];

        if(axis.autorange) {
            sceneBounds[0][i] = Infinity;
            sceneBounds[1][i] = -Infinity;
            for(j = 0; j < this.glplot.objects.length; ++j) {
                var objBounds = this.glplot.objects[j].bounds;
                sceneBounds[0][i] = Math.min(sceneBounds[0][i], objBounds[0][i]);
                sceneBounds[1][i] = Math.max(sceneBounds[1][i], objBounds[1][i]);
            }
            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;
            }
        } else {
            var range = fullSceneLayout[axisProperties[i]].range;
            sceneBounds[0][i] = range[0];
            sceneBounds[1][i] = 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];
    }

    var axesScaleRatio = [],
        maxRange = Math.max.apply(null, axisDataRange);

    for (i = 0; i < 3; ++i) axesScaleRatio[i] = axisDataRange[i] / maxRange;

    /*
     * Dynamically set the aspect ratio depending on the users aspect settings
     */
    var axisAutoScaleFactor = 4;
    var aspectRatio;

    if (fullSceneLayout.aspectmode === 'auto') {
        if (Math.max.apply(null, axesScaleRatio)/Math.min.apply(null, axesScaleRatio) <= axisAutoScaleFactor) {

            /*
             * 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 if (fullSceneLayout.aspectmode === 'cube') {
        aspectRatio = [1, 1, 1];

    } else if (fullSceneLayout.aspectmode === 'data') {
        aspectRatio = axesScaleRatio;

    } else if (fullSceneLayout.aspectmode === 'manual') {
        var userRatio = fullSceneLayout.aspectratio;
        aspectRatio = [userRatio.x, userRatio.y, userRatio.z];

    } 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.
     */
    this.glplot.aspect = aspectRatio;


    //Update frame position for multi plots
    var domain = fullSceneLayout.domain || null,
        size = fullLayout._size || null;

    if (domain && size) {
        var containerStyle = this.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';
    }
};

proto.destroy = function() {
    this.glplot.dispose();
    this.container.parentNode.removeChild(this.container);
};


// for reset camera button in modebar
proto.setCameraToDefault = function setCameraToDefault () {
    this.glplot.camera.lookAt(
        [1.25, 1.25, 1.25],
        [0   , 0   , 0   ],
        [0   , 0   , 1   ]
    );
};

// get camera position in plotly coords from 'orbit-camera' coords
proto.getCameraPosition = function getCameraPosition () {
    this.glplot.camera.view.recalcMatrix(this.camera.view.lastT());

    var up     = this.glplot.camera.up;
    var center = this.glplot.camera.center;
    var eye    = this.glplot.camera.eye;

    return {
        up:     {x:up[0], y:up[1], z:up[2]},
        center: {x:center[0], y:center[1], z:center[2]},
        eye:    {x:eye[0], y:eye[1], z:eye[2]}
    };
};

// set camera position with a set of plotly coords
proto.setCameraPosition = function setCameraPosition (camera) {
    var up      = camera.up;
    var center  = camera.center;
    var eye     = camera.eye;
    this.glplot.camera.lookAt(
      [eye.x, eye.y, eye.z],
      [center.x, center.y, center.z],
      [up.x, up.y, up.z]
    );
};

// save camera to user layout (i.e. gd.layout)
proto.saveCamera = function saveCamera(layout) {
    var cameraposition = this.getCameraPosition();

    // save new camera api
    Plotly.Lib.nestedProperty(layout, this.id + '.camera')
        .set(cameraposition);

    // delete old
    delete layout.scene.cameraposition;
};

// convert old camera position api
proto.cleanCamera = function(cameraposition) {
    if (Array.isArray(cameraposition) && cameraposition[0].length === 4) {
        var rotation = cameraposition[0];
        var center   = cameraposition[1];
        var radius   = cameraposition[2];
        var mat = m4FromQuat([], rotation);
        var eye = [];
        for (var i = 0; i < 3; ++i) {
            eye[i] = center[i] + radius * mat[2+4*i];
        }
        return {
            eye: {x: eye[0], y: eye[1], z: eye[2]},
            center: {x: center[0], y: center[1], z: center[2]},
            up: {x: mat[1], y: mat[5], z: mat[9]}
        };
    }
    else return null;
};

proto.toImage = function (format) {
    if (!format) format = 'png';

    var gl = this.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);

    //Flip pixels
    for(var j=0,k=h-1; j<k; ++j, --k) {
        for(var i=0; i<w; ++i) {
            for(var l=0; l<4; ++l) {
                var tmp = pixels[4*(w*j+i)+l];
                pixels[4*(w*j+i)+l] = pixels[4*(w*k+i)+l];
                pixels[4*(w*k+i)+l] = tmp;
            }
        }
    }

    var canvas = document.createElement('canvas');
    canvas.width = w;
    canvas.height = h;
    var context = canvas.getContext('2d');
    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');
    }

    return dataURL;
};

module.exports = Scene;

{'fraction_non_alphanumeric': 0.13509992928653838, 'fraction_numerical': 0.010904760132494697, 'mean_word_length': 2.094552573995163, 'pattern_counts': {'":': 0, '<': 28, '<?xml version=': 0, '>': 5, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3101103', 'n_tokens_mistral': 8680, 'n_tokens_neox': 8288, 'n_words': 1815}