StackGenVis: Alignment of Data, Algorithms, and Models for Stacking Ensemble Learning Using Performance Metrics https://doi.org/10.1109/TVCG.2020.3030352
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
StackGenVis/frontend/node_modules/gl-plot3d/scene.js

853 lines
20 KiB

'use strict'
var createCamera = require('./camera.js')
var createAxes = require('gl-axes3d')
var axesRanges = require('gl-axes3d/properties')
var createSpikes = require('gl-spikes3d')
var createSelect = require('gl-select-static')
var createFBO = require('gl-fbo')
var drawTriangle = require('a-big-triangle')
var mouseChange = require('mouse-change')
var perspective = require('gl-mat4/perspective')
var ortho = require('gl-mat4/ortho')
var createShader = require('./lib/shader')
var isMobile = require('is-mobile')({ tablet: true, featureDetect: true })
module.exports = {
createScene: createScene,
createCamera: createCamera
}
function MouseSelect() {
this.mouse = [-1,-1]
this.screen = null
this.distance = Infinity
this.index = null
this.dataCoordinate = null
this.dataPosition = null
this.object = null
this.data = null
}
function getContext(canvas, options) {
var gl = null
try {
gl = canvas.getContext('webgl', options)
if(!gl) {
gl = canvas.getContext('experimental-webgl', options)
}
} catch(e) {
return null
}
return gl
}
function roundUpPow10(x) {
var y = Math.round(Math.log(Math.abs(x)) / Math.log(10))
if(y < 0) {
var base = Math.round(Math.pow(10, -y))
return Math.ceil(x*base) / base
} else if(y > 0) {
var base = Math.round(Math.pow(10, y))
return Math.ceil(x/base) * base
}
return Math.ceil(x)
}
function defaultBool(x) {
if(typeof x === 'boolean') {
return x
}
return true
}
function createScene(options) {
options = options || {}
options.camera = options.camera || {}
var canvas = options.canvas
if(!canvas) {
canvas = document.createElement('canvas')
if(options.container) {
var container = options.container
container.appendChild(canvas)
} else {
document.body.appendChild(canvas)
}
}
var gl = options.gl
if(!gl) {
if(options.glOptions) {
isMobile = !!options.glOptions.preserveDrawingBuffer
}
gl = getContext(canvas,
options.glOptions || {
premultipliedAlpha: true,
antialias: true,
preserveDrawingBuffer: isMobile
})
}
if(!gl) {
throw new Error('webgl not supported')
}
//Initial bounds
var bounds = options.bounds || [[-10,-10,-10], [10,10,10]]
//Create selection
var selection = new MouseSelect()
//Accumulation buffer
var accumBuffer = createFBO(gl,
gl.drawingBufferWidth, gl.drawingBufferHeight, {
preferFloat: !isMobile
})
var accumShader = createShader(gl)
var isOrtho =
(options.cameraObject && options.cameraObject._ortho === true) ||
(options.camera.projection && options.camera.projection.type === 'orthographic') ||
false
//Create a camera
var cameraOptions = {
eye: options.camera.eye || [2,0,0],
center: options.camera.center || [0,0,0],
up: options.camera.up || [0,1,0],
zoomMin: options.camera.zoomMax || 0.1,
zoomMax: options.camera.zoomMin || 100,
mode: options.camera.mode || 'turntable',
_ortho: isOrtho
}
//Create axes
var axesOptions = options.axes || {}
var axes = createAxes(gl, axesOptions)
axes.enable = !axesOptions.disable
//Create spikes
var spikeOptions = options.spikes || {}
var spikes = createSpikes(gl, spikeOptions)
//Object list is empty initially
var objects = []
var pickBufferIds = []
var pickBufferCount = []
var pickBuffers = []
//Dirty flag, skip redraw if scene static
var dirty = true
var pickDirty = true
var projection = new Array(16)
var model = new Array(16)
var cameraParams = {
view: null,
projection: projection,
model: model,
_ortho: false
}
var pickDirty = true
var viewShape = [ gl.drawingBufferWidth, gl.drawingBufferHeight ]
var camera = options.cameraObject || createCamera(canvas, cameraOptions)
//Create scene object
var scene = {
gl: gl,
contextLost: false,
pixelRatio: options.pixelRatio || 1,
canvas: canvas,
selection: selection,
camera: camera,
axes: axes,
axesPixels: null,
spikes: spikes,
bounds: bounds,
objects: objects,
shape: viewShape,
aspect: options.aspectRatio || [1,1,1],
pickRadius: options.pickRadius || 10,
zNear: options.zNear || 0.01,
zFar: options.zFar || 1000,
fovy: options.fovy || Math.PI/4,
clearColor: options.clearColor || [0,0,0,0],
autoResize: defaultBool(options.autoResize),
autoBounds: defaultBool(options.autoBounds),
autoScale: !!options.autoScale,
autoCenter: defaultBool(options.autoCenter),
clipToBounds: defaultBool(options.clipToBounds),
snapToData: !!options.snapToData,
onselect: options.onselect || null,
onrender: options.onrender || null,
onclick: options.onclick || null,
cameraParams: cameraParams,
oncontextloss: null,
mouseListener: null,
_stopped: false,
getAspectratio: function() {
return {
x: this.aspect[0],
y: this.aspect[1],
z: this.aspect[2]
}
},
setAspectratio: function(aspectratio) {
this.aspect[0] = aspectratio.x
this.aspect[1] = aspectratio.y
this.aspect[2] = aspectratio.z
pickDirty = true
},
setBounds: function(axisIndex, range) {
this.bounds[0][axisIndex] = range.min
this.bounds[1][axisIndex] = range.max
},
setClearColor: function(clearColor) {
this.clearColor = clearColor
},
clearRGBA: function() {
this.gl.clearColor(
this.clearColor[0],
this.clearColor[1],
this.clearColor[2],
this.clearColor[3]
)
this.gl.clear(
this.gl.COLOR_BUFFER_BIT |
this.gl.DEPTH_BUFFER_BIT
)
}
}
var pickShape = [ (gl.drawingBufferWidth/scene.pixelRatio)|0, (gl.drawingBufferHeight/scene.pixelRatio)|0 ]
function resizeListener() {
if(scene._stopped) {
return
}
if(!scene.autoResize) {
return
}
var parent = canvas.parentNode
var width = 1
var height = 1
if(parent && parent !== document.body) {
width = parent.clientWidth
height = parent.clientHeight
} else {
width = window.innerWidth
height = window.innerHeight
}
var nextWidth = Math.ceil(width * scene.pixelRatio)|0
var nextHeight = Math.ceil(height * scene.pixelRatio)|0
if(nextWidth !== canvas.width || nextHeight !== canvas.height) {
canvas.width = nextWidth
canvas.height = nextHeight
var style = canvas.style
style.position = style.position || 'absolute'
style.left = '0px'
style.top = '0px'
style.width = width + 'px'
style.height = height + 'px'
dirty = true
}
}
if(scene.autoResize) {
resizeListener()
}
window.addEventListener('resize', resizeListener)
function reallocPickIds() {
var numObjs = objects.length
var numPick = pickBuffers.length
for(var i=0; i<numPick; ++i) {
pickBufferCount[i] = 0
}
obj_loop:
for(var i=0; i<numObjs; ++i) {
var obj = objects[i]
var pickCount = obj.pickSlots
if(!pickCount) {
pickBufferIds[i] = -1
continue
}
for(var j=0; j<numPick; ++j) {
if(pickBufferCount[j] + pickCount < 255) {
pickBufferIds[i] = j
obj.setPickBase(pickBufferCount[j]+1)
pickBufferCount[j] += pickCount
continue obj_loop
}
}
//Create new pick buffer
var nbuffer = createSelect(gl, viewShape)
pickBufferIds[i] = numPick
pickBuffers.push(nbuffer)
pickBufferCount.push(pickCount)
obj.setPickBase(1)
numPick += 1
}
while(numPick > 0 && pickBufferCount[numPick-1] === 0) {
pickBufferCount.pop()
pickBuffers.pop().dispose()
}
}
scene.update = function(options) {
if(scene._stopped) {
return
}
options = options || {}
dirty = true
pickDirty = true
}
scene.add = function(obj) {
if(scene._stopped) {
return
}
obj.axes = axes
objects.push(obj)
pickBufferIds.push(-1)
dirty = true
pickDirty = true
reallocPickIds()
}
scene.remove = function(obj) {
if(scene._stopped) {
return
}
var idx = objects.indexOf(obj)
if(idx < 0) {
return
}
objects.splice(idx, 1)
pickBufferIds.pop()
dirty = true
pickDirty = true
reallocPickIds()
}
scene.dispose = function() {
if(scene._stopped) {
return
}
scene._stopped = true
window.removeEventListener('resize', resizeListener)
canvas.removeEventListener('webglcontextlost', checkContextLoss)
scene.mouseListener.enabled = false
if(scene.contextLost) {
return
}
//Destroy objects
axes.dispose()
spikes.dispose()
for(var i=0; i<objects.length; ++i) {
objects[i].dispose()
}
//Clean up buffers
accumBuffer.dispose()
for(var i=0; i<pickBuffers.length; ++i) {
pickBuffers[i].dispose()
}
//Clean up shaders
accumShader.dispose()
//Release all references
gl = null
axes = null
spikes = null
objects = []
}
//Update mouse position
scene._mouseRotating = false
scene._prevButtons = 0
scene.enableMouseListeners = function() {
scene.mouseListener = mouseChange(canvas, function(buttons, x, y) {
if(scene._stopped) {
return
}
var numPick = pickBuffers.length
var numObjs = objects.length
var prevObj = selection.object
selection.distance = Infinity
selection.mouse[0] = x
selection.mouse[1] = y
selection.object = null
selection.screen = null
selection.dataCoordinate = selection.dataPosition = null
var change = false
if(buttons && scene._prevButtons) {
scene._mouseRotating = true
} else {
if(scene._mouseRotating) {
pickDirty = true
}
scene._mouseRotating = false
for(var i=0; i<numPick; ++i) {
var result = pickBuffers[i].query(x, pickShape[1] - y - 1, scene.pickRadius)
if(result) {
if(result.distance > selection.distance) {
continue
}
for(var j=0; j<numObjs; ++j) {
var obj = objects[j]
if(pickBufferIds[j] !== i) {
continue
}
var objPick = obj.pick(result)
if(objPick) {
selection.buttons = buttons
selection.screen = result.coord
selection.distance = result.distance
selection.object = obj
selection.index = objPick.distance
selection.dataPosition = objPick.position
selection.dataCoordinate = objPick.dataCoordinate
selection.data = objPick
change = true
}
}
}
}
}
if(prevObj && prevObj !== selection.object) {
if(prevObj.highlight) {
prevObj.highlight(null)
}
dirty = true
}
if(selection.object) {
if(selection.object.highlight) {
selection.object.highlight(selection.data)
}
dirty = true
}
change = change || (selection.object !== prevObj)
if(change && scene.onselect) {
scene.onselect(selection)
}
if((buttons & 1) && !(scene._prevButtons & 1) && scene.onclick) {
scene.onclick(selection)
}
scene._prevButtons = buttons
})
}
function checkContextLoss() {
if(scene.contextLost) {
return true
}
if(gl.isContextLost()) {
scene.contextLost = true
scene.mouseListener.enabled = false
scene.selection.object = null
if(scene.oncontextloss) {
scene.oncontextloss()
}
}
}
canvas.addEventListener('webglcontextlost', checkContextLoss)
//Render the scene for mouse picking
function renderPick() {
if(checkContextLoss()) {
return
}
gl.colorMask(true, true, true, true)
gl.depthMask(true)
gl.disable(gl.BLEND)
gl.enable(gl.DEPTH_TEST)
gl.depthFunc(gl.LEQUAL)
var numObjs = objects.length
var numPick = pickBuffers.length
for(var j=0; j<numPick; ++j) {
var buf = pickBuffers[j]
buf.shape = pickShape
buf.begin()
for(var i=0; i<numObjs; ++i) {
if(pickBufferIds[i] !== j) {
continue
}
var obj = objects[i]
if(obj.drawPick) {
obj.pixelRatio = 1
obj.drawPick(cameraParams)
}
}
buf.end()
}
}
var nBounds = [
[ Infinity, Infinity, Infinity],
[-Infinity,-Infinity,-Infinity]]
var prevBounds = [nBounds[0].slice(), nBounds[1].slice()]
function redraw() {
if(checkContextLoss()) {
return
}
resizeListener()
//Tick camera
var cameraMoved = scene.camera.tick()
cameraParams.view = scene.camera.matrix
dirty = dirty || cameraMoved
pickDirty = pickDirty || cameraMoved
//Set pixel ratio
axes.pixelRatio = scene.pixelRatio
spikes.pixelRatio = scene.pixelRatio
//Check if any objects changed, recalculate bounds
var numObjs = objects.length
var lo = nBounds[0]
var hi = nBounds[1]
lo[0] = lo[1] = lo[2] = Infinity
hi[0] = hi[1] = hi[2] = -Infinity
for(var i=0; i<numObjs; ++i) {
var obj = objects[i]
//Set the axes properties for each object
obj.pixelRatio = scene.pixelRatio
obj.axes = scene.axes
dirty = dirty || !!obj.dirty
pickDirty = pickDirty || !!obj.dirty
var obb = obj.bounds
if(obb) {
var olo = obb[0]
var ohi = obb[1]
for(var j=0; j<3; ++j) {
lo[j] = Math.min(lo[j], olo[j])
hi[j] = Math.max(hi[j], ohi[j])
}
}
}
//Recalculate bounds
var bounds = scene.bounds
if(scene.autoBounds) {
for(var j=0; j<3; ++j) {
if(hi[j] < lo[j]) {
lo[j] = -1
hi[j] = 1
} else {
if(lo[j] === hi[j]) {
lo[j] -= 1
hi[j] += 1
}
var padding = 0.05 * (hi[j] - lo[j])
lo[j] = lo[j] - padding
hi[j] = hi[j] + padding
}
bounds[0][j] = lo[j]
bounds[1][j] = hi[j]
}
}
var boundsChanged = false
for(var j=0; j<3; ++j) {
boundsChanged = boundsChanged ||
(prevBounds[0][j] !== bounds[0][j]) ||
(prevBounds[1][j] !== bounds[1][j])
prevBounds[0][j] = bounds[0][j]
prevBounds[1][j] = bounds[1][j]
}
//Recalculate bounds
pickDirty = pickDirty || boundsChanged
dirty = dirty || boundsChanged
if(!dirty) {
return
}
if(boundsChanged) {
var tickSpacing = [0,0,0]
for(var i=0; i<3; ++i) {
tickSpacing[i] = roundUpPow10((bounds[1][i]-bounds[0][i]) / 10.0)
}
if(axes.autoTicks) {
axes.update({
bounds: bounds,
tickSpacing: tickSpacing
})
} else {
axes.update({
bounds: bounds
})
}
}
//Get scene
var width = gl.drawingBufferWidth
var height = gl.drawingBufferHeight
viewShape[0] = width
viewShape[1] = height
pickShape[0] = Math.max(width/scene.pixelRatio, 1)|0
pickShape[1] = Math.max(height/scene.pixelRatio, 1)|0
//Compute camera parameters
calcCameraParams(scene, isOrtho)
//Apply axes/clip bounds
for(var i=0; i<numObjs; ++i) {
var obj = objects[i]
//Set axes bounds
obj.axesBounds = bounds
//Set clip bounds
if(scene.clipToBounds) {
obj.clipBounds = bounds
}
}
//Set spike parameters
if(selection.object) {
if(scene.snapToData) {
spikes.position = selection.dataCoordinate
} else {
spikes.position = selection.dataPosition
}
spikes.bounds = bounds
}
//If state changed, then redraw pick buffers
if(pickDirty) {
pickDirty = false
renderPick()
}
//Recalculate pixel data
scene.axesPixels = axesRanges(scene.axes, cameraParams, width, height)
//Call render callback
if(scene.onrender) {
scene.onrender()
}
//Read value
gl.bindFramebuffer(gl.FRAMEBUFFER, null)
gl.viewport(0, 0, width, height)
//General strategy: 3 steps
// 1. render non-transparent objects
// 2. accumulate transparent objects into separate fbo
// 3. composite final scene
//Clear FBO
scene.clearRGBA()
gl.depthMask(true)
gl.colorMask(true, true, true, true)
gl.enable(gl.DEPTH_TEST)
gl.depthFunc(gl.LEQUAL)
gl.disable(gl.BLEND)
gl.disable(gl.CULL_FACE) //most visualization surfaces are 2 sided
//Render opaque pass
var hasTransparent = false
if(axes.enable) {
hasTransparent = hasTransparent || axes.isTransparent()
axes.draw(cameraParams)
}
spikes.axes = axes
if(selection.object) {
spikes.draw(cameraParams)
}
gl.disable(gl.CULL_FACE) //most visualization surfaces are 2 sided
for(var i=0; i<numObjs; ++i) {
var obj = objects[i]
obj.axes = axes
obj.pixelRatio = scene.pixelRatio
if(obj.isOpaque && obj.isOpaque()) {
obj.draw(cameraParams)
}
if(obj.isTransparent && obj.isTransparent()) {
hasTransparent = true
}
}
if(hasTransparent) {
//Render transparent pass
accumBuffer.shape = viewShape
accumBuffer.bind()
gl.clear(gl.DEPTH_BUFFER_BIT)
gl.colorMask(false, false, false, false)
gl.depthMask(true)
gl.depthFunc(gl.LESS)
//Render forward facing objects
if(axes.enable && axes.isTransparent()) {
axes.drawTransparent(cameraParams)
}
for(var i=0; i<numObjs; ++i) {
var obj = objects[i]
if(obj.isOpaque && obj.isOpaque()) {
obj.draw(cameraParams)
}
}
//Render transparent pass
gl.enable(gl.BLEND)
gl.blendEquation(gl.FUNC_ADD)
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA)
gl.colorMask(true, true, true, true)
gl.depthMask(false)
gl.clearColor(0,0,0,0)
gl.clear(gl.COLOR_BUFFER_BIT)
if(axes.isTransparent()) {
axes.drawTransparent(cameraParams)
}
for(var i=0; i<numObjs; ++i) {
var obj = objects[i]
if(obj.isTransparent && obj.isTransparent()) {
obj.drawTransparent(cameraParams)
}
}
//Unbind framebuffer
gl.bindFramebuffer(gl.FRAMEBUFFER, null)
//Draw composite pass
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA)
gl.disable(gl.DEPTH_TEST)
accumShader.bind()
accumBuffer.color[0].bind(0)
accumShader.uniforms.accumBuffer = 0
drawTriangle(gl)
//Turn off blending
gl.disable(gl.BLEND)
}
//Clear dirty flags
dirty = false
for(var i=0; i<numObjs; ++i) {
objects[i].dirty = false
}
}
//Draw the whole scene
function render() {
if(scene._stopped || scene.contextLost) {
return
}
// this order is important: ios safari sometimes has sync raf
redraw()
requestAnimationFrame(render)
}
scene.enableMouseListeners()
render()
//Force redraw of whole scene
scene.redraw = function() {
if(scene._stopped) {
return
}
dirty = true
redraw()
}
return scene
}
function calcCameraParams(scene, isOrtho) {
var bounds = scene.bounds
var cameraParams = scene.cameraParams
var projection = cameraParams.projection
var model = cameraParams.model
var width = scene.gl.drawingBufferWidth
var height = scene.gl.drawingBufferHeight
var zNear = scene.zNear
var zFar = scene.zFar
var fovy = scene.fovy
var r = width / height
if(isOrtho) {
ortho(projection,
-r,
r,
-1,
1,
zNear,
zFar
)
cameraParams._ortho = true
} else {
perspective(projection,
fovy,
r,
zNear,
zFar
)
cameraParams._ortho = false
}
//Compute model matrix
for(var i=0; i<16; ++i) {
model[i] = 0
}
model[15] = 1
var maxS = 0
for(var i=0; i<3; ++i) {
maxS = Math.max(maxS, bounds[1][i] - bounds[0][i])
}
for(var i=0; i<3; ++i) {
if(scene.autoScale) {
model[5*i] = scene.aspect[i] / (bounds[1][i] - bounds[0][i])
} else {
model[5*i] = 1 / maxS
}
if(scene.autoCenter) {
model[12+i] = -model[5*i] * 0.5 * (bounds[0][i] + bounds[1][i])
}
}
}