From f123de2da1b730c94fe6452db5f379484a246de5 Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Sun, 5 Apr 2026 12:24:10 -0400 Subject: [PATCH] feat(DataArray): add preserveTypedArrays option to getState() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit getState({ preserveTypedArrays: true }) preserves TypedArray values without converting and copying to plain Arrays. This avoids out-of-memory crashes for large data arrays (e.g. 268M-element labelmaps where Array.from() would allocate ~1.6GB of boxed Numbers). The default behavior is unchanged — getState() without options still returns JSON-safe plain Arrays. --- Sources/Common/Core/DataArray/index.d.ts | 7 +++-- Sources/Common/Core/DataArray/index.js | 7 +++-- .../Core/DataArray/test/testDataArray.js | 29 +++++++++++++++++++ .../DataModel/DataSetAttributes/FieldData.js | 6 ++-- Sources/interfaces.d.ts | 25 +++++++++++++--- Sources/macros.js | 14 ++++++--- 6 files changed, 72 insertions(+), 16 deletions(-) diff --git a/Sources/Common/Core/DataArray/index.d.ts b/Sources/Common/Core/DataArray/index.d.ts index 1a98eb27773..6383866db06 100644 --- a/Sources/Common/Core/DataArray/index.d.ts +++ b/Sources/Common/Core/DataArray/index.d.ts @@ -1,4 +1,4 @@ -import { vtkObject, vtkRange } from '../../../interfaces'; +import { vtkObject, vtkRange, GetStateOptions } from '../../../interfaces'; import { float, int, Nullable, Range, TypedArray } from '../../../types'; /** @@ -274,9 +274,12 @@ export interface vtkDataArray extends vtkObject { /** * Get the state of this array. + * + * Pass `{ preserveTypedArrays: true }` to keep TypedArray values + * without converting and copying to a plain Array. * @returns {object} */ - getState(): object; + getState(options?: GetStateOptions): object; /** * Deep copy of another vtkDataArray into this one. diff --git a/Sources/Common/Core/DataArray/index.js b/Sources/Common/Core/DataArray/index.js index aa5c9514a9a..aa27298d27d 100644 --- a/Sources/Common/Core/DataArray/index.js +++ b/Sources/Common/Core/DataArray/index.js @@ -450,14 +450,15 @@ function vtkDataArray(publicAPI, model) { }; // Override serialization support - publicAPI.getState = () => { + publicAPI.getState = ({ preserveTypedArrays = false } = {}) => { if (model.deleted) { return null; } const jsonArchive = { ...model, vtkClass: publicAPI.getClassName() }; - // Convert typed array to regular array - jsonArchive.values = Array.from(jsonArchive.values); + if (!preserveTypedArrays) { + jsonArchive.values = Array.from(jsonArchive.values); + } delete jsonArchive.buffer; // Clean any empty data diff --git a/Sources/Common/Core/DataArray/test/testDataArray.js b/Sources/Common/Core/DataArray/test/testDataArray.js index 4787955894c..c01856c22d6 100644 --- a/Sources/Common/Core/DataArray/test/testDataArray.js +++ b/Sources/Common/Core/DataArray/test/testDataArray.js @@ -1,4 +1,5 @@ import test from 'tape'; +import vtk from 'vtk.js/Sources/vtk'; import vtkDataArray from 'vtk.js/Sources/Common/Core/DataArray'; import { VtkDataTypes } from 'vtk.js/Sources/Common/Core/DataArray/Constants'; import * as vtkMath from 'vtk.js/Sources/Common/Core/Math'; @@ -508,3 +509,31 @@ test('Test vtkDataArray resize function', (t) => { t.end(); }); + +test('Test vtkDataArray getState preserveTypedArrays option', (t) => { + const values = new Uint8Array([1, 2, 3, 4, 5]); + const da = vtkDataArray.newInstance({ values }); + + // Default: values converted to plain Array + const state = da.getState(); + t.ok(Array.isArray(state.values), 'default getState returns plain Array'); + + // With option: values preserved as TypedArray + const transferable = da.getState({ preserveTypedArrays: true }); + t.ok( + transferable.values instanceof Uint8Array, + 'TypedArray type is preserved' + ); + + // Round-trip via vtk() works with TypedArray values + const da2 = vtk(transferable); + t.ok(da2, 'Can reconstruct from state with TypedArray values'); + t.deepEqual( + Array.from(da2.getData()), + Array.from(values), + 'Values preserved after round-trip' + ); + t.equal(da2.getDataType(), 'Uint8Array', 'Data type preserved'); + + t.end(); +}); diff --git a/Sources/Common/DataModel/DataSetAttributes/FieldData.js b/Sources/Common/DataModel/DataSetAttributes/FieldData.js index 8039843dacc..ae30f9d67c2 100644 --- a/Sources/Common/DataModel/DataSetAttributes/FieldData.js +++ b/Sources/Common/DataModel/DataSetAttributes/FieldData.js @@ -252,11 +252,11 @@ function vtkFieldData(publicAPI, model) { publicAPI.getNumberOfTuples = () => model.arrays.length > 0 ? model.arrays[0].getNumberOfTuples() : 0; - publicAPI.getState = () => { - const result = superGetState(); + publicAPI.getState = (options) => { + const result = superGetState(options); if (result) { result.arrays = model.arrays.map((item) => ({ - data: item.data.getState(), + data: item.data.getState(options), })); } return result; diff --git a/Sources/interfaces.d.ts b/Sources/interfaces.d.ts index a5d3178b84d..91bcd1e9293 100644 --- a/Sources/interfaces.d.ts +++ b/Sources/interfaces.d.ts @@ -2,6 +2,18 @@ import vtkDataArray from './Common/Core/DataArray'; import { vtkPipelineConnection } from './types'; import { EVENT_ABORT, VOID } from './macros'; +export interface GetStateOptions { + /** + * When true, TypedArrays are preserved without converting and + * copying to plain Arrays. Use for structured clone / postMessage. + * + * Note: the resulting state is not JSON-safe. TypedArrays serialize + * as `{"0":v,"1":v,...}` via `JSON.stringify`, not as arrays. + * @default false + */ + preserveTypedArrays?: boolean; +} + /** * Object returned on any subscription call */ @@ -241,15 +253,20 @@ export interface vtkObject { * Such state can then be reused to clone or rebuild a full * vtkObject tree using the root vtk() function. * - * The following example will grab mapper and dataset that are - * beneath the vtkActor instance as well. - * * ``` * const actorStr = JSON.stringify(actor.getState()); * const newActor = vtk(JSON.parse(actorStr)); * ``` + * + * Pass `{ preserveTypedArrays: true }` to keep TypedArrays + * without converting and copying to plain Arrays. Useful for + * structured clone / postMessage transfers. + * + * ``` + * worker.postMessage(dataset.getState({ preserveTypedArrays: true })); + * ``` */ - getState(): object; + getState(options?: GetStateOptions): object; /** * Used internally by JSON.stringify to get the content to serialize. diff --git a/Sources/macros.js b/Sources/macros.js index 10d1ff0c629..4c2852aae30 100644 --- a/Sources/macros.js +++ b/Sources/macros.js @@ -368,10 +368,11 @@ export function obj(publicAPI = {}, model = {}) { }; // Add serialization support - publicAPI.getState = () => { + publicAPI.getState = ({ preserveTypedArrays = false } = {}) => { if (model.deleted) { return null; } + const options = { preserveTypedArrays }; const jsonArchive = { ...model, vtkClass: publicAPI.getClassName() }; // Convert every vtkObject to its serializable form @@ -383,11 +384,16 @@ export function obj(publicAPI = {}, model = {}) { ) { delete jsonArchive[keyName]; } else if (jsonArchive[keyName].isA) { - jsonArchive[keyName] = jsonArchive[keyName].getState(); + jsonArchive[keyName] = jsonArchive[keyName].getState(options); } else if (Array.isArray(jsonArchive[keyName])) { - jsonArchive[keyName] = jsonArchive[keyName].map(getStateArrayMapFunc); + jsonArchive[keyName] = jsonArchive[keyName].map((item) => + item && item.isA ? item.getState(options) : item + ); } else if (isTypedArray(jsonArchive[keyName])) { - jsonArchive[keyName] = Array.from(jsonArchive[keyName]); + if (!preserveTypedArrays) { + jsonArchive[keyName] = Array.from(jsonArchive[keyName]); + } + // else: keep TypedArray as-is for structured clone / postMessage } });