Home Reference Source

viewer/abstractbufferset.js

import {FatLineRenderer} from "./fatlinerenderer.js";
import {AvlTree} from "./collections/avltree.js";

var counter = 1;

/**
 * @ignore
 */
export class AbstractBufferSet {
    
    constructor(viewer, reuse) {
    	this.viewer = viewer;
    	this.reuse = reuse;
        // Unique id per bufferset, easier to use as Map key
        this.id = counter++;
        
        this.dirty = true;
        
        this.nonceCache = new Map();
        
        this.reset(viewer);
    }

    /**
     *  Creates new buffers, but is more efficient than joinConsecutiveRanges, funky buffer layers looks this way because we can directly send it to the GPU with multiDrawElementsWEBGL
     */
    joinConsecutiveRangesAsBuffers(input) {
    	var result = {
    		offsetsBytes: new Int32Array(input.pos),
    		counts: new Int32Array(input.pos),
    		lineRenderOffsetsBytes: new Int32Array(input.pos),
    		lineRenderCounts: new Int32Array(input.pos),
    		pos: 0
    	};
		for (var i=0; i<input.pos; i++) {
			var offset = input.offsetsBytes[i] / 4;
			var totalCount = input.counts[i];
			var lineRenderOffset = input.lineRenderOffsetsBytes[i] / 4;
			var lineRenderTotalCount = input.lineRenderCounts[i];
			while (i < input.pos && input.offsetsBytes[i] / 4 + input.counts[i] == input.offsetsBytes[i + 1] / 4) {
				i++;
				totalCount += input.counts[i];
				// Assuming a lot here!
				lineRenderTotalCount += input.lineRenderCounts[i];
			}
			result.offsetsBytes[result.pos] = offset * 4;
			result.counts[result.pos] = totalCount;
			result.lineRenderOffsetsBytes[result.pos] = lineRenderOffset * 4;
			result.lineRenderCounts[result.pos] = lineRenderTotalCount;
			result.pos++;
		}
//		console.log("Joined", input.pos, result.pos);
    	return result;
    }
    
    /**
     * More efficient version of complementRanges, but also creates new buffers.
     */
    complementRangesAsBuffers(input) {
    	if (input.pos == 0) {
    		// Special case, inverting all
    		return {
    			counts: new Int32Array([this.nrIndices]),
    			offsetsBytes: new Int32Array([0]),
    			lineRenderCounts: new Int32Array([this.nrLineIndices]),
    			lineRenderOffsetsBytes: new Int32Array([0]),
    			pos: 1
    		}
    	}
    	var maxNrRanges = this.uniqueIdToIndex.size / 2;
    	var complement = {
    		counts: new Int32Array(maxNrRanges),
    		offsetsBytes: new Int32Array(maxNrRanges),
    		lineRenderCounts: new Int32Array(maxNrRanges),
    		lineRenderOffsetsBytes: new Int32Array(maxNrRanges),
    		pos: 0
    	};
    	var previousIndex = 0;
    	var previousLineRenderIndex = 0;
    	for (var i=0; i<=input.pos; i++) {
    		if (i == input.pos) {
    			if (offset + count != this.nrIndices) {
    				// Complement the last range
        			complement.offsetsBytes[complement.pos] = previousIndex * 4;
        			complement.counts[complement.pos] = this.nrIndices - previousIndex;
        			// Assuming a lot here
        			complement.lineRenderOffsetsBytes[complement.pos] = previousLineRenderIndex * 4;
        			complement.lineRenderCounts[complement.pos] = this.nrLineIndices - previousLineRenderIndex;
        			complement.pos++;
    			}
    			continue;
    		}
    		var count = input.counts[i];
    		var offset = input.offsetsBytes[i] / 4;
    		var lineRenderCount = input.lineRenderCounts[i];
    		var lineRenderOffset = input.lineRenderOffsetsBytes[i] / 4;
    		
    		var newCount = offset - previousIndex;
    		var newLineRenderCount = lineRenderOffset - previousLineRenderIndex;
    		if (newCount > 0) {
    			complement.offsetsBytes[complement.pos] = previousIndex * 4;
    			complement.counts[complement.pos] = offset - previousIndex;
    			complement.lineRenderOffsetsBytes[complement.pos] = previousLineRenderIndex * 4;
    			complement.lineRenderCounts[complement.pos] = lineRenderOffset - previousLineRenderIndex;
    			complement.pos++;
    		}
    		previousIndex = offset + count;
    		previousLineRenderIndex = lineRenderOffset + lineRenderCount;
    	}
    	// TODO trim buffers?
//    	console.log(complement.pos, complement.counts.length);
    	return complement;
    }

