diff --git a/modules/core/src/lib/attribute/attribute-manager.js b/modules/core/src/lib/attribute/attribute-manager.js index 7527b93051e..8a06fa5c5d5 100644 --- a/modules/core/src/lib/attribute/attribute-manager.js +++ b/modules/core/src/lib/attribute/attribute-manager.js @@ -232,21 +232,21 @@ export default class AttributeManager { for (const attributeName in this.attributes) { const attribute = this.attributes[attributeName]; - - if ( - attribute.setExternalBuffer( - buffers[attributeName] || (data.attributes && data.attributes[attributeName]) - ) - ) { - // Attribute is using external buffer from the props - } else if (attribute.setConstantValue(props[attribute.settings.accessor])) { - // Attribute is using generic value from the props + const accessorName = attribute.settings.accessor; + attribute.startIndices = startIndices; + + if (attribute.setVirtualBuffer(buffers[attributeName])) { + // Step 1: try set virtual attribute from external buffers + } else if (attribute.setLogicalBuffer(buffers[accessorName], data.startIndices)) { + // Step 2: try set logical attribute from external buffers + } else if (attribute.setConstantValue(props[accessorName])) { + // Step 3: try set constant value from props } else if (attribute.needsUpdate()) { + // Step 4: update via updater callback updated = true; this._updateAttribute({ attribute, numInstances, - startIndices, data, props, context diff --git a/modules/core/src/lib/attribute/attribute.js b/modules/core/src/lib/attribute/attribute.js index bd0ec89d698..4ea36613260 100644 --- a/modules/core/src/lib/attribute/attribute.js +++ b/modules/core/src/lib/attribute/attribute.js @@ -2,7 +2,7 @@ import {_Accessor as Accessor} from '@luma.gl/core'; import DataColumn from './data-column'; import assert from '../../utils/assert'; -import {createIterable} from '../../utils/iterable-utils'; +import {createIterable, getAccessorFromBuffer} from '../../utils/iterable-utils'; import {fillArray} from '../../utils/flatten'; import * as range from '../../utils/range'; import {normalizeTransitionSettings} from './attribute-transition-utils'; @@ -25,14 +25,16 @@ export default class Attribute extends DataColumn { Object.assign(this.settings, { transition, noAlloc, - update: update || (accessor && this._standardAccessor), + update: update || (accessor && this._autoUpdater), accessor, elementOffset: offset / Accessor.getBytesPerElement(this.settings), transform }); Object.assign(this.state, { - lastExternalBuffer: null, + lastVirtualBuffer: null, + lastLogicalBuffer: null, + logicalAccessor: null, needsUpdate: true, needsRedraw: false, updateRanges: range.FULL, @@ -136,7 +138,7 @@ export default class Attribute extends DataColumn { return false; } - updateBuffer({numInstances, startIndices, data, props, context}) { + updateBuffer({numInstances, data, props, context}) { if (!this.needsUpdate()) { return false; } @@ -150,7 +152,7 @@ export default class Attribute extends DataColumn { if (update) { // Custom updater - typically for non-instanced layers for (const [startRow, endRow] of updateRanges) { - update.call(context, this, {data, startRow, endRow, props, numInstances, startIndices}); + update.call(context, this, {data, startRow, endRow, props, numInstances}); } if (!this.value) { // no value was assigned during update @@ -201,27 +203,73 @@ export default class Attribute extends DataColumn { // Use external buffer // Returns true if successful // eslint-disable-next-line max-statements - setExternalBuffer(buffer) { + setVirtualBuffer(buffer) { const {state} = this; if (!buffer) { - state.lastExternalBuffer = null; + state.lastVirtualBuffer = null; return false; } this.clearNeedsUpdate(); - if (state.lastExternalBuffer === buffer) { + if (state.lastVirtualBuffer === buffer) { return true; } - state.lastExternalBuffer = buffer; + state.lastVirtualBuffer = buffer; this.setNeedsRedraw(); this.setData(buffer); + return true; + } + + setLogicalBuffer(buffer, startIndices) { + const {state, settings} = this; + + if (!buffer) { + state.lastLogicalBuffer = null; + state.logicalAccessor = null; + return false; + } + + if (settings.noAlloc) { + // Let the layer handle this + return false; + } + if (state.lastLogicalBuffer === buffer) { + this.clearNeedsUpdate(); + return true; + } + state.lastLogicalBuffer = buffer; + this.setNeedsRedraw(); + + const needsUpdate = settings.transform || startIndices !== this.startIndices; + + if (needsUpdate) { + if (ArrayBuffer.isView(buffer)) { + buffer = {value: buffer}; + } + assert(ArrayBuffer.isView(buffer.value), `invalid ${settings.accessor}`); + const needsNormalize = buffer.size && buffer.size !== this.size; + + state.logicalAccessor = getAccessorFromBuffer(buffer.value, { + size: buffer.size || this.size, + stride: buffer.stride, + offset: buffer.offset, + startIndices, + nested: needsNormalize + }); + // Fall through to auto updater + return false; + } + + this.clearNeedsUpdate(); + this.setData(buffer); return true; } - getVertexOffset(row, startIndices = this.startIndices) { + getVertexOffset(row) { + const {startIndices} = this; const vertexIndex = startIndices ? startIndices[row] : row; return this.settings.elementOffset + vertexIndex * this.size; } @@ -241,15 +289,16 @@ export default class Attribute extends DataColumn { } /* eslint-disable max-depth, max-statements */ - _standardAccessor(attribute, {data, startRow, endRow, props, numInstances, startIndices}) { - const {settings, value, size} = attribute; + _autoUpdater(attribute, {data, startRow, endRow, props, numInstances}) { + const {settings, state, value, size, startIndices} = attribute; const {accessor, transform} = settings; - const accessorFunc = typeof accessor === 'function' ? accessor : props[accessor]; + const accessorFunc = + state.logicalAccessor || (typeof accessor === 'function' ? accessor : props[accessor]); assert(typeof accessorFunc === 'function', `accessor "${accessor}" is not a function`); - let i = attribute.getVertexOffset(startRow, startIndices); + let i = attribute.getVertexOffset(startRow); const {iterable, objectInfo} = createIterable(data, startRow, endRow); for (const object of iterable) { objectInfo.index++; @@ -288,7 +337,6 @@ export default class Attribute extends DataColumn { } } attribute.constant = false; - attribute.startIndices = startIndices; } /* eslint-enable max-depth, max-statements */ diff --git a/modules/core/src/lib/layer.js b/modules/core/src/lib/layer.js index a70f11257a3..3484730f4f0 100644 --- a/modules/core/src/lib/layer.js +++ b/modules/core/src/lib/layer.js @@ -433,7 +433,7 @@ export default class Layer extends Component { startIndices, props, transitions: props.transitions, - buffers: props, + buffers: props.data.attributes, context: this, // Don't worry about non-attribute props ignoreUnknownAttributes: true diff --git a/modules/core/src/utils/iterable-utils.js b/modules/core/src/utils/iterable-utils.js index a323d3b1511..5a470adb068 100644 --- a/modules/core/src/utils/iterable-utils.js +++ b/modules/core/src/utils/iterable-utils.js @@ -60,3 +60,49 @@ export function createIterable(data, startRow = 0, endRow = Infinity) { export function isAsyncIterable(data) { return data && data[Symbol.asyncIterator]; } + +/* + * Create an accessor function from a flat buffer that yields the value at each object index + */ +export function getAccessorFromBuffer(typedArray, {size, stride, offset, startIndices, nested}) { + const bytesPerElement = typedArray.BYTES_PER_ELEMENT; + const elementStride = stride ? stride / bytesPerElement : size; + const elementOffset = offset ? offset / bytesPerElement : 0; + const vertexCount = Math.floor((typedArray.length - elementOffset) / elementStride); + + return (_, {index, target}) => { + if (!startIndices) { + const sourceIndex = index * elementStride + elementOffset; + for (let j = 0; j < size; j++) { + target[j] = typedArray[sourceIndex + j]; + } + return target; + } + const startIndex = startIndices[index]; + const endIndex = startIndices[index + 1] || vertexCount; + let result; + + if (nested) { + result = new Array(endIndex - startIndex); + for (let i = startIndex; i < endIndex; i++) { + const sourceIndex = i * elementStride + elementOffset; + target = new Array(size); + for (let j = 0; j < size; j++) { + target[j] = typedArray[sourceIndex + j]; + } + result[i - startIndex] = target; + } + } else { + result = new typedArray.constructor((endIndex - startIndex) * size); + let targetIndex = 0; + for (let i = startIndex; i < endIndex; i++) { + const sourceIndex = i * elementStride + elementOffset; + for (let j = 0; j < size; j++) { + result[targetIndex++] = typedArray[sourceIndex + j]; + } + } + } + + return result; + }; +} diff --git a/test/modules/core/lib/attribute/attribute-manager.spec.js b/test/modules/core/lib/attribute/attribute-manager.spec.js index 5cbb01d97bc..4810f4cec7d 100644 --- a/test/modules/core/lib/attribute/attribute-manager.spec.js +++ b/test/modules/core/lib/attribute/attribute-manager.spec.js @@ -144,7 +144,7 @@ test('AttributeManager.update - 0 numInstances', t => { t.end(); }); -test('AttributeManager.update - external buffers', t => { +test('AttributeManager.update - external virtual buffers', t => { const attributeManager = new AttributeManager(gl); const dummyUpdate = () => t.fail('updater should not be called when external buffer is present'); @@ -192,6 +192,89 @@ test('AttributeManager.update - external buffers', t => { t.end(); }); +test('AttributeManager.update - external logical buffers', t => { + const attributeManager = new AttributeManager(gl); + + const dummyAccessor = () => + t.fail('accessor should not be called when external buffer is present'); + + attributeManager.add({ + positions: {size: 2, accessor: 'getPosition'}, + colors: {size: 4, type: GL.UNSIGNED_BYTE, accessor: 'getColor', defaultValue: [0, 0, 0, 255]}, + types: {size: 1, accessor: 'getType', transform: x => x - 65} + }); + + // First update, should autoalloc and update the value array + attributeManager.update({ + numInstances: 2, + data: {length: 2}, + props: { + getPosition: dummyAccessor, + getColor: dummyAccessor, + getType: dummyAccessor + }, + buffers: { + getPosition: new Float32Array([1, 1, 2, 2]), + getColor: {value: new Uint8ClampedArray([255, 0, 0, 0, 255, 0]), size: 3}, + getType: new Uint8Array([65, 68]) + } + }); + + let attribute = attributeManager.getAttributes()['positions']; + t.deepEqual(attribute.value, [1, 1, 2, 2], 'positions attribute has value'); + + attribute = attributeManager.getAttributes()['colors']; + t.deepEqual(attribute.value, [255, 0, 0, 0, 255, 0], 'colors attribute has value'); + t.is(attribute.getAccessor().size, 3, 'colors attribute has correct size'); + + attribute = attributeManager.getAttributes()['types']; + t.deepEqual(attribute.value.slice(0, 2), [0, 3], 'types attribute has value'); + + t.end(); +}); + +test('AttributeManager.update - external logical buffers - variable width', t => { + const attributeManager = new AttributeManager(gl); + + const dummyAccessor = () => + t.fail('accessor should not be called when external buffer is present'); + + attributeManager.add({ + positions: {size: 2, accessor: 'getPosition'}, + colors: {size: 4, type: GL.UNSIGNED_BYTE, accessor: 'getColor', defaultValue: [0, 0, 0, 255]} + }); + + // First update, should autoalloc and update the value array + attributeManager.update({ + numInstances: 3, + startIndices: [0, 2], + data: { + length: 2, + vertexStarts: [0, 1] + }, + props: { + getPosition: dummyAccessor, + getColor: dummyAccessor + }, + buffers: { + getPosition: new Float32Array([1, 1, 2, 2]), + getColor: {value: new Uint8ClampedArray([255, 0, 0, 0, 255, 0]), size: 3} + } + }); + + let attribute = attributeManager.getAttributes()['positions']; + t.deepEqual(attribute.value.slice(0, 6), [1, 1, 1, 1, 2, 2], 'positions attribute has value'); + + attribute = attributeManager.getAttributes()['colors']; + t.deepEqual( + attribute.value.slice(0, 12), + [255, 0, 0, 255, 255, 0, 0, 255, 0, 255, 0, 255], + 'colors attribute has value' + ); + + t.end(); +}); + test('AttributeManager.invalidate', t => { const attributeManager = new AttributeManager(gl); attributeManager.add({positions: {size: 2, update}}); diff --git a/test/modules/core/lib/attribute/attribute.spec.js b/test/modules/core/lib/attribute/attribute.spec.js index 1e17c3668b7..620fddeee1b 100644 --- a/test/modules/core/lib/attribute/attribute.spec.js +++ b/test/modules/core/lib/attribute/attribute.spec.js @@ -110,10 +110,10 @@ test('Attribute#allocate', t => { t.ok(attribute.allocate(4), 'allocate successful'); t.is(attribute.value, allocatedValue, 'reused the same typed array'); - attribute.setExternalBuffer(externalValue); + attribute.setVirtualBuffer(externalValue); t.notOk(attributeNoAlloc.allocate(4), 'Should not allocate if external buffer is used'); - attribute.setExternalBuffer(null); + attribute.setVirtualBuffer(null); t.ok(attribute.allocate(4), 'allocate successful'); t.is(attribute.value, allocatedValue, 'reused the same typed array'); @@ -401,10 +401,10 @@ test('Attribute#updateBuffer', t => { for (const param of TEST_PARAMS) { const {attribute} = testCase; attribute.setNeedsUpdate(true); + attribute.startIndices = param.startIndices; attribute.allocate(param.numInstances); attribute.updateBuffer({ numInstances: param.numInstances, - startIndices: param.startIndices, data: TEST_PROPS.data, props: TEST_PROPS }); @@ -483,10 +483,10 @@ test('Attribute#standard accessor - variable width', t => { for (const testCase of TEST_CASES) { const {attribute, result} = testCase; attribute.setNeedsUpdate(true); + attribute.startIndices = [0, 2, 3]; attribute.allocate(10); attribute.updateBuffer({ numInstances: 6, - startIndices: [0, 2, 3], data: TEST_PROPS.data, props: TEST_PROPS }); @@ -647,6 +647,7 @@ test('Attribute#updateBuffer - partial', t => { // reset stats accessorCalled = 0; + attribute.startIndices = testCase.params.startIndices; attribute.allocate(testCase.params.numInstances); attribute.updateBuffer({ ...testCase.params, @@ -666,7 +667,7 @@ test('Attribute#updateBuffer - partial', t => { t.end(); }); -test('Attribute#setExternalBuffer', t => { +test('Attribute#setVirtualBuffer', t => { const attribute = new Attribute(gl, { id: 'test-attribute', type: GL.FLOAT, @@ -688,42 +689,39 @@ test('Attribute#setExternalBuffer', t => { const value2 = new Uint8Array(4); attribute.setNeedsUpdate(); - t.notOk( - attribute.setExternalBuffer(null), - 'should do nothing if setting external buffer to null' - ); + t.notOk(attribute.setVirtualBuffer(null), 'should do nothing if setting external buffer to null'); t.ok(attribute.needsUpdate(), 'attribute still needs update'); - t.ok(attribute.setExternalBuffer(buffer), 'should set external buffer to Buffer object'); + t.ok(attribute.setVirtualBuffer(buffer), 'should set external buffer to Buffer object'); t.is(attribute.getBuffer(), buffer, 'external buffer is set'); t.notOk(attribute.needsUpdate(), 'attribute is updated'); const spy = makeSpy(attribute, 'setData'); t.ok( - attribute.setExternalBuffer(buffer), + attribute.setVirtualBuffer(buffer), 'should successfully set external buffer if setting external buffer to the same object' ); t.notOk(spy.called, 'Should not call update if setting external buffer to the same object'); - t.ok(attribute.setExternalBuffer(value1), 'should set external buffer to typed array'); + t.ok(attribute.setVirtualBuffer(value1), 'should set external buffer to typed array'); t.is(attribute.value, value1, 'external value is set'); t.is(attribute.getAccessor().type, GL.FLOAT, 'attribute type is set correctly'); - t.ok(attribute.setExternalBuffer(value2), 'should set external buffer to typed array'); + t.ok(attribute.setVirtualBuffer(value2), 'should set external buffer to typed array'); t.is(attribute.getBuffer().debugData.constructor.name, 'Uint8Array', 'external value is set'); t.is(attribute.getAccessor().type, GL.UNSIGNED_BYTE, 'attribute type is set correctly'); - t.ok(attribute2.setExternalBuffer(value2), 'external value is set'); + t.ok(attribute2.setVirtualBuffer(value2), 'external value is set'); spy.reset(); t.ok( - attribute.setExternalBuffer(value2), + attribute.setVirtualBuffer(value2), 'should successfully set external buffer if setting external buffer to the same object' ); t.notOk(spy.called, 'Should not call update if setting external buffer to the same object'); t.ok( - attribute.setExternalBuffer({ + attribute.setVirtualBuffer({ offset: 4, stride: 8, value: value1 @@ -795,7 +793,7 @@ test('Attribute#doublePrecision', t0 => { t.deepEqual(attribute.value.slice(0, 6), [0, 1, 2, 1, 1, 2], 'Attribute value is populated'); validateShaderAttributes(t, attribute, true); - attribute.setExternalBuffer(new Uint32Array([3, 4, 5, 4, 4, 5])); + attribute.setVirtualBuffer(new Uint32Array([3, 4, 5, 4, 4, 5])); t.ok(attribute.value instanceof Uint32Array, 'Attribute is Uint32Array'); t.deepEqual( attribute.buffer.debugData.slice(0, 6), @@ -805,11 +803,11 @@ test('Attribute#doublePrecision', t0 => { validateShaderAttributes(t, attribute, false); t.throws( - () => attribute.setExternalBuffer(new Uint8Array([3, 4, 5, 4, 4, 5])), + () => attribute.setVirtualBuffer(new Uint8Array([3, 4, 5, 4, 4, 5])), 'should throw on invalid buffer' ); - attribute.setExternalBuffer(new Float64Array([3, 4, 5, 4, 4, 5])); + attribute.setVirtualBuffer(new Float64Array([3, 4, 5, 4, 4, 5])); t.ok(attribute.value instanceof Float64Array, 'Attribute is Float64Array'); t.deepEqual( attribute.buffer.debugData.slice(0, 6), @@ -819,7 +817,7 @@ test('Attribute#doublePrecision', t0 => { validateShaderAttributes(t, attribute, true); const buffer = new Buffer(gl, 12); - attribute.setExternalBuffer(buffer); + attribute.setVirtualBuffer(buffer); validateShaderAttributes(t, attribute, false); buffer.delete(); @@ -848,7 +846,7 @@ test('Attribute#doublePrecision', t0 => { t.deepEqual(attribute.value.slice(0, 6), [0, 1, 2, 1, 1, 2], 'Attribute value is populated'); validateShaderAttributes(t, attribute, false); - attribute.setExternalBuffer(new Uint32Array([3, 4, 5, 4, 4, 5])); + attribute.setVirtualBuffer(new Uint32Array([3, 4, 5, 4, 4, 5])); t.ok(attribute.value instanceof Uint32Array, 'Attribute is Uint32Array'); t.deepEqual( attribute.buffer.debugData.slice(0, 6), @@ -858,11 +856,11 @@ test('Attribute#doublePrecision', t0 => { validateShaderAttributes(t, attribute, false); t.throws( - () => attribute.setExternalBuffer(new Uint8Array([3, 4, 5, 4, 4, 5])), + () => attribute.setVirtualBuffer(new Uint8Array([3, 4, 5, 4, 4, 5])), 'should throw on invalid buffer' ); - attribute.setExternalBuffer(new Float64Array([3, 4, 5, 4, 4, 5])); + attribute.setVirtualBuffer(new Float64Array([3, 4, 5, 4, 4, 5])); t.ok(attribute.value instanceof Float64Array, 'Attribute is Float64Array'); t.deepEqual( attribute.buffer.debugData.slice(0, 6), @@ -872,7 +870,7 @@ test('Attribute#doublePrecision', t0 => { validateShaderAttributes(t, attribute, true); const buffer = new Buffer(gl, 12); - attribute.setExternalBuffer(buffer); + attribute.setVirtualBuffer(buffer); validateShaderAttributes(t, attribute, false); buffer.delete(); diff --git a/test/modules/core/utils/iterable-utils.spec.js b/test/modules/core/utils/iterable-utils.spec.js index b4a4e5342f4..c929fd8edd1 100644 --- a/test/modules/core/utils/iterable-utils.spec.js +++ b/test/modules/core/utils/iterable-utils.spec.js @@ -1,86 +1,86 @@ import test from 'tape-catch'; -import {createIterable} from '@deck.gl/core/utils/iterable-utils'; +import {createIterable, getAccessorFromBuffer} from '@deck.gl/core/utils/iterable-utils'; -const TEST_CASES = [ - { - title: 'empty data', - input: { - data: null - }, - count: 0 - }, - { - title: 'array data', - input: { - data: [1, 2, 3] +test('createIterable', t => { + const TEST_CASES = [ + { + title: 'empty data', + input: { + data: null + }, + count: 0 }, - count: 3, - firstObject: 1, - firstIndex: 0 - }, - { - title: 'iterable data', - input: { - data: new Map([['x', 1], ['y', 2], ['z', 3]]) + { + title: 'array data', + input: { + data: [1, 2, 3] + }, + count: 3, + firstObject: 1, + firstIndex: 0 }, - count: 3, - firstObject: ['x', 1], - firstIndex: 0 - }, - { - title: 'non-iterable data', - input: { - data: {length: 3} + { + title: 'iterable data', + input: { + data: new Map([['x', 1], ['y', 2], ['z', 3]]) + }, + count: 3, + firstObject: ['x', 1], + firstIndex: 0 }, - count: 3, - firstObject: undefined, - firstIndex: 0 - }, - { - title: 'array data with range', - input: { - data: [1, 2, 3], - startIndex: 0, - endIndex: 1 + { + title: 'non-iterable data', + input: { + data: {length: 3} + }, + count: 3, + firstObject: undefined, + firstIndex: 0 }, - count: 1, - firstObject: 1, - firstIndex: 0 - }, - { - title: 'array data with range', - input: { - data: [1, 2, 3], - startIndex: 1 + { + title: 'array data with range', + input: { + data: [1, 2, 3], + startIndex: 0, + endIndex: 1 + }, + count: 1, + firstObject: 1, + firstIndex: 0 }, - count: 2, - firstObject: 2, - firstIndex: 1 - }, - { - title: 'iterable data with range', - input: { - data: new Map([['x', 1], ['y', 2], ['z', 3]]), - startIndex: 1, - endIndex: 4 + { + title: 'array data with range', + input: { + data: [1, 2, 3], + startIndex: 1 + }, + count: 2, + firstObject: 2, + firstIndex: 1 }, - count: 2, - firstObject: ['y', 2], - firstIndex: 1 - }, - { - title: 'non-iterable data with range', - input: { - data: {length: 3}, - startIndex: 2 + { + title: 'iterable data with range', + input: { + data: new Map([['x', 1], ['y', 2], ['z', 3]]), + startIndex: 1, + endIndex: 4 + }, + count: 2, + firstObject: ['y', 2], + firstIndex: 1 }, - count: 1, - firstObject: undefined, - firstIndex: 2 - } -]; + { + title: 'non-iterable data with range', + input: { + data: {length: 3}, + startIndex: 2 + }, + count: 1, + firstObject: undefined, + firstIndex: 2 + } + ]; -test('createIterable', t => { for (const testCase of TEST_CASES) { const {iterable, objectInfo} = createIterable( testCase.input.data, @@ -107,3 +107,68 @@ test('createIterable', t => { t.end(); }); + +test('getAccessorFromBuffer', t => { + const TEST_CASES = [ + { + title: 'plain buffer', + input: { + value: new Float32Array([1, 1, 0, 2, 2, 0, 3, 3, 0]), + size: 3 + }, + output: [[1, 1, 0], [2, 2, 0], [3, 3, 0]] + }, + { + title: 'buffer with stride and offset', + input: { + value: new Float32Array([0, 0, 0, 1, 1, 0, 2, 2, 0, 3, 3, 0]), + size: 2, + stride: 12, + offset: 12 + }, + output: [[1, 1], [2, 2], [3, 3]] + }, + { + title: 'variable-width buffer', + input: { + value: new Float32Array([1, 1, 0, 2, 2, 0, 3, 3, 0]), + size: 3, + startIndices: [0, 2] + }, + output: [[1, 1, 0, 2, 2, 0], [3, 3, 0]] + }, + { + title: 'variable-width buffer with stride and offset', + input: { + value: new Float32Array([0, 0, 0, 1, 1, 0, 2, 2, 0, 3, 3, 0]), + size: 2, + stride: 12, + offset: 12, + startIndices: [0, 2] + }, + output: [[1, 1, 2, 2], [3, 3]] + }, + { + title: 'variable-width buffer nested', + input: { + value: new Float32Array([1, 1, 0, 2, 2, 0, 3, 3, 0]), + size: 3, + startIndices: [0, 2], + nested: true + }, + output: [[[1, 1, 0], [2, 2, 0]], [[3, 3, 0]]] + } + ]; + + for (const testCase of TEST_CASES) { + t.comment(testCase.title); + const accessor = getAccessorFromBuffer(testCase.input.value, testCase.input); + const context = {index: -1, target: []}; + for (const result of testCase.output) { + context.index++; + t.deepEqual(accessor(null, context), result, `accessor at index ${context.index}`); + } + } + + t.end(); +});