    /**
     * When changing colors, a lot of data is read from the GPU. It seems as though all of this reading is sync, making it a bottle-neck 
     * When wrapping abstractbufferset calls that read from the GPU buffer in batchGpuRead, the complete bufferset is read into memory once, and is removed afterwards  
     */
    batchGpuRead(gl, toCopy, bounds, fn) {
    	if (this.objects) {
    		// Reuse, no need to batch
            fn();
            return;
    	}

    	if (bounds == null) {
    		throw "Not supported anymore";
    		bounds = {
    			startIndex: 0,
    			endIndex: this.nrIndices,
    			minIndex: 0,
    			maxIndex: this.nrPositions
    		};
    	}
    	
    	this.batchGpuBuffers = {
   			indices: new Uint32Array(bounds.endIndex - bounds.startIndex),
   			lineIndices: new Uint32Array(bounds.endLineIndex - bounds.startLineIndex),
   			bounds: bounds
       	};

        let restoreElementBinding = gl.getParameter(gl.ELEMENT_ARRAY_BUFFER_BINDING);
        let restoreArrayBinding = gl.getParameter(gl.ARRAY_BUFFER_BINDING);

        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);
        gl.getBufferSubData(gl.ELEMENT_ARRAY_BUFFER, bounds.startIndex * 4, this.batchGpuBuffers.indices, 0, bounds.endIndex - bounds.startIndex);
        
        if (this.lineIndexBuffer == null) {
        	debugger;
        }
        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.lineIndexBuffer);
        gl.getBufferSubData(gl.ELEMENT_ARRAY_BUFFER, bounds.startLineIndex * 4, this.batchGpuBuffers.lineIndices, 0, bounds.endLineIndex - bounds.startLineIndex);

        for (var name of toCopy) {
            let buffer = this[name];
            let bytes_per_elem = window[buffer.js_type].BYTES_PER_ELEMENT;
            let gpu_data = new window[buffer.js_type]((bounds.maxIndex - bounds.minIndex) * buffer.components);

            this.batchGpuBuffers[name] = gpu_data;
            
            gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
            gl.getBufferSubData(gl.ARRAY_BUFFER, bounds.minIndex * buffer.components * bytes_per_elem, gpu_data, 0, gpu_data.length);
        }

        fn();

        // Restoring after fn() because potentially fn is creating linebuffers
        gl.bindBuffer(gl.ARRAY_BUFFER, restoreArrayBinding);
        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, restoreElementBinding);

        this.batchGpuBuffers = null;
    }

    /*
     * Create a line renderer from instance data, this does not use the GPU batching
     */
    createLineRendererFromInstance(gl, a, b) {
        const lineRenderer = new FatLineRenderer(this.viewer, gl, {
            quantize: this.positionBuffer.js_type !== Float32Array.name
        }, this.unquantizationMatrix);

        lineRenderer.init(b - a);
        
        const positions = new window[this.positionBuffer.js_type](this.positionBuffer.N);
        const indices = new window[this.indexBuffer.js_type](b-a);
        
        // @todo: get only part of positions [min(indices), max(indices)]
        var restoreArrayBinding = gl.getParameter(gl.ARRAY_BUFFER_BINDING);
        gl.bindBuffer(gl.ARRAY_BUFFER, this.positionBuffer);
        gl.getBufferSubData(gl.ARRAY_BUFFER, 0, positions);
        
        var restoreElementBinding = gl.getParameter(gl.ELEMENT_ARRAY_BUFFER_BINDING);
        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);
        gl.getBufferSubData(gl.ELEMENT_ARRAY_BUFFER, a * 4, indices, 0, indices.length);
        
        const s = new Set();
        
        for (let i = 0; i < indices.length; i += 3) {
            let abc = indices.subarray(i, i + 3);

            for (let j = 0; j < 3; ++j) {
                let ab = [abc[j], abc[(j+1)%3]];
                ab.sort();
                let abs = ab.join(":");

                if (s.has(abs)) {
                    s.delete(abs);
                } else {
                    s.add(abs);
                }
            }
        }
        
        for (let e of s) {
            let [a,b] = e.split(":");
            let A = positions.subarray(a * 3).subarray(0,3);
            let B = positions.subarray(b * 3).subarray(0,3);
            lineRenderer.pushVertices(A, B);
        }			

        lineRenderer.finalize();            

        gl.bindBuffer(gl.ARRAY_BUFFER, restoreArrayBinding);
        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, restoreElementBinding);

        return lineRenderer;
    }
    
    createLineRenderer(gl, uniqueId, a, b) {
        const lineRenderer = new FatLineRenderer(this.viewer, gl, {
            quantize: this.positionBuffer.js_type !== Float32Array.name
        }, this.unquantizationMatrix);

		var offset, length;
		if (this.uniqueIdToIndex == null) {
			offset = 0;
			length = this.nrLineIndices;
		} else {
			let index = this.uniqueIdToIndex.get(uniqueId);
			let idx = index[0];
			offset = idx.lineIndicesStart;
			length = idx.lineIndicesLength;
		}

		if (this.lineIndexBuffer != null && typeof(offset) !== 'undefined' && typeof(length) !== 'undefined') {
			// TODO this is where we are
			// Problem here is that the buffer is now already created on the GPU, but we need to convert it to a fatlinerenderer...
			// So we could either generate the line render buffers as fat lines already (taking more network), but real quick to send to GPU, or
			// Not store the data on the GPU when loading, but as a CPU buffer, and then just iterating over the CPU data when creating a LineRenderer using the normal code
			// This last option sucks if we want to always do line rendering of all objects
			// 2 triangles of data per line is a lot...

			const bounds = this.batchGpuBuffers.bounds;
			const vertexOffset = -bounds.minIndex * 3;
			
			let gpu_data = this.batchGpuBuffers.positionBuffer;
						
			var indexOffset = offset - bounds.startLineIndex;

			lineRenderer.init(length);

			const lineIndices = this.batchGpuBuffers.lineIndices;
			for (var i=0; i<length; i+=2) {
				let a = lineIndices[indexOffset + i];
				let b = lineIndices[indexOffset + i + 1];
				const as = vertexOffset + a * 3;
				const bs = vertexOffset + b * 3;
				let A = gpu_data.subarray(as, as + 3);
				let B = gpu_data.subarray(bs, bs + 3);
				lineRenderer.pushVertices(A, B);
			}
			
			lineRenderer.finalize();

			return lineRenderer;
		} else {
			let index = this.uniqueIdToIndex.get(uniqueId);
			let idx = index[0];
			let [offset, length] = [idx.start, idx.length];
			let [minIndex, maxIndex] = [idx.minIndex, idx.maxIndex];

			let gpu_data = this.batchGpuBuffers.positionBuffer;
			let gpu_data_norm = this.batchGpuBuffers.normalBuffer;

			const bounds = this.batchGpuBuffers.bounds;
			
			// A more efficient (and certainly more compact) version that used bitshifting was working fine up until 16bits, unfortunately JS only does bitshifting < 32 bits, so now we have this crappy solution
			
			var indexOffset = offset - bounds.startIndex;
			
			const s = new Set();
			
			const indices = this.batchGpuBuffers.indices;

			// A rather good indiation that we have redundant indices.
			let use_positions = (indices[indices.length - 1] - indices[0]) == indices.length - 1;

			for (var i=0; i<length; i+=3) {
				for (let j = 0; j < 3; ++j) {
					let a = indices[indexOffset + i + j];
					let b = indices[indexOffset + i + (j+1)%3];

					if (use_positions) {
						a -= indices[0];
						b -= indices[0];
						a = [
							gpu_data.subarray(a * 3, a * 3 + 3).join(" "),
							gpu_data_norm.subarray(a * 3, a * 3 + 3).join(" ")
						].join(',');
						b = [
							gpu_data.subarray(b* 3, b * 3 + 3).join(" "),
							gpu_data_norm.subarray(b * 3, b * 3 + 3).join(" ")
						].join(',');
					}
					
					if (a > b) {
						const tmp = a;
						a = b;
						b = tmp;
					}

					let abs;
					if (use_positions) {
						abs = [a, b].join(";");
					} else {
						// First tried to do this with bit shifting, but bit shifting in JS is 32bit
						abs = a * 67108864 + b; // 2^26=67108864. A maximum of 52 bits is used, staying just under 2^53, which is the max safe int
					}
					if (s.has(abs)) {
						s.delete(abs);
					} else {
						s.add(abs);
					}
				}
			}
			
			let AA = new gpu_data.constructor(3);
			let BB = new gpu_data.constructor(3);

			lineRenderer.init(s.size);
			const vertexOffset = -bounds.minIndex * 3;
			for (let e of s) {
				let A, B;
				if (use_positions) {
					let [a_, b_] = e.split(";");
					AA.set(a_.split(",")[0].split(' '));
					BB.set(b_.split(",")[0].split(' '));
					A = AA;
					B = BB;
				} else {
					const a = Math.floor(e / 67108864);
					const b = e - a * 67108864;
					const as = vertexOffset + a * 3;
					const bs = vertexOffset + b * 3;
					let A = gpu_data.subarray(as, as + 3);
					let B = gpu_data.subarray(bs, bs + 3);
				}
				lineRenderer.pushVertices(A, B);
			}
			
			lineRenderer.finalize();

			return lineRenderer;
		}
    }

    getBounds(id_ranges) {
    	var bounds = {};
    	for (const idRange of id_ranges) {
    		const oid = idRange[0];
    		const range = idRange[1];
    		if (this.uniqueIdToIndex) {
    			let idx = this.uniqueIdToIndex.get(oid)[0];
    			
    			// Regular indices
    			if (bounds.startIndex == null || range[0] < bounds.startIndex) {
    				bounds.startIndex = range[0];
    			}
    			if (bounds.endIndex == null || range[1] > bounds.endIndex) {
    				bounds.endIndex = range[1];
    			}
    			if (bounds.minIndex == null || idx.minIndex < bounds.minIndex) {
    				bounds.minIndex = idx.minIndex;
    			}
    			if (bounds.maxIndex == null || idx.maxIndex + 1 > bounds.maxIndex) {
    				// This one seems to be wrong
    				bounds.maxIndex = idx.maxIndex + 1;
    			}
    			
    			// Line indices
    			if (bounds.startLineIndex == null || range[2] < bounds.startLineIndex) {
    				bounds.startLineIndex = range[2];
    			}
    			if (bounds.endLineIndex == null || range[3] > bounds.endLineIndex) {
    				bounds.endLineIndex = range[3];
    			}
    			if (bounds.startLineIndex > bounds.endLineIndex) {
    				debugger;
    			}
    			if (bounds.minLineIndex == null || idx.minLineIndex < bounds.minLineIndex) {
    				bounds.minLineIndex = idx.minLineIndex;
    			}
    			if (bounds.maxLineIndex == null || idx.maxLineIndex + 1 > bounds.maxLineIndex) {
    				// This one seems to be wrong
    				bounds.maxLineIndex = idx.maxLineIndex;
    			}
    		} else {
    			// TODO
    			bounds.startIndex = 0;
    			bounds.endIndex = 0;
    			bounds.minIndex = 0;
    			bounds.maxIndex = 0;
    			bounds.startLineIndex = 0;
    			bounds.endLineIndex = 0;
    			bounds.minLineIndex = 0;
    			bounds.maxLineIndex = 0;
    		}
    	}
    	return bounds;
    }

    computeVisibleInstances(ids_with_or_without, gl) {
    	const ids = ids_with_or_without.with ? ids_with_or_without.with : ids_with_or_without.without;
        const exclude = "without" in ids_with_or_without;
        
		const ids_str = exclude + ':' + ids.frozen;

    	if (ids.nonce) {
    		var nonce = ids.nonce + (ids_with_or_without.with ? "w" : "o");
    		const cachedValue = this.nonceCache.get(nonce);
    		if (cachedValue) {
    			/* Using the nonce of the FrozenBufferSet here so we can skip the potentially very heavy this.visibleRanges.get call (seems to be real slow for large strings), 
    			 * basically we use different layers of cache here.
    			 * It remains to be seen how useful the second layer of caching actually is
    			 */
    			return cachedValue;
    		}
    	}
		
        {
            var cache_lookup;
            if ((cache_lookup = this.visibleRanges.get(ids_str))) {
                return cache_lookup;
            }
        }

        let ranges = {instanceIds: [], hidden: exclude, somethingVisible: null};
        this.objects.forEach((ob, i) => {
            if (ids !== null && ids.has(ob.uniqueId)) {
                // @todo, for large lists of objects, this is not efficient
                ranges.instanceIds.push(i);
            }
        });

        if (ranges.instanceIds.length == this.objects.length) {
            ranges.instanceIds = [];
            ranges.hidden = !ranges.hidden;
        }

        ranges.somethingVisible = ranges.hidden
            ? ranges.instanceIds.length < this.objects.length
            : ranges.instanceIds.length > 0;

        this.visibleRanges.set(ids_str, ranges);
        
        this.storeInCacheAndReturn(null, ranges, nonce);

//        debugger;
        if (!exclude && ranges.instanceIds.length && this.lineIndexBuffers.size === 0) {
            let lineRenderer = this.createLineRendererFromInstance(gl, 0, this.indexBuffer.N);
            // This will result in a different dequantization matrix later on, not sure why
            lineRenderer.uniqueModelId = this.uniqueModelId;
            this.objects.forEach((ob) => {
                lineRenderer.matrixMap.set(ob.uniqueId, ob.matrix);
                this.lineIndexBuffers.set(ob.uniqueId, lineRenderer);
            });
        }

        return ranges;
    }
    
    // generator function that yields ranges in this buffer for the selected ids
    * _(uniqueIdToIndex, ids) {
        var oids;
        for (var i of ids) {
    		if ((oids = uniqueIdToIndex.get(i))) {
    			for (var j = 0; j < oids.length; ++j) {
    				yield [i, [oids[j].start, oids[j].start + oids[j].length]];
    			}
    		}
        }
    }

    getIdRanges(oids) {
    	var iterator1 = this.uniqueIdToIndex.keys();
    	var iterator2 = oids[Symbol.iterator]();
    	const id_ranges = this.uniqueIdToIndex
    	? Array.from(this.findUnion(iterator1, iterator2)).sort((a, b) => (a[1][0] > b[1][0]) - (a[1][0] < b[1][0]))
    			// If we don't have this mapping, we're dealing with a dedicated
    			// non-instanced bufferset for one particular overriden object
    			: [[this.uniqueId & 0x8FFFFFFF, [0, this.nrIndices]]];
    	return id_ranges;
    }
    
    /**
     * Generator function that yields ranges in this buffer for the selected ids
     * This one tries to do better than _ by utilizing the fact (requirement) that both uniqueIdToIndex and ids are numerically ordered beforehand
     * Basically it only iterates through both iterators only once. Could be even faster with a real TreeMap, but we don't have it available
     */
    * findUnion(iterator1, iterator2) {
    	var next1 = iterator1.next();
    	var next2 = iterator2.next();
    	while (!next1.done && !next2.done) {
    		const diff = this.viewer.uniqueIdCompareFunction(next1.value, next2.value);
    		if (diff == 0) {
    			const uniqueId1 = next1.value;
    			var indices = this.uniqueIdToIndex.get(uniqueId1);
    			for (var j = 0; j < indices.length; ++j) {
    				const mapping = indices[j];
    				yield [uniqueId1, [mapping.start, mapping.start + mapping.length, mapping.lineIndicesStart, mapping.lineIndicesStart + mapping.lineIndicesLength]];
    			}
    			next1 = iterator1.next();
    			next2 = iterator2.next();
    		} else {
    			if (diff < 0) {
    				next1 = iterator1.next();
    			} else {
    				next2 = iterator2.next();
    			}
    		}
    	}
    }
	
    generateIdRanges(uniqueId, gl) {
    	if (this.uniqueIdToIndex) {
    		var indices = this.uniqueIdToIndex.get(uniqueId);
    		for (var j = 0; j < indices.length; ++j) {
    			const mapping = indices[j];
    			return [[uniqueId, [mapping.start, mapping.start + mapping.length, mapping.lineIndicesStart, mapping.lineIndicesStart + mapping.lineIndicesLength]]];
    		}
    	} else {
    		return [[uniqueId, [0, this.nrIndices, 0, this.nrLineIndices]]];
    	}
    }
    
    computeVisibleRangesAsBuffers(ids_with_or_without, gl) {
    	if (this.dirty) {
    		// TODO maybe we can reuse something here?
//    		console.log("Clearing visible ranges cache", this.visibleRanges.size);
    		this.visibleRanges.clear();
    		this.nonceCache.clear();
    		this.dirty = false;
    	}
    	
    	var ids = ids_with_or_without.with ? ids_with_or_without.with : ids_with_or_without.without;

    	if (ids.nonce !== undefined) {
    		var nonce = ids.nonce + (ids_with_or_without.with ? "w" : "o");
    		const cachedValue = this.nonceCache.get(nonce);
    		if (cachedValue) {
    			/* Using the nonce of the FrozenBufferSet here so we can skip the potentially very heavy this.visibleRanges.get call (seems to be real slow for large strings), 
    			 * basically we use different layers of cache here.
    			 * It remains to be seen how useful the second layer of caching actually is
    			 */
    			return cachedValue;
    		}
    	}
    	
    	var exclude = "without" in ids_with_or_without;
    	
//    	const ids_str = exclude + ':' +  ids.frozen;
    	
//    	{
//    		var cache_lookup;
//    		if ((cache_lookup = this.visibleRanges.get(ids_str))) {
//    			return cache_lookup;
//    		}
//    	}
    	
    	if (ids === null || ids.size === 0) {
    		let result =  {
    			counts: new Int32Array([this.nrIndices]),
    			offsetsBytes: new Int32Array([0]),
        		lineRenderCounts: new Int32Array([this.nrLineIndices]),
        		lineRenderOffsetsBytes: new Int32Array([0]),
    			pos: 1
    		};
    		return this.storeInCacheAndReturn(null, result, nonce);
    	}
    	
    	var iterator1 = this.uniqueIdToIndex.keys();
    	var iterator2 = ids._set[Symbol.iterator]();
    	
    	var id_ranges = null;
    	if (this.uniqueIdToIndex) {
    		id_ranges = Array.from(this.findUnion(iterator1, iterator2)).sort((a, b) => (a[1][0] > b[1][0]) - (a[1][0] < b[1][0]));
    	} else {
			// If we don't have this mapping, we're dealing with a dedicated
			// non-instanced bufferset for one particular overriden object
			id_ranges = [[this.uniqueId & 0x8FFFFFFF, [0, this.nrIndices, 0, this.nrLineIndices]]];
    	}
    	
    	var result = {
    		counts: new Int32Array(id_ranges.length),
    		offsetsBytes: new Int32Array(id_ranges.length),
    		lineRenderCounts: new Int32Array(id_ranges.length),
    		lineRenderOffsetsBytes: new Int32Array(id_ranges.length),
    		pos: id_ranges.length
    	};
    	
    	var c = 0;
    	for (const range of id_ranges) {
    		const realRange = range[1];
    		result.offsetsBytes[c] = realRange[0] * 4;
    		result.counts[c] = realRange[1] - realRange[0];
    		result.lineRenderOffsetsBytes[c] = realRange[2] * 4;
    		result.lineRenderCounts[c] = realRange[3] - realRange[2];
    		c++;
    	}
    	
    	this.lastIdRanges = id_ranges;
    	this.lastExclude = exclude;
    	
    	result = this.joinConsecutiveRangesAsBuffers(result);
    	
    	if (exclude) {
    		let complement = this.complementRangesAsBuffers(result);
    		return this.storeInCacheAndReturn(null, complement, nonce);
    	}
    	
    	// Create fat line renderings for these elements. This should (a) 
    	// not in the draw loop (b) maybe in something like a web worker
    	
//    	let bounds = this.getBounds(id_ranges);
    	
//    	this.batchGpuRead(gl, ["positionBuffer"], bounds, () => {
//    		id_ranges.forEach((range, i) => {
//    			let [id, [a, b]] = range;
//    			if (this.lineIndexBuffers.has(id)) {
//    				return;
//    			}
//    			let lineRenderer = this.createLineRenderer(gl, id, a, b);
//    			this.lineIndexBuffers.set(id, lineRenderer);
//    		});
//    	});
    	
    	return this.storeInCacheAndReturn(null, result, nonce);
    }
    
    getLines(requestedId, gl) {
    	let lines = this.lineIndexBuffers.get(requestedId);
    	if (lines) {
    		return lines;
    	}
    	if (this.uniqueIdToIndex != null && !this.uniqueIdToIndex.has(requestedId)) {
    		return null;
    	}
    	this.generateLines(requestedId, gl);
    	return null;
    }
    
    generateLines(requestedId, gl) {
    	if (this.reuse) {
            let lineRenderer = this.createLineRendererFromInstance(gl, 0, this.indexBuffer.N);
            // This will result in a different dequantization matrix later on, not sure why
            lineRenderer.uniqueModelId = this.uniqueModelId;
            this.objects.forEach((ob) => {
                lineRenderer.matrixMap.set(ob.uniqueId, ob.matrix);
                this.lineIndexBuffers.set(ob.uniqueId, lineRenderer);
            });
            return;
    	}

    	let id_ranges = this.generateIdRanges(requestedId, gl);
		let bounds = this.getBounds(id_ranges);
		
		if (id_ranges.length == 0) {
			let lineRenderer = this.createLineRenderer(gl, requestedId, 0, 0);
			this.lineIndexBuffers.set(requestedId, lineRenderer);
		} else {
			// @todo normalBuffer is actually only required when we don't have indices,
			// so we need to decide which lines to render based on positions and normals
			this.batchGpuRead(gl, ["positionBuffer", "normalBuffer"], bounds, () => {
				id_ranges.forEach((range, i) => {
					let [id, [a, b]] = range;
					if (id == requestedId) {
						let lineRenderer = this.createLineRenderer(gl, id, a, b);
						this.lineIndexBuffers.set(id, lineRenderer);
					}
				});
				this.viewer.dirty = 2;
			});
		}
    }
    
    storeInCacheAndReturn(key, result, nonce) {
//		this.visibleRanges.set(key, result);
    	if (nonce) {
    		this.lastNonce = nonce;
    		this.lastComputedVisibleRanges = result;
    		this.nonceCache.set(nonce, result);
    	}
		return result;
    }
    
	reset(viewer) {
		this.positionsIndex = 0;
		this.normalsIndex = 0;
		this.pickColorsIndex = 0;
		this.indicesIndex = 0;
		this.lineIndicesIndex = 0;
		this.nrIndices = 0;
		this.bytes = 0;
		this.visibleRanges = new Map();
		this.uniqueIdSet = new Set();
		if (!this.reuse) {
			this.uniqueIdToIndex = new AvlTree(viewer.inverseUniqueIdCompareFunction);
		}
		this.lineIndexBuffers = new Map();
	}

	has(uniqueId) {
		if (this.uniqueIdSet == null) {
			debugger;
		}
		return this.uniqueIdSet.has(uniqueId);
	}
	
	copy(gl, uniqueId) {
        let returnDictionary = {};

        if (this.objects) {
            return this.copyEmpty();
        } else {
    		let idx = this.uniqueIdToIndex.get(uniqueId)[0];

    		let bounds = this.batchGpuBuffers.bounds;

    		let [offset, length] = [idx.start, idx.length];
			const indices = new Uint32Array(length);
			let [minIndex, maxIndex] = [idx.minIndex, idx.maxIndex];
			for (let i=0; i<length; i++) {
				indices[i] = this.batchGpuBuffers.indices[-bounds.startIndex + offset + i] - minIndex;
			}

			let [lineIndexOffset, lineIndexLength] = [idx.lineIndicesStart, idx.lineIndicesLength];
			const lineIndices = new Uint32Array(lineIndexLength);
			let [minLineIndex, maxLineIndex] = [idx.minLineIndex, idx.maxLineIndex];
			
			for (let i=0; i<lineIndexLength; i++) {
				lineIndices[i] = (this.batchGpuBuffers.lineIndices[-bounds.startLineIndex + lineIndexOffset + i] - minLineIndex) + 1;
			}

    		let numVertices = maxIndex - minIndex + 1;
    		
    		let toCopy = ["positionBuffer", "normalBuffer", "colorBuffer", "pickColorBuffer"];
    		
    		for (var name of toCopy) {
    			let buffer = this[name];
    			let gpu_data = this.batchGpuBuffers[name];
    			let new_gpu_data = new window[buffer.js_type](numVertices * buffer.components);

				// @todo this can probably be a combination of subarray() and set()
    			var vertexOffset = (-bounds.minIndex + minIndex) * buffer.components;
    			for (let j=0; j<numVertices * buffer.components; j++) {
    				new_gpu_data[j] = gpu_data[vertexOffset + j];
        		}
    			
    			let shortName = name.replace("Buffer", "") + "s";
    			returnDictionary[shortName] = new_gpu_data;
    			returnDictionary["nr" + shortName.substr(0,1).toUpperCase() + shortName.substr(1)] = new_gpu_data.length;
    		}
    		
    		returnDictionary.isCopy = true;
    		returnDictionary["uniqueIdSet"] = this.uniqueIdSet;
    		returnDictionary["indices"] = indices;
    		returnDictionary["nrIndices"] = indices.length;
    		returnDictionary["lineIndices"] = lineIndices;
    		returnDictionary["nrLineIndices"] = lineIndices.length;
    		returnDictionary["lineIndexBuffers"] = this.lineIndexBuffers;
        }

		return returnDictionary;
	}

	setColor(gl, uniqueId, clr) {
        // Reusing buffer sets always results in a copy
        if (this.objects) {
            return false;
        }

        // Switching transparency states results in a copy
		if (clr.length == 4 && this.hasTransparency != (clr[3] < 1.)) {
			return false;
		}

		var oldColors, newColors, clrArray;

		if (clr.length == 4) {
			let factor = this.colorBuffer.js_type == Uint8Array.name ? 255. : 1.;
			clrArray = new window[this.colorBuffer.js_type](4);
			for (let i = 0; i < 4; ++i) {
				clrArray[i] = clr[i] * factor;
			}
		} else {
			newColors = clr;
		}

		const idxs = this.uniqueIdToIndex.get(uniqueId);
		if (idxs == null) {
			return;
		}
		for (var idx of idxs) {
			let [offset, length] = [idx.color, idx.colorLength];
			let bytes_per_elem = window[this.colorBuffer.js_type].BYTES_PER_ELEMENT;
			
			// Assumes there is just one index pair, this is for now always the case.
			oldColors = new window[this.colorBuffer.js_type](length);

			if (clr.length == 4) {
				newColors = new window[this.colorBuffer.js_type](length);
				for (let i = 0; i < length; i += 4) {
					newColors.set(clrArray, i);
				}
			}

    		var restoreArrayBinding = gl.getParameter(gl.ARRAY_BUFFER_BINDING);
    		gl.bindBuffer(gl.ARRAY_BUFFER, this.colorBuffer);
    		let bounds = this.batchGpuBuffers.bounds;
			let gpu_data = this.batchGpuBuffers.colorBuffer;
			// @todo this can probably be a combination of subarray() and set()
			for (let j=0; j<length; j++) {
				oldColors[j] = gpu_data[offset - (bounds.minIndex * 4) + j];
    		}
    		gl.bufferSubData(gl.ARRAY_BUFFER, offset * bytes_per_elem, newColors, 0, length);
    		gl.bindBuffer(gl.ARRAY_BUFFER, restoreArrayBinding);
		}

		return oldColors;
	}
}