@niivue/niivue 0.67.1-dev.0 → 0.67.1-dev.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/drawing/index.d.ts +1 -1
- package/build/drawing/index.js.map +1 -1
- package/build/index.min.js +1 -1
- package/build/metafile-esm.json +1 -1
- package/build/niivue/index.d.ts +3 -2
- package/build/niivue/index.js +171 -26
- package/build/niivue/index.js.map +1 -1
- package/build/{nvdocument-CcP6istx.d.ts → nvdocument-DrX6RoEG.d.ts} +32 -2
- package/build/utils/index.d.ts +1 -1
- package/dist/index.d.ts +33 -2
- package/dist/index.js +171 -26
- package/dist/index.js.map +1 -1
- package/dist/index.min.js +1 -1
- package/dist/metafile-esm.json +1 -1
- package/dist/niivue.umd.js +31 -31
- package/package.json +108 -108
- package/src/index_zarr_viewer.html +255 -68
- package/src/niivue/descriptives.ts +1 -1
- package/src/niivue/index.ts +40 -3
- package/src/nvimage/index.ts +11 -3
- package/src/nvimage/utils.ts +2 -0
- package/src/nvimage/zarr/NVZarrHelper.ts +174 -25
- package/src/index_zarr.html +0 -152
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/logger.ts","../../src/drawing/rle.ts","../../src/drawing/undo.ts","../../src/nvdocument.ts","../../src/nvserializer.ts","../../src/nvutilities.ts","../../src/nvimage/index.ts","../../src/nvimage/utils.ts","../../src/nvimage/ImageWriter.ts","../../src/nvimage/VolumeUtils.ts","../../src/nvimage/ImageReaders/afni.ts","../../src/nvimage/ImageReaders/brainvoyager.ts","../../src/nvimage/ImageReaders/dsistudio.ts","../../src/nvimage/ImageReaders/ecat.ts","../../src/nvimage/ImageReaders/image.ts","../../src/nvimage/ImageReaders/itk.ts","../../src/nvimage/ImageReaders/mgh.ts","../../src/nvimage/ImageReaders/mrtrix.ts","../../src/nvimage/ImageReaders/nii.ts","../../src/nvimage/ImageReaders/nrrd.ts","../../src/nvimage/ImageReaders/numpy.ts","../../src/nvimage/ImageReaders/zarr.ts","../../src/nvimage/CoordinateTransform.ts","../../src/nvimage/ImageOrientation.ts","../../src/cmaps/index.ts","../../src/cmaps/_itksnap.json","../../src/cmaps/_slicer3d.json","../../src/cmaps/actc.json","../../src/cmaps/batlow.json","../../src/cmaps/bcgwhw_dark.json","../../src/cmaps/bcgwhw.json","../../src/cmaps/blue2red.json","../../src/cmaps/blue2magenta.json","../../src/cmaps/blue2cyan.json","../../src/cmaps/bluegrn.json","../../src/cmaps/blue.json","../../src/cmaps/bone.json","../../src/cmaps/bronze.json","../../src/cmaps/cet_l17.json","../../src/cmaps/cividis.json","../../src/cmaps/cool.json","../../src/cmaps/copper2.json","../../src/cmaps/copper.json","../../src/cmaps/ct_airways.json","../../src/cmaps/ct_artery.json","../../src/cmaps/ct_bones.json","../../src/cmaps/ct_brain_gray.json","../../src/cmaps/ct_brain.json","../../src/cmaps/ct_cardiac.json","../../src/cmaps/ct_head.json","../../src/cmaps/ct_kidneys.json","../../src/cmaps/ct_liver.json","../../src/cmaps/ct_muscles.json","../../src/cmaps/ct_scalp.json","../../src/cmaps/ct_skull.json","../../src/cmaps/ct_soft.json","../../src/cmaps/ct_soft_tissue.json","../../src/cmaps/ct_surface.json","../../src/cmaps/ct_vessels.json","../../src/cmaps/ct_w_contrast.json","../../src/cmaps/cubehelix.json","../../src/cmaps/electric_blue.json","../../src/cmaps/freesurfer.json","../../src/cmaps/ge_color.json","../../src/cmaps/gold.json","../../src/cmaps/gray.json","../../src/cmaps/green.json","../../src/cmaps/green2cyan.json","../../src/cmaps/green2orange.json","../../src/cmaps/hotiron.json","../../src/cmaps/hot.json","../../src/cmaps/hsv.json","../../src/cmaps/inferno.json","../../src/cmaps/jet.json","../../src/cmaps/kry.json","../../src/cmaps/linspecer.json","../../src/cmaps/lipari.json","../../src/cmaps/magma.json","../../src/cmaps/mako.json","../../src/cmaps/navia.json","../../src/cmaps/nih.json","../../src/cmaps/plasma.json","../../src/cmaps/random.json","../../src/cmaps/red.json","../../src/cmaps/redyell.json","../../src/cmaps/rocket.json","../../src/cmaps/surface.json","../../src/cmaps/thermal.json","../../src/cmaps/turbo.json","../../src/cmaps/violet.json","../../src/cmaps/viridis.json","../../src/cmaps/warm.json","../../src/cmaps/winter.json","../../src/cmaps/x_rain.json","../../src/cmaps/afni_blues_inv.json","../../src/cmaps/afni_reds_inv.json","../../src/cmaps/roi_i256.json","../../src/colortables.ts","../../src/nvimage/ImageFactory.ts","../../src/nvimage/ZarrProcessor.ts","../../src/nvimage/zarr/NVZarrHelper.ts","../../src/nvimage/affineUtils.ts","../../src/nvimage/zarr/ZarrChunkClient.ts","../../src/nvmesh.ts","../../src/niivue-object3D.ts","../../src/nvmesh-utilities.ts","../../src/nvmesh-loaders.ts","../../src/utils/legacy-migrate.ts","../../src/drawing/masks.ts"],"sourcesContent":["interface LogLevelMap {\n [key: string]: number\n}\nclass Log {\n level: string\n name: string\n constructor({ name = 'niivue', level = 'info' } = {}) {\n this.name = `${name}`\n this.level = level\n }\n\n // map 'debug' 'info' 'warn' 'error' 'fatal' 'silent' to numbers\n // for comparison\n static levels: LogLevelMap = {\n debug: 0,\n info: 1,\n warn: 2,\n error: 3,\n fatal: 4,\n silent: Infinity\n }\n\n debug(...args: unknown[]): void {\n if (Log.levels[this.level] > Log.levels.debug) {\n return\n }\n // eslint-disable-next-line\n console.debug(`${this.name}-debug`, ...args)\n }\n\n info(...args: unknown[]): void {\n if (Log.levels[this.level] > Log.levels.info) {\n return\n }\n // eslint-disable-next-line\n console.info(`${this.name}-info`, ...args)\n }\n\n warn(...args: unknown[]): void {\n if (Log.levels[this.level] > Log.levels.warn) {\n return\n }\n // eslint-disable-next-line\n console.warn(`${this.name}-warn`, ...args)\n }\n\n error(...args: unknown[]): void {\n if (Log.levels[this.level] > Log.levels.error) {\n return\n }\n // eslint-disable-next-line\n console.error(`${this.name}-error`, ...args)\n }\n\n fatal(...args: unknown[]): void {\n if (Log.levels[this.level] > Log.levels.fatal) {\n return\n }\n // eslint-disable-next-line\n console.error(`${this.name}-fatal`, ...args)\n }\n\n setLogLevel(level: string): void {\n this.level = level\n }\n\n setName(name: string): void {\n this.name = name\n }\n}\n\n// make a log instance and export it\nconst log = new Log({ name: 'niivue', level: 'info' })\nexport { log }\n","import { log } from '@/logger'\n// Internal function to compress drawing using run length encoding\n// inputs\n// data: Uint8Array to compress\n// output\n// returns rle compressed Uint8Array\nexport function encodeRLE(data: Uint8Array): Uint8Array {\n // https://en.wikipedia.org/wiki/PackBits\n // run length encoding\n // input and output are Uint8Array\n // Will compress data with long runs up to x64\n // Worst case encoded size is ~1% larger than input\n const dl = data.length // input length\n let dp = 0 // input position\n // worst case: run length encoding (1+1/127) times larger than input\n const r = new Uint8Array(dl + Math.ceil(0.01 * dl))\n const rI = new Int8Array(r.buffer) // typecast as header can be negative\n let rp = 0 // run length position\n while (dp < dl) {\n // for each byte in input\n let v = data[dp]\n dp++\n let rl = 1 // run length\n while (rl < 129 && dp < dl && data[dp] === v) {\n dp++\n rl++\n }\n if (rl > 1) {\n // header\n rI[rp] = -rl + 1\n rp++\n r[rp] = v\n rp++\n continue\n }\n // count literal length\n while (dp < dl) {\n if (rl > 127) {\n break\n }\n if (dp + 2 < dl) {\n if (v !== data[dp] && data[dp + 2] === data[dp] && data[dp + 1] === data[dp]) {\n break\n }\n }\n v = data[dp]\n dp++\n rl++\n }\n // write header\n r[rp] = rl - 1\n rp++\n for (let i = 0; i < rl; i++) {\n r[rp] = data[dp - rl + i]\n rp++\n }\n }\n log.debug('PackBits ' + dl + ' -> ' + rp + ' bytes (x' + dl / rp + ')')\n return r.slice(0, rp)\n}\n\n// Internal function to decompress drawing using run length encoding\n// inputs\n// rle: packbits compressed stream\n// decodedlen: size of uncompressed data\n// output\n// returns Uint8Array of decodedlen bytes\nexport function decodeRLE(rle: Uint8Array, decodedlen: number): Uint8Array {\n const r = new Uint8Array(rle.buffer)\n const rI = new Int8Array(r.buffer) // typecast as header can be negative\n let rp = 0 // input position in rle array\n // d: output uncompressed data array\n const d = new Uint8Array(decodedlen)\n let dp = 0 // output position in decoded array\n while (rp < r.length) {\n // read header\n const hdr = rI[rp]\n rp++\n if (hdr < 0) {\n // write run\n const v = rI[rp]\n rp++\n for (let i = 0; i < 1 - hdr; i++) {\n d[dp] = v\n dp++\n }\n } else {\n // write literal\n for (let i = 0; i < hdr + 1; i++) {\n d[dp] = rI[rp]\n rp++\n dp++\n }\n }\n }\n return d\n}\n","import { log } from '@/logger'\nimport { decodeRLE } from '@/drawing/rle'\n\ninterface DrawUndoArgs {\n drawUndoBitmaps: Uint8Array[]\n currentDrawUndoBitmap: number\n drawBitmap: Uint8Array\n}\nexport const drawUndo = ({ drawUndoBitmaps, currentDrawUndoBitmap, drawBitmap }: DrawUndoArgs): { drawBitmap: Uint8Array; currentDrawUndoBitmap: number } | undefined => {\n const len = drawUndoBitmaps.length\n if (len < 1) {\n log.debug('undo bitmaps not loaded')\n return\n }\n currentDrawUndoBitmap--\n if (currentDrawUndoBitmap < 0) {\n currentDrawUndoBitmap = len - 1\n }\n if (currentDrawUndoBitmap >= len) {\n currentDrawUndoBitmap = 0\n }\n if (drawUndoBitmaps[currentDrawUndoBitmap].length < 2) {\n log.debug('drawUndo is misbehaving')\n return\n }\n drawBitmap = decodeRLE(drawUndoBitmaps[currentDrawUndoBitmap], drawBitmap.length)\n return { drawBitmap, currentDrawUndoBitmap }\n}\n","// nvdocument.ts (updated)\n// --- imports\nimport { vec3, vec4 } from 'gl-matrix'\nimport { NVSerializer } from '@/nvserializer' // adjust path if needed\n\nimport { NVUtilities } from '@/nvutilities'\nimport { ImageFromUrlOptions, NVIMAGE_TYPE, NVImage } from '@/nvimage'\nimport { NVMesh } from '@/nvmesh'\nimport { NVLabel3D } from '@/nvlabel'\nimport { NVConnectome } from '@/nvconnectome'\n\n/**\n * Represents a completed measurement between two points\n */\nexport interface CompletedMeasurement {\n startMM: vec3 // World coordinates in mm for start point\n endMM: vec3 // World coordinates in mm for end point\n distance: number // Distance between points in mm\n sliceIndex: number\n sliceType: SLICE_TYPE\n slicePosition: number\n}\n\n/**\n * Represents a completed angle measurement between two lines\n */\nexport interface CompletedAngle {\n firstLineMM: { start: vec3; end: vec3 } // World coordinates in mm for first line\n secondLineMM: { start: vec3; end: vec3 } // World coordinates in mm for second line\n angle: number // Angle in degrees\n sliceIndex: number\n sliceType: SLICE_TYPE\n slicePosition: number\n}\n\n/**\n * Slice Type\n * @ignore\n */\nexport enum SLICE_TYPE {\n AXIAL = 0,\n CORONAL = 1,\n SAGITTAL = 2,\n MULTIPLANAR = 3,\n RENDER = 4\n}\n\nexport enum PEN_TYPE {\n PEN = 0,\n RECTANGLE = 1,\n ELLIPSE = 2\n}\n\nexport enum SHOW_RENDER {\n NEVER = 0,\n ALWAYS = 1,\n AUTO = 2\n}\n\n/**\n * Multi-planar layout\n * @ignore\n */\nexport enum MULTIPLANAR_TYPE {\n AUTO = 0,\n COLUMN = 1,\n GRID = 2,\n ROW = 3\n}\n\n/**\n * Drag mode\n * @ignore\n */\nexport enum DRAG_MODE {\n none = 0,\n contrast = 1,\n measurement = 2,\n pan = 3,\n slicer3D = 4,\n callbackOnly = 5,\n roiSelection = 6,\n angle = 7,\n crosshair = 8,\n windowing = 9\n}\n\nexport interface MouseEventConfig {\n leftButton: {\n primary: DRAG_MODE\n withShift?: DRAG_MODE\n withCtrl?: DRAG_MODE\n }\n rightButton: DRAG_MODE\n centerButton: DRAG_MODE\n}\n\nexport interface TouchEventConfig {\n singleTouch: DRAG_MODE\n doubleTouch: DRAG_MODE\n}\n\n/**\n * NVConfigOptions\n */\nexport type NVConfigOptions = {\n // ... (kept unchanged for brevity — same as your original file)\n textHeight: number\n fontSizeScaling: number\n fontMinPx: number\n colorbarHeight: number\n colorbarWidth: number\n showColorbarBorder: boolean\n crosshairWidth: number\n crosshairWidthUnit: 'voxels' | 'mm' | 'percent'\n crosshairGap: number\n rulerWidth: number\n show3Dcrosshair: boolean\n backColor: number[]\n crosshairColor: number[]\n fontColor: Float32List\n selectionBoxColor: number[]\n clipPlaneColor: number[]\n isClipPlanesCutaway: boolean\n paqdUniforms: number[]\n rulerColor: number[]\n colorbarMargin: number\n trustCalMinMax: boolean\n clipPlaneHotKey: string\n cycleClipPlaneHotKey: string\n viewModeHotKey: string\n doubleTouchTimeout: number\n longTouchTimeout: number\n keyDebounceTime: number\n isNearestInterpolation: boolean\n atlasOutline: number\n atlasActiveIndex: number\n isRuler: boolean\n isColorbar: boolean\n isOrientCube: boolean\n tileMargin: number\n multiplanarPadPixels: number\n multiplanarForceRender: boolean\n multiplanarEqualSize: boolean\n multiplanarShowRender: SHOW_RENDER\n isRadiologicalConvention: boolean\n meshThicknessOn2D: number | string\n dragMode: DRAG_MODE\n dragModePrimary: DRAG_MODE\n mouseEventConfig?: MouseEventConfig\n touchEventConfig?: TouchEventConfig\n yoke3Dto2DZoom: boolean\n isDepthPickMesh: boolean\n isCornerOrientationText: boolean\n isOrientationTextVisible: boolean\n showAllOrientationMarkers: boolean\n heroImageFraction: number\n heroSliceType: SLICE_TYPE\n sagittalNoseLeft: boolean\n isSliceMM: boolean\n isV1SliceShader: boolean\n forceDevicePixelRatio: number\n logLevel: 'debug' | 'info' | 'warn' | 'error' | 'fatal' | 'silent'\n loadingText: string\n isForceMouseClickToVoxelCenters: boolean\n dragAndDropEnabled: boolean\n drawingEnabled: boolean\n penValue: number\n penType: PEN_TYPE\n floodFillNeighbors: number\n isFilledPen: boolean\n thumbnail: string\n maxDrawUndoBitmaps: number\n sliceType: SLICE_TYPE\n isAntiAlias: boolean | null\n isAdditiveBlend: boolean\n isResizeCanvas: boolean\n meshXRay: number\n limitFrames4D: number\n showLegend: boolean\n legendBackgroundColor: number[]\n legendTextColor: number[]\n multiplanarLayout: MULTIPLANAR_TYPE\n renderOverlayBlend: number\n sliceMosaicString: string\n centerMosaic: boolean\n interactive: boolean\n penSize: number\n clickToSegment: boolean\n clickToSegmentRadius: number\n clickToSegmentBright: boolean\n clickToSegmentAutoIntensity: boolean\n clickToSegmentIntensityMax: number\n clickToSegmentIntensityMin: number\n clickToSegmentPercent: number\n clickToSegmentMaxDistanceMM: number\n clickToSegmentIs2D: boolean\n selectionBoxLineThickness: number\n selectionBoxIsOutline: boolean\n scrollRequiresFocus: boolean\n showMeasureUnits: boolean\n measureTextJustify: 'start' | 'center' | 'end'\n measureTextColor: number[]\n measureLineColor: number[]\n measureTextHeight: number\n isAlphaClipDark: boolean\n gradientOrder: number\n gradientOpacity: number\n renderSilhouette: number\n gradientAmount: number\n invertScrollDirection: boolean\n is2DSliceShader: boolean\n bounds: [[number, number], [number, number]] | null\n showBoundsBorder?: boolean\n boundsBorderColor?: number[] // [r,g,b,a]\n // Zarr options\n /** Chunk cache size for zarr viewing (default 500) */\n zarrCacheSize: number\n /** Number of chunk rings to prefetch around the visible region for zarr viewing (0 disables, default 1) */\n zarrPrefetchRings: number\n}\n\nexport const DEFAULT_OPTIONS: NVConfigOptions = {\n // ... (same defaults as your original file)\n textHeight: -1.0,\n fontSizeScaling: 0.4,\n fontMinPx: 13,\n colorbarHeight: 0.05,\n colorbarWidth: -1,\n showColorbarBorder: true,\n crosshairWidth: 1,\n crosshairWidthUnit: 'voxels',\n crosshairGap: 0,\n rulerWidth: 4,\n show3Dcrosshair: false,\n backColor: [0, 0, 0, 1],\n crosshairColor: [1, 0, 0, 1],\n fontColor: [0.5, 0.5, 0.5, 1],\n selectionBoxColor: [1, 1, 1, 0.5],\n clipPlaneColor: [0.7, 0, 0.7, 0.5],\n isClipPlanesCutaway: false,\n paqdUniforms: [0.3, 0.5, 0.5, 1.0],\n rulerColor: [1, 0, 0, 0.8],\n colorbarMargin: 0.05,\n trustCalMinMax: true,\n clipPlaneHotKey: 'KeyC',\n cycleClipPlaneHotKey: 'KeyP',\n viewModeHotKey: 'KeyV',\n doubleTouchTimeout: 500,\n longTouchTimeout: 1000,\n keyDebounceTime: 50,\n isNearestInterpolation: false,\n isResizeCanvas: true,\n atlasOutline: 0,\n atlasActiveIndex: 0,\n isRuler: false,\n isColorbar: false,\n isOrientCube: false,\n tileMargin: 0,\n multiplanarPadPixels: 0,\n multiplanarForceRender: false,\n multiplanarEqualSize: false,\n multiplanarShowRender: SHOW_RENDER.AUTO,\n isRadiologicalConvention: false,\n meshThicknessOn2D: Infinity,\n dragMode: DRAG_MODE.contrast,\n dragModePrimary: DRAG_MODE.crosshair,\n mouseEventConfig: undefined,\n touchEventConfig: undefined,\n yoke3Dto2DZoom: false,\n isDepthPickMesh: false,\n isCornerOrientationText: false,\n isOrientationTextVisible: true,\n showAllOrientationMarkers: false,\n heroImageFraction: 0,\n heroSliceType: SLICE_TYPE.RENDER,\n sagittalNoseLeft: false,\n isSliceMM: false,\n isV1SliceShader: false,\n forceDevicePixelRatio: 0,\n logLevel: 'info',\n loadingText: 'loading ...',\n isForceMouseClickToVoxelCenters: false,\n dragAndDropEnabled: true,\n drawingEnabled: false,\n penValue: 1,\n penType: PEN_TYPE.PEN,\n floodFillNeighbors: 6,\n isFilledPen: false,\n thumbnail: '',\n maxDrawUndoBitmaps: 8,\n sliceType: SLICE_TYPE.MULTIPLANAR,\n meshXRay: 0.0,\n isAntiAlias: null,\n limitFrames4D: NaN,\n isAdditiveBlend: false,\n showLegend: true,\n legendBackgroundColor: [0.3, 0.3, 0.3, 0.5],\n legendTextColor: [1.0, 1.0, 1.0, 1.0],\n multiplanarLayout: MULTIPLANAR_TYPE.AUTO,\n renderOverlayBlend: 1.0,\n sliceMosaicString: '',\n centerMosaic: false,\n penSize: 1,\n interactive: true,\n clickToSegment: false,\n clickToSegmentRadius: 3,\n clickToSegmentBright: true,\n clickToSegmentAutoIntensity: false,\n clickToSegmentIntensityMax: NaN,\n clickToSegmentIntensityMin: NaN,\n clickToSegmentPercent: 0,\n clickToSegmentMaxDistanceMM: Number.POSITIVE_INFINITY,\n clickToSegmentIs2D: false,\n selectionBoxLineThickness: 4,\n selectionBoxIsOutline: false,\n scrollRequiresFocus: false,\n showMeasureUnits: true,\n measureTextJustify: 'center',\n measureTextColor: [1, 0, 0, 1],\n measureLineColor: [1, 0, 0, 1],\n measureTextHeight: 0.06,\n isAlphaClipDark: false,\n gradientOrder: 1,\n gradientOpacity: 0.0,\n renderSilhouette: 0.0,\n gradientAmount: 0.0,\n invertScrollDirection: false,\n is2DSliceShader: false,\n bounds: null,\n showBoundsBorder: false,\n boundsBorderColor: [1, 1, 1, 1], // white border by default\n // Zarr options\n zarrCacheSize: 1000,\n zarrPrefetchRings: 10\n}\n\n//\n// -- NEW: Recursive encoded type for NVConfigOptions JSON-safe form\n//\ntype EncodeNumbersIn<T> = T extends number ? number | string : T extends Array<infer U> ? Array<EncodeNumbersIn<U>> : T extends object ? { [K in keyof T]: EncodeNumbersIn<T[K]> } : T\n\ntype EncodedNVConfigOptions = EncodeNumbersIn<NVConfigOptions>\n\n//\n// Utility encode/decode helpers\n//\n\nexport const DEFAULT_SCENE_DATA = {} // placeholder if needed elsewhere (kept for completeness)\n\ntype SceneData = {\n gamma: number\n azimuth: number\n elevation: number\n crosshairPos: vec3\n clipPlanes: number[][]\n clipPlaneDepthAziElevs: number[][]\n volScaleMultiplier: number\n pan2Dxyzmm: vec4\n}\n\nexport const INITIAL_SCENE_DATA = {\n gamma: 1.0,\n azimuth: 110,\n elevation: 10,\n crosshairPos: vec3.fromValues(0.5, 0.5, 0.5),\n clipPlanes: [[0, 0, 0, 0]],\n clipPlaneDepthAziElevs: [[2, 0, 0]],\n volScaleMultiplier: 1.0,\n pan2Dxyzmm: vec4.fromValues(0, 0, 0, 1)\n}\n\nexport type Scene = {\n onAzimuthElevationChange: (azimuth: number, elevation: number) => void\n onZoom3DChange: (scale: number) => void\n sceneData: SceneData\n renderAzimuth: number\n renderElevation: number\n volScaleMultiplier: number\n crosshairPos: vec3\n clipPlane: number[]\n clipPlanes: number[][]\n clipPlaneDepthAziElevs: number[][]\n pan2Dxyzmm: vec4\n _elevation?: number\n _azimuth?: number\n gamma?: number\n}\n\n/**\n * DocumentData / ExportDocumentData types (kept minimal here)\n */\nexport type DocumentData = {\n title?: string\n imageOptionsArray?: ImageFromUrlOptions[]\n meshOptionsArray?: unknown[]\n opts?: Partial<EncodedNVConfigOptions> | Partial<NVConfigOptions>\n previewImageDataURL?: string\n labels?: NVLabel3D[]\n encodedImageBlobs?: string[]\n encodedDrawingBlob?: string\n meshesString?: string\n sceneData?: Partial<SceneData>\n connectomes?: string[]\n customData?: string\n completedMeasurements?: CompletedMeasurement[]\n completedAngles?: CompletedAngle[]\n}\n\nexport type ExportDocumentData = {\n title?: string\n encodedImageBlobs: string[]\n encodedDrawingBlob: string\n previewImageDataURL: string\n imageOptionsMap: Map<string, number>\n imageOptionsArray: ImageFromUrlOptions[]\n sceneData: Partial<SceneData>\n opts: EncodedNVConfigOptions | Partial<EncodedNVConfigOptions>\n meshesString: string\n meshOptionsArray?: unknown[]\n labels: NVLabel3D[]\n connectomes: string[]\n customData: string\n completedMeasurements: CompletedMeasurement[]\n completedAngles: CompletedAngle[]\n}\n\n/**\n * Returns a partial configuration object containing only the fields in the provided\n * options that differ from the DEFAULT_OPTIONS.\n */\n// function diffOptions(opts: NVConfigOptions, defaults: NVConfigOptions): Partial<NVConfigOptions> {\n// const diff: Partial<NVConfigOptions> = {}\n// for (const key in opts) {\n// const value = opts[key]\n// const def = defaults[key]\n// const isArray = Array.isArray(value) && Array.isArray(def)\n\n// if ((isArray && value.some((v, i) => v !== def[i])) || (!isArray && value !== def)) {\n// diff[key] = value\n// }\n// }\n// return diff\n// }\n\n/**\n * NVDocument class (main)\n */\nexport class NVDocument {\n data: DocumentData = {\n title: 'Untitled document',\n imageOptionsArray: [],\n meshOptionsArray: [],\n opts: { ...DEFAULT_OPTIONS } as any,\n previewImageDataURL: '',\n labels: [],\n encodedImageBlobs: [],\n encodedDrawingBlob: ''\n }\n\n scene: Scene\n\n volumes: NVImage[] = []\n meshDataObjects?: Array<NVMesh | NVConnectome>\n meshes: Array<NVMesh | NVConnectome> = []\n drawBitmap: Uint8Array | null = null\n imageOptionsMap = new Map()\n meshOptionsMap = new Map()\n completedMeasurements: CompletedMeasurement[] = []\n completedAngles: CompletedAngle[] = []\n\n private _optsProxy: NVConfigOptions | null = null\n private _optsChangeCallback: ((propertyName: keyof NVConfigOptions, newValue: NVConfigOptions[keyof NVConfigOptions], oldValue: NVConfigOptions[keyof NVConfigOptions]) => void) | null = null\n\n constructor() {\n this.scene = {\n onAzimuthElevationChange: (): void => {},\n onZoom3DChange: (): void => {},\n sceneData: {\n ...INITIAL_SCENE_DATA,\n pan2Dxyzmm: vec4.fromValues(0, 0, 0, 1),\n crosshairPos: vec3.fromValues(0.5, 0.5, 0.5)\n },\n\n get renderAzimuth(): number {\n return this.sceneData.azimuth\n },\n set renderAzimuth(azimuth: number) {\n this.sceneData.azimuth = azimuth\n if (this.onAzimuthElevationChange) {\n this.onAzimuthElevationChange(this.sceneData.azimuth, this.sceneData.elevation)\n }\n },\n\n get renderElevation(): number {\n return this.sceneData.elevation\n },\n set renderElevation(elevation: number) {\n this.sceneData.elevation = elevation\n if (this.onAzimuthElevationChange) {\n this.onAzimuthElevationChange(this.sceneData.azimuth, this.sceneData.elevation)\n }\n },\n\n get volScaleMultiplier(): number {\n return this.sceneData.volScaleMultiplier\n },\n set volScaleMultiplier(scale: number) {\n this.sceneData.volScaleMultiplier = scale\n this.onZoom3DChange(scale)\n },\n\n get crosshairPos(): vec3 {\n return this.sceneData.crosshairPos\n },\n set crosshairPos(crosshairPos: vec3) {\n this.sceneData.crosshairPos = crosshairPos\n },\n\n get clipPlane(): number[] {\n return this.sceneData.clipPlanes[0] ?? []\n },\n set clipPlane(clipPlane) {\n this.sceneData.clipPlanes[0] = clipPlane\n },\n\n get clipPlanes(): number[][] {\n return this.sceneData.clipPlanes\n },\n set clipPlanes(planes: number[][]) {\n this.sceneData.clipPlanes = planes\n },\n\n get clipPlaneDepthAziElevs(): number[][] {\n return this.sceneData.clipPlaneDepthAziElevs\n },\n set clipPlaneDepthAziElevs(values: number[][]) {\n this.sceneData.clipPlaneDepthAziElevs = values\n },\n\n get pan2Dxyzmm(): vec4 {\n return this.sceneData.pan2Dxyzmm\n },\n\n set pan2Dxyzmm(pan2Dxyzmm) {\n this.sceneData.pan2Dxyzmm = pan2Dxyzmm\n },\n\n get gamma(): number {\n return this.sceneData.gamma\n },\n\n set gamma(newGamma) {\n this.sceneData.gamma = newGamma\n }\n }\n }\n\n /**\n * Title of the document\n */\n get title(): string {\n return this.data.title\n }\n\n /**\n * Gets preview image blob\n * @returns dataURL of preview image\n */\n get previewImageDataURL(): string {\n return this.data.previewImageDataURL\n }\n\n /**\n * Sets preview image blob\n * @param dataURL - encoded preview image\n */\n set previewImageDataURL(dataURL: string) {\n this.data.previewImageDataURL = dataURL\n }\n\n /**\n * @param title - title of document\n */\n set title(title: string) {\n this.data.title = title\n }\n\n get imageOptionsArray(): ImageFromUrlOptions[] {\n return this.data.imageOptionsArray\n }\n\n /**\n * Gets the base 64 encoded blobs of associated images\n */\n get encodedImageBlobs(): string[] {\n return this.data.encodedImageBlobs\n }\n\n /**\n * Gets the base 64 encoded blob of the associated drawing\n */\n get encodedDrawingBlob(): string {\n return this.data.encodedDrawingBlob\n }\n\n /**\n * Gets the options of the {@link Niivue} instance\n */\n get opts(): NVConfigOptions {\n if (!this._optsProxy) {\n this._createOptsProxy()\n }\n return this._optsProxy as NVConfigOptions\n }\n\n /**\n * Sets the options of the {@link Niivue} instance\n */\n set opts(opts) {\n this.data.opts = { ...opts } as any\n this._optsProxy = null // Force recreation of proxy\n }\n\n /**\n * Gets the 3D labels of the {@link Niivue} instance\n */\n get labels(): NVLabel3D[] {\n return this.data.labels\n }\n\n /**\n * Sets the 3D labels of the {@link Niivue} instance\n */\n set labels(labels: NVLabel3D[]) {\n this.data.labels = labels\n }\n\n get customData(): string | undefined {\n return this.data.customData\n }\n\n set customData(data: string) {\n this.data.customData = data\n }\n\n /**\n * Checks if document has an image by id\n */\n hasImage(image: NVImage): boolean {\n return this.volumes.find((i) => i.id === image.id) !== undefined\n }\n\n /**\n * Checks if document has an image by url\n */\n hasImageFromUrl(url: string): boolean {\n return this.data.imageOptionsArray.find((i) => i.url === url) !== undefined\n }\n\n /**\n * Adds an image and the options an image was created with\n */\n addImageOptions(image: NVImage, imageOptions: ImageFromUrlOptions): void {\n if (!this.hasImage(image)) {\n if (!imageOptions.name) {\n if (imageOptions.url) {\n const absoluteUrlRE = /^(?:[a-z+]+:)?\\/\\//i\n const url = absoluteUrlRE.test(imageOptions.url) ? new URL(imageOptions.url) : new URL(imageOptions.url, window.location.href)\n\n imageOptions.name = url.pathname.split('/').pop()! // TODO guaranteed?\n if (imageOptions.name.toLowerCase().endsWith('.gz')) {\n imageOptions.name = imageOptions.name.slice(0, -3)\n }\n\n if (!imageOptions.name.toLowerCase().endsWith('.nii')) {\n imageOptions.name += '.nii'\n }\n } else {\n imageOptions.name = 'untitled.nii'\n }\n }\n }\n\n imageOptions.imageType = NVIMAGE_TYPE.NII\n\n this.data.imageOptionsArray.push(imageOptions)\n this.imageOptionsMap.set(image.id, this.data.imageOptionsArray.length - 1)\n }\n\n /**\n * Removes image from the document as well as its options\n */\n removeImage(image: NVImage): void {\n if (this.imageOptionsMap.has(image.id)) {\n const index = this.imageOptionsMap.get(image.id)\n if (this.data.imageOptionsArray.length > index) {\n this.data.imageOptionsArray.splice(index, 1)\n }\n this.imageOptionsMap.delete(image.id)\n }\n this.volumes = this.volumes.filter((i) => i.id !== image.id)\n }\n\n /**\n * Fetch any image data that is missing from this document.\n */\n async fetchLinkedData(): Promise<void> {\n this.data.encodedImageBlobs = []\n if (!this.imageOptionsArray?.length) {\n return\n }\n\n for (const imgOpt of this.imageOptionsArray) {\n if (!imgOpt.url) {\n continue\n }\n\n try {\n const response = await fetch(imgOpt.url)\n if (!response.ok) {\n console.warn('Failed to fetch image:', imgOpt.url)\n continue\n }\n\n const buffer = await response.arrayBuffer()\n const uint8Array = new Uint8Array(buffer)\n const b64 = NVUtilities.uint8tob64(uint8Array)\n this.data.encodedImageBlobs.push(b64)\n\n console.info('fetch linked data fetched from ', imgOpt.url)\n } catch (err) {\n console.warn(`Failed to fetch/encode image from ${imgOpt.url}:`, err)\n }\n }\n }\n\n /**\n * Returns the options for the image if it was added by url\n */\n getImageOptions(image: NVImage): ImageFromUrlOptions | null {\n return this.imageOptionsMap.has(image.id) ? this.data.imageOptionsArray[this.imageOptionsMap.get(image.id)] : null\n }\n\n /**\n * Serialise the document by delegating to NVSerializer.\n */\n json(embedImages = true, embedDrawing = true): ExportDocumentData {\n // NVSerializer is responsible for converting typed arrays, encoding special numbers,\n // producing meshesString, and returning an ExportDocumentData object.\n return NVSerializer.serializeDocument(this, embedImages, embedDrawing)\n }\n\n async download(fileName: string, compress: boolean, opts: { embedImages: boolean } = { embedImages: true }): Promise<void> {\n const data = this.json(opts.embedImages)\n const jsonTxt = JSON.stringify(data)\n const mime = compress ? 'application/gzip' : 'application/json'\n const payload = compress ? await NVUtilities.compressStringToArrayBuffer(jsonTxt) : jsonTxt\n\n NVUtilities.download(payload, fileName, mime)\n }\n\n /**\n * Factory method to return an instance of NVDocument from a URL\n */\n static async loadFromUrl(url: string): Promise<NVDocument> {\n const response = await fetch(url)\n const buffer = await response.arrayBuffer()\n let documentData: DocumentData\n\n if (NVUtilities.isArrayBufferCompressed(buffer)) {\n const documentText = await NVUtilities.decompressArrayBuffer(buffer)\n documentData = JSON.parse(documentText)\n } else {\n const utf8decoder = new TextDecoder()\n documentData = JSON.parse(utf8decoder.decode(buffer))\n }\n\n return NVDocument.loadFromJSON(documentData)\n }\n\n static async loadFromFile(file: Blob): Promise<NVDocument> {\n const arrayBuffer = await NVUtilities.readFileAsync(file)\n let dataString: string\n\n if (NVUtilities.isArrayBufferCompressed(arrayBuffer)) {\n dataString = await NVUtilities.decompressArrayBuffer(arrayBuffer)\n } else {\n const utf8decoder = new TextDecoder()\n dataString = utf8decoder.decode(arrayBuffer)\n }\n\n const documentData = JSON.parse(dataString) as DocumentData\n return NVDocument.loadFromJSON(documentData)\n }\n\n /**\n * Factory method to return an instance of NVDocument from JSON.\n * Delegates the main parsing to NVSerializer, then applies NVDocument-specific\n * post-processing (opts decode, scene defaults, clone measurements/angles).\n */\n static async loadFromJSON(data: DocumentData): Promise<NVDocument> {\n return await NVSerializer.deserializeDocument(data)\n }\n\n /**\n * Sets the callback function to be called when opts properties change\n */\n setOptsChangeCallback(callback: (propertyName: keyof NVConfigOptions, newValue: NVConfigOptions[keyof NVConfigOptions], oldValue: NVConfigOptions[keyof NVConfigOptions]) => void): void {\n this._optsChangeCallback = callback\n this._optsProxy = null // Force recreation with new callback\n }\n\n /**\n * Removes the opts change callback\n */\n removeOptsChangeCallback(): void {\n this._optsChangeCallback = null\n this._optsProxy = null // Force recreation without callback\n }\n\n /**\n * Creates a Proxy wrapper around the opts object to detect changes\n */\n private _createOptsProxy(): void {\n const target = this.data.opts as NVConfigOptions\n\n this._optsProxy = new Proxy(target, {\n set: (obj: any, prop: string | symbol, value: any): boolean => {\n const oldValue = obj[prop]\n\n // Only proceed if the value actually changed\n if (oldValue !== value) {\n obj[prop] = value\n\n // Call the change callback if one is registered\n if (this._optsChangeCallback && typeof prop === 'string' && prop in DEFAULT_OPTIONS) {\n this._optsChangeCallback(prop as keyof NVConfigOptions, value, oldValue)\n }\n }\n\n return true\n },\n\n get: (obj: any, prop: string | symbol): any => {\n return obj[prop]\n }\n })\n }\n}\n","// src/nvserializer.ts\nimport { vec3 } from 'gl-matrix'\nimport { NVUtilities } from '@/nvutilities'\nimport { NVImage, NVIMAGE_TYPE } from '@/nvimage'\nimport { NVMesh, MeshType } from '@/nvmesh'\nimport { NVConnectome } from '@/nvconnectome'\nimport { log } from '@/logger'\nimport { NVDocument, DEFAULT_OPTIONS, DocumentData, ExportDocumentData, INITIAL_SCENE_DATA } from '@/nvdocument'\nimport { migrateLegacyDocument, normalizeMeshForRehydrate } from '@/utils/legacy-migrate' // shared migration utility\n\n/** Helpers for special numeric encoding */\nfunction encodeNumberForJSON(v: number | null | undefined): number | string | undefined {\n if (v === undefined) {\n return undefined\n }\n if (v === null) {\n return null\n }\n if (Number.isFinite(v)) {\n return v\n }\n if (v === Infinity) {\n return 'infinity'\n }\n if (v === -Infinity) {\n return '-infinity'\n }\n return 'NaN'\n}\nfunction decodeNumberFromJSON(v: any): number | null | undefined {\n if (v === undefined) {\n return undefined\n }\n if (v === null) {\n return null\n }\n if (typeof v === 'number') {\n return v\n }\n if (v === 'NaN') {\n return NaN\n }\n if (v === 'infinity') {\n return Infinity\n }\n if (v === '-infinity') {\n return -Infinity\n }\n const n = Number(v)\n return Number.isNaN(n) ? NaN : n\n}\n\n/** Small safe conversion helper that handles arrays, typed arrays, iterables. */\nfunction toPlainArray<T>(maybe: any): T[] | undefined {\n if (maybe === undefined || maybe === null) {\n return undefined\n }\n if (Array.isArray(maybe)) {\n return maybe.slice()\n }\n try {\n // cast via unknown to satisfy TS when given typed arrays / arraybufferviews\n return Array.from(maybe as unknown as Iterable<T>)\n } catch (e) {\n return undefined\n }\n}\n\n/**\n * Convert array-like to Float32Array.\n * Returns undefined if input is null/undefined.\n */\nfunction toFloat32Array(maybe: any): Float32Array | undefined {\n if (maybe === undefined || maybe === null) {\n return undefined\n }\n if (maybe instanceof Float32Array) {\n return maybe\n }\n if (Array.isArray(maybe)) {\n return new Float32Array(maybe)\n }\n if (ArrayBuffer.isView(maybe)) {\n return new Float32Array(Array.from(maybe as unknown as Iterable<number>))\n }\n try {\n return new Float32Array(Array.from(maybe as unknown as Iterable<number>))\n } catch (e) {\n return undefined\n }\n}\n\n/**\n * Convert array-like to Uint32Array.\n * Returns undefined if input is null/undefined.\n */\nfunction toUint32Array(maybe: any): Uint32Array | undefined {\n if (maybe === undefined || maybe === null) {\n return undefined\n }\n if (maybe instanceof Uint32Array) {\n return maybe\n }\n if (Array.isArray(maybe)) {\n return new Uint32Array(maybe)\n }\n if (ArrayBuffer.isView(maybe)) {\n return new Uint32Array(Array.from(maybe as unknown as Iterable<number>))\n }\n try {\n return new Uint32Array(Array.from(maybe as unknown as Iterable<number>))\n } catch (e) {\n return undefined\n }\n}\n\n/**\n * Convert array-like to Uint8Array.\n * Returns undefined if input is null/undefined.\n */\nfunction toUint8Array(maybe: any): Uint8Array | undefined {\n if (maybe === undefined || maybe === null) {\n return undefined\n }\n if (maybe instanceof Uint8Array) {\n return maybe\n }\n if (Array.isArray(maybe)) {\n return new Uint8Array(maybe)\n }\n if (ArrayBuffer.isView(maybe)) {\n return new Uint8Array(Array.from(maybe as unknown as Iterable<number>))\n }\n try {\n return new Uint8Array(Array.from(maybe as unknown as Iterable<number>))\n } catch (e) {\n return undefined\n }\n}\n\n/** replacer to convert typed arrays / array buffers to plain arrays for JSON.stringify */\nfunction jsonReplacer(_key: string, value: any): any {\n if (typeof value === 'function') {\n return undefined\n }\n if (typeof value === 'symbol') {\n return undefined\n }\n\n // Typed arrays & DataView -> array of numbers\n if (ArrayBuffer.isView(value)) {\n if (value instanceof DataView) {\n return Array.from(new Uint8Array((value as DataView).buffer, (value as DataView).byteOffset, (value as DataView).byteLength))\n }\n // cast through unknown to avoid TS complaining about missing iterator\n return Array.from(value as unknown as Iterable<number>)\n }\n\n if (value instanceof ArrayBuffer) {\n return Array.from(new Uint8Array(value))\n }\n\n return value\n}\n\nexport class NVSerializer {\n /**\n * Decode/normalize \"opts\" encoded form into NVConfigOptions shape.\n * Move the decodeOptsFromJSON logic here (serializer owns encoding/decoding).\n */\n static decodeOptsFromJSON(opts: any): any {\n if (opts === undefined || opts === null) {\n return undefined\n }\n\n const decodeRecursive = (v: any): any => {\n if (v === undefined) {\n return undefined\n }\n if (v === null) {\n return null\n }\n if (typeof v === 'number') {\n return v\n }\n if (typeof v === 'string') {\n // try to decode special numeric strings\n if (v === 'NaN') {\n return NaN\n }\n if (v === 'infinity') {\n return Infinity\n }\n if (v === '-infinity') {\n return -Infinity\n }\n return v\n }\n if (Array.isArray(v)) {\n return v.map((el) => decodeRecursive(el))\n }\n if (typeof v === 'object') {\n const out: any = {}\n for (const k of Object.keys(v)) {\n out[k] = decodeRecursive(v[k])\n }\n return out\n }\n return v\n }\n\n const out: any = {}\n for (const k of Object.keys(opts)) {\n const val = opts[k]\n if (val === undefined) {\n continue\n }\n out[k] = decodeRecursive(val)\n }\n return out\n }\n\n /**\n * Serialize an NVDocument-like object into ExportDocumentData (same shape as NVDocument.json)\n * (This function mirrors the responsibilities of NVDocument.json in your code).\n */\n static serializeDocument(document: any, embedImages = true, embedDrawing = true): ExportDocumentData {\n const out: Partial<ExportDocumentData> = {\n encodedImageBlobs: [],\n previewImageDataURL: document.previewImageDataURL ?? '',\n imageOptionsMap: new Map<string, number>()\n }\n\n out.sceneData = { ...(document.scene?.sceneData ?? {}) }\n out.opts = document.opts ?? {}\n out.labels = Array.isArray(document.labels)\n ? document.labels.map((l: any) => {\n const copy = { ...(l || {}) }\n delete copy.onClick\n return copy\n })\n : []\n\n out.customData = document.customData ?? ''\n out.completedMeasurements = document.completedMeasurements ? document.completedMeasurements.slice() : []\n out.completedAngles = document.completedAngles ? document.completedAngles.slice() : []\n\n // images\n const imageOptionsArray: any[] = []\n const volumes = document.volumes ?? []\n if (volumes.length) {\n for (let i = 0; i < volumes.length; i++) {\n const v = volumes[i]\n let imageOptions = document.getImageOptions ? document.getImageOptions(v) : null\n if (imageOptions === null) {\n // fallback minimal options\n imageOptions = {\n name: v?.name ?? '',\n colormap: v?._colormap ?? 'gray',\n opacity: v?._opacity ?? 1.0,\n cal_min: v?.cal_min ?? NaN,\n cal_max: v?.cal_max ?? NaN,\n trustCalMinMax: v?.trustCalMinMax ?? true,\n percentileFrac: v?.percentileFrac ?? 0.02,\n ignoreZeroVoxels: v?.ignoreZeroVoxels ?? false,\n imageType: v?.imageType ?? NVIMAGE_TYPE.NII,\n frame4D: v?.frame4D ?? 0,\n limitFrames4D: v?.limitFrames4D ?? NaN,\n url: v?.url ?? '',\n urlImageData: v?.urlImgData ?? '',\n cal_minNeg: v?.cal_minNeg ?? NaN,\n cal_maxNeg: v?.cal_maxNeg ?? NaN,\n colorbarVisible: v?.colorbarVisible ?? true\n }\n } else {\n if (!('imageType' in imageOptions)) {\n imageOptions.imageType = NVIMAGE_TYPE.NII\n }\n }\n\n imageOptions.colormap = v.colormap\n imageOptions.colormapLabel = v.colormapLabel\n imageOptions.opacity = v.opacity\n imageOptions.cal_max = v.cal_max ?? NaN\n imageOptions.cal_min = v.cal_min ?? NaN\n\n imageOptionsArray.push(imageOptions)\n\n if (embedImages) {\n const blob = NVUtilities.uint8tob64(v.toUint8Array())\n out.encodedImageBlobs!.push(blob)\n }\n out.imageOptionsMap!.set(v.id, i)\n }\n }\n out.imageOptionsArray = imageOptionsArray\n\n // meshes: build safe clones and convert typed arrays to plain arrays\n const meshesForExport: any[] = []\n const meshes = document.meshes || []\n out.connectomes = []\n for (const mesh of meshes) {\n if (!mesh || typeof mesh !== 'object') {\n continue\n }\n if (mesh.type === MeshType.CONNECTOME) {\n try {\n out.connectomes!.push(JSON.stringify((mesh as NVConnectome).json()))\n } catch (e) {\n log.warn('serializeDocument: failed connectome export', e)\n }\n continue\n }\n\n // build layers\n const layersForExport = (mesh.layers || []).map((layer: any) => {\n const exported: any = {\n name: layer?.name,\n key: layer?.key,\n url: layer?.url,\n headers: layer?.headers,\n opacity: layer?.opacity,\n colormap: layer?.colorMap ?? layer?.colormap,\n colormapNegative: layer?.colorMapNegative ?? layer?.colormapNegative,\n colormapInvert: layer?.colormapInvert,\n colormapLabel: layer?.colormapLabel,\n useNegativeCmap: layer?.useNegativeCmap,\n // numeric meta\n global_min: encodeNumberForJSON(layer?.global_min),\n global_max: encodeNumberForJSON(layer?.global_max),\n cal_min: encodeNumberForJSON(layer?.cal_min),\n cal_max: encodeNumberForJSON(layer?.cal_max),\n cal_minNeg: encodeNumberForJSON(layer?.cal_minNeg),\n cal_maxNeg: encodeNumberForJSON(layer?.cal_maxNeg),\n isAdditiveBlend: layer?.isAdditiveBlend,\n frame4D: layer?.frame4D,\n nFrame4D: layer?.nFrame4D,\n outlineBorder: layer?.outlineBorder,\n isTransparentBelowCalMin: layer?.isTransparentBelowCalMin,\n colormapType: layer?.colormapType,\n base64: layer?.base64,\n colorbarVisible: layer?.colorbarVisible,\n showLegend: layer?.showLegend\n }\n\n if (layer?.values != null) {\n const vals = Array.isArray(layer.values) ? layer.values.slice() : Array.from((layer.values as unknown as Iterable<number>) || [])\n exported.values = vals.map((v: any) => {\n const num = Number(v)\n return Number.isFinite(num) ? Math.round(num * 1e12) / 1e12 : num\n })\n }\n if (layer?.atlasValues != null) {\n exported.atlasValues = Array.isArray(layer.atlasValues) ? layer.atlasValues.slice() : Array.from((layer.atlasValues as unknown as Iterable<number>) || [])\n }\n if (Array.isArray(layer?.labels)) {\n exported.labels = layer.labels.map((l: any) => ({ ...(l || {}) }))\n } else if (layer?.labels != null) {\n exported.labels = Array.from((layer.labels as unknown as Iterable<any>) || [])\n }\n return exported\n })\n\n const meshCopy: any = {\n ...Object.assign({}, mesh || {}),\n rgba255: Array.from(mesh.rgba255 || []),\n layers: layersForExport,\n edges: Array.isArray(mesh?.edges) ? mesh.edges.map((e: any) => ({ ...(e || {}) })) : [],\n nodes: Array.isArray(mesh?.nodes) ? mesh.nodes.map((n: any) => ({ ...(n || {}) })) : mesh?.nodes,\n offsetPt0: Array.isArray(mesh?.offsetPt0) ? [...mesh.offsetPt0] : mesh?.offsetPt0\n }\n\n // fiber fields\n if (Array.isArray(mesh.offsetPt0) && mesh.offsetPt0.length > 0) {\n meshCopy.offsetPt0 = Array.isArray(mesh.offsetPt0) ? [...mesh.offsetPt0] : mesh.offsetPt0\n if (Array.isArray(mesh.fiberGroupColormap)) {\n meshCopy.fiberGroupColormap = [...mesh.fiberGroupColormap]\n }\n if (Array.isArray(mesh.fiberColor)) {\n meshCopy.fiberColor = Array.from(mesh.fiberColor)\n }\n meshCopy.fiberDither = mesh.fiberDither\n meshCopy.fiberRadius = mesh.fiberRadius\n meshCopy.colormap = mesh.colormap\n }\n\n meshesForExport.push(meshCopy)\n }\n\n try {\n out.meshesString = JSON.stringify(meshesForExport, jsonReplacer)\n } catch (err) {\n log.warn('serializeDocument: JSON.stringify failed for meshes', err)\n // fallback: per-mesh serialization\n const per: string[] = []\n for (let i = 0; i < meshesForExport.length; i++) {\n try {\n per.push(JSON.stringify(meshesForExport[i], jsonReplacer))\n } catch (e) {\n log.error('serializeDocument: failed to serialize mesh at index', i, e, meshesForExport[i])\n per.push(JSON.stringify({ name: meshesForExport[i]?.name ?? 'unknown', id: meshesForExport[i]?.id ?? null }))\n }\n }\n out.meshesString = `[${per.join(',')}]`\n }\n\n if (embedDrawing && document.drawBitmap) {\n out.encodedDrawingBlob = NVUtilities.uint8tob64(document.drawBitmap)\n } else {\n out.encodedDrawingBlob = document.encodedDrawingBlob ?? ''\n }\n\n out.title = document.title ?? 'untitled'\n out.customData = document.customData ?? ''\n out.labels = Array.isArray(document.labels)\n ? document.labels.map((l: any) => {\n const copy = { ...(l || {}) }\n delete copy.onClick\n return copy\n })\n : []\n\n // Export completed measurements & angles (clone vectors/objects so they survive JSON roundtrip)\n out.completedMeasurements = (document.completedMeasurements || []).map((m: any) => ({\n ...m,\n startMM: Array.isArray(m.startMM) ? [...m.startMM] : m.startMM,\n endMM: Array.isArray(m.endMM) ? [...m.endMM] : m.endMM\n }))\n\n out.completedAngles = (document.completedAngles || []).map((a: any) => ({\n ...a,\n firstLineMM: {\n start: Array.isArray(a.firstLineMM?.start) ? [...a.firstLineMM.start] : a.firstLineMM?.start,\n end: Array.isArray(a.firstLineMM?.end) ? [...a.firstLineMM.end] : a.firstLineMM?.end\n },\n secondLineMM: {\n start: Array.isArray(a.secondLineMM?.start) ? [...a.secondLineMM.start] : a.secondLineMM?.start,\n end: Array.isArray(a.secondLineMM?.end) ? [...a.secondLineMM.end] : a.secondLineMM?.end\n }\n }))\n\n // Make sure sceneData and opts are included (you already had these, but double-check)\n out.sceneData = out.sceneData ?? { ...(document.scene?.sceneData ?? {}) }\n out.opts = out.opts ?? (document.opts ? { ...(document.opts as any) } : {})\n\n return out as ExportDocumentData\n }\n\n /**\n * Rehydrate images into NVImage instances using NVImage.new\n */\n static async rehydrateImages(documentData: DocumentData): Promise<NVImage[]> {\n const imgs: NVImage[] = []\n\n // Normalize legacy shapes first\n const migrated = await migrateLegacyDocument(documentData)\n\n const encoded = migrated.encodedImageBlobs ?? []\n const optsArr = migrated.imageOptionsArray ?? []\n\n for (let i = 0; i < optsArr.length; i++) {\n const imageOptions = { ...(optsArr[i] || {}) } as any\n try {\n const b64 = encoded[i]\n if (!b64) {\n log.debug('NVSerializer.rehydrateImages: no blob for index', i)\n continue\n }\n const u8 = await NVUtilities.b64toUint8(b64)\n if (!u8) {\n log.warn('NVSerializer.rehydrateImages: b64toUint8 returned empty for index', i)\n continue\n }\n\n const name = imageOptions.name ?? imageOptions.url ?? `image-${i}`\n // Ensure colormap keys are strings when we call color-table lookups.\n // Legacy data sometimes stores colormaps in typed-array / object shapes —\n // guard against that so colormapFromKey(name) doesn't throw.\n let colormap: string | any[] = imageOptions.colormap ?? imageOptions.colorMap ?? ''\n if (typeof colormap !== 'string') {\n // Prefer the explicitly provided colorMap string if present\n if (typeof imageOptions.colorMap === 'string') {\n colormap = imageOptions.colorMap\n } else {\n // otherwise treat as \"no named colormap\" so colour-table lookup won't be attempted\n colormap = ''\n }\n }\n\n const opacity = imageOptions.opacity ?? 1.0\n\n const pairedImgData = imageOptions.pairedImgData\n ? typeof imageOptions.pairedImgData === 'string'\n ? (await NVUtilities.b64toUint8(imageOptions.pairedImgData)).buffer\n : imageOptions.pairedImgData\n : null\n const cal_min = decodeNumberFromJSON(imageOptions.cal_min)\n const cal_max = decodeNumberFromJSON(imageOptions.cal_max)\n const trustCalMinMax = imageOptions.trustCalMinMax ?? true\n const percentileFrac = imageOptions.percentileFrac ?? 0.02\n const ignoreZeroVoxels = imageOptions.ignoreZeroVoxels ?? false\n const useQFormNotSForm = imageOptions.useQFormNotSForm ?? false\n const colormapNegative = imageOptions.colormapNegative ?? ''\n const frame4D = imageOptions.frame4D ?? 0\n const imageType = 'imageType' in imageOptions && typeof imageOptions.imageType === 'number' ? imageOptions.imageType : (imageOptions.imageType ?? NVIMAGE_TYPE.UNKNOWN)\n const cal_minNeg = decodeNumberFromJSON(imageOptions.cal_minNeg)\n const cal_maxNeg = decodeNumberFromJSON(imageOptions.cal_maxNeg)\n const colorbarVisible = imageOptions.colorbarVisible ?? true\n const colormapLabel = imageOptions.colormapLabel ?? null\n const colormapType = imageOptions.colormapType ?? 0\n const zarrData = imageOptions.zarrData ?? null\n\n // ensure ArrayBuffer argument\n console.log(`Image ${i} buffer length: ${u8.byteLength}`)\n\n // Check if the data is actually all zeros before Niivue gets it\n const isAllZeros = !u8.some((val) => val !== 0)\n if (isAllZeros) {\n console.error(`CRITICAL: Base64 decoding for ${name} resulted in an all-zero array!`)\n }\n\n const bufferArg = u8.slice().buffer\n\n const img = await NVImage.new(\n bufferArg,\n name,\n colormap as string,\n opacity,\n pairedImgData,\n cal_min as any,\n cal_max as any,\n trustCalMinMax,\n percentileFrac,\n ignoreZeroVoxels,\n useQFormNotSForm,\n colormapNegative,\n frame4D,\n imageType,\n cal_minNeg as any,\n cal_maxNeg as any,\n colorbarVisible,\n colormapLabel,\n colormapType,\n zarrData\n )\n imgs.push(img)\n } catch (err) {\n log.warn('NVSerializer.rehydrateImages: failed for index', i, err)\n }\n }\n return imgs\n }\n\n /**\n * Rehydrate meshes into runtime NVMesh instances.\n * If gl is provided and callUpdateMesh=true, updateMesh(gl) will be called.\n *\n * CRITICAL FIX: Properly convert arrays to TypedArrays before passing to NVMesh constructor\n * to avoid \"underspecified mesh\" warnings.\n */\n static async rehydrateMeshes(documentData: DocumentData, gl?: WebGL2RenderingContext, callUpdateMesh = false): Promise<Array<NVMesh | any>> {\n const out: Array<NVMesh | any> = []\n\n // Normalize legacy shapes first\n const migrated = await migrateLegacyDocument(documentData)\n\n const meshesString = migrated.meshesString ?? '[]'\n\n let parsed: any[] = []\n try {\n parsed = JSON.parse(meshesString)\n } catch (e) {\n console.warn('NVSerializer.rehydrateMeshes: failed to parse meshesString', e)\n parsed = []\n }\n\n for (let i = 0; i < parsed.length; i++) {\n let m = parsed[i] || {}\n try {\n m = normalizeMeshForRehydrate(m)\n // Normalize layer keys and numeric encodings (colorMap -> colormap etc.)\n if (Array.isArray(m.layers)) {\n for (const layer of m.layers) {\n if (!layer) {\n continue\n }\n if ('colorMap' in layer && !('colormap' in layer)) {\n layer.colormap = layer.colorMap\n delete layer.colorMap\n }\n if ('colorMapNegative' in layer && !('colormapNegative' in layer)) {\n layer.colormapNegative = layer.colorMapNegative\n delete layer.colorMapNegative\n }\n\n // decode special numeric strings (in case encodeNumberForJSON used)\n layer.global_min = decodeNumberFromJSON(layer.global_min)\n layer.global_max = decodeNumberFromJSON(layer.global_max)\n layer.cal_min = decodeNumberFromJSON(layer.cal_min)\n layer.cal_max = decodeNumberFromJSON(layer.cal_max)\n layer.cal_minNeg = decodeNumberFromJSON(layer.cal_minNeg)\n layer.cal_maxNeg = decodeNumberFromJSON(layer.cal_maxNeg)\n\n // ensure values/atlasValues are plain arrays (migration already attempted to normalize)\n if (layer.values != null && !Array.isArray(layer.values)) {\n try {\n layer.values = Array.from(layer.values)\n } catch (e) {\n /* leave as-is */\n }\n }\n if (layer.atlasValues != null && !Array.isArray(layer.atlasValues)) {\n try {\n layer.atlasValues = Array.from(layer.atlasValues)\n } catch (e) {\n /* leave as-is */\n }\n }\n }\n }\n\n // CRITICAL: Convert arrays to proper TypedArrays BEFORE creating NVMesh\n // This prevents the \"underspecified mesh\" warning in updateMesh\n\n // Convert rgba255 to Uint8Array\n const rgba255 = toUint8Array(m.rgba255) ?? new Uint8Array([255, 255, 255, 255])\n\n // Convert pts to Float32Array (vertex positions)\n const pts = toFloat32Array(m.pts)\n\n // Convert tris to Uint32Array (triangle indices)\n let tris = toUint32Array(m.tris)\n\n // ensure nodes/edges plain arrays\n if (Array.isArray(m.nodes)) {\n m.nodes = m.nodes.length > 0 && typeof m.nodes[0] === 'object' ? m.nodes.map((n: any) => ({ ...n })) : m.nodes.slice()\n }\n if (Array.isArray(m.edges)) {\n m.edges = m.edges.length > 0 && typeof m.edges[0] === 'object' ? m.edges.map((e: any) => ({ ...e })) : m.edges.slice()\n }\n\n // preserve id if present (important)\n const persistedId = m.id !== undefined ? m.id : null\n\n // If no GL provided, keep as plain object (tests may parse/expect object form)\n if (!gl) {\n // For test environments, keep arrays as-is\n out.push(m)\n continue\n }\n\n // Handle fiber meshes: offsetPt0 becomes tris\n if (Array.isArray(m.offsetPt0) && m.offsetPt0.length > 0) {\n tris = new Uint32Array(m.offsetPt0)\n // Set alpha to 0 for fibers\n if (rgba255) {\n rgba255[3] = 0\n }\n }\n\n // produce a plain Uint8Array backed by ArrayBuffer\n const finalizedRgba = new Uint8Array(Array.isArray(rgba255) ? rgba255 : Array.from(rgba255 as Uint8Array))\n\n const meshInstance = new NVMesh(pts, tris, m.name, finalizedRgba, m.opacity, m.visible, gl, m.connectome, m.dpg, m.dps, m.dpv)\n\n // restore preserved id (overwrite generated id)\n if (persistedId !== null) {\n ;(meshInstance as any).id = persistedId\n }\n\n // fiber metadata\n if (Array.isArray(m.offsetPt0) && m.offsetPt0.length > 0) {\n meshInstance.fiberGroupColormap = m.fiberGroupColormap\n meshInstance.fiberColor = m.fiberColor ? m.fiberColor : undefined\n meshInstance.fiberDither = m.fiberDither\n meshInstance.fiberRadius = m.fiberRadius\n meshInstance.colormap = m.colormap\n }\n\n // layers & shader index\n meshInstance.meshShaderIndex = m.meshShaderIndex\n meshInstance.layers = m.layers || []\n\n if (callUpdateMesh) {\n try {\n meshInstance.updateMesh(gl)\n } catch (e) {\n console.warn('NVSerializer.rehydrateMeshes: updateMesh failed for idx', i, e)\n }\n }\n\n out.push(meshInstance)\n } catch (err) {\n console.warn('NVSerializer.rehydrateMeshes: failed idx', i, err)\n // push the plain object as fallback so lengths/indices remain stable\n out.push(m)\n }\n }\n\n // Defensive: guarantee an array is returned for all (possibly-legacy) inputs.\n try {\n // Defensive: guarantee an array is returned for all (possibly-legacy) inputs.\n if (!Array.isArray(out)) {\n console.warn('NVSerializer.rehydrateMeshes: unexpected non-array output, coercing to array', {\n typeofOut: typeof out,\n isArray: Array.isArray(out),\n outPreview: out && (typeof out === 'object' ? Object.keys(out).slice(0, 10) : out)\n })\n return out ? [out] : []\n }\n\n return out\n } catch (err) {\n console.error('NVSerializer.rehydrateMeshes: final-return error', err)\n return []\n }\n }\n\n static async deserializeDocument(documentData: DocumentData): Promise<NVDocument> {\n // run migration to normalize legacy shapes up-front\n const migrated = await migrateLegacyDocument(documentData)\n\n // Create a fresh NVDocument\n const document = new NVDocument()\n\n // Basic top-level fields (preserve defaults where missing)\n Object.assign(document.data, {\n ...migrated,\n imageOptionsArray: migrated.imageOptionsArray ?? [],\n encodedImageBlobs: migrated.encodedImageBlobs ?? [],\n labels: migrated.labels ?? [],\n meshOptionsArray: migrated.meshOptionsArray ?? [],\n connectomes: migrated.connectomes ?? [],\n encodedDrawingBlob: migrated.encodedDrawingBlob ?? '',\n previewImageDataURL: migrated.previewImageDataURL ?? '',\n customData: migrated.customData ?? '',\n title: migrated.title ?? 'untitled'\n })\n\n // decode opts and merge with defaults\n const decodedOpts = NVSerializer.decodeOptsFromJSON((migrated as any).opts as any)\n document.data.opts = {\n ...DEFAULT_OPTIONS,\n ...(decodedOpts || {})\n } as any\n\n if ((document.data.opts as any).meshThicknessOn2D === 'infinity') {\n ;(document.data.opts as any).meshThicknessOn2D = Infinity\n }\n\n // scene data (merge with initial scene)\n document.scene.sceneData = {\n ...INITIAL_SCENE_DATA,\n ...(migrated.sceneData || {})\n }\n\n // back-compat: single clipPlane fields -> arrays\n const sceneDataAny: any = migrated.sceneData || {}\n if (sceneDataAny.clipPlane && !sceneDataAny.clipPlanes) {\n document.scene.sceneData.clipPlanes = [sceneDataAny.clipPlane]\n }\n if (sceneDataAny.clipPlaneDepthAziElev && !sceneDataAny.clipPlaneDepthAziElevs) {\n document.scene.sceneData.clipPlaneDepthAziElevs = [sceneDataAny.clipPlaneDepthAziElev]\n }\n\n // restore completed measurements & angles (clone vectors)\n if (migrated.completedMeasurements) {\n document.completedMeasurements = migrated.completedMeasurements.map((m: any) => ({\n ...m,\n startMM: vec3.clone(m.startMM),\n endMM: vec3.clone(m.endMM)\n }))\n }\n if (migrated.completedAngles) {\n document.completedAngles = migrated.completedAngles.map((a: any) => ({\n ...a,\n firstLineMM: {\n start: vec3.clone(a.firstLineMM.start),\n end: vec3.clone(a.firstLineMM.end)\n },\n secondLineMM: {\n start: vec3.clone(a.secondLineMM.start),\n end: vec3.clone(a.secondLineMM.end)\n }\n }))\n }\n\n // keep other runtime lists in sync\n // preserve connectome strings (Niivue.loadDocument adds them as runtime meshes later)\n document.data.connectomes = migrated.connectomes ?? []\n\n // drawing blob stays as encoded base64 (consumer will decode)\n document.data.encodedDrawingBlob = migrated.encodedDrawingBlob ?? ''\n\n // preview image\n document.data.previewImageDataURL = migrated.previewImageDataURL ?? ''\n\n return document\n }\n}\n\nexport default NVSerializer\n","import arrayEqual from 'array-equal'\nimport { mat4, vec3, vec4 } from 'gl-matrix'\n\n// TODO: TypedNumberArray also in nvmesh-types.ts\ntype TypedNumberArray = Float64Array | Float32Array | Uint32Array | Uint16Array | Uint8Array | Int32Array | Int16Array | Int8Array\n\n/**\n * Namespace for utility functions\n * @ignore\n */\n\n/**\n * Read ZIP files using asynchronous compression streams API, supports data descriptors\n * todo: check ZIP64 support\n * https://github.com/libyal/assorted/blob/main/documentation/ZIP%20archive%20format.asciidoc\n * https://en.wikipedia.org/wiki/ZIP_(file_format)\n * https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT\n * https://dev.to/ndesmic/writing-a-simple-browser-zip-file-decompressor-with-compressionstreams-5che\n */\n\ninterface Entry {\n signature: string\n version: number\n generalPurpose: number\n compressionMethod: number\n lastModifiedTime: number\n lastModifiedDate: number\n crc: number\n compressedSize: number\n uncompressedSize: number\n fileNameLength: number\n extraLength: number\n fileName: string\n extra: string\n startsAt?: number\n extract?: () => Promise<Uint8Array>\n}\n\ninterface CentralDirectoryEntry {\n versionCreated: number\n versionNeeded: number\n fileCommentLength: number\n diskNumber: number\n internalAttributes: number\n externalAttributes: number\n offset: number\n comments: string\n fileNameLength: number\n extraLength: number\n}\n\ninterface EndOfCentralDirectory {\n numberOfDisks: number\n centralDirectoryStartDisk: number\n numberCentralDirectoryRecordsOnThisDisk: number\n numberCentralDirectoryRecords: number\n centralDirectorySize: number\n centralDirectoryOffset: number\n commentLength: number\n comment: string\n}\n\nexport class Zip {\n #dataView: DataView\n #index: number = 0\n #localFiles: Entry[] = []\n #centralDirectories: CentralDirectoryEntry[] = []\n #endOfCentralDirectory?: EndOfCentralDirectory\n\n constructor(arrayBuffer: ArrayBuffer) {\n this.#dataView = new DataView(arrayBuffer)\n this.read()\n }\n\n async extract(entry: Entry): Promise<Uint8Array> {\n const buffer = new Uint8Array(this.#dataView.buffer.slice(entry.startsAt!, entry.startsAt! + entry.compressedSize))\n if (entry.compressionMethod === 0x00) {\n return buffer\n } else if (entry.compressionMethod === 0x08) {\n const stream = new DecompressionStream('deflate-raw')\n const writer = stream.writable.getWriter()\n writer.write(buffer).catch(console.error)\n const closePromise = writer.close().catch(console.error)\n const response = new Response(stream.readable)\n const result = new Uint8Array(await response.arrayBuffer())\n await closePromise\n return result\n }\n throw new Error(`Unsupported compression method: ${entry.compressionMethod}`)\n }\n\n private read(): void {\n while (!this.#endOfCentralDirectory && this.#index < this.#dataView.byteLength) {\n const signature = this.#dataView.getUint32(this.#index, true)\n if (signature === 0x04034b50) {\n const entry = this.readLocalFile(this.#index)\n entry.extract = this.extract.bind(this, entry)\n this.#localFiles.push(entry)\n const hasDataDescriptor = (entry.generalPurpose & 0x0008) !== 0\n entry.startsAt = this.#index + 30 + entry.fileNameLength + entry.extraLength\n if (entry.compressedSize === 0 && hasDataDescriptor) {\n let scanIndex = entry.startsAt\n while (scanIndex! + 20 <= this.#dataView.byteLength) {\n const possibleSignature = this.#dataView.getUint32(scanIndex!, true)\n if (possibleSignature === 0x08074b50) {\n const nextPK = this.#dataView.getUint16(scanIndex! + 16, true) === 0x4b50\n if (nextPK) {\n scanIndex! += 4\n break\n }\n }\n scanIndex!++\n }\n entry.crc = this.#dataView.getUint32(scanIndex!, true)\n entry.compressedSize = this.#dataView.getUint32(scanIndex! + 4, true)\n entry.uncompressedSize = this.#dataView.getUint32(scanIndex! + 8, true)\n this.#index = scanIndex! + 12\n } else {\n this.#index = entry.startsAt + entry.compressedSize\n }\n } else if (signature === 0x02014b50) {\n const entry = this.readCentralDirectory(this.#index)\n this.#centralDirectories.push(entry)\n this.#index += 46 + entry.fileNameLength + entry.extraLength + entry.fileCommentLength\n } else if (signature === 0x06054b50) {\n this.#endOfCentralDirectory = this.readEndCentralDirectory(this.#index)\n break\n } else if (signature === 0x06064b50) {\n this.#endOfCentralDirectory = this.readEndCentralDirectory64(this.#index)\n break\n } else {\n console.error(`Unexpected ZIP signature 0x${signature.toString(16).padStart(8, '0')} at index ${this.#index}`)\n break\n }\n }\n }\n\n private readLocalFile(offset: number): Entry {\n let compressedSize = this.#dataView.getUint32(offset + 18, true)\n let uncompressedSize = this.#dataView.getUint32(offset + 22, true)\n const fileNameLength = this.#dataView.getUint16(offset + 26, true)\n const extraLength = this.#dataView.getUint16(offset + 28, true)\n const extraOffset = offset + 30 + fileNameLength\n const extra = this.readString(extraOffset, extraLength)\n if (compressedSize === 0xffffffff && uncompressedSize === 0xffffffff) {\n let zip64Offset = extraOffset\n let foundZip64 = false\n while (zip64Offset < extraOffset + extraLength - 4) {\n const fieldSignature = this.#dataView.getUint16(zip64Offset, true)\n const fieldLength = this.#dataView.getUint16(zip64Offset + 2, true)\n zip64Offset += 4 // Move past signature and length\n if (fieldSignature === 0x0001) {\n // ZIP64 Extended Information Extra Field\n if (fieldLength >= 16) {\n // Ensure we have enough bytes\n uncompressedSize = Number(this.#dataView.getBigUint64(zip64Offset, true))\n zip64Offset += 8\n compressedSize = Number(this.#dataView.getBigUint64(zip64Offset, true))\n foundZip64 = true\n break\n } else {\n throw new Error(`ZIP64 extra field found but is too small (expected at least 16 bytes, got ${fieldLength}).`)\n }\n }\n zip64Offset += fieldLength // Move to the next extra field\n }\n if (!foundZip64) {\n throw new Error('ZIP64 format missing extra field with signature 0x0001.')\n }\n }\n return {\n signature: this.readString(offset, 4),\n version: this.#dataView.getUint16(offset + 4, true),\n generalPurpose: this.#dataView.getUint16(offset + 6, true),\n compressionMethod: this.#dataView.getUint16(offset + 8, true),\n lastModifiedTime: this.#dataView.getUint16(offset + 10, true),\n lastModifiedDate: this.#dataView.getUint16(offset + 12, true),\n crc: this.#dataView.getUint32(offset + 14, true),\n compressedSize,\n uncompressedSize,\n fileNameLength,\n extraLength,\n fileName: this.readString(offset + 30, fileNameLength),\n extra: this.readString(offset + 30 + fileNameLength, extraLength)\n }\n }\n\n private readCentralDirectory(offset: number): CentralDirectoryEntry {\n return {\n versionCreated: this.#dataView.getUint16(offset + 4, true),\n versionNeeded: this.#dataView.getUint16(offset + 6, true),\n fileNameLength: this.#dataView.getUint16(offset + 28, true),\n extraLength: this.#dataView.getUint16(offset + 30, true),\n fileCommentLength: this.#dataView.getUint16(offset + 32, true),\n diskNumber: this.#dataView.getUint16(offset + 34, true),\n internalAttributes: this.#dataView.getUint16(offset + 36, true),\n externalAttributes: this.#dataView.getUint32(offset + 38, true),\n offset: this.#dataView.getUint32(offset + 42, true),\n comments: this.readString(offset + 46, this.#dataView.getUint16(offset + 32, true))\n }\n }\n\n private readEndCentralDirectory(offset: number): EndOfCentralDirectory {\n const commentLength = this.#dataView.getUint16(offset + 20, true)\n return {\n numberOfDisks: this.#dataView.getUint16(offset + 4, true),\n centralDirectoryStartDisk: this.#dataView.getUint16(offset + 6, true),\n numberCentralDirectoryRecordsOnThisDisk: this.#dataView.getUint16(offset + 8, true),\n numberCentralDirectoryRecords: this.#dataView.getUint16(offset + 10, true),\n centralDirectorySize: this.#dataView.getUint32(offset + 12, true),\n centralDirectoryOffset: this.#dataView.getUint32(offset + 16, true),\n commentLength,\n comment: this.readString(offset + 22, commentLength)\n }\n }\n\n private readEndCentralDirectory64(offset: number): EndOfCentralDirectory {\n const commentLength = Number(this.#dataView.getBigUint64(offset + 0, true))\n return {\n numberOfDisks: this.#dataView.getUint32(offset + 16, true),\n centralDirectoryStartDisk: this.#dataView.getUint32(offset + 20, true),\n numberCentralDirectoryRecordsOnThisDisk: Number(this.#dataView.getBigUint64(offset + 24, true)),\n numberCentralDirectoryRecords: Number(this.#dataView.getBigUint64(offset + 32, true)),\n centralDirectorySize: Number(this.#dataView.getBigUint64(offset + 40, true)),\n centralDirectoryOffset: Number(this.#dataView.getBigUint64(offset + 48, true)),\n commentLength,\n comment: ''\n }\n }\n\n private readString(offset: number, length: number): string {\n return Array.from({ length }, (_, i) => String.fromCharCode(this.#dataView.getUint8(offset + i))).join('')\n }\n\n get entries(): Entry[] {\n return this.#localFiles\n }\n}\n\nexport class NVUtilities {\n static arrayBufferToBase64(arrayBuffer: ArrayBuffer): string {\n const bytes = new Uint8Array(arrayBuffer)\n return NVUtilities.uint8tob64(bytes)\n }\n\n static async decompress(data: Uint8Array): Promise<Uint8Array> {\n const format =\n data[0] === 31 && data[1] === 139 && data[2] === 8 ? 'gzip' : data[0] === 120 && (data[1] === 1 || data[1] === 94 || data[1] === 156 || data[1] === 218) ? 'deflate' : 'deflate-raw'\n const stream = new DecompressionStream(format)\n const writer = stream.writable.getWriter()\n writer.write(data).catch(console.error) // Do not await this\n // Close without awaiting directly, preventing the hang issue\n const closePromise = writer.close().catch(console.error)\n const response = new Response(stream.readable)\n const result = new Uint8Array(await response.arrayBuffer())\n await closePromise // Ensure close happens eventually\n return result\n }\n\n static async decompressToBuffer(data: Uint8Array): Promise<ArrayBuffer> {\n const decompressed = await NVUtilities.decompress(data)\n return decompressed.buffer.slice(decompressed.byteOffset, decompressed.byteOffset + decompressed.byteLength)\n }\n\n static async readMatV4(buffer: ArrayBuffer, isReplaceDots: boolean = false): Promise<Record<string, TypedNumberArray>> {\n let len = buffer.byteLength\n if (len < 40) {\n throw new Error('File too small to be MAT v4: bytes = ' + buffer.byteLength)\n }\n let reader = new DataView(buffer)\n let magic = reader.getUint16(0, true)\n let _buffer = buffer\n if (magic === 35615 || magic === 8075) {\n // gzip signature 0x1F8B in little and big endian\n const raw = await this.decompress(new Uint8Array(buffer))\n reader = new DataView(raw.buffer)\n magic = reader.getUint16(0, true)\n _buffer = raw.buffer\n len = _buffer.byteLength\n }\n const textDecoder = new TextDecoder('utf-8')\n const bytes = new Uint8Array(_buffer)\n let pos = 0\n const mat: Record<string, TypedNumberArray> = {}\n function getTensDigit(v: number): number {\n return Math.floor(v / 10) % 10\n }\n function readArray(tagDataType: number, tagBytesStart: number, tagBytesEnd: number): TypedNumberArray {\n const byteArray = new Uint8Array(bytes.subarray(tagBytesStart, tagBytesEnd))\n if (tagDataType === 1) {\n return new Float32Array(byteArray.buffer)\n }\n if (tagDataType === 2) {\n return new Int32Array(byteArray.buffer)\n }\n if (tagDataType === 3) {\n return new Int16Array(byteArray.buffer)\n }\n if (tagDataType === 4) {\n return new Uint16Array(byteArray.buffer)\n }\n if (tagDataType === 5) {\n return new Uint8Array(byteArray.buffer)\n }\n return new Float64Array(byteArray.buffer)\n }\n function readTag(): void {\n const mtype = reader.getUint32(pos, true)\n const mrows = reader.getUint32(pos + 4, true)\n const ncols = reader.getUint32(pos + 8, true)\n const imagf = reader.getUint32(pos + 12, true)\n const namlen = reader.getUint32(pos + 16, true)\n pos += 20 // skip header\n if (imagf !== 0) {\n throw new Error('Matlab V4 reader does not support imaginary numbers')\n }\n const tagArrayItems = mrows * ncols\n if (tagArrayItems < 1) {\n throw new Error('mrows * ncols must be greater than one')\n }\n const byteArray = new Uint8Array(bytes.subarray(pos, pos + namlen))\n let tagName = textDecoder.decode(byteArray).trim().replaceAll('\\x00', '')\n // n.b. DSI studio have array mat.dti_fa[] and mat.dti_fa.slope\n if (isReplaceDots) {\n // kludge for invalid DSIstudio FZ files\n tagName = tagName.replaceAll('.', '_')\n }\n const tagDataType = getTensDigit(mtype)\n // 0 double-precision (64-bit) floating-point numbers\n // 1 single-precision (32-bit) floating-point numbers\n // 2 32-bit signed integers\n // 3 16-bit signed integers\n // 4 16-bit unsigned integers\n // 5 8-bit unsigned integers\n let tagBytesPerItem = 8\n if (tagDataType >= 1 && tagDataType <= 2) {\n tagBytesPerItem = 4\n } else if (tagDataType >= 3 && tagDataType <= 4) {\n tagBytesPerItem = 2\n } else if (tagDataType === 5) {\n tagBytesPerItem = 1\n } else if (tagDataType !== 0) {\n throw new Error('impossible Matlab v4 datatype')\n }\n pos += namlen // skip name\n if (mtype > 50) {\n throw new Error('Does not appear to be little-endian V4 Matlab file')\n }\n const posEnd = pos + tagArrayItems * tagBytesPerItem\n mat[tagName] = readArray(tagDataType, pos, posEnd)\n pos = posEnd\n }\n while (pos + 20 < len) {\n readTag()\n }\n return mat\n } // readMatV4()\n\n static b64toUint8(base64: string): Uint8Array {\n const binaryString = atob(base64)\n const length = binaryString.length\n const bytes = new Uint8Array(length)\n for (let i = 0; i < length; i++) {\n bytes[i] = binaryString.charCodeAt(i)\n }\n return bytes\n }\n\n /*\nhttps://gist.github.com/jonleighton/958841\nMIT LICENSE\nCopyright 2011 Jon Leighton\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n*/\n static uint8tob64(bytes: Uint8Array): string {\n // TODO: use TextDecoder instead of shipping own implementation\n\n let base64 = ''\n const encodings = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'\n const byteLength = bytes.byteLength\n const byteRemainder = byteLength % 3\n const mainLength = byteLength - byteRemainder\n\n let a, b, c, d\n let chunk\n\n // Main loop deals with bytes in chunks of 3\n for (let i = 0; i < mainLength; i = i + 3) {\n // Combine the three bytes into a single integer\n chunk = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2]\n\n // Use bitmasks to extract 6-bit segments from the triplet\n a = (chunk & 16515072) >> 18 // 16515072 = (2^6 - 1) << 18\n b = (chunk & 258048) >> 12 // 258048 = (2^6 - 1) << 12\n c = (chunk & 4032) >> 6 // 4032 = (2^6 - 1) << 6\n d = chunk & 63 // 63 = 2^6 - 1\n\n // Convert the raw binary segments to the appropriate ASCII encoding\n base64 += encodings[a] + encodings[b] + encodings[c] + encodings[d]\n }\n\n // Deal with the remaining bytes and padding\n if (byteRemainder === 1) {\n chunk = bytes[mainLength]\n\n a = (chunk & 252) >> 2 // 252 = (2^6 - 1) << 2\n\n // Set the 4 least significant bits to zero\n b = (chunk & 3) << 4 // 3 = 2^2 - 1\n\n base64 += encodings[a] + encodings[b] + '=='\n } else if (byteRemainder === 2) {\n chunk = (bytes[mainLength] << 8) | bytes[mainLength + 1]\n\n a = (chunk & 64512) >> 10 // 64512 = (2^6 - 1) << 10\n b = (chunk & 1008) >> 4 // 1008 = (2^6 - 1) << 4\n\n // Set the 2 least significant bits to zero\n c = (chunk & 15) << 2 // 15 = 2^4 - 1\n\n base64 += encodings[a] + encodings[b] + encodings[c] + '='\n }\n\n return base64\n }\n\n // https://stackoverflow.com/questions/34156282/how-do-i-save-json-to-local-text-file\n static download(content: string | ArrayBuffer, fileName: string, contentType: string): void {\n const a = document.createElement('a')\n const contentArray = Array.isArray(content) ? content : [content]\n const file = new Blob(contentArray, { type: contentType })\n a.href = URL.createObjectURL(file)\n a.download = fileName\n a.click()\n }\n\n static readFileAsync(file: Blob): Promise<ArrayBuffer> {\n return new Promise((resolve, reject) => {\n const reader = new FileReader()\n reader.onload = (): void => {\n resolve(reader.result as ArrayBuffer)\n }\n\n reader.onerror = reject\n\n reader.readAsArrayBuffer(file)\n })\n }\n\n static blobToBase64(blob: Blob): Promise<string> {\n return new Promise((resolve) => {\n const reader = new FileReader()\n reader.onloadend = (): void => resolve(reader.result as string)\n reader.readAsDataURL(blob)\n })\n }\n\n static async decompressBase64String(base64: string): Promise<string> {\n const compressed = atob(base64)\n // convert to an array buffer\n const compressedBuffer = new ArrayBuffer(compressed.length)\n const compressedView = new Uint8Array(compressedBuffer)\n for (let i = 0; i < compressed.length; i++) {\n compressedView[i] = compressed.charCodeAt(i)\n }\n return NVUtilities.decompressArrayBuffer(compressedView)\n }\n\n static async compressToBase64String(string: string): Promise<string> {\n const buf = await NVUtilities.compressStringToArrayBuffer(string)\n return NVUtilities.uint8tob64(new Uint8Array(buf))\n }\n\n /**\n * Converts a string into a Uint8Array for use with compression/decompression methods (101arrowz/fflate: MIT License)\n * @param str The string to encode\n * @param latin1 Whether or not to interpret the data as Latin-1. This should\n * not need to be true unless decoding a binary string.\n * @returns The string encoded in UTF-8/Latin-1 binary\n */\n\n static strToU8(str: string, latin1?: boolean): Uint8Array {\n if (latin1) {\n const ar = new Uint8Array(str.length)\n for (let i = 0; i < str.length; ++i) {\n ar[i] = str.charCodeAt(i)\n }\n return ar\n }\n const l = str.length\n // TODO: strToU8 and strFromU8 both define slc\n // typed array slice - allows garbage collector to free original reference,\n // while being more compatible than .slice\n const slc = (v: Uint8Array, s: number, e?: number): Uint8Array => {\n if (s == null || s < 0) {\n s = 0\n }\n if (e == null || e > v.length) {\n e = v.length\n }\n // can't use .constructor in case user-supplied\n return new Uint8Array(v.subarray(s, e))\n }\n let ar = new Uint8Array(str.length + (str.length >> 1))\n let ai = 0\n const w = (v: number): void => {\n ar[ai++] = v\n }\n for (let i = 0; i < l; ++i) {\n if (ai + 5 > ar.length) {\n const n = new Uint8Array(ai + 8 + ((l - i) << 1))\n n.set(ar)\n ar = n\n }\n let c = str.charCodeAt(i)\n if (c < 128 || latin1) {\n w(c)\n } else if (c < 2048) {\n w(192 | (c >> 6))\n w(128 | (c & 63))\n } else if (c > 55295 && c < 57344) {\n c = (65536 + (c & (1023 << 10))) | (str.charCodeAt(++i) & 1023)\n w(240 | (c >> 18))\n w(128 | ((c >> 12) & 63))\n w(128 | ((c >> 6) & 63))\n w(128 | (c & 63))\n } else {\n c = (65536 + (c & (1023 << 10))) | (str.charCodeAt(++i) & 1023)\n w(240 | (c >> 18))\n w(128 | ((c >> 12) & 63))\n w(128 | ((c >> 6) & 63))\n w(128 | (c & 63))\n }\n }\n return slc(ar, 0, ai)\n }\n\n static async compress(data: Uint8Array, format: CompressionFormat = 'gzip'): Promise<ArrayBuffer> {\n // mimics fflate, use 'deflate-raw' 'deflate' or 'gzip' if needed\n // const format = 'deflate-raw'\n const stream = new CompressionStream(format)\n const writer = stream.writable.getWriter()\n\n writer.write(data).catch(console.error) // Do not await this\n const closePromise = writer.close().catch(console.error)\n\n const response = new Response(stream.readable)\n const result = await response.arrayBuffer()\n\n await closePromise // Ensure close happens eventually\n return result\n }\n\n static async compressStringToArrayBuffer(input: string): Promise<ArrayBuffer> {\n const uint8Array = this.strToU8(input)\n return await this.compress(uint8Array)\n }\n\n static isArrayBufferCompressed(buffer: ArrayBuffer): boolean {\n if (buffer && buffer.byteLength) {\n const arr = new Uint8Array(buffer)\n const magicNumber = (arr[0] << 8) | arr[1]\n return magicNumber === 0x1f8b\n } else {\n return false\n }\n }\n\n /**\n * Converts a Uint8Array to a string (101arrowz/fflate: MIT License)\n * @param dat The data to decode to string\n * @param latin1 Whether or not to interpret the data as Latin-1. This should\n * not need to be true unless encoding to binary string.\n * @returns The original UTF-8/Latin-1 string\n */\n static strFromU8(dat: Uint8Array, latin1?: boolean): string {\n if (latin1) {\n let r = ''\n for (let i = 0; i < dat.length; i += 16384) {\n r += String.fromCharCode.apply(null, dat.subarray(i, i + 16384))\n }\n return r\n } else {\n // typed array slice - allows garbage collector to free original reference,\n // while being more compatible than .slice\n const slc = (v: Uint8Array, s: number, e?: number): Uint8Array => {\n if (s == null || s < 0) {\n s = 0\n }\n if (e == null || e > v.length) {\n e = v.length\n }\n // can't use .constructor in case user-supplied\n return new Uint8Array(v.subarray(s, e))\n }\n // decode UTF8\n const dutf8 = (d: Uint8Array): { s: string; r: Uint8Array } => {\n for (let r = '', i = 0; ; ) {\n let c = d[i++]\n const eb = ((c > 127) as unknown as number) + ((c > 223) as unknown as number) + ((c > 239) as unknown as number)\n if (i + eb > d.length) {\n return { s: r, r: slc(d, i - 1) }\n }\n if (!eb) {\n r += String.fromCharCode(c)\n } else if (eb === 3) {\n c = (((c & 15) << 18) | ((d[i++] & 63) << 12) | ((d[i++] & 63) << 6) | (d[i++] & 63)) - 65536\n r += String.fromCharCode(55296 | (c >> 10), 56320 | (c & 1023))\n } else if (eb & 1) {\n r += String.fromCharCode(((c & 31) << 6) | (d[i++] & 63))\n } else {\n r += String.fromCharCode(((c & 15) << 12) | ((d[i++] & 63) << 6) | (d[i++] & 63))\n }\n }\n }\n const { s, r } = dutf8(dat)\n if (r.length) {\n throw new Error('Unexpected trailing bytes in UTF-8 decoding')\n }\n return s\n }\n }\n\n static async decompressArrayBuffer(buffer: ArrayBuffer): Promise<string> {\n const decompressed = await this.decompress(new Uint8Array(buffer))\n return this.strFromU8(decompressed)\n }\n\n static arraysAreEqual(a: unknown[], b: unknown[]): boolean {\n return arrayEqual(a, b)\n }\n\n /**\n * Generate a pre-filled number array.\n *\n * @param start - start value\n * @param stop - stop value\n * @param step - step value\n * @returns filled number array\n */\n static range(start: number, stop: number, step: number): number[] {\n return Array.from({ length: (stop - start) / step + 1 }, (_, i) => start + i * step)\n }\n\n /**\n * convert spherical AZIMUTH, ELEVATION to Cartesian\n * @param azimuth - azimuth number\n * @param elevation - elevation number\n * @returns the converted [x, y, z] coordinates\n * @example\n * xyz = NVUtilities.sph2cartDeg(42, 42)\n */\n static sph2cartDeg(azimuth: number, elevation: number): number[] {\n // convert spherical AZIMUTH,ELEVATION,RANGE to Cartesian\n // see Matlab's [x,y,z] = sph2cart(THETA,PHI,R)\n // reverse with cart2sph\n const Phi = -elevation * (Math.PI / 180)\n const Theta = ((azimuth - 90) % 360) * (Math.PI / 180)\n const ret = [Math.cos(Phi) * Math.cos(Theta), Math.cos(Phi) * Math.sin(Theta), Math.sin(Phi)]\n const len = Math.sqrt(ret[0] * ret[0] + ret[1] * ret[1] + ret[2] * ret[2])\n if (len <= 0.0) {\n return ret\n }\n ret[0] /= len\n ret[1] /= len\n ret[2] /= len\n return ret\n }\n\n static vox2mm(XYZ: number[], mtx: mat4): vec3 {\n const sform = mat4.clone(mtx)\n mat4.transpose(sform, sform)\n const pos = vec4.fromValues(XYZ[0], XYZ[1], XYZ[2], 1)\n vec4.transformMat4(pos, pos, sform)\n const pos3 = vec3.fromValues(pos[0], pos[1], pos[2])\n return pos3\n }\n}\n","import { NIFTI1, NIFTI2, NIFTIEXTENSION, readHeaderAsync } from 'nifti-reader-js'\nimport { mat4, vec3, vec4 } from 'gl-matrix'\nimport { v4 as uuidv4 } from '@lukeed/uuid'\nimport { ColorMap, LUT } from '@/colortables'\nimport { log } from '@/logger'\nimport { ImageFromBase64, ImageFromFileOptions, ImageFromUrlOptions, ImageMetadata, ImageType, NVIMAGE_TYPE, NiiDataType, NiiIntentCode } from '@/nvimage/utils'\nimport * as ImageWriter from '@/nvimage/ImageWriter'\nimport * as VolumeUtils from '@/nvimage/VolumeUtils'\nimport * as ImageReaders from '@/nvimage/ImageReaders'\nimport * as CoordinateTransform from '@/nvimage/CoordinateTransform'\nimport * as ImageOrientation from '@/nvimage/ImageOrientation'\nimport * as TensorProcessing from '@/nvimage/TensorProcessing'\nimport * as IntensityCalibration from '@/nvimage/IntensityCalibration'\nimport * as ColormapManager from '@/nvimage/ColormapManager'\nimport * as ImageFactory from '@/nvimage/ImageFactory'\nimport * as ImageMetadataModule from '@/nvimage/ImageMetadata'\nimport * as ImageDataProcessor from '@/nvimage/ImageDataProcessor'\nimport * as AffineProcessor from '@/nvimage/AffineProcessor'\nimport * as ZarrProcessor from '@/nvimage/ZarrProcessor'\nimport * as StreamingLoader from '@/nvimage/StreamingLoader'\nimport { NVZarrHelper } from '@/nvimage/zarr/NVZarrHelper'\nimport { AffineTransform, copyAffine, createTransformMatrix, multiplyAffine } from '@/nvimage/affineUtils'\n\nexport * from '@/nvimage/utils'\nexport type TypedVoxelArray = Float32Array | Uint8Array | Int16Array | Float64Array | Uint16Array\n\n/**\n * a NVImage encapsulates some image data and provides methods to query and operate on images\n */\nexport class NVImage {\n name: string\n id: string\n url?: string\n headers?: Record<string, string>\n _colormap: string\n _opacity: number\n percentileFrac: number\n ignoreZeroVoxels: boolean\n trustCalMinMax: boolean\n colormapNegative: string\n // TODO see niivue/loadDocument\n colormapLabel: LUT | null\n colormapInvert?: boolean\n nFrame4D?: number\n frame4D: number // indexed from 0!\n nTotalFrame4D?: number\n cal_minNeg: number\n cal_maxNeg: number\n colorbarVisible = true\n modulationImage: number | null = null\n modulateAlpha = 0 // if !=0, mod transparency with expon power |Alpha|\n // TODO this is some Daikon internal thing\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n series: any = [] // for concatenating dicom images\n nVox3D?: number\n oblique_angle?: number\n maxShearDeg?: number\n useQFormNotSForm: boolean\n\n colormapType?: number\n\n pixDims?: number[]\n matRAS?: mat4\n pixDimsRAS?: number[]\n obliqueRAS?: mat4\n dimsRAS?: number[]\n permRAS?: number[]\n img2RASstep?: number[]\n img2RASstart?: number[]\n toRAS?: mat4\n toRASvox?: mat4\n\n frac2mm?: mat4\n frac2mmOrtho?: mat4\n extentsMinOrtho?: number[]\n extentsMaxOrtho?: number[]\n mm2ortho?: mat4\n\n hdr: NIFTI1 | NIFTI2 | null = null\n extensions?: NIFTIEXTENSION[]\n imageType?: ImageType\n img?: TypedVoxelArray\n imaginary?: Float32Array // only for complex data\n v1?: Float32Array // only for FIB files\n fileObject?: File | File[]\n dims?: number[]\n\n onColormapChange: (img: NVImage) => void = () => {}\n onOpacityChange: (img: NVImage) => void = () => {}\n\n zarrHelper: NVZarrHelper | null = null\n\n mm000?: vec3\n mm100?: vec3\n mm010?: vec3\n mm001?: vec3\n\n cal_min?: number\n cal_max?: number\n robust_min?: number\n robust_max?: number\n global_min?: number\n global_max?: number\n\n // TODO referenced by niivue/loadVolumes\n urlImgData?: string\n isManifest?: boolean\n limitFrames4D?: number\n\n // Original affine matrix stored at load time for reset functionality\n originalAffine?: number[][]\n\n constructor(\n // can be an array of Typed arrays or just a typed array. If an array of Typed arrays then it is assumed you are loading DICOM (perhaps the only real use case?)\n dataBuffer: ArrayBuffer | ArrayBuffer[] | ArrayBufferLike | null = null,\n name = '',\n colormap = 'gray',\n opacity = 1.0,\n pairedImgData: ArrayBuffer | null = null,\n cal_min = NaN,\n cal_max = NaN,\n trustCalMinMax = true,\n percentileFrac = 0.02,\n ignoreZeroVoxels = false,\n // TODO this was marked as true by default in the docs!\n useQFormNotSForm = false,\n colormapNegative = '',\n frame4D = 0,\n imageType = NVIMAGE_TYPE.UNKNOWN,\n cal_minNeg = NaN,\n cal_maxNeg = NaN,\n colorbarVisible = true,\n colormapLabel: LUT | null = null,\n colormapType = 0\n ) {\n this.init(\n dataBuffer,\n name,\n colormap,\n opacity,\n pairedImgData,\n cal_min,\n cal_max,\n trustCalMinMax,\n percentileFrac,\n ignoreZeroVoxels,\n useQFormNotSForm,\n colormapNegative,\n frame4D,\n imageType,\n cal_minNeg,\n cal_maxNeg,\n colorbarVisible,\n colormapLabel,\n colormapType\n )\n }\n\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n init(\n // can be an array of Typed arrays or just a typed array. If an array of Typed arrays then it is assumed you are loading DICOM (perhaps the only real use case?)\n dataBuffer: ArrayBuffer | ArrayBuffer[] | ArrayBufferLike | null = null,\n name = '',\n colormap = '',\n opacity = 1.0,\n _pairedImgData: ArrayBuffer | null = null,\n cal_min = NaN,\n cal_max = NaN,\n trustCalMinMax = true,\n percentileFrac = 0.02,\n ignoreZeroVoxels = false,\n useQFormNotSForm = false,\n colormapNegative = '',\n frame4D = 0,\n imageType = NVIMAGE_TYPE.UNKNOWN,\n cal_minNeg = NaN,\n cal_maxNeg = NaN,\n colorbarVisible = true,\n colormapLabel: LUT | null = null,\n colormapType = 0,\n imgRaw: ArrayBuffer | ArrayBufferLike | null = null\n ): void {\n const isNoColormap = colormap === ''\n if (isNoColormap) {\n colormap = 'gray'\n }\n this.name = name\n this.imageType = imageType\n this.id = uuidv4()\n this._colormap = colormap\n this._opacity = opacity > 1.0 ? 1.0 : opacity // make sure opacity can't be initialized greater than 1 see: #107 and #117 on github\n this.percentileFrac = percentileFrac\n this.ignoreZeroVoxels = ignoreZeroVoxels\n this.trustCalMinMax = trustCalMinMax\n this.colormapNegative = colormapNegative\n this.colormapLabel = colormapLabel\n this.frame4D = frame4D // indexed from 0!\n this.cal_minNeg = cal_minNeg\n this.cal_maxNeg = cal_maxNeg\n this.colorbarVisible = colorbarVisible\n this.colormapType = colormapType // COLORMAP_TYPE MIN_TO_MAX\n // TODO this was missing\n this.useQFormNotSForm = useQFormNotSForm\n // Added to support zerosLike\n // TODO this line causes an absurd amount of handling undefined fields - it would probably be better to isolate this as a separate class.\n if (!dataBuffer) {\n return\n }\n if (isNoColormap && this.hdr && this.hdr.intent_code === 1002) {\n colormap = 'random'\n this._colormap = colormap\n }\n if (this.hdr && typeof this.hdr.magic === 'number') {\n this.hdr.magic = 'n+1'\n } // fix for issue 481, where magic is set to the number 1 rather than a string\n this.nFrame4D = 1\n if (this.hdr) {\n for (let i = 4; i < 7; i++) {\n if (this.hdr.dims[i] > 1) {\n this.nFrame4D *= this.hdr.dims[i]\n }\n }\n }\n this.frame4D = Math.min(this.frame4D, this.nFrame4D - 1)\n this.nTotalFrame4D = this.nFrame4D\n\n if (!this.hdr || !imgRaw) {\n return\n }\n if (this.hdr.dims[1] === 0 && this.hdr.dims[2] === 0 && this.hdr.dims[3] === 0) {\n log.warn('Invalid volume: First three dimensions are all zero')\n }\n // e.g. 2D image has 1 slice, so dim[3] should be at least 1\n this.hdr.dims[1] = Math.max(this.hdr.dims[1], 1)\n this.hdr.dims[2] = Math.max(this.hdr.dims[2], 1)\n this.hdr.dims[3] = Math.max(this.hdr.dims[3], 1)\n\n this.nVox3D = this.hdr.dims[1] * this.hdr.dims[2] * this.hdr.dims[3]\n const bytesPerVol = this.nVox3D * (this.hdr.numBitsPerVoxel / 8)\n const nVol4D = imgRaw.byteLength / bytesPerVol\n if (nVol4D !== this.nFrame4D) {\n if (nVol4D > 0 && nVol4D * bytesPerVol === imgRaw.byteLength) {\n log.debug('Loading the first ' + nVol4D + ' of ' + this.nFrame4D + ' volumes')\n } else {\n log.warn('This header does not match voxel data', this.hdr, imgRaw.byteLength)\n }\n this.nFrame4D = nVol4D\n }\n\n // n.b. NIfTI standard says \"NIFTI_INTENT_RGB_VECTOR\" should be RGBA, but FSL only stores RGB\n if (\n (this.hdr.intent_code === NiiIntentCode.NIFTI_INTENT_VECTOR || this.hdr.intent_code === NiiIntentCode.NIFTI_INTENT_RGB_VECTOR) &&\n this.nFrame4D === 3 &&\n this.hdr.datatypeCode === NiiDataType.DT_FLOAT32\n ) {\n // change data from float32 to rgba32\n imgRaw = this.float32V1asRGBA(new Float32Array(imgRaw)).buffer as ArrayBuffer\n } // NIFTI_INTENT_VECTOR: this is a RGB tensor\n // Process affine matrix: validate, calculate from QForm if needed, repair if defective\n AffineProcessor.processAffine(this.hdr, useQFormNotSForm)\n // Swap bytes if foreign endian, then convert to appropriate typed array\n ImageDataProcessor.swapBytesIfNeeded(imgRaw, this.hdr)\n const conversionResult = ImageDataProcessor.convertDataType(imgRaw, this.hdr)\n this.img = conversionResult.img\n if (conversionResult.imaginary) {\n this.imaginary = conversionResult.imaginary\n }\n if (conversionResult.updatedDatatypeCode !== undefined) {\n this.hdr.datatypeCode = conversionResult.updatedDatatypeCode\n }\n if (conversionResult.updatedNumBitsPerVoxel !== undefined) {\n this.hdr.numBitsPerVoxel = conversionResult.updatedNumBitsPerVoxel\n }\n this.calculateRAS()\n // Store original affine for reset functionality\n this.originalAffine = copyAffine(this.hdr.affine)\n if (!isNaN(cal_min)) {\n this.hdr.cal_min = cal_min\n }\n if (!isNaN(cal_max)) {\n this.hdr.cal_max = cal_max\n }\n this.calMinMax()\n }\n\n static async new(\n // can be an array of Typed arrays or just a typed array. If an array of Typed arrays then it is assumed you are loading DICOM (perhaps the only real use case?)\n dataBuffer: ArrayBuffer | ArrayBuffer[] | ArrayBufferLike | null = null,\n name = '',\n colormap = '',\n opacity = 1.0,\n pairedImgData: ArrayBuffer | null = null,\n cal_min = NaN,\n cal_max = NaN,\n trustCalMinMax = true,\n percentileFrac = 0.02,\n ignoreZeroVoxels = false,\n useQFormNotSForm = false,\n colormapNegative = '',\n frame4D = 0,\n imageType = NVIMAGE_TYPE.UNKNOWN,\n cal_minNeg = NaN,\n cal_maxNeg = NaN,\n colorbarVisible = true,\n colormapLabel: LUT | null = null,\n colormapType = 0,\n zarrData: null | unknown\n ): Promise<NVImage> {\n const newImg = new NVImage()\n const re = /(?:\\.([^.]+))?$/\n let ext = re.exec(name)![1] || '' // TODO ! guaranteed?\n ext = ext.toUpperCase()\n if (ext === 'GZ') {\n ext = re.exec(name.slice(0, -3))![1] // img.trk.gz -> img.trk\n ext = ext.toUpperCase()\n }\n let imgRaw: ArrayBufferLike | Uint8Array | null = null\n if (imageType === NVIMAGE_TYPE.UNKNOWN) {\n imageType = NVIMAGE_TYPE.parse(ext)\n }\n if (dataBuffer instanceof ArrayBuffer && dataBuffer.byteLength >= 2 && imageType === NVIMAGE_TYPE.DCM) {\n // unknown extension defaults to DICOM, which starts `dcm`\n // since NIfTI1 is popular, lets make sure the filename has not been mangled\n const u8s = new Uint8Array(dataBuffer) // Create a view of the buffer\n const isNifti1 = (u8s[0] === 92 && u8s[1] === 1) || (u8s[1] === 92 && u8s[0] === 1)\n if (isNifti1) {\n imageType = NVIMAGE_TYPE.NII\n }\n }\n newImg.imageType = imageType\n switch (imageType) {\n case NVIMAGE_TYPE.DCM_FOLDER:\n case NVIMAGE_TYPE.DCM_MANIFEST:\n case NVIMAGE_TYPE.DCM:\n return\n case NVIMAGE_TYPE.FIB:\n ;[imgRaw, newImg.v1] = await ImageReaders.DsiStudio.readFIB(newImg, dataBuffer as ArrayBuffer)\n break\n case NVIMAGE_TYPE.MIH:\n case NVIMAGE_TYPE.MIF:\n imgRaw = await ImageReaders.Mrtrix.readMIF(newImg, dataBuffer as ArrayBuffer, pairedImgData) // detached\n break\n case NVIMAGE_TYPE.NHDR:\n case NVIMAGE_TYPE.NRRD:\n imgRaw = await ImageReaders.Nrrd.readNrrd(newImg, dataBuffer as ArrayBuffer)\n if (imgRaw === null) {\n throw new Error(`Failed to parse NHDR/NRRD file ${name}`)\n }\n break\n case NVIMAGE_TYPE.MHD:\n case NVIMAGE_TYPE.MHA:\n imgRaw = await ImageReaders.Itk.readMHA(newImg, dataBuffer as ArrayBuffer, pairedImgData)\n break\n case NVIMAGE_TYPE.MGH:\n case NVIMAGE_TYPE.MGZ:\n imgRaw = await ImageReaders.Mgh.readMgh(newImg, dataBuffer as ArrayBuffer, name)\n if (imgRaw === null) {\n throw new Error(`Failed to parse MGH/MGZ file ${name}`)\n }\n break\n case NVIMAGE_TYPE.SRC:\n imgRaw = await ImageReaders.DsiStudio.readSRC(newImg, dataBuffer as ArrayBuffer)\n break\n case NVIMAGE_TYPE.V:\n imgRaw = ImageReaders.Ecat.readECAT(newImg, dataBuffer as ArrayBuffer)\n break\n case NVIMAGE_TYPE.V16:\n imgRaw = ImageReaders.BrainVoyager.readV16(newImg, dataBuffer as ArrayBuffer)\n break\n case NVIMAGE_TYPE.VMR:\n imgRaw = ImageReaders.BrainVoyager.readVMR(newImg, dataBuffer as ArrayBuffer)\n break\n case NVIMAGE_TYPE.HEAD:\n imgRaw = await ImageReaders.Afni.readHEAD(newImg, dataBuffer as ArrayBuffer, pairedImgData) // paired = .BRIK\n break\n case NVIMAGE_TYPE.BMP:\n imgRaw = await ImageReaders.Image.readBMP(newImg, dataBuffer as ArrayBuffer)\n break\n case NVIMAGE_TYPE.NPY:\n imgRaw = await ImageReaders.Numpy.readNPY(newImg, dataBuffer as ArrayBuffer)\n break\n case NVIMAGE_TYPE.NPZ:\n imgRaw = await ImageReaders.Numpy.readNPZ(newImg, dataBuffer as ArrayBuffer)\n break\n case NVIMAGE_TYPE.ZARR:\n imgRaw = await ImageReaders.Zarr.readZARR(newImg, dataBuffer as ArrayBuffer, zarrData)\n break\n case NVIMAGE_TYPE.NII:\n imgRaw = await ImageReaders.Nii.readNifti(newImg, dataBuffer as ArrayBuffer, pairedImgData)\n if (imgRaw === null) {\n throw new Error(`Failed to parse NIfTI file ${name}.`)\n }\n break\n default:\n throw new Error('Image type not supported')\n }\n newImg.init(\n dataBuffer,\n name,\n colormap,\n opacity,\n pairedImgData,\n cal_min,\n cal_max,\n trustCalMinMax,\n percentileFrac,\n ignoreZeroVoxels,\n useQFormNotSForm,\n colormapNegative,\n frame4D,\n imageType,\n cal_minNeg,\n cal_maxNeg,\n colorbarVisible,\n colormapLabel,\n colormapType,\n imgRaw\n )\n return newImg\n }\n\n // not included in public docs\n // detect difference between voxel grid and world space\n // https://github.com/afni/afni/blob/25e77d564f2c67ff480fa99a7b8e48ec2d9a89fc/src/thd_coords.c#L717\n computeObliqueAngle(mtx44: mat4): number {\n return ImageOrientation.computeObliqueAngle(mtx44)\n }\n\n float32V1asRGBA(inImg: Float32Array): Uint8Array {\n return TensorProcessing.float32V1asRGBA(this, inImg)\n }\n\n loadImgV1(isFlipX: boolean = false, isFlipY: boolean = false, isFlipZ: boolean = false): boolean {\n return TensorProcessing.loadImgV1(this, isFlipX, isFlipY, isFlipZ)\n }\n\n // not included in public docs\n // detect difference between voxel grid and world space\n calculateOblique(): void {\n ImageOrientation.calculateOblique(this)\n }\n\n // not included in public docs\n // read DICOM format image and treat it like a NIfTI\n // -----------------\n // readDICOM(buf: ArrayBuffer | ArrayBuffer[]): ArrayBuffer {\n // this.series = new daikon.Series()\n // // parse DICOM file\n // if (Array.isArray(buf)) {\n // for (let i = 0; i < buf.length; i++) {\n // const dataview = new DataView(buf[i])\n // const image = daikon.Series.parseImage(dataview)\n // if (image === null) {\n // log.error(daikon.Series.parserError)\n // } else if (image.hasPixelData()) {\n // // if it's part of the same series, add it\n // if (this.series.images.length === 0 || image.getSeriesId() === this.series.images[0].getSeriesId()) {\n // this.series.addImage(image)\n // }\n // } // if hasPixelData\n // } // for i\n // } else {\n // // not a dicom folder drop\n // const image = daikon.Series.parseImage(new DataView(buf))\n // if (image === null) {\n // log.error(daikon.Series.parserError)\n // } else if (image.hasPixelData()) {\n // // if it's part of the same series, add it\n // if (this.series.images.length === 0 || image.getSeriesId() === this.series.images[0].getSeriesId()) {\n // this.series.addImage(image)\n // }\n // }\n // }\n // // order the image files, determines number of frames, etc.\n // this.series.buildSeries()\n // // output some header info\n // this.hdr = new nifti.NIFTI1()\n // const hdr = this.hdr\n // hdr.scl_inter = 0\n // hdr.scl_slope = 1\n // if (this.series.images[0].getDataScaleIntercept()) {\n // hdr.scl_inter = this.series.images[0].getDataScaleIntercept()\n // }\n // if (this.series.images[0].getDataScaleSlope()) {\n // hdr.scl_slope = this.series.images[0].getDataScaleSlope()\n // }\n // hdr.dims = [3, 1, 1, 1, 0, 0, 0, 0]\n // hdr.pixDims = [1, 1, 1, 1, 1, 0, 0, 0]\n // hdr.dims[1] = this.series.images[0].getCols()\n // hdr.dims[2] = this.series.images[0].getRows()\n // hdr.dims[3] = this.series.images[0].getNumberOfFrames()\n // if (this.series.images.length > 1) {\n // if (hdr.dims[3] > 1) {\n // log.debug('To Do: multiple slices per file and multiple files (XA30 DWI)')\n // }\n // hdr.dims[3] = this.series.images.length\n // }\n // const rc = this.series.images[0].getPixelSpacing() // TODO: order?\n // hdr.pixDims[1] = rc[0]\n // hdr.pixDims[2] = rc[1]\n // if (this.series.images.length > 1) {\n // // Multiple slices. The depth of a pixel is the physical distance between offsets. This is not the same as slice\n // // spacing for tilted slices (skew).\n // const p0 = vec3.fromValues(...(this.series.images[0].getImagePosition() as [number, number, number]))\n // const p1 = vec3.fromValues(...(this.series.images[1].getImagePosition() as [number, number, number]))\n // const n = vec3.fromValues(0, 0, 0)\n // vec3.subtract(n, p0, p1)\n // hdr.pixDims[3] = vec3.length(n)\n // } else {\n // // Single slice. Use the slice thickness as pixel depth.\n // hdr.pixDims[3] = this.series.images[0].getSliceThickness()\n // }\n // hdr.pixDims[4] = this.series.images[0].getTR() / 1000.0 // msec -> sec\n // const dt = this.series.images[0].getDataType() // 2=int,3=uint,4=float,\n // const bpv = this.series.images[0].getBitsAllocated()\n // hdr.numBitsPerVoxel = bpv\n // this.hdr.littleEndian = this.series.images[0].littleEndian\n // if (bpv === 8 && dt === 2) {\n // hdr.datatypeCode = NiiDataType.DT_INT8\n // } else if (bpv === 8 && dt === 3) {\n // hdr.datatypeCode = NiiDataType.DT_UINT8\n // } else if (bpv === 16 && dt === 2) {\n // hdr.datatypeCode = NiiDataType.DT_INT16\n // } else if (bpv === 16 && dt === 3) {\n // hdr.datatypeCode = NiiDataType.DT_UINT16\n // } else if (bpv === 32 && dt === 2) {\n // hdr.datatypeCode = NiiDataType.DT_INT32\n // } else if (bpv === 32 && dt === 3) {\n // hdr.datatypeCode = NiiDataType.DT_UINT32\n // } else if (bpv === 32 && dt === 4) {\n // hdr.datatypeCode = NiiDataType.DT_FLOAT32\n // } else if (bpv === 64 && dt === 4) {\n // hdr.datatypeCode = NiiDataType.DT_FLOAT64\n // } else if (bpv === 1) {\n // hdr.datatypeCode = NiiDataType.DT_BINARY\n // } else {\n // log.warn('Unsupported DICOM format: ' + dt + ' ' + bpv)\n // }\n // const voxelDimensions = hdr.pixDims.slice(1, 4)\n // const m = getBestTransform(\n // this.series.images[0].getImageDirections(),\n // voxelDimensions,\n // this.series.images[0].getImagePosition()\n // )\n // if (m) {\n // hdr.sform_code = 1\n // hdr.affine = [\n // [m[0][0], m[0][1], m[0][2], m[0][3]],\n // [m[1][0], m[1][1], m[1][2], m[1][3]],\n // [m[2][0], m[2][1], m[2][2], m[2][3]],\n // [0, 0, 0, 1]\n // ]\n // }\n // let data\n // let length = this.series.validatePixelDataLength(this.series.images[0])\n // const buffer = new Uint8Array(new ArrayBuffer(length * this.series.images.length))\n // // implementation copied from:\n // // https://github.com/rii-mango/Daikon/blob/bbe08bad9758dfbdf31ca22fb79048c7bad85706/src/series.js#L496\n // for (let i = 0; i < this.series.images.length; i++) {\n // if (this.series.isMosaic) {\n // data = this.series.getMosaicData(this.series.images[i], this.series.images[i].getPixelDataBytes())\n // } else {\n // data = this.series.images[i].getPixelDataBytes()\n // }\n // length = this.series.validatePixelDataLength(this.series.images[i])\n // this.series.images[i].clearPixelData()\n // buffer.set(new Uint8Array(data, 0, length), length * i)\n // } // for images.length\n // return buffer.buffer\n // } // readDICOM()\n // -----------------------\n\n // not included in public docs\n // read ECAT7 format image\n // https://github.com/openneuropet/PET2BIDS/tree/28aae3fab22309047d36d867c624cd629c921ca6/ecat_validation/ecat_info\n readECAT(buffer: ArrayBuffer): ArrayBuffer {\n return ImageReaders.Ecat.readECAT(this, buffer)\n }\n\n readV16(buffer: ArrayBuffer): ArrayBuffer {\n return ImageReaders.BrainVoyager.readV16(this, buffer)\n }\n\n async readNPY(buffer: ArrayBuffer): Promise<ArrayBuffer> {\n return ImageReaders.Numpy.readNPY(this, buffer)\n }\n\n async readNPZ(buffer: ArrayBuffer): Promise<ArrayBuffer | undefined> {\n return ImageReaders.Numpy.readNPZ(this, buffer)\n }\n\n async imageDataFromArrayBuffer(buffer: ArrayBuffer): Promise<ImageData> {\n return ImageReaders.Image.imageDataFromArrayBuffer(buffer)\n }\n\n async readBMP(buffer: ArrayBuffer): Promise<ArrayBuffer> {\n return ImageReaders.Image.readBMP(this, buffer)\n }\n\n async readZARR(buffer: ArrayBuffer, zarrData: unknown): Promise<ArrayBufferLike> {\n return ImageReaders.Zarr.readZARR(this, buffer, zarrData)\n }\n\n // not included in public docs\n // read brainvoyager format VMR image\n // https://support.brainvoyager.com/brainvoyager/automation-development/84-file-formats/343-developer-guide-2-6-the-format-of-vmr-files\n readVMR(buffer: ArrayBuffer): ArrayBuffer {\n return ImageReaders.BrainVoyager.readVMR(this, buffer)\n }\n\n // not included in public docs\n // read DSI-Studio FIB format image\n // https://dsi-studio.labsolver.org/doc/cli_data.html\n async readFIB(buffer: ArrayBuffer): Promise<[ArrayBuffer, Float32Array]> {\n return ImageReaders.DsiStudio.readFIB(this, buffer)\n }\n\n // not included in public docs\n // read DSI-Studio SRC format image\n // https://dsi-studio.labsolver.org/doc/cli_data.html\n async readSRC(buffer: ArrayBuffer): Promise<ArrayBuffer> {\n return ImageReaders.DsiStudio.readSRC(this, buffer)\n }\n\n // not included in public docs\n // read AFNI head/brik format image\n async readHEAD(dataBuffer: ArrayBuffer, pairedImgData: ArrayBuffer | null): Promise<ArrayBuffer> {\n return ImageReaders.Afni.readHEAD(this, dataBuffer, pairedImgData)\n }\n\n // not included in public docs\n // read ITK MHA format image\n // https://itk.org/Wiki/ITK/MetaIO/Documentation#Reading_a_Brick-of-Bytes_.28an_N-Dimensional_volume_in_a_single_file.29\n async readMHA(buffer: ArrayBuffer, pairedImgData: ArrayBuffer | null): Promise<ArrayBuffer> {\n return ImageReaders.Itk.readMHA(this, buffer, pairedImgData)\n }\n\n // not included in public docs\n // read mrtrix MIF format image\n // https://mrtrix.readthedocs.io/en/latest/getting_started/image_data.html#mrtrix-image-formats\n async readMIF(buffer: ArrayBuffer, pairedImgData: ArrayBuffer | null): Promise<ArrayBuffer> {\n return ImageReaders.Mrtrix.readMIF(this, buffer, pairedImgData)\n }\n\n // not included in public docs\n // Transform to orient NIfTI image to Left->Right,Posterior->Anterior,Inferior->Superior (48 possible permutations)\n calculateRAS(): void {\n ImageOrientation.calculateRAS(this)\n }\n\n /**\n * Get a deep copy of the current affine matrix.\n * @returns A 4x4 affine matrix as a 2D array (row-major)\n */\n getAffine(): number[][] {\n if (!this.hdr) {\n throw new Error('Image header not loaded')\n }\n return copyAffine(this.hdr.affine)\n }\n\n /**\n * Set a new affine matrix and recalculate all derived RAS matrices.\n * Call updateGLVolume() on the Niivue instance after this to update rendering.\n * @param affine - A 4x4 affine matrix as a 2D array (row-major)\n */\n setAffine(affine: number[][]): void {\n if (!this.hdr) {\n throw new Error('Image header not loaded')\n }\n this.hdr.affine = copyAffine(affine)\n this.calculateRAS()\n }\n\n /**\n * Apply a transform (translation, rotation, scale) to the current affine matrix.\n * The transform is applied in world coordinate space: newAffine = transform * currentAffine\n * Call updateGLVolume() on the Niivue instance after this to update rendering.\n * @param transform - Transform to apply with translation (mm), rotation (degrees), and scale\n */\n applyTransform(transform: AffineTransform): void {\n if (!this.hdr) {\n throw new Error('Image header not loaded')\n }\n const transformMatrix = createTransformMatrix(transform)\n const newAffine = multiplyAffine(this.hdr.affine, transformMatrix)\n this.hdr.affine = newAffine\n this.calculateRAS()\n }\n\n /**\n * Reset the affine matrix to its original state when the image was first loaded.\n * Call updateGLVolume() on the Niivue instance after this to update rendering.\n */\n resetAffine(): void {\n if (!this.hdr) {\n throw new Error('Image header not loaded')\n }\n if (!this.originalAffine) {\n throw new Error('Original affine not stored')\n }\n this.hdr.affine = copyAffine(this.originalAffine)\n this.calculateRAS()\n }\n\n // Reorient raw header data to RAS\n // assume single volume, use nVolumes to specify, set nVolumes = 0 for same as input\n\n async hdr2RAS(nVolumes: number = 1): Promise<NIFTI1 | NIFTI2> {\n return ImageOrientation.hdr2RAS(this, nVolumes)\n }\n\n // Reorient raw image data to RAS\n // note that GPU-based orient shader is much faster\n // returns single 3D volume even for 4D input. Use nVolume to select volume (0 indexed)\n img2RAS(nVolume: number = 0): TypedVoxelArray {\n return ImageOrientation.img2RAS(this, nVolume)\n }\n\n // not included in public docs\n // convert voxel location (row, column slice, indexed from 0) to world space\n vox2mm(XYZ: number[], mtx: mat4): vec3 {\n return CoordinateTransform.vox2mm(this, XYZ, mtx)\n } // vox2mm()\n\n // not included in public docs\n // convert world space to voxel location (row, column slice, indexed from 0)\n mm2vox(mm: number[], frac = false): Float32Array | vec3 {\n return CoordinateTransform.mm2vox(this, mm, frac)\n } // mm2vox()\n\n // not included in public docs\n // returns boolean: are two arrays identical?\n // TODO this won't work for complex objects. Maybe use array-equal from NPM\n arrayEquals(a: unknown[], b: unknown[]): boolean {\n return CoordinateTransform.arrayEquals(a, b)\n }\n\n // not included in public docs\n // base function for niivue.setColormap()\n // colormaps are continuously interpolated between 256 values (0..256)\n setColormap(cm: string): void {\n ColormapManager.setColormap(this, cm)\n }\n\n // not included in public docs\n // base function for niivue.setColormap()\n // label colormaps are discretely sampled from an arbitrary number of colors\n setColormapLabel(cm: ColorMap): void {\n ColormapManager.setColormapLabel(this, cm)\n }\n\n async setColormapLabelFromUrl(url: string): Promise<void> {\n return ColormapManager.setColormapLabelFromUrl(this, url)\n }\n\n get colormap(): string {\n return ColormapManager.getColormap(this)\n }\n\n get colorMap(): string {\n return ColormapManager.getColormap(this)\n }\n\n // TODO duplicate fields, see niivue/loadDocument\n set colormap(cm: string) {\n ColormapManager.setColormap(this, cm)\n }\n\n set colorMap(cm: string) {\n ColormapManager.setColormap(this, cm)\n }\n\n get opacity(): number {\n return ColormapManager.getOpacity(this)\n }\n\n set opacity(opacity) {\n ColormapManager.setOpacity(this, opacity)\n }\n\n /**\n * set contrast/brightness to robust range (2%..98%)\n * @param vol - volume for estimate (use -1 to use estimate on all loaded volumes; use INFINITY for current volume)\n * @param isBorder - if true (default) only center of volume used for estimate\n * @returns volume brightness and returns array [pct2, pct98, mnScale, mxScale]\n * @see {@link https://niivue.com/demos/features/timeseries2.html | live demo usage}\n */\n calMinMax(vol: number = Number.POSITIVE_INFINITY, isBorder: boolean = true): number[] {\n return IntensityCalibration.calMinMax(this, vol, isBorder)\n }\n\n // not included in public docs\n // convert voxel intensity from stored value to scaled intensity\n intensityRaw2Scaled(raw: number): number {\n return IntensityCalibration.intensityRaw2Scaled(this, raw)\n }\n\n // convert voxel intensity from scaled intensity to stored value\n intensityScaled2Raw(scaled: number): number {\n return IntensityCalibration.intensityScaled2Raw(this, scaled)\n }\n\n /**\n * Converts NVImage to NIfTI compliant byte array, potentially compressed.\n * Delegates to ImageWriter.saveToUint8Array.\n */\n async saveToUint8Array(fnm: string, drawing8: Uint8Array | null = null): Promise<Uint8Array> {\n // Delegate to the writer module, passing the instance 'this'\n return ImageWriter.saveToUint8Array(this, fnm, drawing8)\n }\n\n /**\n * save image as NIfTI volume and trigger download.\n * Delegates to ImageWriter.saveToDisk.\n */\n async saveToDisk(fnm: string = '', drawing8: Uint8Array | null = null): Promise<Uint8Array> {\n // Delegate to the writer module, passing the instance 'this'\n return ImageWriter.saveToDisk(this, fnm, drawing8)\n }\n\n static async fetchDicomData(url: string, headers: Record<string, string> = {}): Promise<Array<{ name: string; data: ArrayBuffer }>> {\n return ImageFactory.fetchDicomData(url, headers)\n }\n\n static async readFirstDecompressedBytes(stream: ReadableStream<Uint8Array>, minBytes: number): Promise<Uint8Array> {\n return ImageFactory.readFirstDecompressedBytes(stream, minBytes)\n }\n\n static extractFilenameFromUrl(url: string): string | null {\n return ImageFactory.extractFilenameFromUrl(url)\n }\n\n static async loadInitialVolumesGz(url = '', headers = {}, limitFrames4D = NaN): Promise<ArrayBuffer | null> {\n return ImageFactory.loadInitialVolumesGz(url, headers, limitFrames4D)\n }\n\n static async loadInitialVolumes(url = '', headers = {}, limitFrames4D = NaN): Promise<ArrayBuffer | null> {\n return ImageFactory.loadInitialVolumes(url, headers, limitFrames4D)\n }\n\n /**\n * factory function to load and return a new NVImage instance from a given URL\n */\n static async loadFromUrl({\n url = '',\n urlImgData = '',\n headers = {},\n name = '',\n colormap = '',\n opacity = 1.0,\n cal_min = NaN,\n cal_max = NaN,\n trustCalMinMax = true,\n percentileFrac = 0.02,\n ignoreZeroVoxels = false,\n useQFormNotSForm = false,\n colormapNegative = '',\n frame4D = 0,\n isManifest = false,\n limitFrames4D = NaN,\n imageType = NVIMAGE_TYPE.UNKNOWN,\n colorbarVisible = true,\n buffer = new ArrayBuffer(0),\n zarrLevel,\n zarrMaxVolumeSize,\n zarrChannel,\n zarrConvertUnits\n }: Partial<Omit<ImageFromUrlOptions, 'url'>> & { url?: string | Uint8Array | ArrayBuffer } = {}): Promise<NVImage> {\n if (url === '') {\n throw Error('url must not be empty')\n }\n let nvimage = null\n let dataBuffer = null\n let zarrData: null | unknown = null\n\n // Handle input buffer types\n if (url instanceof Uint8Array) {\n url = url.slice().buffer as ArrayBuffer\n }\n if (buffer.byteLength > 0) {\n url = buffer\n }\n if (url instanceof ArrayBuffer) {\n dataBuffer = url\n if (name !== '') {\n url = name\n } else {\n const bytes = new Uint8Array(dataBuffer)\n url = bytes[0] === 31 && bytes[1] === 139 ? 'array.nii.gz' : 'array.nii'\n }\n }\n // Resolve paired image URL if necessary\n let ext = ''\n if (name === '') {\n ext = ImageFactory.getPrimaryExtension(url)\n } else {\n ext = ImageFactory.getPrimaryExtension(name)\n }\n if (imageType === NVIMAGE_TYPE.UNKNOWN) {\n imageType = NVIMAGE_TYPE.parse(ext)\n }\n if (imageType === NVIMAGE_TYPE.UNKNOWN && typeof url === 'string') {\n // perhaps we are not identifying an extension because the url is a redirect\n const response = await fetch(url, {})\n if (response.redirected) {\n const rname = this.extractFilenameFromUrl(response.url)\n if (rname && rname.length > 0) {\n if (name === '') {\n name = rname\n ext = ImageFactory.getPrimaryExtension(name)\n imageType = NVIMAGE_TYPE.parse(ext)\n }\n }\n }\n }\n // try url and name attributes to test for .zarr\n if (imageType === NVIMAGE_TYPE.ZARR) {\n if (zarrLevel !== undefined) {\n // Chunked path: create virtual volume with helper\n return await NVImage.createChunkedZarr(url as string, {\n level: zarrLevel,\n maxVolumeSize: zarrMaxVolumeSize,\n channel: zarrChannel,\n convertUnitsToMm: zarrConvertUnits,\n colormap,\n opacity\n })\n }\n // Light path: load entire array (unchanged)\n const zarrResult = await ZarrProcessor.loadZarrData(url)\n dataBuffer = zarrResult.dataBuffer\n zarrData = zarrResult.zarrData\n }\n // DICOM assigned for unknown extensions: therefore test signature to see if mystery file is NIfTI\n const isTestNIfTI = imageType === NVIMAGE_TYPE.DCM || NVIMAGE_TYPE.NII\n if (!dataBuffer && isTestNIfTI) {\n dataBuffer = await this.loadInitialVolumes(url, headers, limitFrames4D)\n }\n // Handle non-limited cases\n if (!dataBuffer) {\n if (isManifest) {\n dataBuffer = await NVImage.fetchDicomData(url, headers)\n imageType = NVIMAGE_TYPE.DCM_MANIFEST\n } else {\n dataBuffer = await StreamingLoader.fetchAndStreamData(url, headers)\n }\n }\n // Handle paired image data for formats with separate header/data files\n const pairedUrl = StreamingLoader.getPairedImageUrl(url, ext.toUpperCase(), urlImgData)\n let pairedImgData = null\n if (pairedUrl) {\n pairedImgData = await StreamingLoader.fetchPairedImageData(pairedUrl, headers)\n }\n\n if (!dataBuffer) {\n throw new Error('Unable to load buffer properly from volume')\n }\n\n // Get filename from URL if not provided\n if (!name) {\n let urlParts: string[]\n try {\n // if a full url like https://domain/path/file.nii.gz?query=filter\n // parse the url and get the pathname component without the query\n urlParts = new URL(url).pathname.split('/')\n } catch (e) {\n // if a relative url then parse the path (assuming no query)\n urlParts = url.split('/')\n }\n name = urlParts.slice(-1)[0] // name will be last part of url (e.g. some/url/image.nii.gz --> image.nii.gz\n if (name.indexOf('?') > -1) {\n name = name.slice(0, name.indexOf('?')) // remove query string if any\n }\n }\n nvimage = await this.new(\n dataBuffer,\n name,\n colormap,\n opacity,\n pairedImgData,\n cal_min,\n cal_max,\n trustCalMinMax,\n percentileFrac,\n ignoreZeroVoxels,\n useQFormNotSForm,\n colormapNegative,\n frame4D,\n imageType,\n NaN,\n NaN,\n true,\n null,\n 0,\n zarrData\n )\n nvimage.url = url\n nvimage.colorbarVisible = colorbarVisible\n return nvimage\n }\n\n /**\n * Factory method: create a chunked zarr NVImage with an attached NVZarrHelper.\n */\n static async createChunkedZarr(\n url: string,\n options: {\n level: number\n maxVolumeSize?: number\n maxTextureSize?: number\n channel?: number\n cacheSize?: number\n convertUnitsToMm?: boolean\n colormap?: string\n opacity?: number\n }\n ): Promise<NVImage> {\n const nvimage = new NVImage()\n nvimage.zarrHelper = await NVZarrHelper.create(nvimage, url, {\n url,\n level: options.level,\n maxVolumeSize: options.maxVolumeSize,\n maxTextureSize: options.maxTextureSize,\n channel: options.channel,\n cacheSize: options.cacheSize,\n convertUnitsToMm: options.convertUnitsToMm\n })\n if (options.colormap) {\n nvimage._colormap = options.colormap\n }\n if (options.opacity !== undefined) {\n nvimage._opacity = options.opacity\n }\n nvimage.url = url\n return nvimage\n }\n\n // not included in public docs\n // loading Nifti files\n static async readFileAsync(file: File, bytesToLoad = NaN): Promise<ArrayBuffer> {\n return ImageFactory.readFileAsync(file, bytesToLoad)\n }\n\n /**\n * factory function to load and return a new NVImage instance from a file in the browser\n */\n static async loadFromFile({\n file, // file can be an array of file objects or a single file object\n name = '',\n colormap = '',\n opacity = 1.0,\n urlImgData = null,\n cal_min = NaN,\n cal_max = NaN,\n trustCalMinMax = true,\n percentileFrac = 0.02,\n ignoreZeroVoxels = false,\n useQFormNotSForm = false,\n colormapNegative = '',\n frame4D = 0,\n limitFrames4D = NaN,\n imageType = NVIMAGE_TYPE.UNKNOWN\n }: ImageFromFileOptions): Promise<NVImage> {\n let nvimage: NVImage | null = null\n let dataBuffer: ArrayBuffer | ArrayBuffer[] = []\n try {\n if (Array.isArray(file)) {\n dataBuffer = await Promise.all(file.map((f) => this.readFileAsync(f)))\n } else {\n if (!isNaN(limitFrames4D)) {\n const headerBuffer = await this.readFileAsync(file, 512)\n const headerView = new Uint8Array(headerBuffer)\n\n const isNifti1 = (headerView[0] === 92 && headerView[1] === 1) || (headerView[1] === 92 && headerView[0] === 1)\n\n if (!isNifti1) {\n dataBuffer = await this.readFileAsync(file)\n } else {\n const hdr = await readHeaderAsync(headerBuffer)\n if (!hdr) {\n throw new Error('could not read nifti header')\n }\n\n const nBytesPerVoxel = hdr.numBitsPerVoxel / 8\n const nVox3D = [1, 2, 3].reduce((acc, i) => acc * (hdr.dims[i] > 1 ? hdr.dims[i] : 1), 1)\n const nFrame4D = [4, 5, 6].reduce((acc, i) => acc * (hdr.dims[i] > 1 ? hdr.dims[i] : 1), 1)\n\n const volsToLoad = Math.max(Math.min(limitFrames4D, nFrame4D), 1)\n const bytesToLoad = hdr.vox_offset + volsToLoad * nVox3D * nBytesPerVoxel\n dataBuffer = await this.readFileAsync(file, bytesToLoad)\n }\n } else {\n dataBuffer = await this.readFileAsync(file)\n }\n name = file.name\n }\n let pairedImgData = null\n if (urlImgData) {\n // @ts-expect-error check data type?\n pairedImgData = await this.readFileAsync(urlImgData)\n }\n nvimage = await this.new(\n dataBuffer,\n name,\n colormap,\n opacity,\n pairedImgData,\n cal_min,\n cal_max,\n trustCalMinMax,\n percentileFrac,\n ignoreZeroVoxels,\n useQFormNotSForm,\n colormapNegative,\n frame4D,\n imageType,\n NaN,\n NaN,\n true,\n null,\n 0,\n null\n )\n // add a reference to the file object as a new property of the NVImage instance\n // is this too hacky?\n nvimage.fileObject = file\n } catch (err) {\n log.error(err)\n throw new Error('could not build NVImage')\n }\n if (nvimage === null) {\n throw new Error('could not build NVImage')\n }\n return nvimage\n }\n\n /**\n * Creates a Uint8Array representing a NIFTI file (header + optional image data).\n * Delegates to ImageWriter.createNiftiArray.\n */\n static createNiftiArray(\n dims: number[] = [256, 256, 256],\n pixDims: number[] = [1, 1, 1],\n affine: number[] = [1, 0, 0, -128, 0, 1, 0, -128, 0, 0, 1, -128, 0, 0, 0, 1],\n datatypeCode = NiiDataType.DT_UINT8,\n img: TypedVoxelArray | Uint8Array = new Uint8Array()\n ): Uint8Array {\n return ImageWriter.createNiftiArray(dims, pixDims, affine, datatypeCode, img)\n }\n\n /**\n * Creates a NIFTI1 header object with basic properties.\n * Delegates to ImageWriter.createNiftiHeader.\n */\n static createNiftiHeader(\n dims: number[] = [256, 256, 256],\n pixDims: number[] = [1, 1, 1],\n affine: number[] = [1, 0, 0, -128, 0, 1, 0, -128, 0, 0, 1, -128, 0, 0, 0, 1],\n datatypeCode = NiiDataType.DT_UINT8\n ): NIFTI1 {\n return ImageWriter.createNiftiHeader(dims, pixDims, affine, datatypeCode)\n }\n\n /**\n * read a 3D slab of voxels from a volume\n * @see {@link https://niivue.com/demos/features/slab_selection.html | live demo usage}\n */\n\n /**\n * read a 3D slab of voxels from a volume, specified in RAS coordinates.\n * Delegates to VolumeUtils.getVolumeData.\n */\n getVolumeData(voxStart: number[] = [-1, 0, 0], voxEnd: number[] = [0, 0, 0], dataType = 'same'): [TypedVoxelArray, number[]] {\n return VolumeUtils.getVolumeData(this, voxStart, voxEnd, dataType)\n }\n\n /**\n * write a 3D slab of voxels from a volume\n * @see {@link https://niivue.com/demos/features/slab_selection.html | live demo usage}\n */\n\n /**\n * write a 3D slab of voxels from a volume, specified in RAS coordinates.\n * Delegates to VolumeUtils.setVolumeData.\n * Input slabData is assumed to be in the correct raw data type for the target image.\n */\n setVolumeData(voxStart: number[] = [-1, 0, 0], voxEnd: number[] = [0, 0, 0], img: TypedVoxelArray = new Uint8Array()): void {\n VolumeUtils.setVolumeData(this, voxStart, voxEnd, img)\n }\n\n /**\n * factory function to load and return a new NVImage instance from a base64 encoded string\n * @example\n * myImage = NVImage.loadFromBase64('SomeBase64String')\n */\n static async loadFromBase64({\n base64,\n name = '',\n colormap = '',\n opacity = 1.0,\n cal_min = NaN,\n cal_max = NaN,\n trustCalMinMax = true,\n percentileFrac = 0.02,\n ignoreZeroVoxels = false,\n useQFormNotSForm = false,\n colormapNegative = '',\n frame4D = 0,\n imageType = NVIMAGE_TYPE.UNKNOWN,\n cal_minNeg = NaN,\n cal_maxNeg = NaN,\n colorbarVisible = true,\n colormapLabel = null\n }: ImageFromBase64): Promise<NVImage> {\n let nvimage = null\n try {\n const dataBuffer = ImageFactory.base64ToArrayBuffer(base64)\n const pairedImgData = null\n nvimage = await this.new(\n dataBuffer,\n name,\n colormap,\n opacity,\n pairedImgData,\n cal_min,\n cal_max,\n trustCalMinMax,\n percentileFrac,\n ignoreZeroVoxels,\n useQFormNotSForm,\n colormapNegative,\n frame4D,\n imageType,\n cal_minNeg,\n cal_maxNeg,\n colorbarVisible,\n colormapLabel,\n 0,\n null\n )\n } catch (err) {\n log.debug(err)\n }\n\n if (nvimage === null) {\n throw new Error('could not load NVImage')\n }\n\n return nvimage\n }\n\n /**\n * make a clone of a NVImage instance and return a new NVImage\n * @example\n * myImage = NVImage.loadFromFile(SomeFileObject) // files can be from dialogs or drag and drop\n * clonedImage = myImage.clone()\n */\n clone(): NVImage {\n const clonedImage = new NVImage()\n // important! the clone should have a new ID to avoid conflicts\n // when referencing images by ID. A user could add the cloned\n // image as a viewable volume in any order.\n clonedImage.id = uuidv4()\n clonedImage.hdr = Object.assign({}, this.hdr)\n clonedImage.img = this.img!.slice()\n clonedImage.calculateRAS()\n clonedImage.calMinMax()\n return clonedImage\n }\n\n /**\n * fill a NVImage instance with zeros for the image data\n * @example\n * myImage = NVImage.loadFromFile(SomeFileObject) // files can be from dialogs or drag and drop\n * clonedImageWithZeros = myImage.clone().zeroImage()\n */\n zeroImage(): void {\n this.img!.fill(0)\n }\n\n /**\n * get nifti specific metadata about the image\n */\n getImageMetadata(): ImageMetadata {\n return ImageMetadataModule.getImageMetadata(this)\n }\n\n /**\n * a factory function to make a zero filled image given a NVImage as a reference\n * @example\n * myImage = NVImage.loadFromFile(SomeFileObject) // files can be from dialogs or drag and drop\n * newZeroImage = NVImage.zerosLike(myImage)\n */\n static zerosLike(nvImage: NVImage, dataType = 'same'): NVImage {\n // dataType can be: 'same', 'uint8'\n // 'same' means that the zeroed image data type is the same as the input image\n const zeroClone = nvImage.clone()\n zeroClone.zeroImage()\n if (dataType === 'uint8') {\n zeroClone.img = Uint8Array.from(zeroClone.img!)\n zeroClone.hdr!.datatypeCode = NiiDataType.DT_UINT8\n zeroClone.hdr!.numBitsPerVoxel = 8\n }\n if (dataType === 'float32') {\n zeroClone.img = Float32Array.from(zeroClone.img!)\n zeroClone.hdr!.datatypeCode = NiiDataType.DT_FLOAT32\n zeroClone.hdr!.numBitsPerVoxel = 32\n }\n return zeroClone\n }\n\n /**\n * Returns voxel intensity at specific native coordinates.\n * Delegates to VolumeUtils.getValue.\n */\n getValue(x: number, y: number, z: number, frame4D = 0, isReadImaginary = false): number {\n return VolumeUtils.getValue(this, x, y, z, frame4D, isReadImaginary)\n }\n\n /**\n * Returns voxel intensities at specific native coordinates.\n * Delegates to VolumeUtils.getValue.\n */\n getValues(x: number, y: number, z: number, frame4D = 0, isReadImaginary = false): number[] {\n return VolumeUtils.getValues(this, x, y, z, frame4D, isReadImaginary)\n }\n\n /**\n * Update options for image\n */\n applyOptionsUpdate(options: ImageFromUrlOptions): void {\n ImageMetadataModule.applyOptionsUpdate(this, options)\n }\n\n getImageOptions(): ImageFromUrlOptions {\n return ImageMetadataModule.getImageOptions(this)\n }\n\n /**\n * Converts NVImage to NIfTI compliant byte array.\n * Handles potential re-orientation of drawing data.\n * Delegates to ImageWriter.toUint8Array.\n */\n toUint8Array(drawingBytes: Uint8Array | null = null): Uint8Array {\n // Delegate to the writer module, passing the instance 'this'\n return ImageWriter.toUint8Array(this, drawingBytes)\n }\n\n // not included in public docs\n convertVox2Frac(vox: vec3): vec3 {\n return CoordinateTransform.convertVox2Frac(this, vox)\n }\n\n // not included in public docs\n convertFrac2Vox(frac: vec3): vec3 {\n return CoordinateTransform.convertFrac2Vox(this, frac)\n }\n\n // not included in public docs\n convertFrac2MM(frac: vec3, isForceSliceMM = false): vec4 {\n return CoordinateTransform.convertFrac2MM(this, frac, isForceSliceMM)\n }\n\n // not included in public docs\n convertMM2Frac(mm: vec3 | vec4, isForceSliceMM = false): vec3 {\n return CoordinateTransform.convertMM2Frac(this, mm, isForceSliceMM)\n }\n}\n","import { vec3 } from 'gl-matrix'\nimport { log } from '@/logger'\nimport { NiftiHeader } from '@/types'\nimport { LUT } from '@/colortables'\n\nexport const isPlatformLittleEndian = (): boolean => {\n // inspired by https://github.com/rii-mango/Papaya\n const buffer = new ArrayBuffer(2)\n new DataView(buffer).setInt16(0, 256, true)\n return new Int16Array(buffer)[0] === 256\n}\n\n/**\n * Enum for NIfTI datatype codes\n * // https://nifti.nimh.nih.gov/pub/dist/src/niftilib/nifti1.h\n */\nexport enum NiiDataType {\n DT_NONE = 0,\n DT_BINARY = 1,\n DT_UINT8 = 2,\n DT_INT16 = 4,\n DT_INT32 = 8,\n DT_FLOAT32 = 16,\n DT_COMPLEX64 = 32,\n DT_FLOAT64 = 64,\n DT_RGB24 = 128,\n DT_INT8 = 256,\n DT_UINT16 = 512,\n DT_UINT32 = 768,\n DT_INT64 = 1024,\n DT_UINT64 = 1280,\n DT_FLOAT128 = 1536,\n DT_COMPLEX128 = 1792,\n DT_COMPLEX256 = 2048,\n DT_RGBA32 = 2304\n}\n\n/**\n * Enum for NIfTI intent codes\n * // https://nifti.nimh.nih.gov/pub/dist/src/niftilib/nifti1.h\n */\nexport enum NiiIntentCode {\n NIFTI_INTENT_LABEL = 1002,\n NIFTI_INTENT_VECTOR = 1007,\n NIFTI_INTENT_RGB_VECTOR = 2003\n}\n\n/**\n * Enum for supported image types (e.g. NII, NRRD, DICOM)\n */\nexport enum ImageType {\n UNKNOWN = 0,\n NII = 1,\n DCM = 2,\n DCM_MANIFEST = 3,\n MIH = 4,\n MIF = 5,\n NHDR = 6,\n NRRD = 7,\n MHD = 8,\n MHA = 9,\n MGH = 10,\n MGZ = 11,\n V = 12,\n V16 = 13,\n VMR = 14,\n HEAD = 15,\n DCM_FOLDER = 16,\n SRC = 17,\n FIB = 18,\n BMP = 19,\n ZARR = 20,\n NPY = 21,\n NPZ = 22,\n HDR = 23\n}\n\nexport const NVIMAGE_TYPE = Object.freeze({\n ...ImageType,\n parse: (ext: string) => {\n let imageType: ImageType = ImageType.UNKNOWN\n switch (ext.toUpperCase()) {\n case '':\n case 'DCM':\n imageType = ImageType.DCM\n break\n case 'TXT':\n imageType = ImageType.DCM_MANIFEST\n break\n case 'FZ':\n case 'GQI':\n case 'QSDR':\n case 'FIB':\n imageType = ImageType.FIB\n break\n case 'HDR':\n case 'NII':\n imageType = ImageType.NII\n break\n case 'MIH':\n imageType = ImageType.MIH\n break\n case 'MIF':\n imageType = ImageType.MIF\n break\n case 'NHDR':\n imageType = ImageType.NHDR\n break\n case 'NRRD':\n imageType = ImageType.NRRD\n break\n case 'MHD':\n imageType = ImageType.MHD\n break\n case 'MHA':\n imageType = ImageType.MHA\n break\n case 'MGH':\n imageType = ImageType.MGH\n break\n case 'MGZ':\n imageType = ImageType.MGZ\n break\n case 'NPY':\n imageType = ImageType.NPY\n break\n case 'NPZ':\n imageType = ImageType.NPZ\n break\n case 'SRC':\n imageType = ImageType.SRC\n break\n case 'V':\n imageType = ImageType.V\n break\n case 'V16':\n imageType = ImageType.V16\n break\n case 'VMR':\n imageType = ImageType.VMR\n break\n case 'HEAD':\n imageType = ImageType.HEAD\n break\n case 'PNG':\n case 'BMP':\n case 'GIF':\n case 'JPG':\n case 'JPEG':\n imageType = ImageType.BMP\n break\n case 'ZARR':\n imageType = ImageType.ZARR\n break\n }\n return imageType\n }\n})\n\nexport type ImageFromUrlOptions = {\n // the resolvable URL pointing to a nifti image to load\n url: string\n // Allows loading formats where header and image are separate files (e.g. nifti.hdr, nifti.img)\n urlImageData?: string\n // headers to use in the fetch call\n headers?: Record<string, string>\n // a name for this image (defaults to empty)\n name?: string\n // a color map to use (defaults to gray)\n colorMap?: string\n // TODO see duplicate usage in niivue/loadDocument\n colormap?: string\n // the opacity for this image (defaults to 1)\n opacity?: number\n // minimum intensity for color brightness/contrast\n cal_min?: number\n // maximum intensity for color brightness/contrast\n cal_max?: number\n // whether or not to trust cal_min and cal_max from the nifti header (trusting results in faster loading, defaults to true)\n trustCalMinMax?: boolean\n // the percentile to use for setting the robust range of the display values (smart intensity setting for images with large ranges, defaults to 0.02)\n percentileFrac?: number\n // whether or not to use QForm over SForm constructing the NVImage instance (defaults to false)\n useQFormNotSForm?: boolean\n // if true, values below cal_min are shown as translucent, not transparent (defaults to false)\n alphaThreshold?: boolean\n // a color map to use for negative intensities\n colormapNegative?: string\n // backwards compatible option\n colorMapNegative?: string\n // minimum intensity for colormapNegative brightness/contrast (NaN for symmetrical cal_min)\n cal_minNeg?: number\n // maximum intensity for colormapNegative brightness/contrast (NaN for symmetrical cal_max)\n cal_maxNeg?: number\n // show/hide colormaps (defaults to true)\n colorbarVisible?: boolean\n // TODO the following fields were not documented\n ignoreZeroVoxels?: boolean\n imageType?: ImageType\n frame4D?: number\n colormapLabel?: LUT | null\n pairedImgData?: null\n limitFrames4D?: number\n isManifest?: boolean\n urlImgData?: string\n buffer?: ArrayBuffer\n // Zarr chunked loading options\n zarrLevel?: number\n zarrMaxVolumeSize?: number\n zarrChannel?: number\n /** Convert OME spatial units to millimeters for NIfTI compatibility (default: true) */\n zarrConvertUnits?: boolean\n}\n\n// TODO centralize shared options\nexport type ImageFromFileOptions = {\n // the file object\n file: File | File[]\n // a name for this image. Default is an empty string\n name?: string\n // a color map to use. default is gray\n colormap?: string\n // the opacity for this image. default is 1\n opacity?: number\n // Allows loading formats where header and image are separate files (e.g. nifti.hdr, nifti.img)\n urlImgData?: File | null | FileSystemEntry\n // minimum intensity for color brightness/contrast\n cal_min?: number\n // maximum intensity for color brightness/contrast\n cal_max?: number\n // whether or not to trust cal_min and cal_max from the nifti header (trusting results in faster loading)\n trustCalMinMax?: boolean\n // the percentile to use for setting the robust range of the display values (smart intensity setting for images with large ranges)\n percentileFrac?: number\n // whether or not to ignore zero voxels in setting the robust range of display values\n ignoreZeroVoxels?: boolean\n // whether or not to use QForm instead of SForm during construction\n useQFormNotSForm?: boolean\n // colormap negative for the image\n colormapNegative?: string\n // image type\n imageType?: ImageType\n frame4D?: number\n limitFrames4D?: number\n}\n\nexport type ImageFromBase64 = {\n // base64 string\n base64: string\n // a name for this image. Default is an empty string\n name?: string\n // a color map to use. default is gray\n colormap?: string\n // the opacity for this image. default is 1\n opacity?: number\n // minimum intensity for color brightness/contrast\n cal_min?: number\n // maximum intensity for color brightness/contrast\n cal_max?: number\n // whether or not to trust cal_min and cal_max from the nifti header (trusting results in faster loading)\n trustCalMinMax?: boolean\n // the percentile to use for setting the robust range of the display values (smart intensity setting for images with large ranges)\n percentileFrac?: number\n // whether or not to ignore zero voxels in setting the robust range of display values\n ignoreZeroVoxels?: boolean\n // whether or not use QForm instead of SForm\n useQFormNotSForm?: boolean\n colormapNegative?: string\n frame4D?: number\n imageType?: ImageType\n cal_minNeg?: number\n cal_maxNeg?: number\n colorbarVisible?: boolean\n colormapLabel?: LUT | null\n}\n\nexport type ImageMetadata = {\n // unique if of image\n id: string\n // data type\n datatypeCode: number\n // number of columns\n nx: number\n // number of rows\n ny: number\n // number of slices\n nz: number\n // number of volumes\n nt: number\n // space between columns\n dx: number\n // space between rows\n dy: number\n // space between slices\n dz: number\n // time between volumes\n dt: number\n // bits per voxel\n // TODO was documented as bpx\n bpv: number\n}\n\nexport const NVImageFromUrlOptions = (\n url: string,\n urlImageData = '',\n name = '',\n colormap = 'gray',\n opacity = 1.0,\n cal_min = NaN,\n cal_max = NaN,\n trustCalMinMax = true,\n percentileFrac = 0.02,\n ignoreZeroVoxels = false,\n useQFormNotSForm = false,\n colormapNegative = '',\n frame4D = 0,\n imageType = NVIMAGE_TYPE.UNKNOWN,\n cal_minNeg = NaN,\n cal_maxNeg = NaN,\n colorbarVisible = true,\n alphaThreshold = false,\n colormapLabel = null\n): ImageFromUrlOptions => {\n return {\n url,\n urlImageData,\n name,\n colormap,\n colorMap: colormap,\n opacity,\n cal_min,\n cal_max,\n trustCalMinMax,\n percentileFrac,\n ignoreZeroVoxels,\n useQFormNotSForm,\n colormapNegative,\n imageType,\n cal_minNeg,\n cal_maxNeg,\n colorbarVisible,\n frame4D,\n alphaThreshold,\n colormapLabel\n }\n}\n\n// not included in public docs\n// create NIfTI format SForm from DICOM frame of reference\nexport function getBestTransform(imageDirections: number[], voxelDimensions: number[], imagePosition: number[]): number[][] | null {\n // https://github.com/rii-mango/Papaya/blob/782a19341af77a510d674c777b6da46afb8c65f1/src/js/volume/dicom/header-dicom.js#L605\n /* Copyright (c) 2012-2015, RII-UTHSCSA\nAll rights reserved.\n\nTHIS PRODUCT IS NOT FOR CLINICAL USE.\n\nRedistribution and use in source and binary forms, with or without modification, are permitted provided that the\nfollowing conditions are met:\n\n - Redistributions of source code must retain the above copyright notice, this list of conditions and the following\n disclaimer.\n\n - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following\n disclaimer in the documentation and/or other materials provided with the distribution.\n\n - Neither the name of the RII-UTHSCSA nor the names of its contributors may be used to endorse or promote products\n derived from this software without specific prior written permission.\n\n THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES,\n INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\n DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\n SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,\n WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n*/\n const cosines = imageDirections\n let m = null\n if (cosines) {\n const vs = {\n colSize: voxelDimensions[0],\n rowSize: voxelDimensions[1],\n sliceSize: voxelDimensions[2]\n }\n const coord = imagePosition\n const cosx = [cosines[0], cosines[1], cosines[2]]\n const cosy = [cosines[3], cosines[4], cosines[5]]\n const cosz = [cosx[1] * cosy[2] - cosx[2] * cosy[1], cosx[2] * cosy[0] - cosx[0] * cosy[2], cosx[0] * cosy[1] - cosx[1] * cosy[0]]\n m = [\n [cosx[0] * vs.colSize * -1, cosy[0] * vs.rowSize * -1, cosz[0] * vs.sliceSize * -1, -1 * coord[0]],\n [cosx[1] * vs.colSize * -1, cosy[1] * vs.rowSize * -1, cosz[1] * vs.sliceSize * -1, -1 * coord[1]],\n [cosx[2] * vs.colSize, cosy[2] * vs.rowSize, cosz[2] * vs.sliceSize, coord[2]],\n [0, 0, 0, 1]\n ]\n }\n return m\n}\n\nfunction str2Buffer(str: string, maxLen: number = 80): number[] {\n // emulate node.js Buffer.from\n // remove characters than could be used for shell expansion\n str = str.replace(/[`$]/g, '')\n const bytes = []\n const len = Math.min(maxLen, str.length)\n for (let i = 0; i < len; i++) {\n const char = str.charCodeAt(i)\n bytes.push(char & 0xff)\n }\n return bytes\n}\n\n// save NIfTI header into UINT8 array for saving to disk\nexport function hdrToArrayBuffer(hdr: NiftiHeader, isDrawing8 = false, isInputEndian = false): Uint8Array {\n const SHORT_SIZE = 2\n const FLOAT32_SIZE = 4\n let isLittleEndian = true\n if (isInputEndian) {\n isLittleEndian = hdr.littleEndian\n }\n const byteArray = new Uint8Array(348)\n const view = new DataView(byteArray.buffer)\n // sizeof_hdr\n view.setInt32(0, 348, isLittleEndian)\n\n // data_type, db_name, extents, session_error, regular are not used\n // regular set to 'r' (ASCII 114) for Analyze compatibility\n view.setUint8(38, 114)\n // dim_info\n view.setUint8(39, hdr.dim_info)\n\n // dims\n for (let i = 0; i < 8; i++) {\n view.setUint16(40 + SHORT_SIZE * i, hdr.dims[i], isLittleEndian)\n }\n\n // intent_p1, intent_p2, intent_p3\n view.setFloat32(56, hdr.intent_p1, isLittleEndian)\n view.setFloat32(60, hdr.intent_p2, isLittleEndian)\n view.setFloat32(64, hdr.intent_p3, isLittleEndian)\n // intent_code, datatype, bitpix, slice_start\n view.setInt16(68, hdr.intent_code, isLittleEndian)\n if (isDrawing8) {\n view.setInt16(70, 2, isLittleEndian) // 2 = DT_UINT8\n view.setInt16(72, 8, isLittleEndian)\n } else {\n view.setInt16(70, hdr.datatypeCode, isLittleEndian)\n view.setInt16(72, hdr.numBitsPerVoxel, isLittleEndian)\n }\n view.setInt16(74, hdr.slice_start, isLittleEndian)\n\n // pixdim[8], vox_offset, scl_slope, scl_inter\n for (let i = 0; i < 8; i++) {\n view.setFloat32(76 + FLOAT32_SIZE * i, hdr.pixDims[i], isLittleEndian)\n }\n if (isDrawing8) {\n view.setFloat32(108, 352, isLittleEndian)\n view.setFloat32(112, 1.0, isLittleEndian)\n view.setFloat32(116, 0.0, isLittleEndian)\n } else {\n // view.setFloat32(108, hdr.vox_offset, isLittleEndian)\n view.setFloat32(108, hdr.vox_offset, isLittleEndian)\n view.setFloat32(112, hdr.scl_slope, isLittleEndian)\n view.setFloat32(116, hdr.scl_inter, isLittleEndian)\n }\n // slice_end\n view.setInt16(120, hdr.slice_end, isLittleEndian)\n\n // slice_code, xyzt_units\n view.setUint8(122, hdr.slice_code)\n if (hdr.xyzt_units === 0) {\n view.setUint8(123, 10)\n } else {\n view.setUint8(123, hdr.xyzt_units)\n }\n\n // cal_max, cal_min, slice_duration, toffset\n if (isDrawing8) {\n view.setFloat32(124, 0, isLittleEndian)\n view.setFloat32(128, 0, isLittleEndian)\n } else {\n view.setFloat32(124, hdr.cal_max, isLittleEndian)\n view.setFloat32(128, hdr.cal_min, isLittleEndian)\n }\n view.setFloat32(132, hdr.slice_duration, isLittleEndian)\n view.setFloat32(136, hdr.toffset, isLittleEndian)\n\n // glmax, glmin are unused\n\n // descrip and aux_file\n // node.js byteArray.set(Buffer.from(hdr.description), 148);\n byteArray.set(str2Buffer(hdr.description), 148)\n // node.js: byteArray.set(Buffer.from(hdr.aux_file), 228);\n byteArray.set(str2Buffer(hdr.aux_file), 228)\n // qform_code, sform_code\n view.setInt16(252, hdr.qform_code, isLittleEndian)\n // if sform unknown, assume NIFTI_XFORM_SCANNER_ANAT\n if (hdr.sform_code < 1 || hdr.sform_code < 1) {\n view.setInt16(254, 1, isLittleEndian)\n } else {\n view.setInt16(254, hdr.sform_code, isLittleEndian)\n }\n\n // quatern_b, quatern_c, quatern_d, qoffset_x, qoffset_y, qoffset_z, srow_x[4], srow_y[4], and srow_z[4]\n view.setFloat32(256, hdr.quatern_b, isLittleEndian)\n view.setFloat32(260, hdr.quatern_c, isLittleEndian)\n view.setFloat32(264, hdr.quatern_d, isLittleEndian)\n view.setFloat32(268, hdr.qoffset_x, isLittleEndian)\n view.setFloat32(272, hdr.qoffset_y, isLittleEndian)\n view.setFloat32(276, hdr.qoffset_z, isLittleEndian)\n const flattened = hdr.affine.flat()\n // we only want the first three rows\n for (let i = 0; i < 12; i++) {\n view.setFloat32(280 + FLOAT32_SIZE * i, flattened[i], isLittleEndian)\n }\n // node.js https://www.w3schools.com/nodejs/met_buffer_from.asp\n // intent_name and magic\n // node.js byteArray.set(Buffer.from(hdr.intent_name), 328);\n // byteArray.set(str2Buffer(hdr.intent_name), 328)\n // node.js byteArray.set(Buffer.from(hdr.magic), 344);\n // byteArray.set(str2Buffer(hdr.magic), 344)\n view.setInt32(344, 3222382, true) // \"n+1\\0\"\n\n return byteArray\n // return byteArray.buffer;\n}\n\ntype Extents = {\n // min bounding point\n min: number[]\n // max bounding point\n max: number[]\n // point furthest from origin\n furthestVertexFromOrigin: number\n // origin\n origin: vec3\n}\n\nexport function getExtents(positions: number[], forceOriginInVolume = true): Extents {\n const nV = Math.round(positions.length / 3) // each vertex has 3 components: XYZ\n const origin = vec3.fromValues(0, 0, 0) // default center of rotation\n const mn = vec3.create()\n const mx = vec3.create()\n let mxDx = 0.0\n let nLoops = 1\n if (forceOriginInVolume) {\n nLoops = 2\n } // second pass to reposition origin\n for (let loop = 0; loop < nLoops; loop++) {\n mxDx = 0.0\n for (let i = 0; i < nV; i++) {\n const v = vec3.fromValues(positions[i * 3], positions[i * 3 + 1], positions[i * 3 + 2])\n if (i === 0) {\n vec3.copy(mn, v)\n vec3.copy(mx, v)\n }\n vec3.min(mn, mn, v)\n vec3.max(mx, mx, v)\n vec3.subtract(v, v, origin)\n const dx = vec3.len(v)\n mxDx = Math.max(mxDx, dx)\n }\n if (loop + 1 >= nLoops) {\n break\n }\n let ok = true\n for (let j = 0; j < 3; ++j) {\n if (mn[j] > origin[j]) {\n ok = false\n }\n if (mx[j] < origin[j]) {\n ok = false\n }\n }\n if (ok) {\n break\n }\n vec3.lerp(origin, mn, mx, 0.5)\n log.debug('origin moved inside volume: ', origin)\n }\n const min = [mn[0], mn[1], mn[2]]\n const max = [mx[0], mx[1], mx[2]]\n const furthestVertexFromOrigin = mxDx\n return { min, max, furthestVertexFromOrigin, origin }\n}\n\nexport function isAffineOK(mtx: number[][]): boolean {\n // A good matrix should not have any components that are not a number\n // A good spatial transformation matrix should not have a row or column that is all zeros\n const iOK = [false, false, false, false]\n const jOK = [false, false, false, false]\n for (let i = 0; i < 4; i++) {\n for (let j = 0; j < 4; j++) {\n if (isNaN(mtx[i][j])) {\n return false\n }\n }\n }\n for (let i = 0; i < 3; i++) {\n for (let j = 0; j < 3; j++) {\n if (mtx[i][j] === 0.0) {\n continue\n }\n iOK[i] = true\n jOK[j] = true\n }\n }\n for (let i = 0; i < 3; i++) {\n if (!iOK[i]) {\n return false\n }\n if (!jOK[i]) {\n return false\n }\n }\n return true\n}\n\nexport async function uncompressStream(stream: ReadableStream<Uint8Array>): Promise<ReadableStream<Uint8Array>> {\n const reader = stream.getReader()\n const { done, value } = await reader.read()\n\n // If the first read is done, return an empty stream\n if (done) {\n reader.releaseLock()\n return new ReadableStream({\n start(controller): void {\n controller.close()\n }\n })\n }\n\n // Too short to be compressed\n if (!value || value.length < 2) {\n reader.releaseLock()\n return new ReadableStream({\n start(controller): void {\n if (value) {\n controller.enqueue(value)\n }\n controller.close()\n }\n })\n }\n\n const isGzip = value[0] === 31 && value[1] === 139\n\n // Create new stream starting with the first chunk\n const uncompressedStream = new ReadableStream<Uint8Array>({\n async start(controller): Promise<void> {\n try {\n // Enqueue the first chunk we already read\n controller.enqueue(value)\n\n // Process remaining chunks\n while (true) {\n const { done, value } = await reader.read()\n if (done) {\n controller.close()\n reader.releaseLock()\n break\n }\n controller.enqueue(value)\n }\n } catch (error) {\n controller.error(error)\n reader.releaseLock()\n }\n }\n })\n\n if (isGzip) {\n return uncompressedStream.pipeThrough(new DecompressionStream('gzip'))\n }\n return uncompressedStream\n}\n","import { NIFTI1, NIFTI2 } from 'nifti-reader-js'\nimport { log } from '@/logger'\nimport { NVUtilities } from '@/nvutilities'\nimport { hdrToArrayBuffer, NiiDataType } from '@/nvimage/utils'\nimport type { NVImage, TypedVoxelArray } from '@/nvimage'\n/**\n * Creates a NIFTI1 header object with basic properties.\n */\nexport function createNiftiHeader(\n dims: number[] = [256, 256, 256],\n pixDims: number[] = [1, 1, 1],\n affine: number[] = [1, 0, 0, -128, 0, 1, 0, -128, 0, 0, 1, -128, 0, 0, 0, 1],\n datatypeCode = NiiDataType.DT_UINT8\n): NIFTI1 {\n const hdr = new NIFTI1()\n hdr.littleEndian = true\n hdr.dims = [3, 1, 1, 1, 0, 0, 0, 0]\n hdr.dims[0] = Math.max(3, dims.length)\n for (let i = 0; i < dims.length; i++) {\n hdr.dims[i + 1] = dims[i]\n }\n hdr.pixDims = [1, 1, 1, 1, 1, 0, 0, 0]\n for (let i = 0; i < dims.length; i++) {\n hdr.pixDims[i + 1] = pixDims[i]\n }\n if (affine.length === 16) {\n let k = 0\n for (let i = 0; i < 4; i++) {\n for (let j = 0; j < 4; j++) {\n hdr.affine[i][j] = affine[k]\n k++\n }\n }\n }\n let bpv = 8\n if (datatypeCode === NiiDataType.DT_INT8 || datatypeCode === NiiDataType.DT_UINT8) {\n bpv = 8\n } else if (datatypeCode === NiiDataType.DT_UINT16 || datatypeCode === NiiDataType.DT_INT16) {\n bpv = 16\n } else if (datatypeCode === NiiDataType.DT_FLOAT32 || datatypeCode === NiiDataType.DT_UINT32 || datatypeCode === NiiDataType.DT_INT32 || datatypeCode === NiiDataType.DT_RGBA32) {\n bpv = 32\n } else if (datatypeCode === NiiDataType.DT_FLOAT64) {\n bpv = 64\n } else {\n log.warn('Unsupported NIfTI datatypeCode for header creation: ' + datatypeCode)\n }\n hdr.datatypeCode = datatypeCode\n hdr.numBitsPerVoxel = bpv\n hdr.scl_inter = 0\n hdr.scl_slope = 1 // Default slope should be 1\n hdr.sform_code = 2 // Assume affine is RAS\n hdr.magic = 'n+1'\n hdr.vox_offset = 352 // Standard offset for NIfTI-1 with no extensions\n return hdr\n}\n\n/**\n * Creates a Uint8Array representing a NIFTI file (header + optional image data).\n */\nexport function createNiftiArray(\n dims: number[] = [256, 256, 256],\n pixDims: number[] = [1, 1, 1],\n affine: number[] = [1, 0, 0, -128, 0, 1, 0, -128, 0, 0, 1, -128, 0, 0, 0, 1],\n datatypeCode = NiiDataType.DT_UINT8,\n img: TypedVoxelArray | Uint8Array = new Uint8Array()\n): Uint8Array {\n const hdr = createNiftiHeader(dims, pixDims, affine, datatypeCode)\n // hdrToArrayBuffer should handle creating the byte array correctly based on header info\n const hdrBytes = hdrToArrayBuffer(hdr, false) // Pass header directly\n\n // Ensure the header reports the correct offset, usually 352 for simple NIfTI-1\n hdr.vox_offset = Math.max(352, hdrBytes.length) // Ensure offset is at least header size\n // Re-generate header bytes if vox_offset changed header size itself (unlikely but possible with extensions)\n const finalHdrBytes = hdrToArrayBuffer(hdr, false)\n\n if (img.length < 1) {\n // Return just the header if no image data\n return finalHdrBytes\n }\n\n // Calculate padding needed to reach vox_offset\n const paddingSize = Math.max(0, hdr.vox_offset - finalHdrBytes.length)\n const padding = new Uint8Array(paddingSize)\n\n // Get the image data bytes correctly\n const imgBytes = new Uint8Array(img.buffer, img.byteOffset, img.byteLength)\n\n // Combine header, padding, and image data\n const totalLength = hdr.vox_offset + imgBytes.length\n const outputData = new Uint8Array(totalLength)\n\n outputData.set(finalHdrBytes, 0)\n outputData.set(padding, finalHdrBytes.length)\n outputData.set(imgBytes, hdr.vox_offset) // Place image data at the offset\n\n return outputData\n}\n\n/**\n * Converts NVImage data (header and image) to a NIfTI compliant Uint8Array.\n * Handles potential re-orientation of drawing data if necessary.\n * @param nvImage - The NVImage instance\n * @param drawingBytes - Optional Uint8Array for drawing overlay (assumed to be in RAS order)\n * @returns Uint8Array representing the NIfTI file\n */\nexport function toUint8Array(nvImage: NVImage, drawingBytes: Uint8Array | null = null): Uint8Array {\n if (!nvImage.hdr) {\n throw new Error('NVImage header is not defined for toUint8Array')\n }\n if (!nvImage.img && drawingBytes === null) {\n throw new Error('NVImage image data is not defined for toUint8Array')\n }\n\n const isDrawing = drawingBytes !== null\n // Create a deep copy of the header to modify safely for output\n const hdrCopy = JSON.parse(JSON.stringify(nvImage.hdr)) as NIFTI1 | NIFTI2\n\n // Handle extensions\n const hasExtensions = nvImage.extensions && nvImage.extensions.length > 0\n const extFlag = new Uint8Array(4)\n extFlag[0] = hasExtensions ? 1 : 0\n\n let extensionsData = new Uint8Array(0)\n if (hasExtensions) {\n const blocks: Uint8Array[] = []\n let totalSize = 0\n for (const ext of nvImage.extensions!) {\n const edataBytes = new Uint8Array(ext.edata)\n const block = new Uint8Array(8 + edataBytes.length)\n const dv = new DataView(block.buffer)\n dv.setInt32(0, ext.esize, true)\n dv.setInt32(4, ext.ecode, true)\n block.set(edataBytes, 8)\n blocks.push(block)\n totalSize += block.length\n }\n extensionsData = new Uint8Array(totalSize)\n let offset = 0\n for (const block of blocks) {\n extensionsData.set(block, offset)\n offset += block.length\n }\n }\n\n const headerSize = 348 // nifti-1 standard\n hdrCopy.vox_offset = Math.max(352, headerSize + extFlag.length + extensionsData.length)\n\n // If saving a drawing, ensure output header reflects drawing data type (UINT8) and resets scaling\n if (isDrawing) {\n hdrCopy.datatypeCode = NiiDataType.DT_UINT8\n hdrCopy.numBitsPerVoxel = 8\n hdrCopy.scl_slope = 1.0\n hdrCopy.scl_inter = 0.0\n }\n\n // Generate header bytes using the potentially modified copy\n const hdrBytes = hdrToArrayBuffer(hdrCopy, isDrawing)\n\n let imageBytesToSave: Uint8Array\n\n if (isDrawing) {\n const drawingBytesCurrent = drawingBytes! // Not null asserted by isDrawing check\n const perm = nvImage.permRAS as number[] | undefined\n\n // Check if reorientation from RAS (drawing space) to native space is needed\n if (perm && (perm[0] !== 1 || perm[1] !== 2 || perm[2] !== 3)) {\n log.debug('Reorienting drawing bytes back to native space for saving...')\n const dims = nvImage.hdr!.dims // Use original native dimensions\n const nVox = dims[1] * dims[2] * dims[3] // Total native voxels\n\n // Ensure drawing length matches expected RAS voxel count based on calculated dimsRAS\n const nVoxRAS = nvImage.dimsRAS ? nvImage.dimsRAS[1] * nvImage.dimsRAS[2] * nvImage.dimsRAS[3] : nVox\n if (drawingBytesCurrent.length !== nVoxRAS) {\n console.warn(`Drawing length (${drawingBytesCurrent.length}) does not match expected RAS voxel count (${nVoxRAS}). Cannot reorient drawing reliably.`)\n imageBytesToSave = drawingBytesCurrent // Use original as fallback\n // Ensure necessary transformation arrays exist\n } else if (!nvImage.img2RASstep || !nvImage.img2RASstart || !nvImage.dimsRAS) {\n console.warn(`Missing RAS transformation info (img2RASstep, img2RASstart, dimsRAS). Cannot reorient drawing reliably.`)\n imageBytesToSave = drawingBytesCurrent // Use original as fallback\n } else {\n const step = nvImage.img2RASstep // [stepX, stepY, stepZ] in native index space for RAS increments\n const start = nvImage.img2RASstart // [startX, startY, startZ] starting native index for RAS[0,0,0]\n const dimsRAS = nvImage.dimsRAS // [4, dimRX, dimRY, dimRZ]\n\n const nativeData = new Uint8Array(nVox)\n nativeData.fill(0) // Initialize with background value (e.g., 0)\n const inputDrawingRAS = drawingBytesCurrent // Source data is RAS ordered\n let rasIndex = 0 // Index for the flat inputDrawingRAS array\n\n // Iterate through the source RAS dimensions\n for (let rz = 0; rz < dimsRAS[3]; rz++) {\n const zi = start[2] + rz * step[2] // Native offset component for this RAS Z\n for (let ry = 0; ry < dimsRAS[2]; ry++) {\n const yi = start[1] + ry * step[1] // Native offset component for this RAS Y\n for (let rx = 0; rx < dimsRAS[1]; rx++) {\n const xi = start[0] + rx * step[0] // Native offset component for this RAS X\n const nativeIndex = xi + yi + zi // Calculate the final index in the native orientation buffer\n\n // Check bounds for safety before writing\n if (nativeIndex >= 0 && nativeIndex < nVox) {\n nativeData[nativeIndex] = inputDrawingRAS[rasIndex] // Place RAS voxel into calculated native position\n } else if (rasIndex < inputDrawingRAS.length) {\n // Log if we calculate an invalid native index but still have RAS data\n console.warn(`Calculated native index ${nativeIndex} is out of bounds [0..${nVox - 1}] during drawing reorientation.`)\n }\n rasIndex++ // Increment index into the flat RAS drawing array\n }\n }\n }\n imageBytesToSave = nativeData // Use the reoriented data\n }\n } else {\n // No reorientation needed (image is already native/RAS or drawing is meant to be native)\n imageBytesToSave = drawingBytesCurrent\n }\n } else {\n // Not a drawing, use the main image data directly\n if (!nvImage.img) {\n throw new Error('NVImage image data is null when trying to save non-drawing.')\n }\n imageBytesToSave = new Uint8Array(nvImage.img.buffer, nvImage.img.byteOffset, nvImage.img.byteLength)\n }\n\n // Calculate padding needed to reach the specified vox_offset in the header\n const preImageBytesSize = hdrBytes.length + extFlag.length + extensionsData.length\n const paddingSize = Math.max(0, hdrCopy.vox_offset - preImageBytesSize)\n const padding = new Uint8Array(paddingSize)\n\n const totalLength = hdrCopy.vox_offset + imageBytesToSave.length\n const outputData = new Uint8Array(totalLength)\n\n let offset = 0\n outputData.set(hdrBytes, offset)\n offset += hdrBytes.length\n\n outputData.set(extFlag, offset)\n offset += extFlag.length\n\n outputData.set(extensionsData, offset)\n offset += extensionsData.length\n\n outputData.set(padding, offset)\n offset += padding.length\n\n outputData.set(imageBytesToSave, hdrCopy.vox_offset)\n\n return outputData\n}\n\n/**\n * Generates the NIfTI file as a Uint8Array and optionally compresses it.\n * @param nvImage - The NVImage instance\n * @param fnm - Filename (used to determine if compression is needed, .gz suffix)\n * @param drawing8 - Optional drawing overlay data\n * @returns Uint8Array (potentially compressed)\n */\nexport async function saveToUint8Array(nvImage: NVImage, fnm: string, drawing8: Uint8Array | null = null): Promise<Uint8Array> {\n // Generate the core NIfTI byte array first\n const odata = toUint8Array(nvImage, drawing8)\n // Check filename extension for compression request\n const compress = fnm.toLowerCase().endsWith('.gz')\n\n if (compress) {\n try {\n // Use NVUtilities to compress the data\n const compressedData = await NVUtilities.compress(odata, 'gzip')\n return new Uint8Array(compressedData)\n } catch (error) {\n log.error('Compression failed:', error)\n log.warn('Returning uncompressed data due to compression error.')\n return odata // Return uncompressed data as fallback\n }\n } else {\n // No compression needed\n return odata\n }\n}\n\n/**\n * Generates the NIfTI file data and triggers a browser download.\n * @param nvImage - The NVImage instance\n * @param fnm - Filename for the downloaded file. If empty, returns data only.\n * @param drawing8 - Optional drawing overlay data\n * @returns The generated Uint8Array (potentially compressed)\n */\nexport async function saveToDisk(nvImage: NVImage, fnm: string = '', drawing8: Uint8Array | null = null): Promise<Uint8Array> {\n // Always generate the data first, handling potential compression based on filename\n const saveData = await saveToUint8Array(nvImage, fnm, drawing8)\n\n if (!fnm) {\n log.debug('saveToDisk: empty file name, returning data as Uint8Array rather than triggering download')\n return saveData // Return data if filename is empty\n }\n\n try {\n // Create a Blob from the final data (compressed or not)\n const blob = new Blob([saveData.buffer], {\n type: 'application/octet-stream' // Standard type for binary download\n })\n // Create a temporary URL for the Blob\n const blobUrl = URL.createObjectURL(blob)\n // Create a link element to trigger the download\n const link = document.createElement('a')\n link.setAttribute('href', blobUrl)\n link.setAttribute('download', fnm) // Set the filename for the download\n link.style.visibility = 'hidden' // Hide the link\n document.body.appendChild(link) // Add link to the document\n link.click() // Simulate a click to trigger download\n document.body.removeChild(link) // Remove the link from the document\n // Revoke the temporary URL after a short delay to allow download initiation\n setTimeout(() => URL.revokeObjectURL(blobUrl), 100)\n } catch (e) {\n log.error('Failed to trigger download:', e)\n }\n\n return saveData // Return the data regardless of download success/triggering\n}\n","import { vec4 } from 'gl-matrix'\nimport { log } from '@/logger'\nimport { NiiDataType } from '@/nvimage/utils'\nimport type { NVImage, TypedVoxelArray } from '@/nvimage'\n\n/**\n * Returns all voxel channel values at the specified coordinates.\n * For scalar images, this will return a single-value array [value].\n * For multi-channel images (e.g., RGBA), this will return an array with multiple values.\n *\n * @param nvImage - The NVImage instance.\n * @param x - X coordinate (0-indexed).\n * @param y - Y coordinate (0-indexed).\n * @param z - Z coordinate (0-indexed).\n * @param frame4D - Optional 4D frame index (default is 0).\n * @param isReadImaginary - If true, returns data from the imaginary component (if present).\n * @returns An array of one or more voxel values at the specified location.\n *\n * @see https://niivue.com/demos/features/voxel.html\n */\nexport function getValues(nvImage: NVImage, x: number, y: number, z: number, frame4D = 0, isReadImaginary = false): number[] {\n if (!nvImage.hdr) {\n throw new Error('getValue: NVImage header is not defined.')\n }\n if (!isReadImaginary && !nvImage.img) {\n throw new Error('getValue: NVImage image data is not defined.')\n }\n if (isReadImaginary && !nvImage.imaginary) {\n log.warn('getValue: Attempted to read imaginary data, but none exists.')\n return [0]\n }\n\n const nx = nvImage.hdr.dims[1]\n const ny = nvImage.hdr.dims[2]\n const nz = nvImage.hdr.dims[3]\n\n const perm = nvImage.permRAS!.slice()\n if (perm[0] !== 1 || perm[1] !== 2 || perm[2] !== 3) {\n const pos = vec4.fromValues(x, y, z, 1)\n vec4.transformMat4(pos, pos, nvImage.toRASvox!)\n x = pos[0]\n y = pos[1]\n z = pos[2]\n }\n\n // Clamp coordinates to valid range\n x = Math.max(0, Math.min(Math.round(x), nx - 1))\n y = Math.max(0, Math.min(Math.round(y), ny - 1))\n z = Math.max(0, Math.min(Math.round(z), nz - 1))\n frame4D = Math.max(0, frame4D)\n\n let vx = x + y * nx + z * nx * ny // voxel index within a 3D volume\n\n // Handle RGB(A) data - calculate luminance\n if (nvImage.hdr.datatypeCode === NiiDataType.DT_RGBA32) {\n if (!nvImage.img) {\n return [0]\n }\n vx *= 4 // 4 bytes per voxel\n // Check bounds for RGBA index access\n if (vx + 3 >= nvImage.img.length) {\n log.warn(`getValue: Calculated index ${vx} out of bounds for RGBA data.`)\n return [0] // Or throw? Return 0 for safety.\n }\n return [nvImage.img[vx], nvImage.img[vx + 1], nvImage.img[vx + 2], nvImage.img[vx + 3]]\n }\n if (nvImage.hdr.datatypeCode === NiiDataType.DT_RGB24) {\n if (!nvImage.img) {\n return [0]\n }\n vx *= 3 // 3 bytes per voxel\n if (vx + 2 >= nvImage.img.length) {\n log.warn(`getValue: Calculated index ${vx} out of bounds for RGB data.`)\n return [0]\n }\n return [nvImage.img[vx], nvImage.img[vx + 1], nvImage.img[vx + 2]]\n }\n\n // Calculate offset for 4D frame\n const nVox3D = nx * ny * nz\n const volOffset = frame4D * nVox3D\n const finalVxIndex = vx + volOffset\n\n // Select the correct data array\n const dataArray = isReadImaginary ? nvImage.imaginary! : nvImage.img!\n\n // Check final index bounds\n if (finalVxIndex < 0 || finalVxIndex >= dataArray.length) {\n return [0]\n }\n\n const rawValue = dataArray[finalVxIndex]\n\n // Apply scaling slope and intercept\n // Use default 1.0 slope if hdr value is 0 or NaN, default 0.0 intercept if NaN\n const slope = isNaN(nvImage.hdr.scl_slope) || nvImage.hdr.scl_slope === 0 ? 1.0 : nvImage.hdr.scl_slope\n const inter = isNaN(nvImage.hdr.scl_inter) ? 0.0 : nvImage.hdr.scl_inter\n\n return [slope * rawValue + inter]\n}\n\n/**\n * Returns voxel intensity at specified coordinates.\n * @param nvImage - The NVImage instance\n * @param x - X coordinate (0-indexed)\n * @param y - Y coordinate (0-indexed)\n * @param z - Z coordinate (0-indexed)\n * @param frame4D - 4D frame index (0-indexed)\n * @param isReadImaginary - Flag to read from imaginary data array if complex\n * @returns Scaled voxel intensity\n */\nexport function getValue(nvImage: NVImage, x: number, y: number, z: number, frame4D = 0, isReadImaginary = false): number {\n const vals = getValues(nvImage, x, y, z, frame4D, isReadImaginary)\n if (vals.length < 3) {\n return vals[0]\n }\n // convert RGB to luminance Y = 0.2126 R + 0.7152 G + 0.0722 B (Rec. 709)\n const lum = vals[0] * 0.2126 + vals[1] * 0.7152 + vals[2] * 0.0722\n return lum\n}\n\n/**\n * Reads a 3D slab of voxels from a volume, specified in RAS coordinates.\n * @param nvImage - The NVImage instance\n * @param voxStartRAS - First row, column, slice (RAS order, 0-indexed) for selection\n * @param voxEndRAS - Final row, column, slice (RAS order, 0-indexed) for selection\n * @param dataType - Output array type: 'same', 'uint8', 'float32', 'scaled', 'normalized', 'windowed'\n * @returns Tuple: [TypedVoxelArray, slabDimensions]\n */\nexport function getVolumeData(nvImage: NVImage, voxStartRAS: number[] = [-1, 0, 0], voxEndRAS: number[] = [0, 0, 0], dataType = 'same'): [TypedVoxelArray, number[]] {\n const defaultResult: [TypedVoxelArray, number[]] = [new Uint8Array(), [0, 0, 0]]\n\n if (!nvImage.hdr || !nvImage.img || !nvImage.dimsRAS || !nvImage.img2RASstep || !nvImage.img2RASstart) {\n log.error('getVolumeData: Missing required NVImage properties (hdr, img, dimsRAS, img2RASstep/start).')\n return defaultResult\n }\n // Ensure input arrays have 3 elements\n voxStartRAS = voxStartRAS.slice(0, 3)\n voxEndRAS = voxEndRAS.slice(0, 3)\n\n if (Math.min(...voxStartRAS) < 0 || Math.min(...voxEndRAS) < 0) {\n log.warn('getVolumeData: Invalid start or end coordinates provided.')\n return defaultResult\n }\n\n const dimsRAS = nvImage.dimsRAS.slice(1, 4) // Get RAS dimensions [nx, ny, nz]\n\n // Clamp coordinates to valid RAS range and ensure start <= end\n for (let i = 0; i < 3; i++) {\n voxStartRAS[i] = Math.max(0, Math.min(Math.round(voxStartRAS[i]), dimsRAS[i] - 1))\n voxEndRAS[i] = Math.max(0, Math.min(Math.round(voxEndRAS[i]), dimsRAS[i] - 1))\n if (voxEndRAS[i] < voxStartRAS[i]) {\n const tmp = voxEndRAS[i]\n voxEndRAS[i] = voxStartRAS[i]\n voxStartRAS[i] = tmp\n }\n }\n\n const slabDims = [voxEndRAS[0] - voxStartRAS[0] + 1, voxEndRAS[1] - voxStartRAS[1] + 1, voxEndRAS[2] - voxStartRAS[2] + 1]\n const slabNVox = slabDims[0] * slabDims[1] * slabDims[2]\n\n if (slabNVox <= 0) {\n log.warn('getVolumeData: Calculated slab size is zero or negative.')\n return defaultResult\n }\n\n let OutputArrayConstructor: new (length: number) => TypedVoxelArray = nvImage.img.constructor as new (length: number) => TypedVoxelArray // Default to same as input\n\n if (dataType === 'uint8') {\n OutputArrayConstructor = Uint8Array\n } else if (dataType === 'int16') {\n OutputArrayConstructor = Int16Array\n } else if (dataType === 'uint16') {\n OutputArrayConstructor = Uint16Array\n } else if (dataType === 'float32' || dataType === 'scaled' || dataType === 'normalized' || dataType === 'windowed') {\n OutputArrayConstructor = Float32Array\n } else if (dataType === 'float64') {\n OutputArrayConstructor = Float64Array\n } else if (dataType !== 'same') {\n log.warn(`getVolumeData: Unsupported dataType '${dataType}'. Using 'same'.`)\n }\n\n // Create the output array\n let outputImg: TypedVoxelArray\n try {\n outputImg = new OutputArrayConstructor(slabNVox)\n } catch (e) {\n log.error(`getVolumeData: Failed to create output array for dataType '${dataType}'.`, e)\n return defaultResult\n }\n\n // Get transformation parameters\n const step = nvImage.img2RASstep\n const start = nvImage.img2RASstart\n const sourceImg = nvImage.img // Source data in native orientation\n\n let outputIndex = 0\n // Iterate through the requested RAS slab dimensions\n for (let rz = voxStartRAS[2]; rz <= voxEndRAS[2]; rz++) {\n const zi = start[2] + rz * step[2] // Native offset component for RAS Z\n for (let ry = voxStartRAS[1]; ry <= voxEndRAS[1]; ry++) {\n const yi = start[1] + ry * step[1] // Native offset component for RAS Y\n for (let rx = voxStartRAS[0]; rx <= voxEndRAS[0]; rx++) {\n const xi = start[0] + rx * step[0] // Native offset component for RAS X\n const nativeIndex = xi + yi + zi // Final index in the native source buffer\n\n let value = 0\n // Safely read from source image\n if (nativeIndex >= 0 && nativeIndex < sourceImg.length) {\n value = sourceImg[nativeIndex]\n }\n\n // Store the raw value in the output array\n outputImg[outputIndex++] = value\n }\n }\n }\n\n // Apply post-processing based on dataType AFTER extracting raw values\n const slope = isNaN(nvImage.hdr.scl_slope) || nvImage.hdr.scl_slope === 0 ? 1.0 : nvImage.hdr.scl_slope\n const inter = isNaN(nvImage.hdr.scl_inter) ? 0.0 : nvImage.hdr.scl_inter\n\n if (dataType === 'scaled' || dataType === 'normalized' || dataType === 'windowed') {\n // Ensure output is Float32 if scaling is requested but wasn't the original type\n if (!(outputImg instanceof Float32Array)) {\n log.warn(`getVolumeData: Converting output to Float32 for scaling type '${dataType}'.`)\n outputImg = Float32Array.from(outputImg)\n }\n for (let i = 0; i < outputImg.length; i++) {\n outputImg[i] = outputImg[i] * slope + inter\n }\n }\n\n if (dataType === 'normalized' || dataType === 'windowed') {\n let minVal = nvImage.cal_min\n let maxVal = nvImage.cal_max\n\n if (dataType === 'normalized') {\n minVal = nvImage.global_min\n maxVal = nvImage.global_max\n }\n\n const range = maxVal - minVal\n const scale = range === 0 ? 0 : 1 / range\n\n for (let i = 0; i < outputImg.length; i++) {\n outputImg[i] = (outputImg[i] - minVal) * scale\n outputImg[i] = Math.max(0, Math.min(outputImg[i], 1))\n }\n }\n\n return [outputImg, slabDims]\n}\n\n/**\n * Writes a 3D slab of voxels into a volume, specified in RAS coordinates.\n * @param nvImage - The NVImage instance to modify\n * @param voxStartRAS - First row, column, slice (RAS order, 0-indexed) for selection\n * @param voxEndRAS - Final row, column, slice (RAS order, 0-indexed) for selection\n * @param slabData - Array of voxel values (TypedVoxelArray) matching slab dimensions\n */\nexport function setVolumeData(nvImage: NVImage, voxStartRAS: number[] = [-1, 0, 0], voxEndRAS: number[] = [0, 0, 0], slabData: TypedVoxelArray = new Uint8Array()): void {\n if (!nvImage.hdr || !nvImage.img || !nvImage.dimsRAS || !nvImage.img2RASstep || !nvImage.img2RASstart) {\n log.error('setVolumeData: Missing required NVImage properties (hdr, img, dimsRAS, img2RASstep/start).')\n return\n }\n\n if (slabData.length < 1) {\n log.warn('setVolumeData: Input slabData is empty.')\n return\n }\n // Ensure input arrays have 3 elements\n voxStartRAS = voxStartRAS.slice(0, 3)\n voxEndRAS = voxEndRAS.slice(0, 3)\n\n if (Math.min(...voxStartRAS) < 0 || Math.min(...voxEndRAS) < 0) {\n log.warn('setVolumeData: Invalid start or end coordinates provided.')\n return\n }\n\n const dimsRAS = nvImage.dimsRAS.slice(1, 4) // Get RAS dimensions [nx, ny, nz]\n\n // Clamp coordinates to valid RAS range and ensure start <= end\n for (let i = 0; i < 3; i++) {\n voxStartRAS[i] = Math.max(0, Math.min(Math.round(voxStartRAS[i]), dimsRAS[i] - 1))\n voxEndRAS[i] = Math.max(0, Math.min(Math.round(voxEndRAS[i]), dimsRAS[i] - 1))\n if (voxEndRAS[i] < voxStartRAS[i]) {\n const tmp = voxEndRAS[i]\n voxEndRAS[i] = voxStartRAS[i]\n voxStartRAS[i] = tmp\n }\n }\n\n const slabDims = [voxEndRAS[0] - voxStartRAS[0] + 1, voxEndRAS[1] - voxStartRAS[1] + 1, voxEndRAS[2] - voxStartRAS[2] + 1]\n const slabNVox = slabDims[0] * slabDims[1] * slabDims[2]\n\n if (slabNVox <= 0) {\n log.warn('setVolumeData: Calculated slab size is zero or negative.')\n return\n }\n\n if (slabData.length < slabNVox) {\n log.error(`setVolumeData: Input slabData length (${slabData.length}) is less than the calculated slab size (${slabNVox}).`)\n return\n }\n\n // Get transformation parameters\n const step = nvImage.img2RASstep\n const start = nvImage.img2RASstart\n const targetImg = nvImage.img // Target data in native orientation\n\n let sourceIndex = 0\n // Iterate through the requested RAS slab dimensions\n for (let rz = voxStartRAS[2]; rz <= voxEndRAS[2]; rz++) {\n const zi = start[2] + rz * step[2] // Native offset component for RAS Z\n for (let ry = voxStartRAS[1]; ry <= voxEndRAS[1]; ry++) {\n const yi = start[1] + ry * step[1] // Native offset component for RAS Y\n for (let rx = voxStartRAS[0]; rx <= voxEndRAS[0]; rx++) {\n const xi = start[0] + rx * step[0] // Native offset component for RAS X\n const nativeIndex = xi + yi + zi // Final index in the native target buffer\n\n if (nativeIndex >= 0 && nativeIndex < targetImg.length) {\n targetImg[nativeIndex] = slabData[sourceIndex]\n }\n sourceIndex++\n }\n }\n }\n // Note: This function does NOT handle inverse scaling (converting scaled slabData back to raw).\n // The input slabData is assumed to be in the correct raw data type for the target nvImage.img.\n}\n","import { NIFTI1, NIFTIEXTENSION } from 'nifti-reader-js'\nimport { mat4, vec3, vec4 } from 'gl-matrix'\nimport { log } from '@/logger'\nimport type { NVImage } from '@/nvimage'\nimport { NiiDataType } from '@/nvimage/utils'\nimport { NVUtilities } from '@/nvutilities'\n\n/**\n * Helper function to convert voxel location (row, column slice, indexed from 0) to world space.\n * @param XYZ - Voxel coordinates [x, y, z]\n * @param mtx - 4x4 transformation matrix\n * @returns World space coordinates as vec3\n */\nfunction vox2mm(XYZ: number[], mtx: mat4): vec3 {\n const sform = mat4.clone(mtx)\n mat4.transpose(sform, sform)\n const pos = vec4.fromValues(XYZ[0], XYZ[1], XYZ[2], 1)\n vec4.transformMat4(pos, pos, sform)\n const pos3 = vec3.fromValues(pos[0], pos[1], pos[2])\n return pos3\n}\n\n/**\n * Helper function to determine spacing voxel centers (rows, columns, slices).\n * @param nvImage - The NVImage instance whose header will be modified\n */\nfunction setPixDimFromSForm(nvImage: NVImage): void {\n if (!nvImage.hdr) {\n throw new Error('hdr not defined')\n }\n const m = nvImage.hdr.affine\n const mat = mat4.fromValues(m[0][0], m[0][1], m[0][2], m[0][3], m[1][0], m[1][1], m[1][2], m[1][3], m[2][0], m[2][1], m[2][2], m[2][3], m[3][0], m[3][1], m[3][2], m[3][3])\n const mm000 = vox2mm([0, 0, 0], mat)\n const mm100 = vox2mm([1, 0, 0], mat)\n vec3.subtract(mm100, mm100, mm000)\n const mm010 = vox2mm([0, 1, 0], mat)\n vec3.subtract(mm010, mm010, mm000)\n const mm001 = vox2mm([0, 0, 1], mat)\n vec3.subtract(mm001, mm001, mm000)\n nvImage.hdr.pixDims[1] = vec3.length(mm100)\n nvImage.hdr.pixDims[2] = vec3.length(mm010)\n nvImage.hdr.pixDims[3] = vec3.length(mm001)\n}\n\n/**\n * Helper function to convert AFNI head/brik space to NIfTI format.\n *\n * Based on AFNI source code:\n * https://github.com/afni/afni/blob/d6997e71f2b625ac1199460576d48f3136dac62c/src/thd_niftiwrite.c#L315\n *\n * @param nvImage - The NVImage instance whose header will be modified\n * @param xyzDelta - Voxel spacing in each dimension\n * @param xyzOrigin - Origin coordinates\n * @param orientSpecific - Orientation codes for each axis\n */\nfunction THD_daxes_to_NIFTI(nvImage: NVImage, xyzDelta: number[], xyzOrigin: number[], orientSpecific: number[]): void {\n const hdr = nvImage.hdr\n\n if (hdr === null) {\n throw new Error('HDR is not set')\n }\n\n hdr.sform_code = 2\n const ORIENT_xyz = 'xxyyzzg' // note strings indexed from 0!\n let nif_x_axnum = -1\n let nif_y_axnum = -1\n let nif_z_axnum = -1\n const axcode = ['x', 'y', 'z']\n axcode[0] = ORIENT_xyz[orientSpecific[0]]\n axcode[1] = ORIENT_xyz[orientSpecific[1]]\n axcode[2] = ORIENT_xyz[orientSpecific[2]]\n const axstep = xyzDelta.slice(0, 3)\n const axstart = xyzOrigin.slice(0, 3)\n for (let ii = 0; ii < 3; ii++) {\n if (axcode[ii] === 'x') {\n nif_x_axnum = ii\n } else if (axcode[ii] === 'y') {\n nif_y_axnum = ii\n } else {\n nif_z_axnum = ii\n }\n }\n if (nif_x_axnum < 0 || nif_y_axnum < 0 || nif_z_axnum < 0) {\n return\n } // not assigned\n if (nif_x_axnum === nif_y_axnum || nif_x_axnum === nif_z_axnum || nif_y_axnum === nif_z_axnum) {\n return\n } // not assigned\n hdr.pixDims[1] = Math.abs(axstep[0])\n hdr.pixDims[2] = Math.abs(axstep[1])\n hdr.pixDims[3] = Math.abs(axstep[2])\n hdr.affine = [\n [1, 0, 0, 0],\n [0, 1, 0, 0],\n [0, 0, 1, 0],\n [0, 0, 0, 1]\n ]\n hdr.affine[0][nif_x_axnum] = -axstep[nif_x_axnum]\n hdr.affine[1][nif_y_axnum] = -axstep[nif_y_axnum]\n hdr.affine[2][nif_z_axnum] = axstep[nif_z_axnum]\n hdr.affine[0][3] = -axstart[nif_x_axnum]\n hdr.affine[1][3] = -axstart[nif_y_axnum]\n hdr.affine[2][3] = axstart[nif_z_axnum]\n}\n\n/**\n * Reads AFNI HEAD/BRIK format image, modifying the provided NVImage header\n * and returning the raw image data buffer.\n *\n * HEAD files contain the header information as text attributes.\n * BRIK files contain the binary image data.\n *\n * Format specification: https://afni.nimh.nih.gov/pub/dist/doc/program_help/README.attributes.html\n *\n * @param nvImage - The NVImage instance whose header will be modified.\n * @param dataBuffer - ArrayBuffer containing the HEAD file data.\n * @param pairedImgData - ArrayBuffer containing the BRIK file data (required).\n * @returns Promise resolving to ArrayBuffer containing the image data.\n * @throws Error if pairedImgData is not provided.\n */\nexport async function readHEAD(nvImage: NVImage, dataBuffer: ArrayBuffer, pairedImgData: ArrayBuffer | null): Promise<ArrayBuffer> {\n nvImage.hdr = new NIFTI1()\n const hdr = nvImage.hdr\n hdr.dims[0] = 3\n hdr.pixDims = [1, 1, 1, 1, 1, 0, 0, 0]\n let orientSpecific = [0, 0, 0]\n let xyzOrigin = [0, 0, 0]\n let xyzDelta = [1, 1, 1]\n const txt = new TextDecoder().decode(dataBuffer)\n const lines = txt.split(/\\r?\\n/)\n // embed entire AFNI HEAD text as NIfTI extension\n const mod = (dataBuffer.byteLength + 8) % 16\n const len = dataBuffer.byteLength + (16 - mod)\n log.debug(dataBuffer.byteLength, 'len', len)\n const extBuffer = new ArrayBuffer(len)\n new Uint8Array(extBuffer).set(new Uint8Array(dataBuffer))\n const newExtension = new NIFTIEXTENSION(len + 8, 42, extBuffer, true)\n hdr.addExtension(newExtension)\n hdr.extensionCode = 42\n hdr.extensionFlag[0] = 1\n hdr.extensionSize = len + 8\n // Done creating an extension\n const nlines = lines.length\n let i = 0\n let hasIJK_TO_DICOM_REAL = false\n while (i < nlines) {\n let line = lines[i] // e.g. 'type = string-attribute'\n i++\n if (!line.startsWith('type')) {\n continue\n } // n.b. white space varies, \"type =\" vs \"type =\"\n const isInt = line.includes('integer-attribute')\n const isFloat = line.includes('float-attribute')\n line = lines[i] // e.g. 'name = IDCODE_DATE'\n i++\n if (!line.startsWith('name')) {\n continue\n }\n let items: Array<string | number> = line.split('= ')\n const key = items[1] // e.g. 'IDCODE_DATE'\n line = lines[i] // e.g. 'count = 5'\n i++\n items = line.split('= ')\n let count = parseInt(items[1] as string) // e.g. '5'\n if (count < 1) {\n continue\n }\n line = lines[i] // e.g. ''LSB_FIRST~'\n i++\n items = line.trim().split(/\\s+/)\n if (isFloat || isInt) {\n // read arrays written on multiple lines\n while (items.length < count) {\n line = lines[i] // e.g. ''LSB_FIRST~'\n i++\n const items2 = line.trim().split(/\\s+/)\n items.push(...items2)\n }\n for (let j = 0; j < count; j++) {\n items[j] = parseFloat(items[j] as string)\n }\n }\n switch (key) {\n case 'BYTEORDER_STRING':\n if ((items[0] as string).includes('LSB_FIRST')) {\n hdr.littleEndian = true\n } else if ((items[0] as string).includes('MSB_FIRST')) {\n hdr.littleEndian = false\n }\n break\n case 'BRICK_TYPES':\n {\n hdr.dims[4] = count\n const datatype = parseInt(items[0] as string)\n if (datatype === 0) {\n hdr.numBitsPerVoxel = 8\n hdr.datatypeCode = NiiDataType.DT_UINT8\n } else if (datatype === 1) {\n hdr.numBitsPerVoxel = 16\n hdr.datatypeCode = NiiDataType.DT_INT16\n } else if (datatype === 3) {\n hdr.numBitsPerVoxel = 32\n hdr.datatypeCode = NiiDataType.DT_FLOAT32\n } else {\n log.warn('Unknown BRICK_TYPES ', datatype)\n }\n }\n break\n case 'IJK_TO_DICOM_REAL':\n if (count < 12) {\n break\n }\n hasIJK_TO_DICOM_REAL = true\n hdr.sform_code = 2\n // note DICOM space is LPS while NIfTI is RAS\n hdr.affine = [\n [-items[0], -items[1], -items[2], -items[3]],\n [-items[4], -items[5], -items[6], -items[7]],\n // TODO don't reuse items for numeric values\n [items[8] as number, items[9] as number, items[10] as number, items[11] as number],\n [0, 0, 0, 1]\n ]\n break\n case 'DATASET_DIMENSIONS':\n count = Math.max(count, 3)\n for (let j = 0; j < count; j++) {\n hdr.dims[j + 1] = items[j] as number\n }\n break\n case 'ORIENT_SPECIFIC':\n orientSpecific = items as number[]\n break\n case 'ORIGIN':\n xyzOrigin = items as number[]\n break\n case 'DELTA':\n xyzDelta = items as number[]\n break\n case 'TAXIS_FLOATS':\n hdr.pixDims[4] = items[0] as number\n break\n default:\n log.warn('Unknown:', key)\n } // read item\n } // read all lines\n if (!hasIJK_TO_DICOM_REAL) {\n THD_daxes_to_NIFTI(nvImage, xyzDelta, xyzOrigin, orientSpecific)\n } else {\n setPixDimFromSForm(nvImage)\n }\n const nBytes = (hdr.numBitsPerVoxel / 8) * hdr.dims[1] * hdr.dims[2] * hdr.dims[3] * hdr.dims[4]\n if (!pairedImgData) {\n throw new Error('pairedImgData not set')\n }\n if (pairedImgData.byteLength < nBytes) {\n // n.b. npm run dev implicitly extracts gz, npm run demo does not!\n return await NVUtilities.decompressToBuffer(new Uint8Array(pairedImgData))\n }\n return pairedImgData.slice(0)\n}\n","import { NIFTI1 } from 'nifti-reader-js'\nimport { log } from '@/logger'\nimport type { NVImage } from '@/nvimage'\nimport { NiiDataType } from '@/nvimage/utils'\n\n/**\n * Reads BrainVoyager V16 format image, modifying the provided NVImage header\n * and returning the raw image data buffer.\n * @param nvImage - The NVImage instance whose header will be modified.\n * @param buffer - ArrayBuffer containing the V16 file data.\n * @returns ArrayBuffer containing the image data.\n */\nexport function readV16(nvImage: NVImage, buffer: ArrayBuffer): ArrayBuffer {\n nvImage.hdr = new NIFTI1()\n const hdr = nvImage.hdr\n hdr.dims = [3, 1, 1, 1, 0, 0, 0, 0]\n hdr.pixDims = [1, 1, 1, 1, 1, 0, 0, 0]\n const reader = new DataView(buffer)\n hdr.dims[1] = reader.getUint16(0, true)\n hdr.dims[2] = reader.getUint16(2, true)\n hdr.dims[3] = reader.getUint16(4, true)\n const nBytes = 2 * hdr.dims[1] * hdr.dims[2] * hdr.dims[3]\n if (nBytes + 6 !== buffer.byteLength) {\n log.warn('This does not look like a valid BrainVoyager V16 file')\n }\n hdr.numBitsPerVoxel = 16\n hdr.datatypeCode = NiiDataType.DT_UINT16\n log.warn('Warning: V16 files have no spatial transforms')\n hdr.affine = [\n [0, 0, -hdr.pixDims[1], (hdr.dims[1] - 2) * 0.5 * hdr.pixDims[1]],\n [-hdr.pixDims[2], 0, 0, (hdr.dims[2] - 2) * 0.5 * hdr.pixDims[2]],\n [0, -hdr.pixDims[3], 0, (hdr.dims[3] - 2) * 0.5 * hdr.pixDims[3]],\n [0, 0, 0, 1]\n ]\n hdr.littleEndian = true\n return buffer.slice(6)\n}\n\n/**\n * Reads BrainVoyager VMR format image, modifying the provided NVImage header\n * and returning the raw image data buffer.\n *\n * Format specification:\n * https://support.brainvoyager.com/brainvoyager/automation-development/84-file-formats/343-developer-guide-2-6-the-format-of-vmr-files\n *\n * @param nvImage - The NVImage instance whose header will be modified.\n * @param buffer - ArrayBuffer containing the VMR file data.\n * @returns ArrayBuffer containing the image data.\n */\nexport function readVMR(nvImage: NVImage, buffer: ArrayBuffer): ArrayBuffer {\n nvImage.hdr = new NIFTI1()\n const hdr = nvImage.hdr\n hdr.dims = [3, 1, 1, 1, 0, 0, 0, 0]\n hdr.pixDims = [1, 1, 1, 1, 1, 0, 0, 0]\n const reader = new DataView(buffer)\n const version = reader.getUint16(0, true)\n if (version !== 4) {\n log.warn('Not a valid version 4 VMR image')\n }\n hdr.dims[1] = reader.getUint16(2, true)\n hdr.dims[2] = reader.getUint16(4, true)\n hdr.dims[3] = reader.getUint16(6, true)\n const nBytes = hdr.dims[1] * hdr.dims[2] * hdr.dims[3]\n if (version >= 4) {\n let pos = 8 + nBytes // offset to post header\n // let xoff = reader.getUint16(pos, true);\n // let yoff = reader.getUint16(pos + 2, true);\n // let zoff = reader.getUint16(pos + 4, true);\n // let framingCube = reader.getUint16(pos + 6, true);\n // let posInfo = reader.getUint32(pos + 8, true);\n // let coordSys = reader.getUint32(pos + 12, true);\n // let XmmStart = reader.getFloat32(pos + 16, true);\n // let YmmStart = reader.getFloat32(pos + 20, true);\n // let ZmmStart = reader.getFloat32(pos + 24, true);\n // let XmmEnd = reader.getFloat32(pos + 28, true);\n // let YmmEnd = reader.getFloat32(pos + 32, true);\n // let ZmmEnd = reader.getFloat32(pos + 36, true);\n // let Xsl = reader.getFloat32(pos + 40, true);\n // let Ysl = reader.getFloat32(pos + 44, true);\n // let Zsl = reader.getFloat32(pos + 48, true);\n // let colDirX = reader.getFloat32(pos + 52, true);\n // let colDirY = reader.getFloat32(pos + 56, true);\n // let colDirZ = reader.getFloat32(pos + 60, true);\n // let nRow = reader.getUint32(pos + 64, true);\n // let nCol = reader.getUint32(pos + 68, true);\n // let FOVrow = reader.getFloat32(pos + 72, true);\n // let FOVcol = reader.getFloat32(pos + 76, true);\n // let sliceThickness = reader.getFloat32(pos + 80, true);\n // let gapThickness = reader.getFloat32(pos + 84, true);\n const nSpatialTransforms = reader.getUint32(pos + 88, true)\n pos = pos + 92\n if (nSpatialTransforms > 0) {\n const len = buffer.byteLength\n for (let i = 0; i < nSpatialTransforms; i++) {\n // read variable length name name...\n while (pos < len && reader.getUint8(pos) !== 0) {\n pos++\n }\n pos++\n // let typ = reader.getUint32(pos, true);\n pos += 4\n // read variable length name name...\n while (pos < len && reader.getUint8(pos) !== 0) {\n pos++\n }\n pos++\n const nValues = reader.getUint32(pos, true)\n pos += 4\n for (let j = 0; j < nValues; j++) {\n pos += 4\n }\n }\n }\n // let LRconv = reader.getUint8(pos);\n // let ref = reader.getUint8(pos + 1);\n hdr.pixDims[1] = reader.getFloat32(pos + 2, true)\n hdr.pixDims[2] = reader.getFloat32(pos + 6, true)\n hdr.pixDims[3] = reader.getFloat32(pos + 10, true)\n // let isVer = reader.getUint8(pos + 14);\n // let isTal = reader.getUint8(pos + 15);\n // let minInten = reader.getInt32(pos + 16, true);\n // let meanInten = reader.getInt32(pos + 20, true);\n // let maxInten = reader.getInt32(pos + 24, true);\n }\n log.warn('Warning: VMR spatial transform not implemented')\n // if (XmmStart === XmmEnd) { // https://brainvoyager.com/bv/sampledata/index.html??\n hdr.affine = [\n [0, 0, -hdr.pixDims[1], (hdr.dims[1] - 2) * 0.5 * hdr.pixDims[1]],\n [-hdr.pixDims[2], 0, 0, (hdr.dims[2] - 2) * 0.5 * hdr.pixDims[2]],\n [0, -hdr.pixDims[3], 0, (hdr.dims[3] - 2) * 0.5 * hdr.pixDims[3]],\n [0, 0, 0, 1]\n ]\n // }\n log.debug(hdr)\n hdr.numBitsPerVoxel = 8\n hdr.datatypeCode = NiiDataType.DT_UINT8\n return buffer.slice(8, 8 + nBytes)\n}\n","import { NIFTI1 } from 'nifti-reader-js'\nimport type { NVImage } from '@/nvimage'\nimport { NiiDataType } from '@/nvimage/utils'\nimport { NVUtilities } from '@/nvutilities'\n\n/**\n * Reads DSI Studio FIB format (fiber orientation data), modifying the provided\n * NVImage header and returning the raw image data buffer and V1 vector data.\n *\n * Format specification:\n * https://dsi-studio.labsolver.org/doc/cli_data.html\n *\n * @param nvImage - The NVImage instance whose header will be modified.\n * @param buffer - ArrayBuffer containing the FIB file data.\n * @returns Promise resolving to [ArrayBuffer, Float32Array] - image data and V1 vectors.\n * @throws Error if the file is not a valid DSI Studio FIB file.\n */\n// eslint-disable-next-line @typescript-eslint/no-unused-vars\nexport async function readFIB(nvImage: NVImage, buffer: ArrayBuffer): Promise<[ArrayBuffer, Float32Array]> {\n nvImage.hdr = new NIFTI1()\n const hdr = nvImage.hdr\n hdr.littleEndian = false // MGH always big endian\n hdr.dims = [3, 1, 1, 1, 0, 0, 0, 0]\n hdr.pixDims = [1, 1, 1, 1, 1, 0, 0, 0]\n const mat = await NVUtilities.readMatV4(buffer, true)\n if (!('dimension' in mat) || !('dti_fa' in mat)) {\n throw new Error('Not a valid DSIstudio FIB file')\n }\n const hasV1 = 'index0' in mat && 'index1' in mat && 'index2' in mat && 'odf_vertices' in mat\n // const hasV1 = false\n hdr.numBitsPerVoxel = 32\n hdr.datatypeCode = NiiDataType.DT_FLOAT32\n hdr.dims[1] = mat.dimension[0]\n hdr.dims[2] = mat.dimension[1]\n hdr.dims[3] = mat.dimension[2]\n hdr.dims[4] = 1\n hdr.pixDims[1] = mat.voxel_size[0]\n hdr.pixDims[2] = mat.voxel_size[1]\n hdr.pixDims[3] = mat.voxel_size[2]\n hdr.sform_code = 1\n const xmm = (hdr.dims[1] - 1) * 0.5 * hdr.pixDims[1]\n const ymm = (hdr.dims[2] - 1) * 0.5 * hdr.pixDims[2]\n const zmm = (hdr.dims[3] - 1) * 0.5 * hdr.pixDims[3]\n hdr.affine = [\n [hdr.pixDims[1], 0, 0, -xmm],\n [0, -hdr.pixDims[2], 0, ymm],\n [0, 0, hdr.pixDims[2], -zmm],\n [0, 0, 0, 1]\n ]\n hdr.littleEndian = true\n const nVox3D = hdr.dims[1] * hdr.dims[2] * hdr.dims[3]\n const nBytes3D = nVox3D * Math.ceil(hdr.numBitsPerVoxel / 8)\n const nBytes = nBytes3D * hdr.dims[4]\n const buff8v1 = new Uint8Array(new ArrayBuffer(nVox3D * 4 * 3)) // 4=Float32, 3=x,y,z\n if (hasV1) {\n // read directions, stored as index\n const nvox = hdr.dims[1] * hdr.dims[2] * hdr.dims[3]\n const dir0 = new Float32Array(nvox)\n const dir1 = new Float32Array(nvox)\n const dir2 = new Float32Array(nvox)\n const idxs = mat.index0\n const dirs = mat.odf_vertices\n for (let i = 0; i < nvox; i++) {\n const idx = idxs[i] * 3\n dir0[i] = dirs[idx + 0]\n dir1[i] = dirs[idx + 1]\n dir2[i] = -dirs[idx + 2]\n }\n buff8v1.set(new Uint8Array(dir0.buffer, dir0.byteOffset, dir0.byteLength), 0 * nBytes3D)\n buff8v1.set(new Uint8Array(dir1.buffer, dir1.byteOffset, dir1.byteLength), 1 * nBytes3D)\n buff8v1.set(new Uint8Array(dir2.buffer, dir2.byteOffset, dir2.byteLength), 2 * nBytes3D)\n }\n if ('report' in mat) {\n hdr.description = new TextDecoder().decode(mat.report.subarray(0, Math.min(79, mat.report.byteLength)))\n }\n const buff8 = new Uint8Array(new ArrayBuffer(nBytes))\n const arrFA = Float32Array.from(mat.dti_fa)\n if ('mask' in mat) {\n let slope = 1\n if ('dti_fa_slope' in mat) {\n slope = mat.dti_fa_slope[0]\n }\n let inter = 1\n if ('dti_fa_inter' in mat) {\n inter = mat.dti_fa_inter[0]\n }\n const nvox = hdr.dims[1] * hdr.dims[2] * hdr.dims[3]\n const mask = mat.mask\n const f32 = new Float32Array(nvox)\n let j = 0\n for (let i = 0; i < nvox; i++) {\n if (mask[i] !== 0) {\n f32[i] = arrFA[j] * slope + inter\n j++\n }\n }\n return [f32.buffer, new Float32Array(buff8v1.buffer)]\n }\n // read FA\n const imgFA = new Uint8Array(arrFA.buffer, arrFA.byteOffset, arrFA.byteLength)\n buff8.set(imgFA, 0)\n return [buff8.buffer, new Float32Array(buff8v1.buffer)]\n}\n\n/**\n * Reads DSI Studio SRC format (DWI source data), modifying the provided\n * NVImage header and returning the raw image data buffer.\n *\n * Format specification:\n * https://dsi-studio.labsolver.org/doc/cli_data.html\n *\n * @param nvImage - The NVImage instance whose header will be modified.\n * @param buffer - ArrayBuffer containing the SRC file data.\n * @returns Promise resolving to ArrayBuffer containing the image data.\n * @throws Error if the file is not a valid DSI Studio SRC file.\n */\n// eslint-disable-next-line @typescript-eslint/no-unused-vars\nexport async function readSRC(nvImage: NVImage, buffer: ArrayBuffer): Promise<ArrayBuffer> {\n nvImage.hdr = new NIFTI1()\n const hdr = nvImage.hdr\n hdr.littleEndian = false // MGH always big ending\n hdr.dims = [3, 1, 1, 1, 0, 0, 0, 0]\n hdr.pixDims = [1, 1, 1, 1, 1, 0, 0, 0]\n const mat = await NVUtilities.readMatV4(buffer)\n if (!('dimension' in mat) || !('image0' in mat)) {\n throw new Error('Not a valid DSIstudio SRC file')\n }\n let n = 0\n let len = 0\n for (const [key, value] of Object.entries(mat)) {\n if (!key.startsWith('image')) {\n continue\n }\n if (n === 0) {\n len = value.length\n } else if (len !== value.length) {\n len = -1\n }\n if (value.constructor !== Uint16Array) {\n throw new Error('DSIstudio SRC files always use Uint16 datatype')\n }\n n++\n }\n if (len < 1 || n < 1) {\n throw new Error('SRC file not valid DSI Studio data. The image(s) should have the same length')\n }\n hdr.numBitsPerVoxel = 16\n hdr.datatypeCode = NiiDataType.DT_UINT16\n hdr.dims[1] = mat.dimension[0]\n hdr.dims[2] = mat.dimension[1]\n hdr.dims[3] = mat.dimension[2]\n hdr.dims[4] = n\n if (hdr.dims[4] > 1) {\n hdr.dims[0] = 4\n }\n hdr.pixDims[1] = mat.voxel_size[0]\n hdr.pixDims[2] = mat.voxel_size[1]\n hdr.pixDims[3] = mat.voxel_size[2]\n hdr.sform_code = 1\n const xmm = (hdr.dims[1] - 1) * 0.5 * hdr.pixDims[1]\n const ymm = (hdr.dims[2] - 1) * 0.5 * hdr.pixDims[2]\n const zmm = (hdr.dims[3] - 1) * 0.5 * hdr.pixDims[3]\n hdr.affine = [\n [hdr.pixDims[1], 0, 0, -xmm],\n [0, -hdr.pixDims[2], 0, ymm],\n [0, 0, hdr.pixDims[2], -zmm],\n [0, 0, 0, 1]\n ]\n hdr.littleEndian = true\n const nBytes3D = hdr.dims[1] * hdr.dims[2] * hdr.dims[3] * (hdr.numBitsPerVoxel / 8)\n const nBytes = nBytes3D * hdr.dims[4]\n const buff8 = new Uint8Array(new ArrayBuffer(nBytes))\n let offset = 0\n for (let i = 0; i < n; i++) {\n const arr = mat[`image${i}`]\n const img8 = new Uint8Array(arr.buffer, arr.byteOffset, arr.byteLength)\n buff8.set(img8, offset)\n offset += nBytes3D\n }\n if ('report' in mat) {\n hdr.description = new TextDecoder().decode(mat.report.subarray(0, Math.min(79, mat.report.byteLength)))\n }\n return buff8.buffer\n}\n","import { NIFTI1 } from 'nifti-reader-js'\nimport { log } from '@/logger'\nimport type { NVImage } from '@/nvimage'\nimport { NiiDataType } from '@/nvimage/utils'\n\n/**\n * Reads ECAT7 PET format image, modifying the provided NVImage header\n * and returning the raw image data buffer.\n * @param nvImage - The NVImage instance whose header will be modified.\n * @param buffer - ArrayBuffer containing the ECAT file data.\n * @returns ArrayBuffer containing the image data.\n * @throws Error if the file is not a valid ECAT file.\n */\nexport function readECAT(nvImage: NVImage, buffer: ArrayBuffer): ArrayBuffer {\n nvImage.hdr = new NIFTI1()\n const hdr = nvImage.hdr\n hdr.dims = [3, 1, 1, 1, 0, 0, 0, 0]\n hdr.pixDims = [1, 1, 1, 1, 1, 0, 0, 0]\n const reader = new DataView(buffer)\n\n const signature = reader.getInt32(0, false) // \"MATR\"\n const filetype = reader.getInt16(50, false)\n if (signature !== 1296127058 || filetype < 1 || filetype > 14) {\n throw new Error('Not a valid ECAT file')\n }\n // list header, starts at 512 bytes: int32_t hdr[4], r[31][4];\n let pos = 512 // 512=main header, 4*32-bit hdr\n let vols = 0\n const frame_duration = []\n let rawImg = new Float32Array()\n while (true) {\n // read 512 block lists\n const hdr0 = reader.getInt32(pos, false)\n const hdr3 = reader.getInt32(pos + 12, false)\n if (hdr0 + hdr3 !== 31) {\n break\n }\n let lpos = pos + 20 // skip hdr and read slice offset (r[0][1])\n let r = 0\n let voloffset = 0\n while (r < 31) {\n // r[0][1]...r[30][1]\n voloffset = reader.getInt32(lpos, false)\n lpos += 16 // e.g. r[0][1] to r[1][1]\n if (voloffset === 0) {\n break\n }\n r++\n let ipos = voloffset * 512 // image start position\n const spos = ipos - 512 // subheader for matrix image, immediately before image\n const data_type = reader.getUint16(spos, false)\n hdr.dims[1] = reader.getUint16(spos + 4, false)\n hdr.dims[2] = reader.getUint16(spos + 6, false)\n hdr.dims[3] = reader.getUint16(spos + 8, false)\n const scale_factor = reader.getFloat32(spos + 26, false)\n hdr.pixDims[1] = reader.getFloat32(spos + 34, false) * 10.0 // cm -> mm\n hdr.pixDims[2] = reader.getFloat32(spos + 38, false) * 10.0 // cm -> mm\n hdr.pixDims[3] = reader.getFloat32(spos + 42, false) * 10.0 // cm -> mm\n hdr.pixDims[4] = reader.getUint32(spos + 46, false) / 1000.0 // ms -> sec\n frame_duration.push(hdr.pixDims[4])\n const nvox3D = hdr.dims[1] * hdr.dims[2] * hdr.dims[3]\n const newImg = new Float32Array(nvox3D) // convert to float32 as scale varies\n if (data_type === 1) {\n // uint8\n for (let i = 0; i < nvox3D; i++) {\n newImg[i] = reader.getUint8(ipos) * scale_factor\n ipos++\n }\n } else if (data_type === 6) {\n // uint16\n for (let i = 0; i < nvox3D; i++) {\n newImg[i] = reader.getUint16(ipos, false) * scale_factor\n ipos += 2\n }\n } else if (data_type === 7) {\n // uint32\n for (let i = 0; i < nvox3D; i++) {\n newImg[i] = reader.getUint32(ipos, false) * scale_factor\n ipos += 4\n }\n } else {\n log.warn('Unknown ECAT data type ' + data_type)\n }\n const prevImg = rawImg.slice(0)\n rawImg = new Float32Array(prevImg.length + newImg.length)\n rawImg.set(prevImg)\n rawImg.set(newImg, prevImg.length)\n vols++\n }\n if (voloffset === 0) {\n break\n }\n pos += 512 // possible to have multiple 512-byte lists of images\n }\n hdr.dims[4] = vols\n hdr.pixDims[4] = frame_duration[0]\n if (vols > 1) {\n hdr.dims[0] = 4\n let isFDvaries = false\n for (let i = 0; i < vols; i++) {\n if (frame_duration[i] !== frame_duration[0]) {\n isFDvaries = true\n }\n }\n if (isFDvaries) {\n log.warn('Frame durations vary')\n }\n }\n hdr.sform_code = 1\n hdr.affine = [\n [-hdr.pixDims[1], 0, 0, (hdr.dims[1] - 2) * 0.5 * hdr.pixDims[1]],\n [0, -hdr.pixDims[2], 0, (hdr.dims[2] - 2) * 0.5 * hdr.pixDims[2]],\n [0, 0, -hdr.pixDims[3], (hdr.dims[3] - 2) * 0.5 * hdr.pixDims[3]],\n [0, 0, 0, 1]\n ]\n hdr.numBitsPerVoxel = 32\n hdr.datatypeCode = NiiDataType.DT_FLOAT32\n return rawImg.buffer as ArrayBuffer\n}\n","import { NIFTI1 } from 'nifti-reader-js'\nimport type { NVImage } from '@/nvimage'\nimport { NiiDataType } from '@/nvimage/utils'\n\n/**\n * Helper function to convert an ArrayBuffer to ImageData using the browser's Image API.\n * @param buffer - ArrayBuffer containing image data (BMP, PNG, JPG, etc.)\n * @returns Promise resolving to ImageData\n */\nexport async function imageDataFromArrayBuffer(buffer: ArrayBuffer): Promise<ImageData> {\n return new Promise<ImageData>((resolve, reject): void => {\n const blob = new Blob([buffer]) // Convert ArrayBuffer to Blob\n const url = URL.createObjectURL(blob) // Create a Blob URL\n const img = new Image()\n img.crossOrigin = 'Anonymous' // Allow CORS if needed\n img.src = url\n img.onload = (): void => {\n URL.revokeObjectURL(url) // Clean up the object URL\n const canvas = document.createElement('canvas')\n canvas.width = img.width\n canvas.height = img.height\n const ctx = canvas.getContext('2d')\n if (!ctx) {\n reject(new Error('Failed to get 2D context'))\n return\n }\n ctx.drawImage(img, 0, 0)\n resolve(ctx.getImageData(0, 0, img.width, img.height))\n }\n img.onerror = (err): void => {\n URL.revokeObjectURL(url) // Ensure cleanup on error\n reject(err)\n }\n })\n}\n\n/**\n * Reads standard image formats (BMP, PNG, JPG) using the browser's Image API,\n * modifying the provided NVImage header and returning the raw image data buffer.\n *\n * Automatically detects grayscale images and converts RGB data accordingly.\n *\n * @param nvImage - The NVImage instance whose header will be modified.\n * @param buffer - ArrayBuffer containing the image file data.\n * @returns Promise resolving to ArrayBuffer containing the image data.\n */\nexport async function readBMP(nvImage: NVImage, buffer: ArrayBuffer): Promise<ArrayBuffer> {\n const imageData = await imageDataFromArrayBuffer(buffer)\n const { width, height, data } = imageData\n nvImage.hdr = new NIFTI1()\n const hdr = nvImage.hdr\n hdr.dims = [3, width, height, 1, 0, 0, 0, 0]\n hdr.pixDims = [1, 1, 1, 1, 1, 0, 0, 0]\n hdr.affine = [\n [hdr.pixDims[1], 0, 0, -(hdr.dims[1] - 2) * 0.5 * hdr.pixDims[1]],\n [0, -hdr.pixDims[2], 0, (hdr.dims[2] - 2) * 0.5 * hdr.pixDims[2]],\n [0, 0, -hdr.pixDims[3], (hdr.dims[3] - 2) * 0.5 * hdr.pixDims[3]],\n [0, 0, 0, 1]\n ]\n hdr.numBitsPerVoxel = 8\n hdr.datatypeCode = NiiDataType.DT_RGBA32\n let isGrayscale = true\n for (let i = 0; i < data.length; i += 4) {\n if (data[i] !== data[i + 1] || data[i] !== data[i + 2]) {\n isGrayscale = false\n break\n }\n }\n if (isGrayscale) {\n hdr.datatypeCode = NiiDataType.DT_UINT8\n const grayscaleData = new Uint8Array(width * height)\n for (let i = 0, j = 0; i < data.length; i += 4, j++) {\n grayscaleData[j] = data[i]\n }\n return grayscaleData.buffer\n }\n return data.buffer as ArrayBuffer\n}\n","import { NIFTI1 } from 'nifti-reader-js'\nimport { mat3, vec3 } from 'gl-matrix'\nimport { log } from '@/logger'\nimport type { NVImage } from '@/nvimage'\nimport { NiiDataType } from '@/nvimage/utils'\nimport { NVUtilities } from '@/nvutilities'\n\n/**\n * Reads ITK MetaImage format (MHA/MHD), modifying the provided NVImage header\n * and returning the raw image data buffer.\n *\n * MHA files contain both header and image data in one file.\n * MHD files contain only the header, with image data in a separate file.\n *\n * Format specification: https://itk.org/Wiki/ITK/MetaIO/Documentation\n *\n * @param nvImage - The NVImage instance whose header will be modified.\n * @param buffer - ArrayBuffer containing the MHA/MHD file data.\n * @param pairedImgData - Optional paired image data for detached MHD headers.\n * @returns Promise resolving to ArrayBuffer containing the image data.\n * @throws Error if the file is too small or has unsupported data types.\n */\nexport async function readMHA(nvImage: NVImage, buffer: ArrayBuffer, pairedImgData: ArrayBuffer | null): Promise<ArrayBuffer> {\n const len = buffer.byteLength\n if (len < 20) {\n throw new Error('File too small to be VTK: bytes = ' + buffer.byteLength)\n }\n const bytes = new Uint8Array(buffer)\n let pos = 0\n function eol(c: number): boolean {\n return c === 10 || c === 13 // c is either a line feed character (10) or carriage return character (13)\n }\n function readStr(): string {\n while (pos < len && eol(bytes[pos])) {\n pos++\n } // Skip blank lines\n const startPos = pos\n while (pos < len && !eol(bytes[pos])) {\n pos++\n } // Forward until end of line\n if (pos - startPos < 2) {\n return ''\n }\n return new TextDecoder().decode(buffer.slice(startPos, pos))\n }\n let line = readStr() // 1st line: signature\n nvImage.hdr = new NIFTI1()\n const hdr = nvImage.hdr\n hdr.pixDims = [1, 1, 1, 1, 1, 0, 0, 0]\n hdr.dims = [1, 1, 1, 1, 1, 1, 1, 1]\n hdr.littleEndian = true\n let isGz = false\n let isDetached = false\n const mat33 = mat3.fromValues(NaN, 0, 0, 0, 1, 0, 0, 0, 1)\n const offset = vec3.fromValues(0, 0, 0)\n while (line !== '') {\n let items = line.split(' ')\n if (items.length > 2) {\n items = items.slice(2)\n }\n if (line.startsWith('BinaryDataByteOrderMSB') && items[0].includes('False')) {\n hdr.littleEndian = true\n }\n if (line.startsWith('BinaryDataByteOrderMSB') && items[0].includes('True')) {\n hdr.littleEndian = false\n }\n if (line.startsWith('CompressedData') && items[0].includes('True')) {\n isGz = true\n }\n if (line.startsWith('TransformMatrix')) {\n for (let d = 0; d < 9; d++) {\n mat33[d] = parseFloat(items[d])\n }\n }\n if (line.startsWith('Offset')) {\n for (let d = 0; d < Math.min(items.length, 3); d++) {\n offset[d] = parseFloat(items[d])\n }\n }\n // if (line.startsWith(\"AnatomicalOrientation\")) //we can ignore, tested with Slicer3D converting NIfTIspace images\n if (line.startsWith('ElementSpacing')) {\n for (let d = 0; d < items.length; d++) {\n hdr.pixDims[d + 1] = parseFloat(items[d])\n }\n }\n if (line.startsWith('DimSize')) {\n hdr.dims[0] = items.length\n for (let d = 0; d < items.length; d++) {\n hdr.dims[d + 1] = parseInt(items[d])\n }\n }\n if (line.startsWith('ElementType')) {\n switch (items[0]) {\n case 'MET_UCHAR':\n hdr.numBitsPerVoxel = 8\n hdr.datatypeCode = NiiDataType.DT_UINT8\n break\n case 'MET_CHAR':\n hdr.numBitsPerVoxel = 8\n hdr.datatypeCode = NiiDataType.DT_INT8\n break\n case 'MET_SHORT':\n hdr.numBitsPerVoxel = 16\n hdr.datatypeCode = NiiDataType.DT_INT16\n break\n case 'MET_USHORT':\n hdr.numBitsPerVoxel = 16\n hdr.datatypeCode = NiiDataType.DT_UINT16\n break\n case 'MET_INT':\n hdr.numBitsPerVoxel = 32\n hdr.datatypeCode = NiiDataType.DT_INT32\n break\n case 'MET_UINT':\n hdr.numBitsPerVoxel = 32\n hdr.datatypeCode = NiiDataType.DT_UINT32\n break\n case 'MET_FLOAT':\n hdr.numBitsPerVoxel = 32\n hdr.datatypeCode = NiiDataType.DT_FLOAT32\n break\n case 'MET_DOUBLE':\n hdr.numBitsPerVoxel = 64\n hdr.datatypeCode = NiiDataType.DT_FLOAT64\n break\n default:\n throw new Error('Unsupported MHA data type: ' + items[0])\n }\n }\n if (line.startsWith('ObjectType') && !items[0].includes('Image')) {\n log.warn('Only able to read ObjectType = Image, not ' + line)\n }\n if (line.startsWith('ElementDataFile')) {\n if (items[0] !== 'LOCAL') {\n isDetached = true\n }\n break\n }\n line = readStr()\n }\n const mmMat = mat3.fromValues(hdr.pixDims[1], 0, 0, 0, hdr.pixDims[2], 0, 0, 0, hdr.pixDims[3])\n mat3.multiply(mat33, mat33, mmMat)\n hdr.affine = [\n [-mat33[0], -mat33[3], -mat33[6], -offset[0]],\n [-mat33[1], -mat33[4], -mat33[7], -offset[1]],\n [mat33[2], mat33[5], mat33[8], offset[2]],\n [0, 0, 0, 1]\n ]\n while (bytes[pos] === 10) {\n pos++\n }\n hdr.vox_offset = pos\n if (isDetached && pairedImgData) {\n if (isGz) {\n return await NVUtilities.decompressToBuffer(new Uint8Array(pairedImgData.slice(0)))\n }\n return pairedImgData.slice(0)\n }\n if (isGz) {\n return await NVUtilities.decompressToBuffer(new Uint8Array(buffer.slice(hdr.vox_offset)))\n }\n return buffer.slice(hdr.vox_offset)\n}\n","import { NIFTI1, NIFTI2 } from 'nifti-reader-js'\nimport { mat4 } from 'gl-matrix'\nimport { log } from '@/logger'\nimport { NVUtilities } from '@/nvutilities'\nimport type { NVImage } from '@/nvimage'\nimport { isPlatformLittleEndian, NiiDataType } from '@/nvimage/utils'\n\n/**\n * @internal\n * Read all occurrences of a metadata tag from the MGH image footer.\n *\n * MGH images can contain strings in an optional footer.\n * These items are discriminated by their \"tag\" number.\n * Tags can be used to detect indexed atlases.\n * Note: Some tags (like tag 1) include a combination of ASCII and binary data.\n *\n * @param view - DataView representing the footer bytes\n * @param offset - Byte offset to the start of the footer\n * @param footerLength - Length of the footer\n * @param tagToRead - Tag identifier to extract (default = 1)\n * @returns Concatenated string of all matched tag contents, separated by double newlines\n */\nfunction readTag(view: DataView, offset: number, footerLength: number, tagToRead: number = 1): string {\n const end = offset + footerLength\n let pos = offset\n const results: string[] = []\n while (pos + 12 <= end) {\n const tag = view.getInt32(pos, false) // tag (little-endian)\n // skip 4 bytes (padding), read 4-byte length\n const length = view.getInt32(pos + 8, false) // length of data\n pos += 12\n if (length <= 0 || pos + length > end) {\n break // corrupt or truncated footer\n }\n if (tag !== tagToRead) {\n pos += length\n continue\n }\n let strLen = length\n let contentPos = pos\n if (tagToRead === 1) {\n if (pos + 4 > end) {\n break\n }\n strLen = view.getInt32(pos, false)\n contentPos += 4\n }\n if (strLen > 1 && contentPos + strLen <= end) {\n const raw = new Uint8Array(view.buffer, contentPos, strLen)\n const str = new TextDecoder('utf-8').decode(raw.slice(0, -1)) // remove null terminator\n results.push(str)\n }\n pos += length\n }\n return results.join('\\n\\n')\n}\n\n/**\n * @internal\n * Optimize FreeSurfer label image data by converting from float/int32 to the smallest suitable integer type.\n * Returns the raw image buffer if the input type is unsupported or values are not valid label indices.\n *\n * This function:\n * - Handles byte-swapping for little-endian systems.\n * - Ensures all values are finite integers within valid label ranges.\n * - Converts data to INT32, INT16, or UINT8 to reduce memory usage when possible.\n *\n * @param hdr - The NIfTI header object, which will be updated in-place.\n * @param imgRaw - The raw image data as an ArrayBuffer.\n * @returns A possibly transformed ArrayBuffer, or the original buffer if optimization is not possible.\n */\nexport function optimizeFreeSurferLabels(hdr: NIFTI1 | NIFTI2, imgRaw: ArrayBuffer): ArrayBuffer {\n hdr.intent_code = 1002\n if (hdr.datatypeCode !== NiiDataType.DT_FLOAT32 && hdr.datatypeCode !== NiiDataType.DT_INT32) {\n return imgRaw\n }\n // Parse input to float or int array\n let img: Float32Array | Int32Array = new Float32Array(imgRaw)\n if (hdr.datatypeCode === NiiDataType.DT_INT32) {\n img = new Int32Array(imgRaw)\n }\n // Byte-swap if needed\n if (isPlatformLittleEndian()) {\n const u32 = new Uint32Array(imgRaw)\n for (let i = 0; i < u32.length; i++) {\n const val = u32[i]\n u32[i] = ((val & 0x000000ff) << 24) | ((val & 0x0000ff00) << 8) | ((val & 0x00ff0000) >>> 8) | ((val & 0xff000000) >>> 24)\n }\n }\n hdr.littleEndian = isPlatformLittleEndian()\n // Validate values\n let isInteger = true\n let mn = Infinity\n let mx = -Infinity\n for (let i = 0; i < img.length; i++) {\n const v = img[i]\n if (!Number.isFinite(v)) {\n continue\n }\n if (!Number.isInteger(v)) {\n isInteger = false\n }\n if (v < mn) {\n mn = v\n }\n if (v > mx) {\n mx = v\n }\n }\n if (!isInteger || mn < 0 || mx > 2147483647) {\n log.warn(`FreeSurfer Labels must be integers in INT32 range. range ${mn}..${mx}`)\n return imgRaw\n }\n // Optimize datatype\n if (mx > 32767) {\n hdr.datatypeCode = NiiDataType.DT_INT32\n const out = new Int32Array(img.length)\n for (let i = 0; i < img.length; i++) {\n out[i] = Math.trunc(img[i])\n }\n return out.buffer\n } else if (mx > 255) {\n hdr.datatypeCode = NiiDataType.DT_INT16\n hdr.numBitsPerVoxel = 16\n const out = new Int16Array(img.length)\n for (let i = 0; i < img.length; i++) {\n out[i] = Math.trunc(img[i])\n }\n return out.buffer\n } else {\n hdr.datatypeCode = NiiDataType.DT_UINT8\n hdr.numBitsPerVoxel = 8\n const out = new Uint8Array(img.length)\n for (let i = 0; i < img.length; i++) {\n out[i] = Math.trunc(img[i])\n }\n return out.buffer\n }\n}\n\n/**\n * @internal\n * Determine if an MGH file is a FreeSurfer label image by inspecting the footer.\n * MGH label images often include additional metadata after the image data.\n * This function checks for specific patterns in the footer to infer if the image\n * represents labeled data (e.g., label2vol-generated volumes or files referencing LUTs).\n *\n * @param raw - The complete ArrayBuffer of the MGH file.\n * @param hdr - The parsed NIfTI header, including vox_offset and datatypeCode.\n * @param expectedBytes - The expected size of the image data in bytes.\n * @returns A boolean indicating whether the file is likely a labeled atlas.\n * @see https://niivue.com/demos/features/labels.html\n */\nexport function isFreeSurferLabelImage(raw: ArrayBuffer, hdr: NIFTI1 | NIFTI2, expectedBytes: number): boolean {\n const remainingBytes = raw.byteLength - hdr.vox_offset\n if (remainingBytes < expectedBytes) {\n log.error(`MGH image data size mismatch: expected ${expectedBytes}, found ${remainingBytes}`)\n return false\n }\n\n if (remainingBytes === expectedBytes) {\n return false\n }\n // Skip the first 20 bytes (5 * float32) of the MGH footer\n const footerStart = hdr.vox_offset + expectedBytes + 20\n const footerLength = raw.byteLength - footerStart\n if (footerLength <= 12) {\n return false\n }\n const tag1 = readTag(new DataView(raw), footerStart, footerLength)\n if (tag1.toLowerCase().endsWith('lut.txt')) {\n return true\n }\n const tag3 = readTag(new DataView(raw), footerStart, footerLength, 3)\n return tag3.includes('mri_label2vol')\n}\n\n/**\n * Reads FreeSurfer MGH/MGZ format image, modifying the provided NVImage header\n * and returning the raw image data buffer.\n * @param nvImage - The NVImage instance whose header will be modified.\n * @param buffer - ArrayBuffer containing the MGH/MGZ file data.\n * @param name - optional name of image, allowing label detection.\n * @returns Promise resolving to the imgRaw ArrayBuffer or null on critical error.\n */\nexport async function readMgh(nvImage: NVImage, buffer: ArrayBuffer, name: string = ''): Promise<ArrayBuffer | null> {\n if (!nvImage.hdr) {\n log.debug('readMgh called before nvImage.hdr was initialized. Creating default.')\n nvImage.hdr = new NIFTI1() // Ensure header object exists\n }\n const hdr = nvImage.hdr\n hdr.littleEndian = false\n\n let raw = buffer\n let reader = new DataView(raw)\n\n // Decompression logic\n if (raw.byteLength >= 2 && reader.getUint8(0) === 31 && reader.getUint8(1) === 139) {\n try {\n raw = await NVUtilities.decompressToBuffer(new Uint8Array(buffer))\n reader = new DataView(raw)\n } catch (err) {\n log.error('Failed to decompress MGZ file.', err)\n return null\n }\n }\n\n if (raw.byteLength < 284) {\n log.error('File too small to be a valid MGH/MGZ header.')\n return null\n }\n\n // --- Read MGH Header Fields ---\n const version = reader.getInt32(0, false)\n const width = reader.getInt32(4, false)\n const height = reader.getInt32(8, false)\n const depth = reader.getInt32(12, false)\n const nframes = reader.getInt32(16, false)\n const mtype = reader.getInt32(20, false)\n const spacingX = reader.getFloat32(30, false)\n const spacingY = reader.getFloat32(34, false)\n const spacingZ = reader.getFloat32(38, false)\n const xr = reader.getFloat32(42, false)\n const xa = reader.getFloat32(46, false)\n const xs = reader.getFloat32(50, false)\n const yr = reader.getFloat32(54, false)\n const ya = reader.getFloat32(58, false)\n const ys = reader.getFloat32(62, false)\n const zr = reader.getFloat32(66, false)\n const za = reader.getFloat32(70, false)\n const zs = reader.getFloat32(74, false)\n const cr = reader.getFloat32(78, false)\n const ca = reader.getFloat32(82, false)\n const cs = reader.getFloat32(86, false)\n\n if (version !== 1 && version !== 257) {\n log.warn(`Unexpected MGH version: ${version}.`)\n }\n if (width <= 0 || height <= 0 || depth <= 0) {\n log.error(`Invalid MGH dimensions: ${width}x${height}x${depth}`)\n return null\n }\n\n // Map MGH data type directly onto nvImage.hdr\n switch (mtype) {\n case 0:\n hdr.numBitsPerVoxel = 8\n hdr.datatypeCode = NiiDataType.DT_UINT8\n break\n case 4:\n hdr.numBitsPerVoxel = 16\n hdr.datatypeCode = NiiDataType.DT_INT16\n break\n case 1:\n hdr.numBitsPerVoxel = 32\n hdr.datatypeCode = NiiDataType.DT_INT32\n break\n case 3:\n hdr.numBitsPerVoxel = 32\n hdr.datatypeCode = NiiDataType.DT_FLOAT32\n break\n default:\n log.error(`Unsupported MGH data type: ${mtype}`)\n return null\n }\n\n // Set dimensions directly onto nvImage.hdr\n hdr.dims[1] = width\n hdr.dims[2] = height\n hdr.dims[3] = depth\n hdr.dims[4] = Math.max(1, nframes)\n hdr.dims[0] = hdr.dims[4] > 1 ? 4 : 3\n\n // Set pixel dimensions directly onto nvImage.hdr (using abs)\n hdr.pixDims[0] = 1\n hdr.pixDims[1] = Math.abs(spacingX)\n hdr.pixDims[2] = Math.abs(spacingY)\n hdr.pixDims[3] = Math.abs(spacingZ)\n hdr.pixDims[4] = 0\n\n hdr.sform_code = 1\n hdr.qform_code = 0\n hdr.sform_code = 1\n const rot44 = mat4.fromValues(\n xr * hdr.pixDims[1],\n yr * hdr.pixDims[2],\n zr * hdr.pixDims[3],\n 0,\n xa * hdr.pixDims[1],\n ya * hdr.pixDims[2],\n za * hdr.pixDims[3],\n 0,\n xs * hdr.pixDims[1],\n ys * hdr.pixDims[2],\n zs * hdr.pixDims[3],\n 0,\n 0,\n 0,\n 0,\n 1\n )\n const Pcrs = [hdr.dims[1] / 2.0, hdr.dims[2] / 2.0, hdr.dims[3] / 2.0, 1]\n\n const PxyzOffset = [0, 0, 0, 0]\n for (let i = 0; i < 3; i++) {\n PxyzOffset[i] = 0\n for (let j = 0; j < 3; j++) {\n PxyzOffset[i] = PxyzOffset[i] + rot44[j + i * 4] * Pcrs[j]\n }\n }\n hdr.affine = [\n [rot44[0], rot44[1], rot44[2], cr - PxyzOffset[0]],\n [rot44[4], rot44[5], rot44[6], ca - PxyzOffset[1]],\n [rot44[8], rot44[9], rot44[10], cs - PxyzOffset[2]],\n [0, 0, 0, 1]\n ]\n\n hdr.vox_offset = 284\n hdr.magic = 'n+1'\n\n // Check data size\n const nBytesPerVoxel = hdr.numBitsPerVoxel / 8\n const nVoxels = width * height * depth * hdr.dims[4]\n const expectedBytes = nVoxels * nBytesPerVoxel\n // Return only the raw image data buffer\n const imgRaw = raw.slice(hdr.vox_offset, hdr.vox_offset + expectedBytes)\n // label detection based on:\n // https://github.com/pwighton/mgz-optimize/blob/main/mgz_optimize.py\n // option 1: detect label by version number\n let isLabel = version === 257\n // option 2: detect label by filename\n if (!isLabel) {\n const mgLabelFiles = [\n 'aparc.DKTatlas+aseg.deep.mg',\n 'aparc+aseg.mg',\n 'aparc.DKTatlas+aseg.mg',\n 'aparc.a2005s+aseg.mg',\n 'aparc.a2009s+aseg.mg',\n 'apas+head.mg',\n 'apas+head.samseg.mg',\n 'aseg.auto.mg',\n 'aseg.auto_noCCseg.mg',\n 'aseg.mg',\n 'aseg.presurf.hypos.mg',\n 'aseg.presurf.mg',\n 'brainstemSsLabels.v13.FSvoxelSpace.mg',\n 'brainstemSsLabels.v13.mg',\n 'ctrl_pts.mg',\n 'filled.auto.mg',\n 'filled.mg',\n 'gtmseg.mg',\n 'hypothalamic_subunits_seg.v1.mg',\n 'lh.hippoAmygLabels-T1.v22.CA.FSvoxelSpace.mg',\n 'lh.hippoAmygLabels-T1.v22.CA.mg',\n 'lh.hippoAmygLabels-T1.v22.FS60.FSvoxelSpace.mg',\n 'lh.hippoAmygLabels-T1.v22.FS60.mg',\n 'lh.hippoAmygLabels-T1.v22.FSvoxelSpace.mg',\n 'lh.hippoAmygLabels-T1.v22.HBT.FSvoxelSpace.mg',\n 'lh.hippoAmygLabels-T1.v22.HBT.mg',\n 'lh.hippoAmygLabels-T1.v22.mg',\n 'lh.ribbon.mg',\n 'mca-dura.mg',\n 'rh.hippoAmygLabels-T1.v22.CA.FSvoxelSpace.mg',\n 'rh.hippoAmygLabels-T1.v22.CA.mg',\n 'rh.hippoAmygLabels-T1.v22.FS60.FSvoxelSpace.mg',\n 'rh.hippoAmygLabels-T1.v22.FS60.mg',\n 'rh.hippoAmygLabels-T1.v22.FSvoxelSpace.mg',\n 'rh.hippoAmygLabels-T1.v22.HBT.FSvoxelSpace.mg',\n 'rh.hippoAmygLabels-T1.v22.HBT.mg',\n 'rh.hippoAmygLabels-T1.v22.mg',\n 'rh.ribbon.mg',\n 'ribbon.mg',\n 'synthseg.mg',\n 'synthseg.rca.mg',\n 'vsinus.mg',\n 'subcort.mask.1mm.mg',\n 'subcort.mask.mg',\n 'surface.defects.mg',\n 'ThalamicNuclei.v13.T1.FSvoxelSpace.mg',\n 'ThalamicNuclei.v13.T1.mg',\n 'wm.asegedit.mg',\n 'wmparc.mg'\n ]\n isLabel = mgLabelFiles.some((label) => name.includes(label))\n }\n // option 3: detect label using MGH footer\n if (!isLabel) {\n isLabel = isFreeSurferLabelImage(raw, hdr, expectedBytes)\n }\n if (isLabel) {\n return optimizeFreeSurferLabels(hdr, imgRaw)\n }\n return imgRaw\n}\n","import { NIFTI1 } from 'nifti-reader-js'\nimport { log } from '@/logger'\nimport type { NVImage } from '@/nvimage'\nimport { NiiDataType } from '@/nvimage/utils'\nimport { NVUtilities } from '@/nvutilities'\n\n/**\n * Reads MRtrix MIF format image, modifying the provided NVImage header\n * and returning the raw image data buffer.\n *\n * MIF files typically contain 3D (anatomical) or 4D (fMRI, DWI) data.\n * 5D data is rarely seen. This reader supports up to 5D.\n *\n * Format specification: https://mrtrix.readthedocs.io/en/latest/\n *\n * @param nvImage - The NVImage instance whose header will be modified.\n * @param buffer - ArrayBuffer containing the MIF file data.\n * @param pairedImgData - Optional paired image data for detached MIH headers.\n * @returns Promise resolving to ArrayBuffer containing the image data.\n * @throws Error if the file is not a valid MIF file.\n */\nexport async function readMIF(nvImage: NVImage, buffer: ArrayBuffer, pairedImgData: ArrayBuffer | null): Promise<ArrayBuffer> {\n // MIF files typically 3D (e.g. anatomical), 4D (fMRI, DWI). 5D rarely seen\n // This read currently supports up to 5D. To create test: \"mrcat -axis 4 a4d.mif b4d.mif out5d.mif\"\n nvImage.hdr = new NIFTI1()\n const hdr = nvImage.hdr\n hdr.pixDims = [1, 1, 1, 1, 1, 0, 0, 0]\n hdr.dims = [1, 1, 1, 1, 1, 1, 1, 1]\n let len = buffer.byteLength\n if (len < 20) {\n throw new Error('File too small to be MIF: bytes = ' + len)\n }\n let bytes = new Uint8Array(buffer)\n if (bytes[0] === 31 && bytes[1] === 139) {\n log.debug('MIF with GZ decompression')\n buffer = await NVUtilities.decompressToBuffer(new Uint8Array(buffer))\n len = buffer.byteLength\n bytes = new Uint8Array(buffer)\n }\n let pos = 0\n function readStr(): string {\n while (pos < len && bytes[pos] === 10) {\n pos++\n } // skip blank lines\n const startPos = pos\n while (pos < len && bytes[pos] !== 10) {\n pos++\n }\n pos++ // skip EOLN\n if (pos - startPos < 1) {\n return ''\n }\n return new TextDecoder().decode(buffer.slice(startPos, pos - 1))\n }\n let line = readStr() // 1st line: signature 'mrtrix tracks'\n if (!line.startsWith('mrtrix image')) {\n throw new Error('Not a valid MIF file')\n }\n const layout = []\n let isBit = false\n let nTransform = 0\n let TR = 0\n let isDetached = false\n // let isTensor = false\n line = readStr()\n while (pos < len && !line.startsWith('END')) {\n let items = line.split(':') // \"vox: 1,1,1\" -> \"vox\", \" 1,1,1\"\n line = readStr()\n if (items.length < 2) {\n break\n } //\n const tag = items[0] // \"datatype\", \"dim\"\n items = items[1].split(',') // \" 1,1,1\" -> \" 1\", \"1\", \"1\"\n for (let i = 0; i < items.length; i++) {\n items[i] = items[i].trim()\n } // \" 1\", \"1\", \"1\" -> \"1\", \"1\", \"1\"\n switch (tag) {\n case 'dim':\n hdr.dims[0] = items.length\n for (let i = 0; i < items.length; i++) {\n hdr.dims[i + 1] = parseInt(items[i])\n }\n break\n case 'vox':\n for (let i = 0; i < items.length; i++) {\n hdr.pixDims[i + 1] = parseFloat(items[i])\n if (isNaN(hdr.pixDims[i + 1])) {\n hdr.pixDims[i + 1] = 0.0\n }\n }\n break\n case 'layout':\n for (let i = 0; i < items.length; i++) {\n layout.push(parseInt(items[i]))\n } // n.b. JavaScript preserves sign for -0\n break\n case 'datatype':\n {\n const dt = items[0]\n if (dt.startsWith('Bit')) {\n isBit = true\n hdr.datatypeCode = NiiDataType.DT_UINT8\n } else if (dt.startsWith('Int8')) {\n hdr.datatypeCode = NiiDataType.DT_INT8\n } else if (dt.startsWith('UInt8')) {\n hdr.datatypeCode = NiiDataType.DT_UINT8\n } else if (dt.startsWith('Int16')) {\n hdr.datatypeCode = NiiDataType.DT_INT16\n } else if (dt.startsWith('UInt16')) {\n hdr.datatypeCode = NiiDataType.DT_UINT16\n } else if (dt.startsWith('Int32')) {\n hdr.datatypeCode = NiiDataType.DT_INT32\n } else if (dt.startsWith('UInt32')) {\n hdr.datatypeCode = NiiDataType.DT_UINT32\n } else if (dt.startsWith('Float32')) {\n hdr.datatypeCode = NiiDataType.DT_FLOAT32\n } else if (dt.startsWith('Float64')) {\n hdr.datatypeCode = NiiDataType.DT_FLOAT64\n } else {\n log.warn('Unsupported datatype ' + dt)\n }\n if (dt.includes('8')) {\n hdr.numBitsPerVoxel = 8\n } else if (dt.includes('16')) {\n hdr.numBitsPerVoxel = 16\n } else if (dt.includes('32')) {\n hdr.numBitsPerVoxel = 32\n } else if (dt.includes('64')) {\n hdr.numBitsPerVoxel = 64\n }\n hdr.littleEndian = true // native, to do support big endian readers\n if (dt.endsWith('LE')) {\n hdr.littleEndian = true\n }\n if (dt.endsWith('BE')) {\n hdr.littleEndian = false\n }\n }\n break\n case 'transform':\n if (nTransform > 2 || items.length !== 4) {\n break\n }\n hdr.affine[nTransform][0] = parseFloat(items[0])\n hdr.affine[nTransform][1] = parseFloat(items[1])\n hdr.affine[nTransform][2] = parseFloat(items[2])\n hdr.affine[nTransform][3] = parseFloat(items[3])\n nTransform++\n break\n case 'comments':\n hdr.description = items[0].substring(0, Math.min(79, items[0].length))\n break\n /* case 'command_history':\n if (items[0].startsWith('dwi2tensor')) {\n isTensor = true\n }\n break */\n case 'RepetitionTime':\n TR = parseFloat(items[0])\n break\n case 'file':\n isDetached = !items[0].startsWith('. ')\n if (!isDetached) {\n items = items[0].split(' ') // \". 2336\" -> \". \", \"2336\"\n hdr.vox_offset = parseInt(items[1])\n }\n break\n }\n }\n const ndim = hdr.dims[0]\n if (ndim > 5) {\n log.warn('reader only designed for a maximum of 5 dimensions (XYZTD)')\n }\n let nvox = 1\n for (let i = 0; i < ndim; i++) {\n nvox *= Math.max(hdr.dims[i + 1], 1)\n }\n // let nvox = hdr.dims[1] * hdr.dims[2] * hdr.dims[3] * hdr.dims[4];\n for (let i = 0; i < 3; i++) {\n for (let j = 0; j < 3; j++) {\n // hdr.affine[i][j] *= hdr.pixDims[i + 1];\n hdr.affine[i][j] *= hdr.pixDims[j + 1]\n }\n }\n log.debug('mif affine:' + hdr.affine[0])\n if (TR > 0) {\n hdr.pixDims[4] = TR\n }\n if (isDetached && !pairedImgData) {\n log.warn('MIH header provided without paired image data')\n }\n let rawImg: ArrayBuffer\n if (pairedImgData && isDetached) {\n rawImg = pairedImgData.slice(0)\n } else if (isBit) {\n hdr.numBitsPerVoxel = 8\n const img8 = new Uint8Array(nvox)\n const buffer1 = buffer.slice(hdr.vox_offset, hdr.vox_offset + Math.ceil(nvox / 8))\n const img1 = new Uint8Array(buffer1)\n let j = 0\n for (let i = 0; i < nvox; i++) {\n const bit = i % 8\n img8[i] = (img1[j] >> (7 - bit)) & 1\n if (bit === 7) {\n j++\n }\n }\n rawImg = img8.buffer\n } else {\n // n.b. mrconvert can pad files? See dtitest_Siemens_SC 4_dti_nopf_x2_pitch\n rawImg = buffer.slice(hdr.vox_offset, hdr.vox_offset + nvox * (hdr.numBitsPerVoxel / 8))\n }\n if (layout.length !== hdr.dims[0]) {\n log.warn('dims does not match layout')\n }\n // estimate strides:\n let stride = 1\n const instride = [1, 1, 1, 1, 1]\n const inflip = [false, false, false, false, false]\n for (let i = 0; i < layout.length; i++) {\n for (let j = 0; j < layout.length; j++) {\n const a = Math.abs(layout[j])\n if (a !== i) {\n continue\n }\n instride[j] = stride\n // detect -0: https://medium.com/coding-at-dawn/is-negative-zero-0-a-number-in-javascript-c62739f80114\n if (layout[j] < 0 || Object.is(layout[j], -0)) {\n inflip[j] = true\n }\n stride *= hdr.dims[j + 1]\n }\n }\n // lookup table for flips and stride offsets:\n let xlut = NVUtilities.range(0, hdr.dims[1] - 1, 1)\n if (inflip[0]) {\n xlut = NVUtilities.range(hdr.dims[1] - 1, 0, -1)\n }\n for (let i = 0; i < hdr.dims[1]; i++) {\n xlut[i] *= instride[0]\n }\n let ylut = NVUtilities.range(0, hdr.dims[2] - 1, 1)\n if (inflip[1]) {\n ylut = NVUtilities.range(hdr.dims[2] - 1, 0, -1)\n }\n for (let i = 0; i < hdr.dims[2]; i++) {\n ylut[i] *= instride[1]\n }\n let zlut = NVUtilities.range(0, hdr.dims[3] - 1, 1)\n if (inflip[2]) {\n zlut = NVUtilities.range(hdr.dims[3] - 1, 0, -1)\n }\n for (let i = 0; i < hdr.dims[3]; i++) {\n zlut[i] *= instride[2]\n }\n let tlut = NVUtilities.range(0, hdr.dims[4] - 1, 1)\n if (inflip[3]) {\n tlut = NVUtilities.range(hdr.dims[4] - 1, 0, -1)\n }\n for (let i = 0; i < hdr.dims[4]; i++) {\n tlut[i] *= instride[3]\n }\n let dlut = NVUtilities.range(0, hdr.dims[5] - 1, 1)\n if (inflip[4]) {\n dlut = NVUtilities.range(hdr.dims[5] - 1, 0, -1)\n }\n for (let i = 0; i < hdr.dims[5]; i++) {\n dlut[i] *= instride[4]\n }\n // input and output arrays\n let j = 0\n let inVs: Int8Array | Uint8Array | Int16Array | Uint16Array | Int32Array | Uint32Array | Float32Array | Float64Array\n let outVs: Int8Array | Uint8Array | Int16Array | Uint16Array | Int32Array | Uint32Array | Float32Array | Float64Array\n switch (hdr.datatypeCode) {\n case NiiDataType.DT_INT8:\n inVs = new Int8Array(rawImg)\n outVs = new Int8Array(nvox)\n break\n case NiiDataType.DT_UINT8:\n inVs = new Uint8Array(rawImg)\n outVs = new Uint8Array(nvox)\n break\n case NiiDataType.DT_INT16:\n inVs = new Int16Array(rawImg)\n outVs = new Int16Array(nvox)\n break\n case NiiDataType.DT_UINT16:\n inVs = new Uint16Array(rawImg)\n outVs = new Uint16Array(nvox)\n break\n case NiiDataType.DT_INT32:\n inVs = new Int32Array(rawImg)\n outVs = new Int32Array(nvox)\n break\n case NiiDataType.DT_UINT32:\n inVs = new Uint32Array(rawImg)\n outVs = new Uint32Array(nvox)\n break\n case NiiDataType.DT_FLOAT32:\n inVs = new Float32Array(rawImg)\n outVs = new Float32Array(nvox)\n break\n case NiiDataType.DT_FLOAT64:\n inVs = new Float64Array(rawImg)\n outVs = new Float64Array(nvox)\n break\n default:\n throw new Error('unknown datatypeCode')\n }\n for (let d = 0; d < hdr.dims[5]; d++) {\n for (let t = 0; t < hdr.dims[4]; t++) {\n for (let z = 0; z < hdr.dims[3]; z++) {\n for (let y = 0; y < hdr.dims[2]; y++) {\n for (let x = 0; x < hdr.dims[1]; x++) {\n outVs[j] = inVs[xlut[x] + ylut[y] + zlut[z] + tlut[t] + dlut[d]]\n j++\n } // for x\n } // for y\n } // for z\n } // for t (time)\n } // for d (direction, phase/real, etc)\n return outVs.buffer as ArrayBuffer\n}\n","import { isCompressed, decompressAsync, readHeaderAsync, readImage, hasExtension } from 'nifti-reader-js'\nimport { log } from '@/logger'\nimport type { NVImage } from '@/nvimage'\nimport { NiiDataType } from '@/nvimage/utils'\n\n/**\n * Reads NIfTI format image (.nii, .nii.gz), modifying the provided NVImage header\n * and returning the raw image data buffer.\n * @param nvImage - The NVImage instance whose header will be modified.\n * @param buffer - ArrayBuffer containing the NIfTI file data.\n * @returns Promise resolving to the imgRaw ArrayBufferLike or null on critical error.\n */\nexport async function readNifti(nvImage: NVImage, buffer: ArrayBuffer, pairedImageData?: ArrayBuffer): Promise<ArrayBufferLike | null> {\n let dataBuffer = buffer // Work with a local variable\n let imgRaw: ArrayBufferLike | null = null\n\n try {\n if (isCompressed(dataBuffer as ArrayBuffer)) {\n log.debug(`Decompressing NIfTI file: ${nvImage.name}`) // Use name from nvImage instance\n dataBuffer = await decompressAsync(dataBuffer as ArrayBuffer)\n log.debug(`Decompression complete for: ${nvImage.name}`)\n }\n\n // Ensure buffer is still valid after potential decompression\n if (!dataBuffer || dataBuffer.byteLength === 0) {\n throw new Error('Buffer became invalid after decompression attempt.')\n }\n\n nvImage.hdr = await readHeaderAsync(dataBuffer as ArrayBuffer, pairedImageData != null)\n if (hasExtension(nvImage.hdr)) {\n nvImage.extensions = nvImage.hdr.extensions\n }\n\n if (nvImage.hdr === null) {\n throw new Error(`Failed to read NIfTI header: ${nvImage.name}`)\n }\n\n if (nvImage.hdr.cal_min === 0 && nvImage.hdr.cal_max === 255 && nvImage.hdr.datatypeCode !== NiiDataType.DT_UINT8) {\n log.debug(`Resetting suspicious cal_min/max (0/255) for non-uint8 NIfTI: ${nvImage.name}`)\n nvImage.hdr.cal_min = 0.0 // Use 0.0 to signal unset/recalculate later\n nvImage.hdr.cal_max = 0.0\n }\n\n imgRaw = pairedImageData ? readImage(nvImage.hdr, pairedImageData) : readImage(nvImage.hdr, dataBuffer as ArrayBuffer)\n\n if (imgRaw === null) {\n throw new Error(`nifti-reader-js readImage returned null for ${nvImage.name}`)\n }\n\n return imgRaw\n } catch (err) {\n log.error(`Error processing NIfTI file ${nvImage.name}:`, err)\n nvImage.hdr = null\n return null\n }\n}\n","import { NIFTI1 } from 'nifti-reader-js'\nimport { mat3, mat4, vec3 } from 'gl-matrix'\nimport { log } from '@/logger'\nimport { NVUtilities } from '@/nvutilities'\nimport type { NVImage } from '@/nvimage'\nimport { NiiDataType } from '@/nvimage/utils'\n\n/**\n * Reads NRRD/NHDR format image, modifying the provided NVImage header\n * and returning the raw image data buffer.\n * @param nvImage - The NVImage instance whose header will be modified.\n * @param dataBuffer - ArrayBuffer containing the NRRD header or full file.\n * @param pairedImgData - Optional ArrayBuffer for detached data file (used by NHDR).\n * @returns Promise resolving to the imgRaw ArrayBuffer or null on critical error.\n */\nexport async function readNrrd(nvImage: NVImage, dataBuffer: ArrayBuffer, pairedImgData: ArrayBuffer | null = null): Promise<ArrayBuffer | null> {\n if (!nvImage.hdr) {\n log.warn('readNrrd called before nvImage.hdr was initialized. Creating default.')\n nvImage.hdr = new NIFTI1()\n }\n const hdr = nvImage.hdr // Use nvImage.hdr directly\n hdr.pixDims = [1, 1, 1, 1, 1, 0, 0, 0]\n hdr.dims = [0, 0, 0, 0, 0, 0, 0, 0]\n const len = dataBuffer.byteLength\n let txt: string | null = null\n const bytes = new Uint8Array(dataBuffer)\n\n for (let i = 1; i < len; i++) {\n if (bytes[i - 1] === 10 && bytes[i] === 10) {\n const v = dataBuffer.slice(0, i - 1)\n txt = new TextDecoder().decode(v)\n hdr.vox_offset = i + 1 // Set based on header end position\n break\n }\n }\n\n if (txt === null) {\n log.error('readNrrd: could not extract txt')\n return null\n }\n\n const lines = txt.split('\\n')\n if (!lines[0].startsWith('NRRD')) {\n log.error('Invalid NRRD image (magic signature missing)')\n return null\n }\n\n const n = lines.length\n let isGz = false\n let isMicron = false\n let isDetached = false\n const mat33 = mat3.fromValues(NaN, 0, 0, 0, 1, 0, 0, 0, 1)\n const offset = vec3.fromValues(0, 0, 0)\n let rot33 = mat3.create() // Initialize space correction matrix\n\n for (let i = 1; i < n; i++) {\n let str = lines[i]\n if (str.length === 0 || str[0] === '#') {\n if (str.startsWith('#')) {\n continue\n }\n if (str.trim().length === 0) {\n continue\n }\n }\n str = str.toLowerCase()\n const items = str.split(':')\n if (items.length < 2) {\n continue\n }\n const key = items[0].trim()\n let value = items[1].trim()\n\n value = value.replaceAll(')', ' ')\n value = value.replaceAll('(', ' ')\n value = value.trim()\n\n switch (key) {\n case 'data file':\n isDetached = true\n break\n case 'encoding':\n if (value.includes('raw')) {\n isGz = false\n } else if (value.includes('gz')) {\n isGz = true\n } else {\n log.error(`Unsupported NRRD encoding : ${value}`)\n return null\n }\n break\n case 'type':\n switch (value) {\n case 'uchar':\n case 'unsigned char':\n case 'uint8':\n case 'uint8_t':\n hdr.numBitsPerVoxel = 8\n hdr.datatypeCode = NiiDataType.DT_UINT8\n break\n case 'signed char':\n case 'int8':\n case 'int8_t':\n hdr.numBitsPerVoxel = 8\n hdr.datatypeCode = NiiDataType.DT_INT8\n break\n case 'short':\n case 'short int':\n case 'signed short':\n case 'signed short int':\n case 'int16':\n case 'int16_t':\n hdr.numBitsPerVoxel = 16\n hdr.datatypeCode = NiiDataType.DT_INT16\n break\n case 'ushort':\n case 'unsigned short':\n case 'unsigned short int':\n case 'uint16':\n case 'uint16_t':\n hdr.numBitsPerVoxel = 16\n hdr.datatypeCode = NiiDataType.DT_UINT16\n break\n case 'int':\n case 'signed int':\n case 'int32':\n case 'int32_t':\n hdr.numBitsPerVoxel = 32\n hdr.datatypeCode = NiiDataType.DT_INT32\n break\n case 'uint':\n case 'unsigned int':\n case 'uint32':\n case 'uint32_t':\n hdr.numBitsPerVoxel = 32\n hdr.datatypeCode = NiiDataType.DT_UINT32\n break\n case 'float':\n hdr.numBitsPerVoxel = 32\n hdr.datatypeCode = NiiDataType.DT_FLOAT32\n break\n case 'double':\n hdr.numBitsPerVoxel = 64\n hdr.datatypeCode = NiiDataType.DT_FLOAT64\n break\n default:\n log.error('Unsupported NRRD data type: ' + value)\n return null\n }\n break\n case 'spacings':\n {\n const values = value.split(/[ ,]+/)\n for (let d = 0; d < values.length; d++) {\n hdr.pixDims[d + 1] = parseFloat(values[d])\n }\n }\n break\n case 'sizes':\n {\n const dims = value.split(/[ ,]+/)\n hdr.dims[0] = dims.length\n for (let d = 0; d < dims.length; d++) {\n hdr.dims[d + 1] = parseInt(dims[d])\n }\n }\n break\n case 'endian':\n if (value.includes('little')) {\n hdr.littleEndian = true\n } else if (value.includes('big')) {\n hdr.littleEndian = false\n }\n break\n case 'space directions':\n {\n const vs = value.split(/[ ,]+/)\n if (vs.length === 9) {\n for (let d = 0; d < 9; d++) {\n mat33[d] = parseFloat(vs[d])\n }\n }\n }\n break\n case 'space origin':\n {\n const ts = value.split(/[ ,]+/)\n if (ts.length === 3) {\n offset[0] = parseFloat(ts[0])\n offset[1] = parseFloat(ts[1])\n offset[2] = parseFloat(ts[2])\n }\n }\n break\n case 'space units':\n if (value.includes('microns')) {\n isMicron = true\n }\n break\n case 'space':\n if (value.includes('right-anterior-superior') || value.includes('ras')) {\n rot33 = mat3.fromValues(1, 0, 0, 0, 1, 0, 0, 0, 1)\n } else if (value.includes('left-anterior-superior') || value.includes('las')) {\n rot33 = mat3.fromValues(-1, 0, 0, 0, 1, 0, 0, 0, 1)\n } else if (value.includes('left-posterior-superior') || value.includes('lps')) {\n rot33 = mat3.fromValues(-1, 0, 0, 0, -1, 0, 0, 0, 1)\n } else {\n log.warn('Unsupported NRRD space value:', value)\n }\n break\n default:\n log.warn('Unknown:', key)\n break\n }\n }\n\n if (!isNaN(mat33[0])) {\n hdr.sform_code = 2\n if (isMicron) {\n // @ts-expect-error FIXME: converting mat3 to mat4\n mat4.multiplyScalar(mat33, mat33, 0.001)\n offset[0] *= 0.001\n offset[1] *= 0.001\n offset[2] *= 0.001\n }\n if (rot33[0] < 0) {\n offset[0] = -offset[0]\n }\n if (rot33[4] < 0) {\n offset[1] = -offset[1]\n }\n if (rot33[8] < 0) {\n offset[2] = -offset[2]\n }\n mat3.multiply(mat33, rot33, mat33)\n\n const mat = mat4.fromValues(mat33[0], mat33[3], mat33[6], offset[0], mat33[1], mat33[4], mat33[7], offset[1], mat33[2], mat33[5], mat33[8], offset[2], 0, 0, 0, 1)\n\n // Ensure vox2mm function is accessible via nvImage\n if (!nvImage.vox2mm) {\n return null\n }\n const mm000 = nvImage.vox2mm([0, 0, 0], mat)\n const mm100 = nvImage.vox2mm([1, 0, 0], mat)\n vec3.subtract(mm100, mm100, mm000)\n const mm010 = nvImage.vox2mm([0, 1, 0], mat)\n vec3.subtract(mm010, mm010, mm000)\n const mm001 = nvImage.vox2mm([0, 0, 1], mat)\n vec3.subtract(mm001, mm001, mm000)\n hdr.pixDims[0] = 1\n hdr.pixDims[1] = vec3.length(mm100)\n hdr.pixDims[2] = vec3.length(mm010)\n hdr.pixDims[3] = vec3.length(mm001)\n\n hdr.affine = [\n [mat[0], mat[1], mat[2], mat[3]],\n [mat[4], mat[5], mat[6], mat[7]],\n [mat[8], mat[9], mat[10], mat[11]],\n [0, 0, 0, 1]\n ]\n }\n let imgRaw: ArrayBuffer | null = null // Use null for error case\n\n // Data source depends on whether header was detached\n const sourceBuffer = isDetached ? pairedImgData : dataBuffer\n // Offset where data starts within the sourceBuffer\n const sourceOffset = isDetached ? 0 : hdr.vox_offset // Use hdr.vox_offset set during header parsing\n\n if (isDetached && !sourceBuffer) {\n log.warn('Missing data: NRRD header describes detached data file but only one URL provided')\n return null\n }\n\n if (!sourceBuffer || sourceOffset >= sourceBuffer.byteLength) {\n log.error(`NRRD data offset (${sourceOffset}) invalid for buffer length (${sourceBuffer?.byteLength ?? 0})`)\n return null\n }\n\n // Slice the data section\n let dataSection = sourceBuffer.slice(sourceOffset)\n\n // Decompress if necessary\n if (isGz) {\n try {\n log.debug('Decompressing NRRD data...')\n dataSection = await NVUtilities.decompressToBuffer(new Uint8Array(dataSection))\n log.debug('Decompression complete.')\n } catch (err) {\n log.error('Failed to decompress NRRD data.', err)\n return null\n }\n }\n\n const nBytesPerVoxel = hdr.numBitsPerVoxel / 8\n const nVoxels = hdr.dims.slice(1, hdr.dims[0] + 1).reduce((acc, dim) => acc * Math.max(1, dim), 1)\n const expectedBytes = nVoxels * nBytesPerVoxel\n if (dataSection.byteLength < expectedBytes) {\n log.error(`NRRD image data size mismatch: expected ${expectedBytes}, found ${dataSection.byteLength}`)\n return null\n } else if (dataSection.byteLength > expectedBytes) {\n log.warn(`NRRD has extra ${dataSection.byteLength - expectedBytes} bytes after expected image data. Truncating.`)\n dataSection = dataSection.slice(0, expectedBytes)\n }\n\n imgRaw = dataSection // Assign the final buffer\n\n // Ensure header has essential NIFTI fields if missing defaults\n if (!hdr.datatypeCode) {\n log.error('NRRD parsing failed to set datatypeCode.')\n return null\n }\n if (!hdr.numBitsPerVoxel) {\n log.error('NRRD parsing failed to set numBitsPerVoxel.')\n return null\n }\n\n return imgRaw // Return the image data buffer\n}\n","import { NIFTI1 } from 'nifti-reader-js'\nimport type { NVImage } from '@/nvimage'\nimport { Zip } from '@/nvutilities'\n\n/**\n * Helper function to determine byte size per element from numpy dtype string.\n * @param dtype - NumPy dtype string (e.g., '<f4', '<i2', '|b1')\n * @returns Byte size of the data type\n */\nfunction getTypeSize(dtype: string): number {\n const typeMap: Record<string, number> = {\n '|b1': 1, // Boolean\n '<i1': 1, // Int8\n '<u1': 1, // UInt8\n '<i2': 2, // Int16\n '<u2': 2, // UInt16\n '<i4': 4, // Int32\n '<u4': 4, // UInt32\n '<f4': 4, // Float32\n '<f8': 8 // Float64\n }\n return typeMap[dtype] ?? 1\n}\n\n/**\n * Helper function to determine NIfTI datatype code from numpy dtype string.\n * @param dtype - NumPy dtype string (e.g., '<f4', '<i2', '|b1')\n * @returns NIfTI datatype code\n */\nfunction getDataTypeCode(dtype: string): number {\n const typeMap: Record<string, number> = {\n '|b1': 2, // DT_BINARY\n '<i1': 256, // DT_INT8\n '<u1': 2, // DT_UINT8\n '<i2': 4, // DT_INT16\n '<u2': 512, // DT_UINT16\n '<i4': 8, // DT_INT32\n '<u4': 768, // DT_UINT32\n '<f4': 16, // DT_FLOAT32\n '<f8': 64 // DT_FLOAT64\n }\n return typeMap[dtype] ?? 16 // Default to FLOAT32\n}\n\n/**\n * Reads NumPy NPY format file, modifying the provided NVImage header\n * and returning the raw image data buffer.\n * @param nvImage - The NVImage instance whose header will be modified.\n * @param buffer - ArrayBuffer containing the NPY file data.\n * @returns Promise resolving to ArrayBuffer containing the image data.\n * @throws Error if the file is not a valid NPY file.\n */\nexport async function readNPY(nvImage: NVImage, buffer: ArrayBuffer): Promise<ArrayBuffer> {\n const dv = new DataView(buffer)\n // Verify magic number\n const magicBytes = [dv.getUint8(0), dv.getUint8(1), dv.getUint8(2), dv.getUint8(3), dv.getUint8(4), dv.getUint8(5)]\n\n // Expected magic number: [0x93, 0x4E, 0x55, 0x4D, 0x50, 0x59] ('\\x93NUMPY')\n const expectedMagic = [0x93, 0x4e, 0x55, 0x4d, 0x50, 0x59]\n\n if (!magicBytes.every((byte, i) => byte === expectedMagic[i])) {\n throw new Error('Not a valid NPY file: Magic number mismatch')\n }\n\n // Extract version and header length\n // const _version = dv.getUint8(6)\n // const _minorVersion = dv.getUint8(7)\n const headerLen = dv.getUint16(8, true) // Little-endian\n // Decode header as ASCII string\n const headerText = new TextDecoder('utf-8').decode(buffer.slice(10, 10 + headerLen))\n\n // Extract shape from header\n const shapeMatch = headerText.match(/'shape': \\((.*?)\\)/)\n if (!shapeMatch) {\n throw new Error('Invalid NPY header: Shape not found')\n }\n const shape = shapeMatch[1]\n .split(',')\n .map((s) => s.trim())\n .filter((s) => s !== '')\n .map(Number)\n\n // Determine data type (assumes '|b1' (bool), '<f4' (float32), etc.)\n const dtypeMatch = headerText.match(/'descr': '([^']+)'/)\n if (!dtypeMatch) {\n throw new Error('Invalid NPY header: Data type not found')\n }\n const dtype = dtypeMatch[1]\n // Compute number of elements\n const numElements = shape.reduce((a, b) => a * b, 1)\n // Extract data start position\n const dataStart = 10 + headerLen\n // Read data as an ArrayBuffer\n const dataBuffer = buffer.slice(dataStart, dataStart + numElements * getTypeSize(dtype))\n // Interpret as 2D/3D data\n const width = shape.length > 0 ? shape[shape.length - 1] : 1\n const height = shape.length > 1 ? shape[shape.length - 2] : 1\n const slices = shape.length > 2 ? shape[shape.length - 3] : 1\n // Create NIFTI header\n nvImage.hdr = new NIFTI1()\n const hdr = nvImage.hdr\n hdr.dims = [3, width, height, slices, 0, 0, 0, 0]\n hdr.pixDims = [1, 1, 1, 1, 1, 0, 0, 0]\n hdr.affine = [\n [hdr.pixDims[1], 0, 0, -(hdr.dims[1] - 2) * 0.5 * hdr.pixDims[1]],\n [0, -hdr.pixDims[2], 0, (hdr.dims[2] - 2) * 0.5 * hdr.pixDims[2]],\n [0, 0, -hdr.pixDims[3], (hdr.dims[3] - 2) * 0.5 * hdr.pixDims[3]],\n [0, 0, 0, 1]\n ]\n hdr.numBitsPerVoxel = getTypeSize(dtype) * 8\n hdr.datatypeCode = getDataTypeCode(dtype)\n return dataBuffer\n}\n\n/**\n * Reads NumPy NPZ format file (zipped NPY arrays), modifying the provided\n * NVImage header and returning the raw image data buffer.\n *\n * Note: Currently only reads the first NPY file found in the archive.\n * TODO: Support reading multiple NPY images from a single NPZ file.\n *\n * @param nvImage - The NVImage instance whose header will be modified.\n * @param buffer - ArrayBuffer containing the NPZ file data.\n * @returns Promise resolving to ArrayBuffer containing the image data.\n */\nexport async function readNPZ(nvImage: NVImage, buffer: ArrayBuffer): Promise<ArrayBuffer | undefined> {\n // todo: a single NPZ file can contain multiple NPY images\n const zip = new Zip(buffer)\n for (let i = 0; i < zip.entries.length; i++) {\n const entry = zip.entries[i]\n if (entry.fileName.toLowerCase().endsWith('.npy')) {\n const data = await entry.extract()\n return await readNPY(nvImage, data.buffer as ArrayBuffer)\n }\n }\n}\n","import { NIFTI1 } from 'nifti-reader-js'\nimport type { NVImage } from '@/nvimage'\nimport { NiiDataType } from '@/nvimage/utils'\n\n/**\n * Helper function to convert ZXY RGB data to XYZ format for Zarr arrays.\n * @param data - Input data in ZXY order\n * @param X - Width dimension\n * @param Y - Height dimension\n * @param Z - Depth dimension\n * @returns Uint8Array in XYZ order\n */\nfunction zxy2xyz(data: any, X: number, Y: number, Z: number): Uint8Array {\n const voxelCount = X * Y\n const rgb = new Uint8Array(voxelCount * Z * 3)\n const offsets = new Array(Z)\n for (let s = 0; s < Z; s++) {\n offsets[s] = voxelCount * 3 * s\n }\n let srcIndex = 0\n let dstIndex = 0\n for (let v = 0; v < voxelCount; v++) {\n for (let s = 0; s < Z; s++) {\n rgb[offsets[s] + dstIndex] = data[srcIndex++] // R\n rgb[offsets[s] + dstIndex + 1] = data[srcIndex++] // G\n rgb[offsets[s] + dstIndex + 2] = data[srcIndex++] // B\n }\n dstIndex += 3\n }\n return rgb\n}\n\n/**\n * Reads Zarr multi-dimensional array format, modifying the provided\n * NVImage header and returning the raw image data buffer.\n *\n * Format specification: https://zarrita.dev/get-started.html\n *\n * Note: RGB channels may be returned as depth dimension.\n *\n * @param nvImage - The NVImage instance whose header will be modified.\n * @param buffer - ArrayBuffer containing the Zarr file data (unused, kept for consistency).\n * @param zarrData - Parsed Zarr data object containing width, height, depth, and data.\n * @returns Promise resolving to ArrayBuffer containing the image data.\n * @throws Error if data dimensions don't match expected size.\n */\nexport async function readZARR(nvImage: NVImage, buffer: ArrayBuffer, zarrData: unknown): Promise<ArrayBufferLike> {\n let { width, height, depth = 1, data } = (zarrData ?? {}) as any\n let expectedLength = width * height * depth * 3\n let isRGB = expectedLength === data.length\n if (!isRGB) {\n expectedLength = width * height * depth\n if (depth === 3) {\n // see https://zarrita.dev/get-started.html R,G,B channels returns as depth!\n isRGB = true\n depth = 1\n }\n }\n if (expectedLength !== data.length) {\n throw new Error(`Expected RGB ${width}×${height}×${depth}×3 = ${expectedLength}, but ZARR length ${data.length}`)\n }\n nvImage.hdr = new NIFTI1()\n const hdr = nvImage.hdr\n hdr.dims = [3, width, height, depth, 1, 1, 1, 1]\n hdr.pixDims = [1, 1, 1, 1, 0, 0, 0, 0]\n\n hdr.affine = [\n [hdr.pixDims[1], 0, 0, -(hdr.dims[1] - 2) * 0.5 * hdr.pixDims[1]],\n [0, -hdr.pixDims[2], 0, (hdr.dims[2] - 2) * 0.5 * hdr.pixDims[2]],\n [0, 0, -hdr.pixDims[3], (hdr.dims[3] - 2) * 0.5 * hdr.pixDims[3]],\n [0, 0, 0, 1]\n ]\n if (!isRGB) {\n hdr.numBitsPerVoxel = 8\n hdr.datatypeCode = NiiDataType.DT_UINT8\n // if data is a Uint8Array, convert to ArrayBuffer\n if (data instanceof Uint8Array) {\n const retBuffer = new ArrayBuffer(data.length)\n const retView = new Uint8Array(retBuffer)\n retView.set(data)\n return retBuffer\n }\n return data\n }\n hdr.numBitsPerVoxel = 24\n hdr.datatypeCode = NiiDataType.DT_RGB24\n const retData = zxy2xyz(data, hdr.dims[1], hdr.dims[2], hdr.dims[3])\n // convert retData Uint8Array to ArrayBuffer\n const retBuffer = new ArrayBuffer(retData.length)\n const retView = new Uint8Array(retBuffer)\n retView.set(retData)\n return retBuffer\n}\n","/**\n * CoordinateTransform Module\n *\n * Handles all coordinate system transformations for NVImage:\n * - Voxel ↔ Millimeter conversions\n * - Voxel ↔ Fractional texture coordinate conversions\n * - Fractional ↔ Millimeter conversions\n *\n * This module provides pure functions for transforming between different\n * coordinate spaces used in medical imaging visualization.\n */\n\nimport { mat4, vec3, vec4 } from 'gl-matrix'\nimport type { NVImage } from './index'\n\n/**\n * Convert voxel location to millimeter coordinates.\n *\n * Transforms from 0-indexed voxel space (row, column, slice) to world space\n * using the provided transformation matrix.\n *\n * @param _nvImage - The NVImage instance (unused, for API consistency)\n * @param XYZ - Voxel coordinates [x, y, z]\n * @param mtx - Transformation matrix (typically matRAS)\n * @returns Millimeter coordinates in world space\n */\nexport function vox2mm(_nvImage: NVImage, XYZ: number[], mtx: mat4): vec3 {\n const sform = mat4.clone(mtx)\n mat4.transpose(sform, sform)\n const pos = vec4.fromValues(XYZ[0], XYZ[1], XYZ[2], 1)\n vec4.transformMat4(pos, pos, sform)\n const pos3 = vec3.fromValues(pos[0], pos[1], pos[2])\n return pos3\n}\n\n/**\n * Convert millimeter coordinates to voxel location.\n *\n * Transforms from world space (millimeters) to 0-indexed voxel space\n * using the image's matRAS transformation matrix.\n *\n * @param nvImage - The NVImage instance\n * @param mm - Millimeter coordinates [x, y, z]\n * @param frac - If true, return fractional voxel coordinates; if false, return rounded integers\n * @returns Voxel coordinates (Float32Array if rounded, vec3 if fractional)\n * @throws Error if matRAS is undefined\n */\nexport function mm2vox(nvImage: NVImage, mm: number[], frac = false): Float32Array | vec3 {\n if (!nvImage.matRAS) {\n throw new Error('matRAS undefined')\n }\n\n const sform = mat4.clone(nvImage.matRAS)\n const out = mat4.clone(sform)\n mat4.transpose(out, sform)\n mat4.invert(out, out)\n const pos = vec4.fromValues(mm[0], mm[1], mm[2], 1)\n vec4.transformMat4(pos, pos, out)\n const pos3 = vec3.fromValues(pos[0], pos[1], pos[2])\n if (frac) {\n return pos3\n }\n return new Float32Array([Math.round(pos3[0]), Math.round(pos3[1]), Math.round(pos3[2])])\n}\n\n/**\n * Convert voxel coordinates to fractional texture coordinates.\n *\n * Transforms from 0-indexed voxel space [0..dim[i]-1] to normalized\n * texture space [0..1]. Voxel centers are positioned at fractional\n * coordinates (e.g., for 3 voxels: centers at 0.25, 0.5, 0.75).\n *\n * @param nvImage - The NVImage instance\n * @param vox - Voxel coordinates [x, y, z]\n * @returns Fractional texture coordinates [0..1, 0..1, 0..1]\n */\nexport function convertVox2Frac(nvImage: NVImage, vox: vec3): vec3 {\n // convert from 0-index voxel space [0..dim[1]-1, 0..dim[2]-1, 0..dim[3]-1] to normalized texture space XYZ= [0..1, 0..1 ,0..1]\n // consider dimension with 3 voxels, the voxel centers are at 0.25, 0.5, 0.75 corresponding to 0,1,2\n const frac = vec3.fromValues((vox[0] + 0.5) / nvImage.dimsRAS![1], (vox[1] + 0.5) / nvImage.dimsRAS![2], (vox[2] + 0.5) / nvImage.dimsRAS![3])\n return frac\n}\n\n/**\n * Convert fractional texture coordinates to voxel coordinates.\n *\n * Transforms from normalized texture space [0..1] to 0-indexed voxel\n * space, rounding to the nearest voxel center.\n *\n * @param nvImage - The NVImage instance\n * @param frac - Fractional texture coordinates [0..1, 0..1, 0..1]\n * @returns Voxel coordinates (rounded to nearest integer)\n */\nexport function convertFrac2Vox(nvImage: NVImage, frac: vec3): vec3 {\n const vox = vec3.fromValues(\n Math.round(frac[0] * nvImage.dims![1] - 0.5), // dims === RAS\n Math.round(frac[1] * nvImage.dims![2] - 0.5), // dims === RAS\n Math.round(frac[2] * nvImage.dims![3] - 0.5) // dims === RAS\n )\n return vox\n}\n\n/**\n * Convert fractional texture coordinates to millimeter coordinates.\n *\n * Transforms from normalized texture space [0..1] to world space (millimeters)\n * using either the oblique-corrected or original transformation matrix.\n *\n * @param nvImage - The NVImage instance\n * @param frac - Fractional texture coordinates [0..1, 0..1, 0..1]\n * @param isForceSliceMM - If true, use frac2mm; if false, use frac2mmOrtho\n * @returns Millimeter coordinates in world space (vec4 with homogeneous coordinate)\n */\nexport function convertFrac2MM(nvImage: NVImage, frac: vec3, isForceSliceMM = false): vec4 {\n const pos = vec4.fromValues(frac[0], frac[1], frac[2], 1)\n if (isForceSliceMM) {\n vec4.transformMat4(pos, pos, nvImage.frac2mm!)\n } else {\n vec4.transformMat4(pos, pos, nvImage.frac2mmOrtho!)\n }\n return pos\n}\n\n/**\n * Convert millimeter coordinates to fractional texture coordinates.\n *\n * Transforms from world space (millimeters) to normalized texture space [0..1].\n * Uses either the oblique-corrected or original inverse transformation.\n *\n * @param nvImage - The NVImage instance\n * @param mm - Millimeter coordinates [x, y, z] or [x, y, z, w]\n * @param isForceSliceMM - If true, use matRAS inverse; if false, use frac2mmOrtho inverse\n * @returns Fractional texture coordinates [0..1, 0..1, 0..1]\n */\nexport function convertMM2Frac(nvImage: NVImage, mm: vec3 | vec4, isForceSliceMM = false): vec3 {\n // given mm, return volume fraction\n // convert from object space in millimeters to normalized texture space XYZ= [0..1, 0..1 ,0..1]\n const mm4 = vec4.fromValues(mm[0], mm[1], mm[2], 1)\n const d = nvImage.dimsRAS\n const frac = vec3.fromValues(0, 0, 0)\n if (typeof d === 'undefined') {\n return frac\n }\n if (!isForceSliceMM) {\n const xform = mat4.clone(nvImage.frac2mmOrtho!)\n mat4.invert(xform, xform)\n vec4.transformMat4(mm4, mm4, xform)\n frac[0] = mm4[0]\n frac[1] = mm4[1]\n frac[2] = mm4[2]\n return frac\n }\n if (d[1] < 1 || d[2] < 1 || d[3] < 1) {\n return frac\n }\n const sform = mat4.clone(nvImage.matRAS!)\n mat4.invert(sform, sform)\n mat4.transpose(sform, sform)\n vec4.transformMat4(mm4, mm4, sform)\n frac[0] = (mm4[0] + 0.5) / d[1]\n frac[1] = (mm4[1] + 0.5) / d[2]\n frac[2] = (mm4[2] + 0.5) / d[3]\n return frac\n}\n\n/**\n * Check if two arrays are equal.\n *\n * Compares two arrays element-by-element for equality.\n * Note: This is a shallow comparison and won't work correctly for complex objects.\n *\n * @param a - First array\n * @param b - Second array\n * @returns true if arrays have the same length and all elements are equal\n */\nexport function arrayEquals(a: unknown[], b: unknown[]): boolean {\n return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((val, index) => val === b[index])\n}\n","/**\n * ImageOrientation module\n *\n * Handles image orientation, RAS calculation, and reorientation.\n * This module contains functions for:\n * - Calculating RAS (Right-Anterior-Superior) orientation from NIfTI headers\n * - Computing oblique transformation matrices\n * - Detecting misalignment between voxel and world space\n * - Reorienting image data and headers to RAS\n */\n\nimport { mat3, mat4, vec3, vec4 } from 'gl-matrix'\nimport { NIFTI1, NIFTI2, readHeaderAsync } from 'nifti-reader-js'\nimport type { NVImage, TypedVoxelArray } from './index'\nimport { log } from '@/logger'\nimport { hdrToArrayBuffer } from '@/nvimage/utils'\nimport * as CoordinateTransform from '@/nvimage/CoordinateTransform'\n\n/**\n * Detect difference between voxel grid and world space.\n * https://github.com/afni/afni/blob/25e77d564f2c67ff480fa99a7b8e48ec2d9a89fc/src/thd_coords.c#L717\n *\n * @param mtx44 - Transformation matrix\n * @returns Oblique angle in degrees\n */\nexport function computeObliqueAngle(mtx44: mat4): number {\n const mtx = mat4.clone(mtx44)\n mat4.transpose(mtx, mtx44)\n const dxtmp = Math.sqrt(mtx[0] * mtx[0] + mtx[1] * mtx[1] + mtx[2] * mtx[2])\n const xmax = Math.max(Math.max(Math.abs(mtx[0]), Math.abs(mtx[1])), Math.abs(mtx[2])) / dxtmp\n const dytmp = Math.sqrt(mtx[4] * mtx[4] + mtx[5] * mtx[5] + mtx[6] * mtx[6])\n const ymax = Math.max(Math.max(Math.abs(mtx[4]), Math.abs(mtx[5])), Math.abs(mtx[6])) / dytmp\n const dztmp = Math.sqrt(mtx[8] * mtx[8] + mtx[9] * mtx[9] + mtx[10] * mtx[10])\n const zmax = Math.max(Math.max(Math.abs(mtx[8]), Math.abs(mtx[9])), Math.abs(mtx[10])) / dztmp\n const fig_merit = Math.min(Math.min(xmax, ymax), zmax)\n let oblique_angle = Math.abs((Math.acos(fig_merit) * 180.0) / 3.141592653)\n if (oblique_angle > 0.01) {\n log.warn('Warning voxels not aligned with world space: ' + oblique_angle + ' degrees from plumb.\\n')\n } else {\n oblique_angle = 0.0\n }\n return oblique_angle\n}\n\n/**\n * Calculate oblique transformation matrices.\n * Detects difference between voxel grid and world space.\n *\n * @param nvImage - The NVImage instance\n */\nexport function calculateOblique(nvImage: NVImage): void {\n if (!nvImage.matRAS) {\n throw new Error('matRAS not defined')\n }\n if (nvImage.pixDimsRAS === undefined) {\n throw new Error('pixDimsRAS not defined')\n }\n if (!nvImage.dimsRAS) {\n throw new Error('dimsRAS not defined')\n }\n\n nvImage.oblique_angle = computeObliqueAngle(nvImage.matRAS)\n const LPI = CoordinateTransform.vox2mm(nvImage, [0.0, 0.0, 0.0], nvImage.matRAS)\n const X1mm = CoordinateTransform.vox2mm(nvImage, [1.0 / nvImage.pixDimsRAS[1], 0.0, 0.0], nvImage.matRAS)\n const Y1mm = CoordinateTransform.vox2mm(nvImage, [0.0, 1.0 / nvImage.pixDimsRAS[2], 0.0], nvImage.matRAS)\n const Z1mm = CoordinateTransform.vox2mm(nvImage, [0.0, 0.0, 1.0 / nvImage.pixDimsRAS[3]], nvImage.matRAS)\n vec3.subtract(X1mm, X1mm, LPI)\n vec3.subtract(Y1mm, Y1mm, LPI)\n vec3.subtract(Z1mm, Z1mm, LPI)\n const oblique = mat4.fromValues(X1mm[0], X1mm[1], X1mm[2], 0, Y1mm[0], Y1mm[1], Y1mm[2], 0, Z1mm[0], Z1mm[1], Z1mm[2], 0, 0, 0, 0, 1)\n nvImage.obliqueRAS = mat4.clone(oblique)\n const XY = Math.abs(90 - vec3.angle(X1mm, Y1mm) * (180 / Math.PI))\n const XZ = Math.abs(90 - vec3.angle(X1mm, Z1mm) * (180 / Math.PI))\n const YZ = Math.abs(90 - vec3.angle(Y1mm, Z1mm) * (180 / Math.PI))\n nvImage.maxShearDeg = Math.max(Math.max(XY, XZ), YZ)\n if (nvImage.maxShearDeg > 0.1) {\n log.warn('Warning: voxels are rhomboidal, maximum shear is %f degrees.', nvImage.maxShearDeg)\n }\n // compute a matrix to transform vectors from factional space to mm:\n const dim = vec4.fromValues(nvImage.dimsRAS[1], nvImage.dimsRAS[2], nvImage.dimsRAS[3], 1)\n const sform = mat4.clone(nvImage.matRAS)\n mat4.transpose(sform, sform)\n const shim = vec4.fromValues(-0.5, -0.5, -0.5, 0) // bitmap with 5 voxels scaled 0..1, voxel centers are 0.1,0.3,0.5,0.7,0.9\n mat4.translate(sform, sform, vec3.fromValues(shim[0], shim[1], shim[2]))\n // mat.mat4.scale(sform, sform, dim);\n sform[0] *= dim[0]\n sform[1] *= dim[0]\n sform[2] *= dim[0]\n sform[4] *= dim[1]\n sform[5] *= dim[1]\n sform[6] *= dim[1]\n sform[8] *= dim[2]\n sform[9] *= dim[2]\n sform[10] *= dim[2]\n nvImage.frac2mm = mat4.clone(sform)\n const pixdimX = nvImage.pixDimsRAS[1] // vec3.length(X1mm);\n const pixdimY = nvImage.pixDimsRAS[2] // vec3.length(Y1mm);\n const pixdimZ = nvImage.pixDimsRAS[3] // vec3.length(Z1mm);\n // orthographic view\n const oform = mat4.clone(sform)\n oform[0] = pixdimX * dim[0]\n oform[1] = 0\n oform[2] = 0\n oform[4] = 0\n oform[5] = pixdimY * dim[1]\n oform[6] = 0\n oform[8] = 0\n oform[9] = 0\n oform[10] = pixdimZ * dim[2]\n const originVoxel = CoordinateTransform.mm2vox(nvImage, [0, 0, 0], true)\n // set matrix translation for distance from origin\n oform[12] = (-originVoxel[0] - 0.5) * pixdimX\n oform[13] = (-originVoxel[1] - 0.5) * pixdimY\n oform[14] = (-originVoxel[2] - 0.5) * pixdimZ\n nvImage.frac2mmOrtho = mat4.clone(oform)\n nvImage.extentsMinOrtho = [oform[12], oform[13], oform[14]]\n nvImage.extentsMaxOrtho = [oform[0] + oform[12], oform[5] + oform[13], oform[10] + oform[14]]\n nvImage.mm2ortho = mat4.create()\n mat4.invert(nvImage.mm2ortho, oblique)\n}\n\n/**\n * Transform to orient NIfTI image to Left->Right, Posterior->Anterior, Inferior->Superior (48 possible permutations).\n * Port of Matlab reorient() https://github.com/xiangruili/dicm2nii/blob/master/nii_viewer.m\n *\n * @param nvImage - The NVImage instance\n */\nexport function calculateRAS(nvImage: NVImage): void {\n if (!nvImage.hdr) {\n throw new Error('hdr not set')\n }\n // not elegant, as JavaScript arrays are always 1D\n const a = nvImage.hdr.affine\n const header = nvImage.hdr\n const absR = mat3.fromValues(\n Math.abs(a[0][0]),\n Math.abs(a[0][1]),\n Math.abs(a[0][2]),\n Math.abs(a[1][0]),\n Math.abs(a[1][1]),\n Math.abs(a[1][2]),\n Math.abs(a[2][0]),\n Math.abs(a[2][1]),\n Math.abs(a[2][2])\n )\n // 1st column = x\n const ixyz = [1, 1, 1]\n if (absR[3] > absR[0]) {\n ixyz[0] = 2 // (absR[1][0] > absR[0][0]) ixyz[0] = 2;\n }\n if (absR[6] > absR[0] && absR[6] > absR[3]) {\n ixyz[0] = 3 // ((absR[2][0] > absR[0][0]) && (absR[2][0]> absR[1][0])) ixyz[0] = 3;\n } // 2nd column = y\n ixyz[1] = 1\n if (ixyz[0] === 1) {\n if (absR[4] > absR[7]) {\n // (absR[1][1] > absR[2][1])\n ixyz[1] = 2\n } else {\n ixyz[1] = 3\n }\n } else if (ixyz[0] === 2) {\n if (absR[1] > absR[7]) {\n // (absR[0][1] > absR[2][1])\n ixyz[1] = 1\n } else {\n ixyz[1] = 3\n }\n } else {\n if (absR[1] > absR[4]) {\n // (absR[0][1] > absR[1][1])\n ixyz[1] = 1\n } else {\n ixyz[1] = 2\n }\n }\n // 3rd column = z: constrained as x+y+z = 1+2+3 = 6\n ixyz[2] = 6 - ixyz[1] - ixyz[0]\n let perm = [1, 2, 3]\n perm[ixyz[0] - 1] = 1\n perm[ixyz[1] - 1] = 2\n perm[ixyz[2] - 1] = 3\n let rotM = mat4.fromValues(a[0][0], a[0][1], a[0][2], a[0][3], a[1][0], a[1][1], a[1][2], a[1][3], a[2][0], a[2][1], a[2][2], a[2][3], 0, 0, 0, 1)\n // n.b. 0.5 in these values to account for voxel centers, e.g. a 3-pixel wide bitmap in unit space has voxel centers at 0.25, 0.5 and 0.75\n nvImage.mm000 = CoordinateTransform.vox2mm(nvImage, [-0.5, -0.5, -0.5], rotM)\n nvImage.mm100 = CoordinateTransform.vox2mm(nvImage, [header.dims[1] - 0.5, -0.5, -0.5], rotM)\n nvImage.mm010 = CoordinateTransform.vox2mm(nvImage, [-0.5, header.dims[2] - 0.5, -0.5], rotM)\n nvImage.mm001 = CoordinateTransform.vox2mm(nvImage, [-0.5, -0.5, header.dims[3] - 0.5], rotM)\n const R = mat4.create()\n mat4.copy(R, rotM)\n for (let i = 0; i < 3; i++) {\n for (let j = 0; j < 3; j++) {\n R[i * 4 + j] = rotM[i * 4 + perm[j] - 1] // rotM[i+(4*(perm[j]-1))];//rotM[i],[perm[j]-1];\n }\n }\n const flip = [0, 0, 0]\n if (R[0] < 0) {\n flip[0] = 1\n } // R[0][0]\n if (R[5] < 0) {\n flip[1] = 1\n } // R[1][1]\n if (R[10] < 0) {\n flip[2] = 1\n } // R[2][2]\n nvImage.dimsRAS = [header.dims[0], header.dims[perm[0]], header.dims[perm[1]], header.dims[perm[2]]]\n nvImage.pixDimsRAS = [header.pixDims[0], header.pixDims[perm[0]], header.pixDims[perm[1]], header.pixDims[perm[2]]]\n nvImage.permRAS = perm.slice()\n for (let i = 0; i < 3; i++) {\n if (flip[i] === 1) {\n nvImage.permRAS[i] = -nvImage.permRAS[i]\n }\n }\n if (CoordinateTransform.arrayEquals(perm, [1, 2, 3]) && CoordinateTransform.arrayEquals(flip, [0, 0, 0])) {\n nvImage.toRAS = mat4.create() // aka fromValues(1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1);\n nvImage.matRAS = mat4.clone(rotM)\n calculateOblique(nvImage)\n nvImage.img2RASstep = [1, nvImage.dimsRAS[1], nvImage.dimsRAS[1] * nvImage.dimsRAS[2]]\n nvImage.img2RASstart = [0, 0, 0]\n return // no rotation required!\n }\n mat4.identity(rotM)\n rotM[0 + 0 * 4] = 1 - flip[0] * 2\n rotM[1 + 1 * 4] = 1 - flip[1] * 2\n rotM[2 + 2 * 4] = 1 - flip[2] * 2\n rotM[3 + 0 * 4] = (header.dims[perm[0]] - 1) * flip[0]\n rotM[3 + 1 * 4] = (header.dims[perm[1]] - 1) * flip[1]\n rotM[3 + 2 * 4] = (header.dims[perm[2]] - 1) * flip[2]\n const residualR = mat4.create()\n mat4.invert(residualR, rotM)\n mat4.multiply(residualR, residualR, R)\n nvImage.matRAS = mat4.clone(residualR)\n rotM = mat4.fromValues(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1)\n rotM[perm[0] - 1 + 0 * 4] = -flip[0] * 2 + 1\n rotM[perm[1] - 1 + 1 * 4] = -flip[1] * 2 + 1\n rotM[perm[2] - 1 + 2 * 4] = -flip[2] * 2 + 1\n rotM[3 + 0 * 4] = flip[0]\n rotM[3 + 1 * 4] = flip[1]\n rotM[3 + 2 * 4] = flip[2]\n nvImage.toRAS = mat4.clone(rotM) // webGL unit textures\n // voxel based column-major,\n rotM[3] = 0\n rotM[7] = 0\n rotM[11] = 0\n rotM[12] = 0\n if (nvImage.permRAS[0] === -1 || nvImage.permRAS[1] === -1 || nvImage.permRAS[2] === -1) {\n rotM[12] = header.dims[1] - 1\n }\n rotM[13] = 0\n if (nvImage.permRAS[0] === -2 || nvImage.permRAS[1] === -2 || nvImage.permRAS[2] === -2) {\n rotM[13] = header.dims[2] - 1\n }\n rotM[14] = 0\n if (nvImage.permRAS[0] === -3 || nvImage.permRAS[1] === -3 || nvImage.permRAS[2] === -3) {\n rotM[14] = header.dims[3] - 1\n }\n nvImage.toRASvox = mat4.clone(rotM)\n log.debug(nvImage.hdr.dims)\n log.debug(nvImage.dimsRAS)\n\n // compute img2RASstep[] and img2RASstart[] for rapid native<->RAS conversion\n // TODO: replace all other outStep/outStart calculations with img2RASstep/img2RASstart\n const hdr = nvImage.hdr\n perm = nvImage.permRAS\n const aperm = [Math.abs(perm[0]), Math.abs(perm[1]), Math.abs(perm[2])]\n const outdim = [hdr.dims[aperm[0]], hdr.dims[aperm[1]], hdr.dims[aperm[2]]]\n const inStep = [1, hdr.dims[1], hdr.dims[1] * hdr.dims[2]] // increment i,j,k\n const outStep = [inStep[aperm[0] - 1], inStep[aperm[1] - 1], inStep[aperm[2] - 1]]\n const outStart = [0, 0, 0]\n for (let p = 0; p < 3; p++) {\n // flip dimensions\n if (perm[p] < 0) {\n outStart[p] = outStep[p] * (outdim[p] - 1)\n outStep[p] = -outStep[p]\n }\n }\n nvImage.img2RASstep = outStep\n nvImage.img2RASstart = outStart\n\n calculateOblique(nvImage)\n}\n\n/**\n * Reorient raw header data to RAS.\n * Assume single volume, use nVolumes to specify, set nVolumes = 0 for same as input.\n *\n * @param nvImage - The NVImage instance\n * @param nVolumes - Number of volumes (default: 1)\n * @returns Promise<NIFTI1 | NIFTI2> - Reoriented header\n */\nexport async function hdr2RAS(nvImage: NVImage, nVolumes: number = 1): Promise<NIFTI1 | NIFTI2> {\n if (!nvImage.permRAS) {\n throw new Error('permRAS undefined')\n }\n if (!nvImage.hdr) {\n throw new Error('hdr undefined')\n }\n // make a deep clone\n const hdrBytes = hdrToArrayBuffer({ ...nvImage.hdr!, vox_offset: 352 }, false)\n const hdr = await readHeaderAsync(hdrBytes.buffer as ArrayBuffer, true)\n // n.b. if nVolumes < 1, input volumes = output volumess\n if (nVolumes === 1) {\n // 3D\n hdr.dims[0] = 3\n hdr.dims[4] = 1\n } else if (nVolumes > 1) {\n // 4D\n hdr.dims[0] = 4\n hdr.dims[4] = nVolumes\n }\n const perm = nvImage.permRAS.slice()\n if (perm[0] === 1 && perm[1] === 2 && perm[2] === 3) {\n return hdr\n } // header is already in RAS\n hdr.qform_code = 0\n for (let i = 1; i < 4; i++) {\n hdr.dims[i] = nvImage.dimsRAS[i]\n }\n\n for (let i = 0; i < nvImage.pixDimsRAS.length; i++) {\n hdr.pixDims[i] = nvImage.pixDimsRAS[i]\n }\n let k = 0\n for (let i = 0; i < 4; i++) {\n for (let j = 0; j < 4; j++) {\n hdr.affine[i][j] = nvImage.matRAS[k]\n k++\n }\n }\n return hdr\n}\n\n/**\n * Reorient raw image data to RAS.\n * Note that GPU-based orient shader is much faster.\n * Returns single 3D volume even for 4D input. Use nVolume to select volume (0 indexed).\n *\n * @param nvImage - The NVImage instance\n * @param nVolume - Volume index (default: 0)\n * @returns TypedVoxelArray - Reoriented image data\n */\nexport function img2RAS(nvImage: NVImage, nVolume: number = 0): TypedVoxelArray {\n if (!nvImage.permRAS) {\n throw new Error('permRAS undefined')\n }\n if (!nvImage.img) {\n throw new Error('img undefined')\n }\n if (!nvImage.hdr) {\n throw new Error('hdr undefined')\n }\n\n const perm = nvImage.permRAS.slice()\n if (perm[0] === 1 && perm[1] === 2 && perm[2] === 3) {\n return nvImage.img\n } // image is already in RAS\n const hdr = nvImage.hdr\n const nVox = hdr.dims[1] * hdr.dims[2] * hdr.dims[3]\n let volSkip = nVolume * nVox\n if (volSkip + nVox > nvImage.img.length || volSkip < 0) {\n volSkip = 0\n log.warn(`img2RAS nVolume (${nVolume}) out of bounds (${nVolume}+1)×${nVox} > ${nvImage.img.length}`)\n }\n // preallocate/clone image (only 3D for 4D datasets!)\n const imgRAS = nvImage.img.slice(0, nVox)\n const aperm = [Math.abs(perm[0]), Math.abs(perm[1]), Math.abs(perm[2])]\n const outdim = [hdr.dims[aperm[0]], hdr.dims[aperm[1]], hdr.dims[aperm[2]]]\n const inStep = [1, hdr.dims[1], hdr.dims[1] * hdr.dims[2]] // increment i,j,k\n const outStep = [inStep[aperm[0] - 1], inStep[aperm[1] - 1], inStep[aperm[2] - 1]]\n const outStart = [0, 0, 0]\n for (let p = 0; p < 3; p++) {\n // flip dimensions\n if (perm[p] < 0) {\n outStart[p] = outStep[p] * (outdim[p] - 1)\n outStep[p] = -outStep[p]\n }\n }\n let j = 0\n for (let z = 0; z < outdim[2]; z++) {\n const zi = outStart[2] + z * outStep[2]\n for (let y = 0; y < outdim[1]; y++) {\n const yi = outStart[1] + y * outStep[1]\n for (let x = 0; x < outdim[0]; x++) {\n const xi = outStart[0] + x * outStep[0]\n imgRAS[j] = nvImage.img[xi + yi + zi + volSkip]\n j++\n } // for x\n } // for y\n } // for z\n return imgRAS\n}\n","export { default as $itksnap } from './_itksnap.json'\nexport { default as $slicer3d } from './_slicer3d.json'\nexport { default as actc } from './actc.json'\nexport { default as batlow } from './batlow.json'\nexport { default as bcgwhw_dark } from './bcgwhw_dark.json'\nexport { default as bcgwhw } from './bcgwhw.json'\nexport { default as blue2red } from './blue2red.json'\nexport { default as blue2magenta } from './blue2magenta.json'\nexport { default as blue2cyan } from './blue2cyan.json'\nexport { default as bluegrn } from './bluegrn.json'\nexport { default as blue } from './blue.json'\nexport { default as bone } from './bone.json'\nexport { default as bronze } from './bronze.json'\nexport { default as cet_l17 } from './cet_l17.json'\nexport { default as cividis } from './cividis.json'\nexport { default as cool } from './cool.json'\nexport { default as copper2 } from './copper2.json'\nexport { default as copper } from './copper.json'\nexport { default as ct_airways } from './ct_airways.json'\nexport { default as ct_artery } from './ct_artery.json'\nexport { default as ct_bones } from './ct_bones.json'\nexport { default as ct_brain_gray } from './ct_brain_gray.json'\nexport { default as ct_brain } from './ct_brain.json'\nexport { default as ct_cardiac } from './ct_cardiac.json'\nexport { default as ct_head } from './ct_head.json'\nexport { default as ct_kidneys } from './ct_kidneys.json'\nexport { default as ct_liver } from './ct_liver.json'\nexport { default as ct_muscles } from './ct_muscles.json'\nexport { default as ct_scalp } from './ct_scalp.json'\nexport { default as ct_skull } from './ct_skull.json'\nexport { default as ct_soft } from './ct_soft.json'\nexport { default as ct_soft_tissue } from './ct_soft_tissue.json'\nexport { default as ct_surface } from './ct_surface.json'\nexport { default as ct_vessels } from './ct_vessels.json'\nexport { default as ct_w_contrast } from './ct_w_contrast.json'\nexport { default as cubehelix } from './cubehelix.json'\nexport { default as electric_blue } from './electric_blue.json'\nexport { default as freesurfer } from './freesurfer.json'\nexport { default as ge_color } from './ge_color.json'\nexport { default as gold } from './gold.json'\nexport { default as gray } from './gray.json'\nexport { default as green } from './green.json'\nexport { default as green2cyan } from './green2cyan.json'\nexport { default as green2orange } from './green2orange.json'\nexport { default as hotiron } from './hotiron.json'\nexport { default as hot } from './hot.json'\nexport { default as hsv } from './hsv.json'\nexport { default as inferno } from './inferno.json'\nexport { default as jet } from './jet.json'\nexport { default as kry } from './kry.json'\nexport { default as linspecer } from './linspecer.json'\nexport { default as lipari } from './lipari.json'\nexport { default as magma } from './magma.json'\nexport { default as mako } from './mako.json'\nexport { default as navia } from './navia.json'\nexport { default as nih } from './nih.json'\nexport { default as plasma } from './plasma.json'\nexport { default as random } from './random.json'\nexport { default as red } from './red.json'\nexport { default as redyell } from './redyell.json'\nexport { default as rocket } from './rocket.json'\nexport { default as surface } from './surface.json'\nexport { default as thermal } from './thermal.json'\nexport { default as turbo } from './turbo.json'\nexport { default as violet } from './violet.json'\nexport { default as viridis } from './viridis.json'\nexport { default as warm } from './warm.json'\nexport { default as winter } from './winter.json'\nexport { default as x_rain } from './x_rain.json'\nexport { default as afni_blues_inv } from './afni_blues_inv.json'\nexport { default as afni_reds_inv } from './afni_reds_inv.json'\nexport { default as roi_i256 } from './roi_i256.json'\n","{\n \"R\": [\n 0, 255, 0, 0, 255, 0, 255, 255, 0, 205, 210, 102, 0, 0, 46, 255, 106, 221,\n 233, 165, 255, 147, 218, 75, 255, 60, 255, 255, 218, 0, 188, 255, 255, 222,\n 127, 139, 124, 255, 70, 0, 238, 238, 240, 245, 184, 32, 255, 25, 112, 34,\n 248, 245, 255, 144, 173, 65, 255, 250, 128, 50, 244, 255, 123, 255, 173,\n 255, 127, 255, 143, 220, 253, 255, 0, 0, 128, 255, 250, 148, 178, 255, 135,\n 100, 240, 250, 255, 107, 135, 0, 139, 245, 186, 255, 255, 0, 210, 255, 47,\n 72, 175, 128, 176, 255, 139, 240, 255, 216, 119, 219, 72, 255, 199, 154,\n 189, 240, 230, 0, 85, 64, 153, 205, 250, 95, 0, 255, 224, 176, 138, 30, 240,\n 152, 160\n ],\n \"G\": [\n 0, 0, 255, 0, 255, 255, 0, 239, 0, 133, 180, 205, 0, 139, 139, 228, 90, 160,\n 150, 42, 250, 112, 112, 0, 182, 179, 235, 228, 165, 128, 143, 105, 218, 184,\n 255, 69, 252, 255, 130, 100, 130, 232, 255, 222, 134, 178, 20, 25, 128, 139,\n 248, 255, 160, 238, 255, 105, 99, 240, 0, 205, 164, 255, 104, 165, 216, 192,\n 255, 140, 188, 20, 245, 250, 206, 255, 0, 250, 128, 0, 34, 127, 206, 149,\n 230, 235, 245, 142, 206, 0, 0, 245, 85, 228, 222, 191, 105, 248, 79, 61,\n 238, 128, 224, 240, 0, 255, 215, 191, 136, 112, 209, 0, 21, 205, 183, 248,\n 230, 250, 107, 224, 50, 92, 250, 158, 128, 69, 255, 196, 43, 144, 128, 251,\n 82\n ],\n \"B\": [\n 0, 0, 0, 255, 0, 255, 255, 213, 205, 63, 140, 170, 128, 139, 87, 225, 205,\n 221, 122, 42, 250, 219, 214, 130, 193, 113, 205, 196, 32, 128, 143, 180,\n 185, 135, 0, 19, 0, 224, 180, 0, 238, 170, 240, 179, 11, 170, 147, 112, 144,\n 34, 255, 250, 122, 144, 47, 225, 71, 230, 0, 50, 96, 240, 238, 0, 230, 203,\n 212, 0, 143, 60, 230, 240, 209, 127, 128, 205, 114, 211, 34, 80, 235, 237,\n 140, 215, 238, 35, 250, 139, 139, 220, 211, 181, 173, 255, 30, 220, 79, 139,\n 238, 0, 230, 245, 0, 255, 0, 216, 153, 147, 204, 255, 133, 50, 107, 255,\n 250, 154, 47, 208, 204, 92, 210, 160, 0, 0, 255, 222, 226, 255, 128, 152, 45\n ],\n \"A\": [\n 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,\n 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,\n 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,\n 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,\n 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,\n 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,\n 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,\n 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,\n 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255\n ],\n \"I\": [\n 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,\n 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39,\n 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58,\n 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77,\n 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96,\n 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112,\n 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127,\n 128, 129, 130\n ]\n}\n","{\n \"labels\": [\n \"background\",\n \"tissue\",\n \"bone\",\n \"skin\",\n \"connective tissue\",\n \"blood\",\n \"organ\",\n \"mass\",\n \"muscle\",\n \"foreign object\",\n \"waste\",\n \"teeth\",\n \"fat\",\n \"gray matter\",\n \"white matter\",\n \"nerve\",\n \"vein\",\n \"artery\",\n \"capillary\",\n \"ligament\",\n \"tendon\",\n \"cartilage\",\n \"meniscus\",\n \"lymph node\",\n \"lymphatic vessel\",\n \"cerebro-spinal fluid\",\n \"bile\",\n \"urine\",\n \"feces\",\n \"gas\",\n \"fluid\",\n \"edema\",\n \"bleeding\",\n \"necrosis\",\n \"clot\",\n \"embolism\",\n \"head\",\n \"central nervous system\",\n \"brain\",\n \"gray matter of brain\",\n \"telencephalon\",\n \"cerebral cortex\",\n \"right frontal lobe\",\n \"left frontal lobe\",\n \"right temporal lobe\",\n \"left temporal lobe\",\n \"right parietal lobe\",\n \"left parietal lobe\",\n \"right occipital lobe\",\n \"left occipital lobe\",\n \"right insular lobe\",\n \"left insular lobe\",\n \"right limbic lobe\",\n \"left limbic lobe\",\n \"right striatum\",\n \"left striatum\",\n \"right caudate nucleus\",\n \"left caudate nucleus\",\n \"right putamen\",\n \"left putamen\",\n \"right pallidum\",\n \"left pallidum\",\n \"right amygdaloid complex\",\n \"left amygdaloid complex\",\n \"diencephalon\",\n \"thalamus\",\n \"right thalamus\",\n \"left thalamus\",\n \"pineal gland\",\n \"midbrain\",\n \"substantia nigra\",\n \"right substantia nigra\",\n \"left substantia nigra\",\n \"cerebral white matter\",\n \"right superior longitudinal fasciculus\",\n \"left superior longitudinal fasciculus\",\n \"right inferior longitudinal fasciculus\",\n \"left inferior longitudinal fasciculus\",\n \"right arcuate fasciculus\",\n \"left arcuate fasciculus\",\n \"right uncinate fasciculus\",\n \"left uncinate fasciculus\",\n \"right cingulum bundle\",\n \"left cingulum bundle\",\n \"projection fibers\",\n \"right corticospinal tract\",\n \"left corticospinal tract\",\n \"right optic radiation\",\n \"left optic radiation\",\n \"right medial lemniscus\",\n \"left medial lemniscus\",\n \"right superior cerebellar peduncle\",\n \"left superior cerebellar peduncle\",\n \"right middle cerebellar peduncle\",\n \"left middle cerebellar peduncle\",\n \"right inferior cerebellar peduncle\",\n \"left inferior cerebellar peduncle\",\n \"optic chiasm\",\n \"right optic tract\",\n \"left optic tract\",\n \"right fornix\",\n \"left fornix\",\n \"commissural fibers\",\n \"corpus callosum\",\n \"posterior commissure\",\n \"cerebellar white matter\",\n \"CSF space\",\n \"ventricles of brain\",\n \"right lateral ventricle\",\n \"left lateral ventricle\",\n \"right third ventricle\",\n \"left third ventricle\",\n \"cerebral aqueduct\",\n \"fourth ventricle\",\n \"subarachnoid space\",\n \"spinal cord\",\n \"gray matter of spinal cord\",\n \"white matter of spinal cord\",\n \"endocrine system of brain\",\n \"pituitary gland\",\n \"adenohypophysis\",\n \"neurohypophysis\",\n \"meninges\",\n \"dura mater\",\n \"arachnoid\",\n \"pia mater\",\n \"muscles of head\",\n \"salivary glands\",\n \"lips\",\n \"nose\",\n \"tongue\",\n \"soft palate\",\n \"right inner ear\",\n \"left inner ear\",\n \"right external ear\",\n \"left external ear\",\n \"right middle ear\",\n \"left middle ear\",\n \"right eyeball\",\n \"left eyeball\",\n \"skull\",\n \"right frontal bone\",\n \"left frontal bone\",\n \"right parietal bone\",\n \"left parietal bone\",\n \"right temporal bone\",\n \"left temporal bone\",\n \"right sphenoid bone\",\n \"left sphenoid bone\",\n \"right ethmoid bone\",\n \"left ethmoid bone\",\n \"occipital bone\",\n \"maxilla\",\n \"right zygomatic bone\",\n \"right lacrimal bone\",\n \"vomer bone\",\n \"right palatine bone\",\n \"left palatine bone\",\n \"mandible\",\n \"neck\",\n \"muscles of neck\",\n \"pharynx\",\n \"larynx\",\n \"thyroid gland\",\n \"right parathyroid glands\",\n \"left parathyroid glands\",\n \"skeleton of neck\",\n \"hyoid bone\",\n \"cervical vertebral column\",\n \"thorax\",\n \"trachea\",\n \"bronchi\",\n \"right lung\",\n \"left lung\",\n \"superior lobe of right lung\",\n \"superior lobe of left lung\",\n \"middle lobe of right lung\",\n \"inferior lobe of right lung\",\n \"inferior lobe of left lung\",\n \"pleura\",\n \"heart\",\n \"right atrium\",\n \"left atrium\",\n \"atrial septum\",\n \"ventricular septum\",\n \"right ventricle of heart\",\n \"left ventricle of heart\",\n \"mitral valve\",\n \"tricuspid valve\",\n \"aortic valve\",\n \"pulmonary valve\",\n \"aorta\",\n \"pericardium\",\n \"pericardial cavity\",\n \"esophagus\",\n \"thymus\",\n \"mediastinum\",\n \"skin of thoracic wall\",\n \"muscles of thoracic wall\",\n \"skeleton of thorax\",\n \"thoracic vertebral column\",\n \"ribs\",\n \"sternum\",\n \"right clavicle\",\n \"left clavicle\",\n \"abdominal cavity\",\n \"abdomen\",\n \"peritoneum\",\n \"omentum\",\n \"peritoneal cavity\",\n \"retroperitoneal space\",\n \"stomach\",\n \"duodenum\",\n \"small bowel\",\n \"colon\",\n \"anus\",\n \"liver\",\n \"biliary tree\",\n \"gallbladder\",\n \"pancreas\",\n \"spleen\",\n \"urinary system\",\n \"right kidney\",\n \"left kidney\",\n \"right ureter\",\n \"left ureter\",\n \"urinary bladder\",\n \"urethra\",\n \"right adrenal gland\",\n \"left adrenal gland\",\n \"female internal genitalia\",\n \"uterus\",\n \"right fallopian tube\",\n \"left fallopian tube\",\n \"right ovary\",\n \"left ovary\",\n \"vagina\",\n \"male internal genitalia\",\n \"prostate\",\n \"right seminal vesicle\",\n \"left seminal vesicle\",\n \"right deferent duct\",\n \"left deferent duct\",\n \"skin of abdominal wall\",\n \"muscles of abdominal wall\",\n \"skeleton of abdomen\",\n \"lumbar vertebral column\",\n \"female external genitalia\",\n \"male external genitalia\",\n \"skeleton of upper limb\",\n \"muscles of upper limb\",\n \"right upper limb\",\n \"left upper limb\",\n \"right shoulder\",\n \"left shoulder\",\n \"right arm\"\n ],\n \"R\": [\n 0, 128, 241, 177, 111, 216, 221, 144, 192, 220, 78, 255, 230, 200, 250, 244,\n 0, 216, 183, 183, 152, 111, 178, 68, 111, 85, 0, 214, 78, 218, 170, 140,\n 188, 216, 145, 150, 177, 244, 250, 200, 68, 128, 83, 83, 162, 162, 141, 141,\n 182, 182, 188, 188, 154, 154, 177, 177, 30, 30, 210, 210, 48, 48, 98, 98,\n 69, 166, 122, 122, 253, 145, 46, 0, 0, 250, 127, 127, 159, 159, 125, 125,\n 106, 106, 154, 154, 126, 201, 201, 78, 78, 174, 174, 139, 139, 148, 148,\n 186, 186, 99, 156, 156, 64, 64, 138, 97, 126, 194, 85, 88, 88, 88, 88, 88,\n 88, 88, 88, 244, 200, 250, 82, 57, 60, 92, 255, 255, 255, 255, 201, 70, 188,\n 177, 166, 182, 229, 229, 174, 174, 201, 201, 194, 194, 241, 203, 203, 229,\n 229, 255, 255, 209, 209, 248, 248, 255, 196, 255, 255, 255, 242, 242, 222,\n 177, 213, 184, 150, 62, 62, 62, 242, 250, 255, 177, 182, 175, 197, 197, 172,\n 172, 202, 224, 224, 255, 206, 210, 203, 233, 195, 181, 152, 159, 166, 218,\n 225, 224, 255, 184, 211, 47, 255, 173, 188, 255, 226, 253, 244, 205, 205,\n 186, 177, 255, 234, 204, 180, 216, 255, 205, 204, 255, 221, 0, 139, 249,\n 157, 203, 185, 185, 247, 247, 222, 124, 249, 249, 244, 255, 255, 227, 213,\n 213, 193, 216, 230, 245, 245, 241, 241, 177, 171, 217, 212, 185, 185, 198,\n 194, 177, 177, 177, 177, 177\n ],\n \"G\": [\n 0, 174, 214, 122, 184, 101, 130, 238, 104, 245, 63, 250, 220, 200, 250, 214,\n 151, 101, 156, 214, 189, 184, 212, 172, 197, 188, 145, 230, 63, 255, 250,\n 224, 65, 191, 60, 98, 122, 214, 250, 200, 131, 174, 146, 146, 115, 115, 93,\n 93, 166, 166, 135, 135, 150, 150, 140, 140, 111, 111, 157, 157, 129, 129,\n 153, 153, 110, 113, 101, 101, 135, 92, 101, 108, 108, 250, 150, 150, 116,\n 116, 102, 102, 174, 174, 146, 146, 126, 160, 160, 152, 152, 140, 140, 126,\n 126, 120, 120, 135, 135, 106, 171, 171, 123, 123, 95, 113, 161, 195, 188,\n 106, 106, 106, 106, 106, 106, 106, 106, 214, 200, 250, 174, 157, 143, 162,\n 244, 244, 244, 244, 121, 163, 91, 122, 84, 105, 147, 147, 122, 122, 112,\n 112, 142, 142, 213, 179, 179, 204, 204, 243, 243, 185, 185, 223, 223, 230,\n 172, 255, 250, 237, 217, 217, 198, 122, 124, 105, 208, 162, 162, 162, 206,\n 210, 255, 122, 228, 216, 165, 165, 138, 138, 164, 186, 186, 245, 110, 115,\n 108, 138, 100, 85, 55, 63, 70, 123, 130, 97, 244, 122, 171, 150, 244, 121,\n 95, 239, 202, 232, 217, 179, 179, 124, 122, 255, 234, 142, 119, 132, 253,\n 167, 168, 224, 130, 145, 150, 180, 108, 136, 102, 102, 182, 182, 154, 186,\n 186, 186, 170, 181, 190, 153, 141, 141, 123, 146, 158, 172, 172, 172, 172,\n 124, 85, 198, 188, 135, 135, 175, 98, 122, 122, 122, 122, 122\n ],\n \"B\": [\n 0, 128, 145, 101, 210, 79, 101, 144, 88, 20, 0, 220, 70, 235, 210, 49, 206,\n 79, 220, 211, 207, 210, 242, 100, 131, 255, 30, 130, 0, 255, 250, 228, 28,\n 216, 66, 83, 101, 49, 225, 215, 98, 128, 164, 164, 105, 105, 137, 137, 110,\n 110, 166, 166, 201, 201, 190, 190, 85, 85, 166, 166, 126, 126, 112, 112, 53,\n 137, 38, 38, 192, 109, 131, 112, 112, 225, 88, 88, 163, 163, 154, 154, 155,\n 155, 83, 83, 55, 133, 133, 141, 141, 103, 103, 177, 177, 72, 72, 135, 135,\n 24, 108, 108, 147, 147, 74, 158, 197, 164, 255, 215, 215, 215, 215, 215,\n 215, 215, 215, 49, 215, 225, 128, 110, 83, 109, 209, 209, 209, 209, 77, 117,\n 95, 101, 94, 107, 118, 118, 90, 90, 73, 73, 0, 0, 144, 77, 77, 109, 109,\n 152, 152, 85, 85, 131, 131, 138, 68, 167, 160, 145, 123, 123, 101, 101, 109,\n 108, 243, 114, 114, 114, 142, 139, 207, 101, 255, 244, 145, 145, 115, 115,\n 140, 162, 162, 217, 84, 89, 81, 112, 73, 57, 13, 27, 38, 97, 104, 76, 209,\n 154, 143, 103, 209, 88, 76, 172, 134, 158, 154, 108, 108, 161, 101, 220,\n 194, 178, 153, 105, 229, 142, 143, 199, 101, 30, 98, 111, 162, 116, 83, 83,\n 164, 164, 132, 223, 150, 150, 147, 158, 165, 130, 113, 113, 103, 127, 140,\n 147, 147, 151, 151, 92, 68, 131, 102, 134, 134, 125, 79, 101, 101, 101, 101,\n 101\n ],\n \"A\": [\n 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,\n 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,\n 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,\n 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,\n 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,\n 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,\n 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,\n 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,\n 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,\n 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,\n 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,\n 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,\n 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,\n 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,\n 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,\n 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,\n 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,\n 255\n ],\n \"I\": [\n 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,\n 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39,\n 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58,\n 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77,\n 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96,\n 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112,\n 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127,\n 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142,\n 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157,\n 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172,\n 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187,\n 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202,\n 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217,\n 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232,\n 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247,\n 248, 249, 250, 251, 252, 253, 254, 255\n ]\n}\n","{\n \"R\": [0, 0, 24, 248, 255],\n \"G\": [0, 0, 177, 254, 0],\n \"B\": [0, 136, 0, 0, 0],\n \"A\": [0, 32, 64, 78, 128],\n \"I\": [0, 64, 128, 156, 255]\n}\n","{\n \"R\": [1, 10, 15, 17, 21, 27, 36, 49, 65, 82, 99, 118, 140, 161, 183, 203, 222, 238, 248, 253, 253, 253, 252, 250],\n \"G\": [25, 42, 56, 68, 79, 88, 97, 105, 111, 116, 122, 127, 133, 138, 142, 146, 150, 155, 162, 170, 178, 186, 195, 204],\n \"B\": [89, 92, 95, 96, 98, 98, 96, 91, 83, 74, 64, 55, 46, 43, 49, 62, 79, 100, 126, 151, 175, 199, 223, 250],\n \"A\": [0, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64],\n \"I\": [0, 11, 22, 33, 44, 55, 66, 77, 88, 99, 110, 121, 133, 144, 155, 166, 177, 188, 199, 210, 221, 232, 243, 255]\n}\n","{\n \"R\": [\n 0, 248, 242, 235, 229, 222, 212, 202, 193, 183, 173, 145, 117, 78, 39, 0, 0,\n 0, 0, 0, 0, 64, 128, 191, 255, 255, 255, 255, 255, 238, 221, 204, 214, 224,\n 235, 245, 255, 249, 242, 236, 229, 223, 217, 210, 204, 197, 191, 184, 176,\n 169, 161, 154, 146, 139, 132, 124, 117, 109, 102, 0\n ],\n \"G\": [\n 0, 251, 247, 243, 239, 235, 235, 235, 235, 235, 235, 216, 197, 174, 150,\n 127, 153, 178, 204, 229, 255, 255, 255, 255, 255, 220, 185, 150, 115, 77,\n 38, 0, 31, 61, 92, 122, 153, 138, 122, 107, 92, 77, 61, 46, 31, 15, 0, 0, 0,\n 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0\n ],\n \"B\": [\n 0, 254, 253, 252, 251, 250, 251, 252, 253, 254, 255, 226, 198, 132, 66, 0,\n 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 40, 80, 120, 160, 200, 199,\n 198, 197, 196, 196, 195, 194, 193, 192, 191, 184, 176, 169, 161, 154, 146,\n 139, 132, 124, 117, 109, 102, 0\n ],\n \"A\": [\n 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256,\n 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256,\n 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256,\n 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256\n ],\n \"I\": [\n 0, 4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 44, 48, 52, 56, 60, 64, 68, 72, 76,\n 80, 84, 88, 92, 96, 100, 104, 108, 112, 116, 120, 124, 128, 132, 136, 140,\n 144, 148, 152, 156, 160, 164, 168, 172, 176, 180, 184, 188, 192, 196, 200,\n 204, 208, 212, 216, 220, 224, 228, 232, 236, 240, 244, 248, 255\n ]\n}\n","{\n \"R\": [\n 255, 248, 242, 235, 229, 222, 212, 202, 193, 183, 173, 145, 117, 78, 39, 0,\n 0, 0, 0, 0, 0, 64, 128, 191, 255, 255, 255, 255, 255, 238, 221, 204, 214,\n 224, 235, 245, 255, 249, 242, 236, 229, 223, 217, 210, 204, 197, 191, 184,\n 176, 169, 161, 154, 146, 139, 132, 124, 117, 109, 102, 0\n ],\n \"G\": [\n 255, 251, 247, 243, 239, 235, 235, 235, 235, 235, 235, 216, 197, 174, 150,\n 127, 153, 178, 204, 229, 255, 255, 255, 255, 255, 220, 185, 150, 115, 77,\n 38, 0, 31, 61, 92, 122, 153, 138, 122, 107, 92, 77, 61, 46, 31, 15, 0, 0, 0,\n 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0\n ],\n \"B\": [\n 255, 254, 253, 252, 251, 250, 251, 252, 253, 254, 255, 226, 198, 132, 66, 0,\n 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 40, 80, 120, 160, 200, 199,\n 198, 197, 196, 196, 195, 194, 193, 192, 191, 184, 176, 169, 161, 154, 146,\n 139, 132, 124, 117, 109, 102, 0\n ],\n \"A\": [\n 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256,\n 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256,\n 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256,\n 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256\n ],\n \"I\": [\n 0, 4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 44, 48, 52, 56, 60, 64, 68, 72, 76,\n 80, 84, 88, 92, 96, 100, 104, 108, 112, 116, 120, 124, 128, 132, 136, 140,\n 144, 148, 152, 156, 160, 164, 168, 172, 176, 180, 184, 188, 192, 196, 200,\n 204, 208, 212, 216, 220, 224, 228, 232, 236, 240, 244, 248, 255\n ]\n}\n","{\n \"R\": [0, 0, 0, 0, 196, 255],\n \"G\": [0, 32, 128, 128, 128, 32],\n \"B\": [0, 255, 196, 0, 0, 0],\n \"A\": [0, 128, 64, 64, 64, 128],\n \"I\": [0, 1, 64, 128, 192, 255]\n}\n","{\n \"R\": [0, 255],\n \"G\": [0, 0],\n \"B\": [255, 255],\n \"A\": [0, 128],\n \"I\": [0, 255]\n}\n","{\n \"R\": [0, 0],\n \"G\": [0, 255],\n \"B\": [255, 255],\n \"A\": [0, 128],\n \"I\": [0, 255]\n}\n","{\n \"R\": [0, 0, 0, 0],\n \"G\": [0, 1, 128, 255],\n \"B\": [0, 222, 127, 32],\n \"A\": [0, 0, 64, 128],\n \"I\": [0, 1, 128, 255]\n}\n","{\n \"R\": [0, 0, 0],\n \"G\": [0, 0, 0],\n \"B\": [0, 128, 255],\n \"A\": [0, 64, 128],\n \"I\": [0, 128, 255]\n}\n","{\n \"R\": [0, 103, 255],\n \"G\": [0, 126, 255],\n \"B\": [0, 165, 255],\n \"A\": [0, 76, 128],\n \"I\": [0, 153, 255]\n}\n","{\n \"R\": [0, 43, 103, 199, 216, 255],\n \"G\": [0, 0, 37, 155, 213, 255],\n \"B\": [0, 0, 20, 97, 201, 255],\n \"A\": [0, 44, 48, 54, 56, 56],\n \"I\": [0, 64, 128, 196, 240, 255]\n}\n","{\n \"R\": [\n 0, 9, 24, 33, 40, 46, 52, 57, 62, 66, 70, 74, 78, 81, 85, 88, 91, 94, 98,\n 101, 103, 106, 109, 112, 114, 117, 119, 121, 124, 126, 128, 130, 133, 135,\n 137, 139, 141, 143, 145, 147, 149, 151, 153, 155, 157, 159, 161, 163, 164,\n 166, 168, 169, 171, 172, 174, 175, 177, 178, 180, 181, 183, 184, 186, 187,\n 189, 190, 191, 193, 194, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205,\n 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220,\n 221, 222, 222, 223, 224, 224, 225, 226, 226, 227, 228, 228, 229, 229, 230,\n 231, 231, 232, 233, 233, 234, 234, 235, 235, 236, 236, 236, 237, 237, 237,\n 238, 238, 238, 239, 239, 239, 240, 240, 240, 241, 241, 241, 242, 242, 242,\n 243, 243, 243, 243, 243, 243, 243, 243, 244, 244, 244, 244, 244, 244, 244,\n 244, 244, 244, 244, 245, 245, 245, 245, 245, 245, 245, 245, 245, 245, 245,\n 245, 245, 245, 245, 245, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244,\n 244, 244, 244, 244, 244, 243, 243, 243, 243, 243, 243, 243, 243, 243, 243,\n 243, 243, 242, 242, 242, 242, 242, 242, 242, 242, 241, 242, 242, 242, 242,\n 242, 242, 242, 242, 242, 242, 242, 242, 242, 242, 242, 242, 242, 242, 242,\n 242, 242, 242, 242, 243, 243, 244, 244, 245, 246, 246, 247, 247, 248, 249,\n 249, 250, 250, 251, 251, 252, 252, 253, 253, 254, 254, 254\n ],\n \"G\": [\n 42, 41, 41, 41, 40, 40, 40, 39, 39, 39, 38, 38, 37, 37, 36, 36, 35, 35, 34,\n 34, 33, 32, 32, 31, 30, 30, 30, 30, 29, 29, 29, 29, 28, 28, 28, 27, 27, 27,\n 26, 26, 26, 25, 25, 25, 24, 24, 24, 24, 24, 25, 25, 26, 27, 27, 28, 28, 29,\n 30, 30, 31, 31, 32, 33, 33, 34, 34, 35, 36, 36, 37, 38, 39, 40, 42, 43, 44,\n 45, 47, 48, 49, 50, 51, 53, 54, 55, 56, 57, 58, 59, 60, 62, 63, 64, 65, 66,\n 68, 69, 71, 72, 73, 75, 76, 77, 79, 80, 81, 83, 84, 85, 87, 88, 89, 90, 92,\n 93, 94, 95, 97, 98, 100, 101, 102, 104, 105, 107, 108, 109, 111, 112, 113,\n 115, 116, 117, 119, 120, 121, 122, 124, 125, 126, 128, 129, 130, 132, 133,\n 134, 136, 137, 138, 140, 141, 142, 144, 145, 146, 147, 149, 150, 151, 153,\n 154, 155, 156, 158, 159, 160, 161, 163, 164, 165, 167, 168, 169, 170, 172,\n 173, 174, 175, 176, 178, 179, 180, 181, 183, 184, 185, 186, 187, 189, 190,\n 191, 192, 193, 194, 196, 197, 198, 199, 200, 201, 203, 204, 205, 206, 207,\n 208, 209, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223,\n 224, 225, 226, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 238,\n 239, 240, 241, 241, 242, 243, 244, 244, 245, 246, 247, 248, 248, 249, 250,\n 251, 251, 252, 253, 254, 255\n ],\n \"B\": [\n 167, 167, 166, 166, 166, 165, 165, 165, 164, 164, 164, 163, 163, 162, 162,\n 162, 161, 161, 161, 160, 160, 160, 159, 159, 158, 158, 157, 157, 156, 155,\n 155, 154, 154, 153, 153, 152, 151, 151, 150, 150, 149, 149, 148, 147, 147,\n 146, 146, 145, 144, 144, 143, 143, 142, 141, 141, 140, 139, 139, 138, 137,\n 137, 136, 135, 135, 134, 133, 133, 132, 131, 131, 130, 130, 129, 128, 128,\n 127, 127, 126, 125, 125, 124, 124, 123, 123, 122, 121, 121, 120, 119, 119,\n 118, 118, 117, 116, 116, 116, 115, 115, 114, 114, 113, 113, 113, 112, 112,\n 111, 111, 110, 110, 109, 109, 109, 108, 108, 107, 107, 106, 106, 106, 106,\n 106, 105, 105, 105, 105, 105, 105, 105, 104, 104, 104, 104, 104, 103, 103,\n 103, 103, 103, 102, 102, 102, 103, 103, 103, 103, 104, 104, 104, 104, 104,\n 105, 105, 105, 105, 106, 106, 106, 106, 106, 107, 107, 107, 107, 108, 108,\n 109, 110, 111, 111, 112, 113, 113, 114, 115, 115, 116, 117, 117, 118, 119,\n 120, 120, 121, 122, 122, 123, 124, 125, 126, 128, 129, 130, 131, 132, 134,\n 135, 136, 137, 138, 140, 141, 142, 143, 144, 146, 147, 148, 149, 150, 152,\n 153, 155, 157, 158, 160, 162, 163, 165, 167, 168, 170, 172, 173, 175, 177,\n 178, 180, 182, 183, 185, 187, 188, 190, 193, 196, 199, 201, 204, 207, 210,\n 212, 215, 218, 221, 224, 226, 229, 232, 235, 238, 240, 243, 246, 249, 252,\n 255\n ],\n \"A\": [\n 0, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64\n ],\n \"I\": [\n 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,\n 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39,\n 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58,\n 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77,\n 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96,\n 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112,\n 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127,\n 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142,\n 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157,\n 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172,\n 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187,\n 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202,\n 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217,\n 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232,\n 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247,\n 248, 249, 250, 251, 252, 253, 254, 255\n ]\n}\n","{\n \"R\": [0, 86, 166, 255],\n \"G\": [32, 92, 156, 233],\n \"B\": [76, 108, 117, 69],\n \"A\": [0, 56, 80, 88],\n \"I\": [0, 64, 192, 255]\n}\n","{\n \"R\": [0, 0, 0],\n \"G\": [127, 196, 254],\n \"B\": [255, 255, 255],\n \"A\": [0, 64, 128],\n \"I\": [0, 128, 255]\n}\n","{\n \"R\": [0, 61, 122, 183, 244, 255],\n \"G\": [0, 41, 81, 122, 163, 255],\n \"B\": [0, 25, 51, 76, 102, 255],\n \"A\": [0, 25, 51, 71, 102, 128],\n \"I\": [0, 51, 102, 153, 204, 255]\n}\n","{\n \"R\": [0, 61, 122, 183, 244, 255],\n \"G\": [0, 41, 81, 122, 163, 203],\n \"B\": [0, 25, 51, 76, 102, 127],\n \"A\": [0, 25, 51, 71, 102, 128],\n \"I\": [0, 51, 102, 153, 204, 255]\n}\n","{\n \"min\": -643,\n \"max\": -235,\n \"R\": [0, 0, 0],\n \"G\": [154, 154, 154],\n \"B\": [179, 179, 101],\n \"A\": [0, 32, 0],\n \"I\": [0, 163, 255]\n}\n","{\n \"min\": 114,\n \"max\": 800,\n \"R\": [0, 255, 255, 255],\n \"G\": [0, 0, 129, 255],\n \"B\": [0, 0, 0, 255],\n \"A\": [0, 64, 88, 228],\n \"I\": [0, 80, 160, 255]\n}\n","{\n \"min\": 180,\n \"max\": 600,\n \"R\": [0, 0, 113, 255],\n \"G\": [0, 0, 109, 250],\n \"B\": [0, 0, 101, 245],\n \"A\": [0, 0, 100, 160],\n \"I\": [0, 1, 128, 255]\n}\n","{\n \"min\": -10,\n \"max\": 110,\n \"R\": [0, 127, 255],\n \"G\": [0, 127, 255],\n \"B\": [0, 127, 255],\n \"A\": [0, 48, 128],\n \"I\": [0, 124, 255]\n}\n","{\n \"min\": -10,\n \"max\": 110,\n \"R\": [0, 199, 255],\n \"G\": [0, 127, 255],\n \"B\": [0, 127, 255],\n \"A\": [0, 48, 128],\n \"I\": [0, 124, 255]\n}\n","{\n \"min\": -80,\n \"max\": 1000,\n \"R\": [0, 189, 150, 150, 150, 150, 255],\n \"G\": [0, 169, 54, 54, 54, 54, 240],\n \"B\": [0, 153, 52, 52, 52, 52, 242],\n \"A\": [0, 32, 64, 0, 0, 64, 64],\n \"I\": [0, 1, 82, 92, 234, 242, 255]\n}\n","{\n \"min\": -590,\n \"max\": 600,\n \"R\": [0, 241, 241, 248, 248, 178, 178, 232, 255, 255, 255],\n \"G\": [0, 156, 156, 222, 222, 36, 36, 51, 255, 255, 255],\n \"B\": [0, 130, 130, 169, 169, 24, 24, 37, 255, 255, 255],\n \"A\": [0, 8, 0, 0, 0, 64, 64, 0, 0, 222, 222],\n \"I\": [0, 2, 3, 64, 122, 142, 172, 182, 252, 253, 255]\n}\n","{\n \"min\": 114,\n \"max\": 302,\n \"R\": [0, 255, 255],\n \"G\": [0, 129, 255],\n \"B\": [0, 0, 255],\n \"A\": [0, 88, 228],\n \"I\": [0, 103, 255]\n}\n","{\n \"min\": -23,\n \"max\": 246,\n \"R\": [0, 44, 255, 255, 255],\n \"G\": [0, 128, 90, 255, 255],\n \"B\": [0, 0, 70, 0, 255],\n \"A\": [0, 0, 82, 184, 228],\n \"I\": [0, 64, 131, 196, 255]\n}\n","{\n \"min\": -100,\n \"max\": 246,\n \"R\": [0, 128, 159, 255, 255, 255, 255],\n \"G\": [0, 0, 56, 90, 0, 255, 255],\n \"B\": [0, 0, 41, 70, 0, 0, 255],\n \"A\": [0, 63, 105, 135, 167, 184, 228],\n \"I\": [0, 100, 128, 155, 180, 209, 255]\n}\n","{\n \"min\": -590,\n \"max\": 600,\n \"R\": [0, 241, 241, 248, 248, 178, 232, 255, 255],\n \"G\": [0, 156, 156, 222, 222, 36, 51, 255, 255],\n \"B\": [0, 130, 130, 169, 169, 24, 37, 255, 255],\n \"A\": [0, 63, 105, 135, 167, 184, 228, 228, 228],\n \"I\": [0, 1, 52, 127, 137, 162, 172, 252, 255]\n}\n","{\n \"min\": 140,\n \"max\": 1024,\n \"R\": [0, 2, 113, 255],\n \"G\": [0, 1, 109, 250],\n \"B\": [0, 1, 101, 245],\n \"A\": [0, 1, 96, 168],\n \"I\": [0, 1, 128, 255]\n}\n","{\n \"min\": -923,\n \"max\": 679,\n \"R\": [0, 0, 0, 0, 0, 255, 255, 255],\n \"G\": [154, 154, 154, 154, 0, 0, 254, 255],\n \"B\": [179, 179, 179, 179, 0, 0, 0, 255],\n \"A\": [0, 3, 8, 0, 0, 10, 15, 20],\n \"I\": [0, 30, 62, 88, 170, 200, 232, 255]\n}\n","{\n \"min\": -10,\n \"max\": 110,\n \"R\": [0, 199, 255],\n \"G\": [0, 127, 255],\n \"B\": [0, 127, 255],\n \"A\": [0, 48, 128],\n \"I\": [0, 124, 255]\n}\n","{\n \"min\": -600,\n \"max\": 100,\n \"R\": [0, 134, 255],\n \"G\": [0, 109, 250],\n \"B\": [0, 101, 245],\n \"A\": [0, 60, 148],\n \"I\": [0, 128, 255]\n}\n","{\n \"min\": 114,\n \"max\": 246,\n \"R\": [0, 255, 255],\n \"G\": [0, 128, 255],\n \"B\": [0, 128, 255],\n \"A\": [0, 64, 96],\n \"I\": [0, 87, 255]\n}\n","{\n \"min\": 50,\n \"max\": 1000,\n \"R\": [98, 210, 169, 128, 255],\n \"G\": [94, 26, 77, 128, 255],\n \"B\": [45, 21, 74, 128, 255],\n \"A\": [0, 25, 0, 4, 168],\n \"I\": [0, 41, 87, 154, 255]\n}\n","{\n \"R\": [\n 0, 13, 21, 26, 27, 25, 22, 21, 22, 28, 39, 54, 75, 98, 124, 148, 171, 189,\n 202, 210, 213, 211, 206, 200, 195, 193, 195, 201, 211, 225, 240, 255\n ],\n \"G\": [\n 0, 5, 11, 20, 31, 44, 58, 72, 86, 99, 109, 116, 120, 122, 122, 122, 121,\n 121, 124, 129, 137, 147, 161, 175, 190, 205, 218, 229, 238, 245, 251, 255\n ],\n \"B\": [\n 0, 14, 30, 46, 61, 71, 77, 78, 75, 68, 60, 52, 48, 47, 53, 65, 83, 105, 131,\n 157, 183, 205, 222, 235, 241, 243, 242, 240, 239, 240, 245, 255\n ],\n \"A\": [\n 0, 4, 8, 12, 17, 21, 25, 29, 33, 37, 41, 45, 50, 54, 58, 62, 66, 70, 74, 78,\n 83, 87, 91, 95, 99, 103, 107, 111, 116, 120, 124, 128\n ],\n \"I\": [\n 0, 8, 16, 25, 33, 41, 49, 58, 66, 74, 82, 90, 99, 107, 115, 123, 132, 140,\n 148, 156, 165, 173, 181, 189, 197, 206, 214, 222, 230, 239, 247, 255\n ]\n}\n","{\n \"R\": [0, 10, 136, 255],\n \"G\": [0, 39, 220, 255],\n \"B\": [0, 223, 253, 255],\n \"A\": [0, 48, 64, 70],\n \"I\": [0, 92, 192, 255]\n}\n","{\n \"R\": [\n 0, 245, 205, 120, 196, 220, 230, 0, 122, 236, 12, 204, 42, 119, 220, 103,\n 60, 255, 165, 160, 0, 245, 205, 120, 196, 220, 230, 0, 122, 236, 13, 220,\n 103, 255, 165, 160, 0, 120, 200, 255, 255, 164, 164, 164, 234, 0, 0, 0, 0, 0\n ],\n \"G\": [\n 0, 245, 62, 18, 58, 248, 148, 118, 186, 13, 48, 182, 204, 159, 216, 255, 60,\n 165, 42, 32, 200, 245, 62, 18, 58, 248, 148, 118, 186, 13, 48, 216, 255,\n 165, 42, 32, 200, 190, 70, 148, 148, 108, 108, 108, 169, 0, 0, 0, 0, 0\n ],\n \"B\": [\n 0, 245, 78, 134, 250, 164, 34, 14, 220, 176, 255, 142, 164, 176, 20, 255,\n 60, 0, 42, 240, 200, 245, 78, 134, 250, 164, 34, 14, 220, 176, 255, 20, 255,\n 0, 42, 240, 221, 150, 255, 10, 10, 226, 226, 226, 30, 64, 112, 160, 208, 255\n ],\n \"A\": [\n 0, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64\n ],\n \"I\": [\n 0, 2, 3, 4, 5, 7, 8, 10, 11, 12, 13, 14, 15, 16, 17, 18, 24, 26, 28, 30, 31,\n 41, 42, 43, 44, 46, 47, 49, 50, 51, 52, 53, 54, 58, 60, 62, 63, 72, 77, 78,\n 79, 80, 81, 82, 85, 251, 252, 253, 254, 255\n ]\n}\n","{\n \"R\": [0, 0, 128, 255, 255],\n \"G\": [0, 128, 0, 128, 255],\n \"B\": [0, 125, 255, 0, 255],\n \"A\": [0, 32, 64, 96, 128],\n \"I\": [0, 63, 128, 192, 255]\n}\n","{\n \"R\": [0, 142, 227, 255],\n \"G\": [0, 85, 170, 255],\n \"B\": [0, 14, 76, 255],\n \"A\": [0, 42, 84, 128],\n \"I\": [0, 85, 170, 255]\n}\n","{\n \"R\": [0, 255],\n \"G\": [0, 255],\n \"B\": [0, 255],\n \"A\": [0, 128],\n \"I\": [0, 255]\n}\n","{\n \"R\": [0, 0, 0],\n \"G\": [0, 128, 255],\n \"B\": [0, 0, 0],\n \"A\": [0, 64, 128],\n \"I\": [0, 128, 255]\n}\n","{\n \"R\": [0, 0, 0],\n \"G\": [72, 72, 255],\n \"B\": [0, 255, 255],\n \"A\": [0, 64, 128],\n \"I\": [0, 88, 255]\n}\n","{\n \"R\": [0, 255, 255],\n \"G\": [72, 88, 255],\n \"B\": [0, 0, 0],\n \"A\": [0, 64, 128],\n \"I\": [0, 88, 255]\n}\n","{\n \"R\": [0, 255, 255, 255],\n \"G\": [0, 0, 126, 255],\n \"B\": [0, 0, 0, 255],\n \"A\": [0, 64, 96, 128],\n \"I\": [0, 128, 191, 255]\n}\n","{\n \"R\": [3, 255, 255, 255],\n \"G\": [0, 0, 255, 255],\n \"B\": [0, 0, 0, 255],\n \"A\": [0, 48, 96, 128],\n \"I\": [0, 95, 191, 255]\n}\n","{\n \"R\": [255, 255, 0, 0, 0, 255, 255],\n \"G\": [0, 255, 255, 255, 0, 0, 0],\n \"B\": [0, 0, 0, 255, 255, 255, 0],\n \"A\": [0, 14, 28, 43, 57, 71, 85],\n \"I\": [0, 43, 85, 128, 170, 213, 255]\n}\n","{\n \"R\": [0, 120, 237, 240],\n \"G\": [0, 28, 105, 249],\n \"B\": [4, 109, 37, 33],\n \"A\": [0, 56, 80, 88],\n \"I\": [0, 64, 192, 255]\n}\n","{\n \"R\": [0, 0, 127, 255, 127],\n \"G\": [0, 127, 255, 127, 0],\n \"B\": [127, 255, 127, 0, 0],\n \"A\": [0, 32, 64, 96, 128],\n \"I\": [0, 63, 128, 192, 255]\n}\n","{\n \"R\": [0, 255, 255],\n \"G\": [0, 0, 255],\n \"B\": [0, 0, 0],\n \"A\": [0, 64, 64],\n \"I\": [0, 86, 255]\n}\n","{\n \"R\": [94, 50, 90, 152, 215, 238, 249, 254, 252, 241, 209, 158],\n \"G\": [79, 131, 186, 214, 240, 244, 237, 210, 157, 100, 57, 1],\n \"B\": [162, 189, 167, 164, 155, 169, 168, 123, 86, 68, 79, 66],\n \"A\": [0, 12, 23, 35, 47, 58, 70, 81, 93, 105, 116, 128],\n \"I\": [0, 23, 46, 70, 93, 116, 139, 162, 185, 209, 232, 255]\n}\n","{\n \"R\": [3, 7, 13, 25, 45, 67, 84, 98, 110, 124, 138, 154, 173, 191, 210, 225, 233, 233, 231, 229, 230, 235, 243, 253],\n \"G\": [19, 33, 48, 63, 77, 87, 92, 94, 95, 96, 96, 97, 99, 101, 106, 118, 133, 149, 163, 177, 192, 208, 225, 245],\n \"B\": [38, 58, 79, 99, 114, 122, 122, 120, 117, 113, 110, 105, 101, 97, 94, 96, 103, 112, 122, 134, 149, 169, 192, 218],\n \"A\": [0, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64],\n \"I\": [0, 11, 22, 33, 44, 55, 66, 77, 88, 99, 110, 121, 133, 144, 155, 166, 177, 188, 199, 210, 221, 232, 243, 255]\n}\n","{\n \"R\": [0, 148, 183, 223, 247, 252],\n \"G\": [0, 44, 55, 74, 112, 253],\n \"B\": [4, 128, 121, 104, 92, 191],\n \"A\": [0, 44, 53, 64, 75, 107],\n \"I\": [0, 107, 128, 154, 179, 255]\n}\n","{\n \"R\": [11, 59, 55, 222],\n \"G\": [4, 45, 165, 245],\n \"B\": [5, 91, 172, 229],\n \"A\": [0, 23, 70, 107],\n \"I\": [0, 56, 167, 255]\n}\n","{\n \"R\": [3, 5, 6, 8, 12, 19, 28, 36, 42, 48, 54, 61, 68, 76, 86, 96, 110, 129, 154, 181, 205, 224, 239, 252],\n \"G\": [19, 32, 44, 58, 72, 86, 98, 108, 116, 122, 128, 134, 141, 149, 157, 167, 178, 191, 204, 216, 225, 232, 239, 244],\n \"B\": [39, 60, 82, 104, 123, 137, 144, 145, 142, 138, 134, 130, 126, 121, 116, 110, 105, 104, 112, 131, 155, 178, 198, 217],\n \"A\": [0, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64],\n \"I\": [0, 11, 22, 33, 44, 55, 66, 77, 88, 99, 110, 121, 133, 144, 155, 166, 177, 188, 199, 210, 221, 232, 243, 255]\n}\n","{\n \"R\": [0, 85, 0, 0, 0, 0, 0, 0, 85, 255, 255, 255, 172],\n \"G\": [0, 0, 0, 0, 85, 170, 255, 255, 255, 255, 85, 0, 0],\n \"B\": [0, 170, 85, 255, 255, 170, 170, 0, 85, 0, 0, 0, 0],\n \"A\": [0, 5, 10, 21, 26, 32, 37, 42, 48, 53, 64, 72, 85],\n \"I\": [0, 15, 31, 63, 79, 95, 111, 127, 143, 159, 191, 217, 255]\n}\n","{\n \"R\": [13, 156, 237, 240],\n \"G\": [8, 23, 121, 249],\n \"B\": [135, 158, 83, 33],\n \"A\": [0, 56, 80, 88],\n \"I\": [0, 64, 192, 255]\n}\n","{\n \"R\": [\n 208, 71, 33, 192, 32, 195, 208, 173, 233, 202, 25, 210, 145, 89, 87, 245,\n 246, 38, 3, 25, 57, 167, 245, 86, 227, 208, 81, 64, 90, 199, 140, 48, 212,\n 180, 70, 120, 9, 192, 245, 177, 65, 157, 9, 193, 100, 181, 125, 145, 62, 8,\n 108, 36, 140, 237, 242, 248, 161, 189, 41, 114, 65, 121, 97, 50, 238, 149,\n 44, 214, 124, 167, 40, 167, 127, 178, 231, 30, 173, 244, 193, 203, 204, 238,\n 139, 135, 71, 234, 234, 217, 66, 14, 129, 19, 97, 165, 112, 244, 35, 73,\n 192, 12, 149, 71, 33, 192, 32, 195, 208, 173, 233, 202, 25, 210, 145, 89,\n 87, 245, 246, 38, 3, 25, 57, 167, 245, 86, 227, 208, 81, 64, 90, 199, 140,\n 48, 212, 180, 70, 120, 9, 192, 245, 177, 65, 157, 9, 193, 100, 181, 125,\n 145, 62, 8, 108, 36, 140, 237, 242, 248, 161, 189, 41, 114, 65, 121, 97, 50,\n 238, 149, 44, 214, 124, 167, 40, 167, 127, 178, 231, 30, 173, 244, 193, 203,\n 204, 238, 139, 135, 71, 234, 234, 217, 66, 14, 129, 19, 97, 165, 112, 244,\n 35, 73, 192, 12, 149, 71, 33, 192, 32, 195, 208, 173, 233, 202, 25, 210,\n 145, 89, 87, 245, 246, 38, 3, 25, 57, 167, 245, 86, 227, 208, 81, 64, 90,\n 199, 140, 48, 212, 180, 70, 120, 9, 192, 245, 177, 65, 157, 9, 193, 100,\n 181, 125, 145, 62, 8, 108, 36, 140, 237, 242, 248\n ],\n \"G\": [\n 182, 46, 78, 199, 79, 89, 41, 208, 135, 20, 154, 35, 21, 43, 230, 113, 191,\n 147, 208, 37, 28, 27, 86, 203, 25, 209, 148, 187, 139, 111, 48, 102, 76,\n 110, 106, 130, 37, 160, 34, 222, 90, 165, 245, 222, 102, 47, 19, 130, 4,\n 232, 137, 211, 240, 11, 140, 21, 42, 22, 241, 61, 99, 115, 199, 166, 114,\n 190, 204, 60, 233, 66, 115, 230, 125, 103, 203, 125, 13, 176, 94, 131, 39,\n 198, 167, 124, 67, 175, 254, 1, 15, 198, 62, 237, 159, 31, 218, 58, 244, 47,\n 61, 67, 94, 46, 78, 199, 79, 89, 41, 208, 135, 20, 154, 35, 21, 43, 230,\n 113, 191, 147, 208, 37, 28, 27, 86, 203, 25, 209, 148, 187, 139, 111, 48,\n 102, 76, 110, 106, 130, 37, 160, 34, 222, 90, 165, 245, 222, 102, 47, 19,\n 130, 4, 232, 137, 211, 240, 11, 140, 21, 42, 22, 241, 61, 99, 115, 199, 166,\n 114, 190, 204, 60, 233, 66, 115, 230, 125, 103, 203, 125, 13, 176, 94, 131,\n 39, 198, 167, 124, 67, 175, 254, 1, 15, 198, 62, 237, 159, 31, 218, 58, 244,\n 47, 61, 67, 94, 46, 78, 199, 79, 89, 41, 208, 135, 20, 154, 35, 21, 43, 230,\n 113, 191, 147, 208, 37, 28, 27, 86, 203, 25, 209, 148, 187, 139, 111, 48,\n 102, 76, 110, 106, 130, 37, 160, 34, 222, 90, 165, 245, 222, 102, 47, 19,\n 130, 4, 232, 137, 211, 240, 11, 140, 21\n ],\n \"B\": [\n 191, 154, 43, 10, 207, 204, 164, 231, 136, 58, 239, 30, 147, 230, 101, 111,\n 150, 35, 128, 57, 252, 79, 173, 120, 25, 126, 81, 85, 8, 7, 122, 237, 190,\n 152, 246, 182, 130, 219, 67, 76, 167, 178, 235, 250, 28, 61, 186, 250, 199,\n 67, 58, 50, 86, 182, 108, 77, 89, 112, 59, 125, 226, 50, 205, 227, 125, 128,\n 104, 27, 59, 66, 53, 133, 159, 203, 97, 125, 139, 159, 158, 7, 215, 47, 140,\n 226, 223, 231, 44, 110, 184, 61, 233, 47, 67, 148, 22, 120, 173, 156, 117,\n 181, 94, 154, 43, 10, 207, 204, 164, 231, 136, 58, 239, 30, 147, 230, 101,\n 111, 150, 35, 128, 57, 252, 79, 173, 120, 25, 126, 81, 85, 8, 7, 122, 237,\n 190, 152, 246, 182, 130, 219, 67, 76, 167, 178, 235, 250, 28, 61, 186, 250,\n 199, 67, 58, 50, 86, 182, 108, 77, 89, 112, 59, 125, 226, 50, 205, 227, 125,\n 128, 104, 27, 59, 66, 53, 133, 159, 203, 97, 125, 139, 159, 158, 7, 215, 47,\n 140, 226, 223, 231, 44, 110, 184, 61, 233, 47, 67, 148, 22, 120, 173, 156,\n 117, 181, 94, 154, 43, 10, 207, 204, 164, 231, 136, 58, 239, 30, 147, 230,\n 101, 111, 150, 35, 128, 57, 252, 79, 173, 120, 25, 126, 81, 85, 8, 7, 122,\n 237, 190, 152, 246, 182, 130, 219, 67, 76, 167, 178, 235, 250, 28, 61, 186,\n 250, 199, 67, 58, 50, 86, 182, 108, 77\n ],\n \"A\": [\n 0, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64\n ],\n \"I\": [\n 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,\n 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39,\n 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58,\n 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77,\n 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96,\n 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112,\n 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127,\n 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142,\n 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157,\n 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172,\n 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187,\n 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202,\n 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217,\n 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232,\n 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247,\n 248, 249, 250, 251, 252, 253, 254, 255\n ]\n}\n","{\n \"R\": [0, 128, 255],\n \"G\": [0, 0, 0],\n \"B\": [0, 0, 0],\n \"A\": [0, 64, 128],\n \"I\": [0, 128, 255]\n}\n","{\n \"R\": [192, 224, 255],\n \"G\": [1, 128, 255],\n \"B\": [0, 0, 0],\n \"A\": [0, 64, 128],\n \"I\": [0, 128, 255]\n}\n","{\n \"R\": [3, 112, 144, 188, 236, 246, 255],\n \"G\": [5, 31, 29, 22, 76, 158, 250],\n \"B\": [26, 87, 91, 86, 62, 117, 235],\n \"A\": [0, 30, 38, 49, 67, 85, 107],\n \"I\": [0, 73, 92, 118, 160, 205, 255]\n}\n","{\n \"R\": [1, 240, 255],\n \"G\": [1, 128, 255],\n \"B\": [1, 128, 255],\n \"A\": [0, 76, 128],\n \"I\": [0, 153, 255]\n}\n","{\n \"R\": [0, 5, 18, 34, 53, 72, 90, 107, 126, 144, 161, 178, 194, 209, 222, 233, 242, 248, 251, 251, 249, 244, 241, 252],\n \"G\": [0, 4, 10, 11, 9, 11, 17, 23, 30, 36, 43, 50, 59, 70, 83, 98, 117, 136, 157, 177, 199, 220, 240, 254],\n \"B\": [3, 24, 50, 76, 96, 106, 109, 110, 108, 104, 97, 89, 79, 67, 55, 42, 26, 12, 6, 22, 47, 79, 121, 164],\n \"A\": [0, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64],\n \"I\": [0, 11, 22, 33, 44, 55, 66, 77, 88, 99, 110, 121, 133, 144, 155, 166, 177, 188, 199, 210, 221, 232, 243, 255]\n}\n","{\n \"R\": [48, 48, 64, 70, 65, 25, 132, 195, 244, 254, 218, 122],\n \"G\": [18, 18, 64, 107, 150, 226, 255, 241, 199, 158, 57, 4],\n \"B\": [59, 59, 162, 227, 255, 187, 81, 52, 58, 47, 7, 3],\n \"A\": [0, 22, 26, 30, 34, 43, 52, 57, 63, 67, 77, 86],\n \"I\": [0, 1, 16, 32, 49, 83, 118, 140, 164, 181, 219, 255]\n}\n","{\n \"R\": [0, 128, 255],\n \"G\": [0, 0, 0],\n \"B\": [0, 128, 255],\n \"A\": [0, 64, 128],\n \"I\": [0, 128, 255]\n}\n","{\n \"R\": [68, 49, 53, 253],\n \"G\": [1, 104, 183, 231],\n \"B\": [84, 142, 121, 37],\n \"A\": [0, 56, 80, 88],\n \"I\": [0, 64, 192, 255]\n}\n","{\n \"R\": [255, 255, 255],\n \"G\": [127, 196, 254],\n \"B\": [0, 0, 0],\n \"A\": [0, 64, 128],\n \"I\": [0, 128, 255]\n}\n","{\n \"R\": [0, 0, 0],\n \"G\": [0, 128, 255],\n \"B\": [255, 196, 128],\n \"A\": [0, 64, 128],\n \"I\": [0, 128, 255]\n}\n","{\n \"R\": [3, 64, 0, 0, 255, 255, 255],\n \"G\": [0, 0, 0, 255, 255, 192, 3],\n \"B\": [0, 32, 48, 56, 64, 96, 128],\n \"A\": [0, 8, 16, 24, 32, 52, 80],\n \"I\": [0, 32, 64, 96, 160, 192, 255]\n}\n","{\n \"min\": 0,\n \"max\": 0,\n \"R\": [0, 37],\n \"G\": [242, 0],\n \"B\": [255, 255],\n \"A\": [0, 64],\n \"I\": [0, 255]\n}\n","{\n \"R\": [255, 255],\n \"G\": [255, 11],\n \"B\": [0, 0],\n \"A\": [0, 64],\n \"I\": [0, 255]\n}\n","{\n \"min\": 0,\n \"max\": 0,\n \"R\": [\n 65, 10, 223, 120, 216, 207, 251, 93, 252, 217, 21, 253, 131, 165, 173, 143,\n 144, 217, 86, 89, 63, 254, 172, 1, 142, 107, 42, 78, 221, 8, 222, 71, 127,\n 126, 167, 33, 104, 2, 208, 216, 0, 251, 7, 105, 2, 101, 248, 190, 253, 62,\n 255, 224, 166, 37, 110, 138, 45, 34, 6, 37, 69, 201, 43, 122, 199, 37, 173,\n 194, 103, 211, 75, 159, 96, 4, 239, 98, 110, 193, 253, 166, 40, 255, 48,\n 130, 140, 243, 101, 9, 177, 220, 133, 32, 4, 81, 48, 48, 210, 109, 60, 132,\n 1, 119, 1, 159, 247, 33, 212, 187, 253, 144, 196, 254, 168, 79, 64, 196, 39,\n 97, 73, 173, 104, 216, 217, 43, 101, 119, 254, 5, 237, 103, 203, 122, 57,\n 87, 251, 164, 19, 75, 200, 2, 252, 150, 66, 0, 255, 157, 23, 254, 55, 16,\n 240, 161, 69, 253, 207, 195, 5, 60, 255, 251, 200, 217, 134, 123, 253, 180,\n 27, 246, 130, 136, 250, 232, 4, 125, 140, 22, 253, 255, 13, 180, 123, 61,\n 254, 111, 10, 185, 76, 192, 255, 223, 186, 61, 198, 5, 172, 13, 83, 172,\n 171, 6, 23, 73, 134, 133, 109, 61, 213, 55, 57, 132, 36, 209, 2, 144, 1,\n 253, 68, 155, 3, 160, 2, 77, 121, 70, 67, 176, 223, 131, 4, 162, 232, 255,\n 150, 94, 235, 191, 207, 10, 246, 0, 225, 4, 209, 116, 57, 112, 172, 253, 1,\n 6, 92, 227, 73, 62, 135, 223\n ],\n \"G\": [\n 162, 50, 112, 245, 4, 124, 176, 243, 56, 125, 183, 139, 182, 68, 189, 4,\n 225, 10, 20, 223, 7, 46, 31, 193, 225, 250, 219, 41, 2, 100, 83, 181, 34,\n 150, 72, 223, 168, 248, 80, 253, 7, 117, 214, 2, 248, 10, 3, 59, 179, 160,\n 90, 244, 146, 4, 207, 4, 125, 255, 96, 100, 1, 208, 130, 51, 116, 181, 236,\n 53, 244, 5, 165, 28, 181, 86, 96, 42, 254, 1, 134, 194, 214, 92, 41, 204,\n 137, 86, 207, 3, 143, 3, 100, 239, 164, 94, 65, 251, 131, 244, 173, 57, 143,\n 107, 217, 53, 210, 0, 148, 250, 45, 198, 81, 40, 61, 218, 252, 88, 171, 0,\n 248, 24, 248, 4, 41, 100, 187, 46, 208, 145, 43, 168, 121, 46, 175, 250,\n 125, 107, 233, 112, 207, 231, 174, 215, 53, 9, 43, 153, 52, 124, 128, 65,\n 210, 147, 255, 147, 63, 200, 148, 254, 0, 179, 241, 42, 239, 2, 230, 8, 82,\n 135, 87, 16, 3, 87, 86, 151, 254, 8, 255, 86, 53, 19, 7, 192, 171, 201, 253,\n 247, 197, 103, 251, 126, 0, 149, 54, 183, 61, 126, 79, 113, 10, 103, 184,\n 75, 11, 195, 222, 136, 149, 131, 8, 99, 240, 177, 252, 255, 198, 16, 7, 68,\n 178, 66, 191, 150, 73, 26, 211, 109, 78, 209, 240, 254, 1, 166, 247, 131, 2,\n 0, 167, 127, 133, 10, 43, 99, 235, 3, 214, 142, 176, 82, 132, 38, 10, 249,\n 255, 215, 44, 81\n ],\n \"B\": [\n 176, 182, 248, 37, 117, 35, 96, 69, 32, 152, 108, 20, 237, 250, 2, 89, 141,\n 216, 111, 251, 211, 149, 10, 44, 214, 103, 31, 251, 3, 32, 252, 246, 97, 1,\n 219, 167, 197, 4, 36, 116, 206, 118, 106, 43, 205, 204, 114, 69, 127, 205,\n 87, 80, 41, 251, 145, 204, 253, 161, 247, 1, 149, 9, 43, 253, 97, 72, 136,\n 161, 171, 181, 26, 255, 108, 80, 218, 214, 231, 255, 84, 31, 109, 4, 218, 3,\n 217, 36, 68, 85, 241, 39, 221, 2, 240, 2, 173, 42, 206, 5, 110, 46, 103, 27,\n 212, 184, 2, 207, 246, 45, 116, 72, 110, 253, 38, 105, 248, 159, 243, 81,\n 192, 93, 141, 145, 24, 157, 234, 131, 57, 178, 62, 75, 65, 176, 148, 40,\n 253, 66, 76, 240, 51, 154, 17, 251, 139, 253, 207, 9, 114, 49, 200, 254, 96,\n 73, 138, 118, 204, 102, 137, 89, 145, 161, 4, 112, 66, 234, 147, 178, 212,\n 205, 185, 11, 203, 131, 2, 250, 118, 169, 1, 185, 154, 53, 171, 197, 61,\n 175, 249, 96, 15, 254, 95, 5, 222, 75, 246, 194, 2, 61, 180, 25, 133, 165,\n 15, 233, 59, 35, 221, 140, 109, 7, 114, 255, 198, 0, 115, 168, 252, 23, 242,\n 80, 75, 142, 137, 255, 12, 182, 68, 201, 4, 111, 37, 228, 83, 248, 24, 192,\n 249, 5, 54, 223, 160, 122, 160, 114, 145, 119, 252, 31, 253, 250, 10, 214,\n 8, 47, 0, 142, 222, 70\n ],\n \"A\": [\n 0, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64\n ],\n \"I\": [\n 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,\n 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39,\n 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58,\n 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77,\n 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96,\n 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112,\n 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127,\n 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142,\n 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157,\n 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172,\n 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187,\n 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202,\n 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217,\n 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232,\n 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247,\n 248, 249, 250, 251, 252, 253, 254, 255\n ]\n}\n","import * as cmaps from '@/cmaps'\nimport { log } from '@/logger'\n\nexport enum COLORMAP_TYPE {\n MIN_TO_MAX = 0,\n ZERO_TO_MAX_TRANSPARENT_BELOW_MIN = 1,\n ZERO_TO_MAX_TRANSLUCENT_BELOW_MIN = 2\n}\n\nexport type ColorMap = {\n R: number[]\n G: number[]\n B: number[]\n A: number[]\n I: number[]\n\n min?: number\n max?: number\n labels?: string[]\n}\n\nexport type LUT = {\n lut: Uint8ClampedArray\n min?: number\n max?: number\n labels?: string[]\n}\n\nexport class ColorTables {\n gamma = 1.0\n version = 0.1\n cluts: Record<string, ColorMap> = {}\n\n /**\n * Sets cluts to alphabetically sorted cmaps\n */\n constructor() {\n const cmapKeys = Object.keys(cmaps) as Array<keyof typeof cmaps>\n\n const cmapsSorted = cmapKeys\n .filter((k) => !k.startsWith('$')) // ignore drawing maps\n .sort(new Intl.Collator('en').compare) // case insensitive, e.g. \"ROI_i256\" > \"actc\"\n\n for (const key of cmapsSorted) {\n this.cluts[key] = cmaps[key]\n }\n }\n\n addColormap(key: string, cmap: ColorMap): void {\n this.cluts[key] = cmap\n }\n\n colormaps(): Array<keyof typeof this.cluts> {\n return Object.keys(this.cluts) as Array<keyof typeof this.cluts>\n }\n\n // for backward compatibility: prior to v0.34 \"colormaps\" used to be \"colorMaps\"\n colorMaps(): Array<keyof typeof this.cluts> {\n return this.colormaps()\n }\n\n // returns key name if it exists, otherwise returns default \"gray\"\n colormapFromKey(name: string): ColorMap {\n let cmap = this.cluts[name]\n if (cmap !== undefined) {\n return cmap\n }\n\n cmap = this.cluts[name.toLowerCase()]\n if (cmap !== undefined) {\n return cmap\n }\n\n if (name.length > 0) {\n log.warn('No color map named ' + name)\n }\n return {\n min: 0,\n max: 0,\n R: [0, 255],\n G: [0, 255],\n B: [0, 255],\n A: [0, 255],\n I: [0, 255]\n }\n }\n\n // not included in public docs\n colormap(key = '', isInvert = false): Uint8ClampedArray {\n const cmap = this.colormapFromKey(key)\n return this.makeLut(cmap.R, cmap.G, cmap.B, cmap.A, cmap.I, isInvert)\n }\n\n makeLabelLut(cm: ColorMap, alphaFill = 255, maxIdx = Infinity): LUT {\n if (cm.R === undefined || cm.G === undefined || cm.B === undefined) {\n throw new Error(`Invalid colormap table: ${cm}`)\n }\n const nLabels = cm.R.length\n\n // if indices are not provided, indices default to 0..(nLabels-1)\n const idxs = cm.I ?? [...Array(nLabels).keys()]\n\n // --- Check for out-of-range values: issue 1441\n let hasInvalid = false\n for (let i = 0; i < idxs.length; i++) {\n if (idxs[i] > maxIdx) {\n hasInvalid = true\n idxs[i] = maxIdx // clamp\n }\n }\n if (hasInvalid) {\n log.warn(`Some colormap indices clamped to match label range.`)\n }\n\n if (nLabels !== cm.G.length || nLabels !== cm.B.length || nLabels !== idxs.length) {\n throw new Error(`colormap does not make sense: ${cm} Rs ${cm.R.length} Gs ${cm.G.length} Bs ${cm.B.length} Is ${idxs.length}`)\n }\n\n let As = new Uint8ClampedArray(nLabels).fill(alphaFill)\n As[0] = 0\n if (cm.A !== undefined) {\n As = Uint8ClampedArray.from(cm.A)\n }\n\n const mnIdx = Math.min(...idxs)\n const mxIdx = Math.max(...idxs)\n\n // n.b. number of input labels can be sparse: I:[0,3,4] output is dense [0,1,2,3,4]\n const nLabelsDense = mxIdx - mnIdx + 1\n const lut = new Uint8ClampedArray(nLabelsDense * 4).fill(0)\n for (let i = 0; i < nLabels; i++) {\n let k = (idxs[i] - mnIdx) * 4\n lut[k++] = cm.R[i] // Red\n lut[k++] = cm.G[i] // Green\n lut[k++] = cm.B[i] // Blue\n lut[k++] = As[i] // Alpha\n }\n\n const cmap: LUT = {\n lut,\n min: mnIdx,\n max: mxIdx\n }\n // labels are optional\n if (cm.labels) {\n const nL = cm.labels.length\n if (nL === nLabelsDense) {\n cmap.labels = cm.labels\n } else if (nL === nLabels) {\n cmap.labels = Array(nLabelsDense).fill('?')\n for (let i = 0; i < nLabels; i++) {\n const idx = idxs[i]\n cmap.labels[idx] = cm.labels[i]\n }\n }\n }\n return cmap\n }\n\n async makeLabelLutFromUrl(name: string, alphaFill = 255, maxIdx = Infinity): Promise<LUT> {\n const response = await fetch(name)\n const cm = await response.json()\n return this.makeLabelLut(cm, alphaFill, maxIdx)\n }\n\n // not included in public docs\n // The drawing colormap is a variant of the label colormap with precisely 256 colors\n makeDrawLut(name: string | ColorMap): LUT {\n let cmap: ColorMap = typeof name === 'object' ? name : cmaps[name as keyof typeof cmaps]\n\n if (cmap === undefined) {\n log.warn('colormap undefined ', name)\n cmap = this.colormapFromKey('')\n }\n\n const cm = this.makeLabelLut(cmap, 255)\n\n if (cm.labels === undefined) {\n cm.labels = []\n }\n if (cm.labels.length < 256) {\n const j = cm.labels.length\n for (let i = j; i < 256; i++) {\n // make all unused slots opaque red\n cm.labels.push(i.toString())\n }\n }\n\n const lut = new Uint8ClampedArray(256 * 4)\n let k = 0\n for (let i = 0; i < 256; i++) {\n lut[k++] = 255 // Red\n lut[k++] = 0 // Green\n lut[k++] = 0 // Blue\n lut[k++] = 255 // Alpha\n }\n lut[3] = 0 // make first alpha transparent: not part of drawing\n\n // drawings can have no more than 256 colors\n const explicitLUTbytes = Math.min(cm.lut.length, 256 * 4)\n if (explicitLUTbytes > 0) {\n for (let i = 0; i < explicitLUTbytes; i++) {\n lut[i] = cm.lut[i]\n }\n }\n\n return {\n lut,\n labels: cm.labels\n }\n }\n\n // not included in public docs\n makeLut(Rsi: number[], Gsi: number[], Bsi: number[], Asi: number[], Isi: number[], isInvert: boolean): Uint8ClampedArray {\n // create color lookup table provided arrays of reds, greens, blues, alphas and intensity indices\n // intensity indices should be in increasing order with the first value 0 and the last 255.\n // this.makeLut([0, 255], [0, 0], [0,0], [0,128],[0,255]); //red gradient\n const nIdx = Rsi.length\n const Rs = [...Rsi]\n const Gs = [...Gsi]\n const Bs = [...Bsi]\n if (!Isi) {\n Isi = new Array(nIdx)\n for (let i = 0; i < nIdx; i++) {\n Isi[i] = (i / (nIdx - 1)) * 255\n }\n }\n if (!Asi) {\n Asi = new Array(nIdx).fill(64)\n Asi[0] = 0\n }\n let As = Uint8ClampedArray.from(Asi)\n let Is = Uint8ClampedArray.from(Isi)\n if (isInvert) {\n for (let i = 0; i < nIdx; i++) {\n Rs[i] = Rsi[nIdx - 1 - i]\n Gs[i] = Gsi[nIdx - 1 - i]\n Bs[i] = Bsi[nIdx - 1 - i]\n As[i] = 255 - Asi[nIdx - 1 - i]\n Is[i] = 255 - Isi[nIdx - 1 - i]\n }\n }\n const lut = new Uint8ClampedArray(256 * 4)\n if (typeof Is === 'undefined') {\n Is = new Uint8ClampedArray(nIdx).fill(0)\n for (let i = 0; i < nIdx; i++) {\n Is[i] = Math.round((i * 255.0) / (nIdx - 1))\n }\n }\n if (typeof As === 'undefined') {\n As = new Uint8ClampedArray(nIdx).fill(64)\n As[0] = 0\n }\n for (let i = 0; i < nIdx - 1; i++) {\n const idxLo = Is[i]\n let idxHi = Is[i + 1]\n if (i === 0 && idxLo !== 0) {\n log.warn('colormap issue: indices expected to start with 0 not ', idxLo)\n }\n if (i === Is.length - 2 && idxHi !== 255) {\n log.warn('padding colormap: indices expected end with 255 not ', idxHi)\n idxHi = 255\n }\n const idxRng = idxHi - idxLo\n let k = idxLo * 4\n for (let j = idxLo; j <= idxHi; j++) {\n const f = (j - idxLo) / idxRng\n lut[k++] = Rs[i] + f * (Rs[i + 1] - Rs[i]) // Red\n lut[k++] = Gs[i] + f * (Gs[i + 1] - Gs[i]) // Green\n lut[k++] = Bs[i] + f * (Bs[i + 1] - Bs[i]) // Blue\n lut[k++] = As[i] + f * (As[i + 1] - As[i]) // Alpha\n }\n }\n if (this.gamma === 1.0) {\n return lut\n }\n for (let i = 0; i < 255 * 4; i++) {\n if (i % 4 === 3) {\n continue\n } // gamma changes RGB, not Alpha\n lut[i] = Math.pow(lut[i] / 255, 1 / this.gamma) * 255\n }\n return lut\n }\n}\n\nexport const cmapper = new ColorTables()\n","/**\n * ImageFactory Module\n *\n * Pure utility functions for loading and creating NVImage instances.\n * This module contains helper functions for:\n * - Fetching data from URLs and files\n * - Stream decompression and partial loading\n * - Format detection and filename parsing\n * - Data conversion utilities\n *\n * These functions are intentionally pure and do not depend on the NVImage class,\n * avoiding circular dependencies.\n */\n\nimport { Gunzip } from 'fflate'\nimport { readHeaderAsync } from 'nifti-reader-js'\nimport { uncompressStream } from '@/nvimage/utils'\n\n/**\n * Fetch DICOM manifest and return array of file data\n * @param url - URL to DICOM manifest or folder\n * @param headers - HTTP headers for requests\n * @returns Array of {name, data} objects for each DICOM file\n */\nexport async function fetchDicomData(url: string, headers: Record<string, string> = {}): Promise<Array<{ name: string; data: ArrayBuffer }>> {\n if (url === '') {\n throw Error('url must not be empty')\n }\n\n // https://stackoverflow.com/questions/10687099/how-to-test-if-a-url-string-is-absolute-or-relative\n const absoluteUrlRE = /^(?:[a-z+]+:)?\\/\\//i\n\n let manifestUrl = absoluteUrlRE.test(url) ? url : new URL(url, window.location.href)\n const extensionRE = /(?:.([^.]+))?$/\n const extension = extensionRE.exec((manifestUrl as URL).pathname)\n if (!extension) {\n manifestUrl = new URL('niivue-manifest.txt', url)\n }\n\n let response = await fetch(manifestUrl, { headers })\n if (!response.ok) {\n throw Error(response.statusText)\n }\n const text = await response.text()\n const lines = text.split('\\n')\n\n const baseUrlRE = /(.*\\/).*/\n const folderUrl = baseUrlRE.exec(manifestUrl as string)![0]\n const dataBuffer = []\n for (const line of lines) {\n const fileUrl = new URL(line, folderUrl)\n response = await fetch(fileUrl, { headers })\n if (!response.ok) {\n throw Error(response.statusText)\n }\n const contents = await response.arrayBuffer()\n dataBuffer.push({ name: line, data: contents })\n }\n return dataBuffer\n}\n\n/**\n * Read and decompress first N bytes from a gzipped stream\n * @param stream - ReadableStream of compressed data\n * @param minBytes - Minimum bytes to read (decompressed)\n * @returns Decompressed bytes\n */\nexport async function readFirstDecompressedBytes(stream: ReadableStream<Uint8Array>, minBytes: number): Promise<Uint8Array> {\n const reader: ReadableStreamDefaultReader<Uint8Array> = stream.getReader()\n const gunzip = new Gunzip()\n\n const decompressedChunks: Uint8Array[] = []\n let totalDecompressed = 0\n let doneReading = false\n\n let resolveFn: (value: Uint8Array) => void\n let rejectFn: (reason?: any) => void\n\n const promise = new Promise<Uint8Array>((resolve, reject): undefined => {\n resolveFn = resolve\n rejectFn = reject\n return undefined\n })\n\n function finalize(): void {\n // Combine chunks into a single Uint8Array\n const result = new Uint8Array(totalDecompressed)\n let offset = 0\n for (const chunk of decompressedChunks) {\n result.set(chunk, offset)\n offset += chunk.length\n }\n resolveFn(result)\n }\n\n gunzip.ondata = (chunk: Uint8Array): void => {\n decompressedChunks.push(chunk)\n totalDecompressed += chunk.length\n if (totalDecompressed >= minBytes) {\n doneReading = true\n reader.cancel().catch(() => {})\n finalize()\n }\n }\n ;(async (): Promise<void> => {\n try {\n while (!doneReading) {\n const { done, value } = await reader.read()\n if (done) {\n doneReading = true\n gunzip.push(new Uint8Array(), true) // Signal end-of-stream\n return\n }\n gunzip.push(value, false) // Push data into fflate\n }\n } catch (err) {\n rejectFn(err)\n }\n })().catch(() => {})\n\n return promise\n}\n\n/**\n * Extract filename from URL query parameters or pathname\n * @param url - URL string to parse\n * @returns Extracted filename or null\n */\nexport function extractFilenameFromUrl(url: string): string | null {\n const params = new URL(url).searchParams\n const contentDisposition = params.get('response-content-disposition')\n if (contentDisposition) {\n const match = contentDisposition.match(/filename\\*?=(?:UTF-8'')?\"?([^\";]+)\"?/)\n if (match) {\n return decodeURIComponent(match[1])\n }\n }\n // Fallback: extract from pathname if possible\n return url.split('/').pop()!.split('?')[0]\n}\n\n/**\n * Load partial 4D volumes from gzipped NIfTI file\n * @param url - URL to gzipped NIfTI file\n * @param headers - HTTP headers\n * @param limitFrames4D - Maximum number of frames to load\n * @returns ArrayBuffer with partial data or null if full load needed\n */\nexport async function loadInitialVolumesGz(url = '', headers = {}, limitFrames4D = NaN): Promise<ArrayBuffer | null> {\n if (isNaN(limitFrames4D)) {\n return null\n }\n const response = await fetch(url, { headers, cache: 'force-cache' })\n let hdrBytes = 352\n let hdrU8s = await readFirstDecompressedBytes(response.body!, hdrBytes)\n const hdrView = new DataView(hdrU8s.buffer, hdrU8s.byteOffset, hdrU8s.byteLength)\n const u16 = hdrView.getUint16(0, true)\n const isNIfTI1 = u16 === 348\n const isNIfTI1be = u16 === 23553\n if (!isNIfTI1 && !isNIfTI1be) {\n return null\n }\n // start of edge cases: huge header extensions with small gz block size\n if (hdrU8s.length > 111) {\n hdrBytes = hdrView.getFloat32(108, isNIfTI1)\n }\n if (hdrBytes > hdrU8s.length) {\n hdrU8s = await readFirstDecompressedBytes(response.body!, hdrBytes)\n }\n // end of edge case\n const isNifti1 = (hdrU8s[0] === 92 && hdrU8s[1] === 1) || (hdrU8s[1] === 92 && hdrU8s[0] === 1)\n if (!isNifti1) {\n return null\n }\n const hdr = await readHeaderAsync(hdrU8s.buffer as ArrayBuffer)\n if (!hdr) {\n throw new Error('Could not read NIfTI header')\n }\n // Calculate required data size\n const nBytesPerVoxel = hdr.numBitsPerVoxel / 8\n const nVox3D = [1, 2, 3].reduce((acc, i) => acc * (hdr.dims[i] > 1 ? hdr.dims[i] : 1), 1)\n const nFrame4D = [4, 5, 6].reduce((acc, i) => acc * (hdr.dims[i] > 1 ? hdr.dims[i] : 1), 1)\n const volsToLoad = Math.max(Math.min(limitFrames4D, nFrame4D), 1)\n const bytesToLoad = hdr.vox_offset + volsToLoad * nVox3D * nBytesPerVoxel\n if (volsToLoad === nFrame4D) {\n // read entire file: compression streams is faster than fflate\n return null\n }\n const responseImg = await fetch(url, { headers, cache: 'force-cache' })\n const dataBytes = await readFirstDecompressedBytes(responseImg.body!, bytesToLoad)\n return dataBytes.buffer.slice(0, bytesToLoad) as ArrayBuffer\n}\n\n/**\n * Load partial 4D volumes from uncompressed NIfTI file\n * @param url - URL to NIfTI file\n * @param headers - HTTP headers\n * @param limitFrames4D - Maximum number of frames to load\n * @returns ArrayBuffer with partial data or null if full load needed\n */\nexport async function loadInitialVolumes(url = '', headers = {}, limitFrames4D = NaN): Promise<ArrayBuffer | null> {\n if (isNaN(limitFrames4D)) {\n return null\n }\n const response = await fetch(url, { headers, cache: 'force-cache' })\n const reader = response.body!.getReader()\n const { value, done } = await reader.read()\n let hdrU8s = value!\n if (done || !hdrU8s || hdrU8s.length < 2) {\n throw new Error('Not enough data to determine compression')\n }\n const hdrView = new DataView(hdrU8s.buffer, hdrU8s.byteOffset, hdrU8s.byteLength)\n const u16 = hdrView.getUint16(0, true)\n const isGz = u16 === 35615\n if (isGz) {\n await reader.cancel() // Stop streaming and release the lock\n return loadInitialVolumesGz(url, headers, limitFrames4D)\n }\n const isNIfTI1 = u16 === 348\n const isNIfTI1be = u16 === 23553\n if (!isNIfTI1 && !isNIfTI1be) {\n await reader.cancel()\n return null\n }\n // start of edge cases: huge header extensions with degraded packet size\n let hdrBytes = 352\n if (hdrU8s.length > 111) {\n hdrBytes = hdrView.getFloat32(108, isNIfTI1)\n }\n while (hdrU8s.length < hdrBytes) {\n const { value, done } = await reader.read()\n if (done || !value) {\n break\n }\n function concatU8s(arr1: Uint8Array, arr2: Uint8Array): Uint8Array {\n const result = new Uint8Array(arr1.length + arr2.length)\n result.set(arr1, 0)\n result.set(arr2, arr1.length)\n return result\n }\n hdrU8s = concatU8s(hdrU8s, value)\n }\n // end of edge cases\n const hdr = await readHeaderAsync(hdrU8s.buffer as ArrayBuffer)\n if (!hdr) {\n throw new Error('Could not read NIfTI header')\n }\n // Calculate required data size\n const nBytesPerVoxel = hdr.numBitsPerVoxel / 8\n const nVox3D = [1, 2, 3].reduce((acc, i) => acc * (hdr.dims[i] > 1 ? hdr.dims[i] : 1), 1)\n const nFrame4D = [4, 5, 6].reduce((acc, i) => acc * (hdr.dims[i] > 1 ? hdr.dims[i] : 1), 1)\n const volsToLoad = Math.max(Math.min(limitFrames4D, nFrame4D), 1)\n const bytesToLoad = hdr.vox_offset + volsToLoad * nVox3D * nBytesPerVoxel\n const imgU8s = new Uint8Array(bytesToLoad)\n // Ensure we don't copy more than needed from hdrU8s\n const hdrCopyLength = Math.min(hdrU8s.length, bytesToLoad)\n imgU8s.set(hdrU8s.subarray(0, hdrCopyLength), 0)\n let bytesRead = hdrCopyLength\n while (bytesRead < bytesToLoad) {\n const { value, done } = await reader.read()\n if (done || !value) {\n await reader.cancel()\n return null\n }\n // Ensure we only copy up to bytesToLoad\n const remaining = Math.min(value.length, bytesToLoad - bytesRead)\n imgU8s.set(value.subarray(0, remaining), bytesRead)\n bytesRead += remaining\n }\n await reader.cancel()\n return imgU8s.buffer\n}\n\n/**\n * Read File object with streaming decompression\n * @param file - Browser File object\n * @param bytesToLoad - Optional limit on bytes to read\n * @returns ArrayBuffer with file data\n */\nexport async function readFileAsync(file: File, bytesToLoad = NaN): Promise<ArrayBuffer> {\n let stream = file.stream()\n\n if (!isNaN(bytesToLoad)) {\n let bytesRead = 0\n const limiter = new TransformStream({\n transform(chunk: Uint8Array, controller: TransformStreamDefaultController): void {\n if (bytesRead >= bytesToLoad) {\n controller.terminate()\n return\n }\n const remainingBytes = bytesToLoad - bytesRead\n if (chunk.length > remainingBytes) {\n controller.enqueue(chunk.slice(0, remainingBytes))\n controller.terminate()\n } else {\n controller.enqueue(chunk)\n }\n bytesRead += chunk.length\n }\n })\n stream = stream.pipeThrough(limiter)\n }\n\n const uncompressedStream = await uncompressStream(stream)\n\n const chunks: Uint8Array[] = []\n const reader = uncompressedStream.getReader()\n\n while (true) {\n const { done, value } = await reader.read()\n if (done) {\n break\n }\n chunks.push(value)\n }\n\n const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0)\n const result = new ArrayBuffer(totalLength)\n const resultView = new Uint8Array(result)\n\n let offset = 0\n for (const chunk of chunks) {\n resultView.set(chunk, offset)\n offset += chunk.length\n }\n\n return result\n}\n\n/**\n * Convert base64 string to ArrayBuffer\n * @param base64 - Base64 encoded string\n * @returns Decoded ArrayBuffer\n */\nexport function base64ToArrayBuffer(base64: string): ArrayBuffer {\n const binary_string = window.atob(base64)\n const len = binary_string.length\n const bytes = new Uint8Array(len)\n for (let i = 0; i < len; i++) {\n bytes[i] = binary_string.charCodeAt(i)\n }\n return bytes.buffer\n}\n\n/**\n * Get primary file extension (without compression suffix)\n * @param filename - Filename to parse\n * @returns Primary extension (e.g., \"nii\" from \"file.nii.gz\")\n */\nexport function getPrimaryExtension(filename: string): string {\n // .nii.gz -> .nii\n const match = filename.match(/\\.([^.]+)(?:\\.gz|\\.bz2|\\.xz)?$/)\n return match ? match[1] : ''\n}\n","/**\n * ZarrProcessor module\n *\n * Handles Zarr format-specific loading logic:\n * - Parses Zarr URL parameters for slice selection\n * - Fetches Zarr array data from remote stores\n * - Handles 3D and 4D array slicing\n * - Validates slice indices against array dimensions\n *\n * Zarr is a cloud-optimized format for chunked, compressed N-dimensional arrays.\n */\n\nimport * as zarr from 'zarrita'\n\n/**\n * Result of Zarr data loading, containing the raw data buffer\n * and metadata about the array structure.\n */\nexport interface ZarrLoadResult {\n dataBuffer: ArrayBuffer\n zarrData: {\n data: ArrayBuffer\n width: number\n height: number\n depth: number\n channels?: number\n }\n}\n\n/**\n * Load Zarr array data from a URL with optional slice selection.\n * Supports query parameters ?x=N, ?y=N, ?z=N for slice selection.\n *\n * @param url - Zarr URL (may include query parameters for slicing)\n * @returns Load result with data buffer and metadata\n *\n * @example\n * ```typescript\n * // Load full array\n * const result = await loadZarrData('https://example.com/data.zarr')\n *\n * // Load specific slice\n * const result = await loadZarrData('https://example.com/data.zarr?z=50&y=100&x=100')\n * ```\n */\nexport async function loadZarrData(url: string): Promise<ZarrLoadResult> {\n // Get the z, x, y slice indices from the query string\n const urlParams = new URL(url).searchParams\n const zIndex = urlParams.get('z')\n const yIndex = urlParams.get('y')\n const xIndex = urlParams.get('x')\n\n const zRange = zIndex ? zarr.slice(parseInt(zIndex), parseInt(zIndex) + 1) : null\n const yRange = yIndex ? zarr.slice(parseInt(yIndex), parseInt(yIndex) + 1) : null\n const xRange = xIndex ? zarr.slice(parseInt(xIndex), parseInt(xIndex) + 1) : null\n\n // Remove the query string from the original URL\n const zarrUrl = url.split('?')[0]\n\n // If multiscale, must provide the full path to the zarr array data\n const store = new zarr.FetchStore(zarrUrl)\n const root = zarr.root(store)\n\n let arr\n try {\n // Try to resolve the full URL path first\n arr = await zarr.open(root.resolve(url), { kind: 'array' })\n } catch (e) {\n // Fall back to opening the root directly\n arr = await zarr.open(root, { kind: 'array' })\n }\n\n let view\n if (arr.shape.length === 4) {\n // 4D array (e.g., RGB/RGBA volume data)\n const cRange = null // Load all channels\n\n // Make sure we are not exceeding the array shape. If so, clamp to max\n const zDim = arr.shape[2]\n const yDim = arr.shape[1]\n const xDim = arr.shape[0]\n\n if (zRange && zRange[0] >= zDim) {\n zRange[0] = zDim - 1\n }\n if (yRange && yRange[0] >= yDim) {\n yRange[0] = yDim - 1\n }\n if (xRange && xRange[0] >= xDim) {\n xRange[0] = xDim - 1\n }\n\n view = await zarr.get(arr, [xRange, yRange, zRange, cRange])\n } else {\n // 3D array\n view = await zarr.get(arr, [xRange, yRange, zRange])\n }\n\n const dataBuffer = view.data as ArrayBuffer\n const [height, width, zDim, cDim] = view.shape\n\n const zarrData = {\n data: dataBuffer,\n width,\n height,\n depth: zDim,\n channels: cDim\n }\n\n return {\n dataBuffer,\n zarrData\n }\n}\n","/**\n * NVZarrHelper - Simplified zarr chunk management for NVImage.\n *\n * Attaches to a host NVImage and manages chunked loading of OME-Zarr data.\n * No zoom, no prefetching - just pan and level switching.\n * All coordinates are in current-level pixel space.\n *\n * Spatial dimensions are kept in OME metadata order throughout.\n * The mapping to NIfTI layout is:\n * - OME dim[0] (slowest in C-order) → NIfTI dim 3 (depth, slowest in Fortran-order)\n * - OME dim[1] → NIfTI dim 2 (height)\n * - OME dim[2] (fastest in C-order) → NIfTI dim 1 (width, fastest in Fortran-order)\n * This means chunk data can be copied directly without stride remapping.\n * The affine matrix maps NIfTI (i, j, k) indices to physical (x, y, z) space\n * using the OME axis names.\n */\n\nimport { NIFTI1 } from 'nifti-reader-js'\n\nimport { v4 as uuidv4 } from '@lukeed/uuid'\nimport { copyAffine } from '../affineUtils'\nimport { ZarrChunkClient, type ZarrPyramidInfo, type ChunkCoord } from './ZarrChunkClient.js'\nimport { ZarrChunkCache, type TypedArray } from './ZarrChunkCache.js'\nimport { NiiDataType } from '@/nvimage/utils.js'\nimport type { NVImage, TypedVoxelArray } from '@/nvimage/index.js'\n\nexport interface NVZarrHelperOptions {\n url: string\n level: number\n maxVolumeSize?: number\n maxTextureSize?: number\n channel?: number\n cacheSize?: number\n /** Convert OME spatial units to millimeters for NIfTI compatibility (default: true) */\n convertUnitsToMm?: boolean\n}\n\n/**\n * Map zarr dtype string to NIfTI datatype code\n */\nfunction zarrDtypeToNifti(dtype: string): number {\n const normalized = dtype.toLowerCase().replace(/[<>|]/g, '')\n if (normalized === 'u1' || normalized === 'uint8') {\n return NiiDataType.DT_UINT8\n }\n if (normalized === 'i1' || normalized === 'int8') {\n return NiiDataType.DT_INT8\n }\n if (normalized === 'u2' || normalized === 'uint16') {\n return NiiDataType.DT_UINT16\n }\n if (normalized === 'i2' || normalized === 'int16') {\n return NiiDataType.DT_INT16\n }\n if (normalized === 'u4' || normalized === 'uint32') {\n return NiiDataType.DT_UINT32\n }\n if (normalized === 'i4' || normalized === 'int32') {\n return NiiDataType.DT_INT32\n }\n if (normalized === 'f4' || normalized === 'float32') {\n return NiiDataType.DT_FLOAT32\n }\n if (normalized === 'f8' || normalized === 'float64') {\n return NiiDataType.DT_FLOAT64\n }\n console.warn(`Unknown zarr dtype: ${dtype}, defaulting to uint8`)\n return NiiDataType.DT_UINT8\n}\n\nfunction getBytesPerVoxel(datatypeCode: number): number {\n switch (datatypeCode) {\n case NiiDataType.DT_UINT8:\n case NiiDataType.DT_INT8:\n return 1\n case NiiDataType.DT_UINT16:\n case NiiDataType.DT_INT16:\n return 2\n case NiiDataType.DT_UINT32:\n case NiiDataType.DT_INT32:\n case NiiDataType.DT_FLOAT32:\n return 4\n case NiiDataType.DT_FLOAT64:\n return 8\n default:\n return 1\n }\n}\n\nfunction createTypedVoxelArray(datatypeCode: number, size: number): TypedVoxelArray {\n switch (datatypeCode) {\n case NiiDataType.DT_UINT8:\n case NiiDataType.DT_INT8:\n return new Uint8Array(size)\n case NiiDataType.DT_UINT16:\n return new Uint16Array(size)\n case NiiDataType.DT_INT16:\n case NiiDataType.DT_INT32:\n return new Int16Array(size)\n case NiiDataType.DT_UINT32:\n case NiiDataType.DT_FLOAT32:\n return new Float32Array(size)\n case NiiDataType.DT_FLOAT64:\n return new Float64Array(size)\n default:\n return new Uint8Array(size)\n }\n}\n\n/**\n * Convert a value from an OME spatial unit to millimeters.\n * Returns the value unchanged if the unit is unrecognized or absent.\n */\nfunction omeUnitToMm(value: number, unit?: string): number {\n if (!unit) {\n return value\n }\n switch (unit.toLowerCase()) {\n case 'micrometer':\n case 'um':\n case 'µm':\n return value / 1000\n case 'nanometer':\n case 'nm':\n return value / 1_000_000\n case 'millimeter':\n case 'mm':\n return value\n case 'centimeter':\n case 'cm':\n return value * 10\n case 'meter':\n case 'm':\n return value * 1000\n default:\n return value\n }\n}\n\nexport class NVZarrHelper {\n private hostImage: NVImage\n private chunkClient: ZarrChunkClient\n private chunkCache: ZarrChunkCache\n private pyramidInfo: ZarrPyramidInfo\n private datatypeCode: number\n\n private pyramidLevel: number\n /** Level dimensions in OME metadata order: depth=dim[0], height=dim[1], width=dim[2] */\n private levelDims: { width: number; height: number; depth: number }\n private volumeDims: { width: number; height: number; depth: number }\n private chunkSize: { width: number; height: number; depth: number }\n\n /** Voxel scales in OME metadata order: depth=dim[0], height=dim[1], width=dim[2] */\n private voxelScales: { width: number; height: number; depth: number }\n /** Voxel translations in OME metadata order */\n private voxelTranslations: { width: number; height: number; depth: number }\n private hasTranslations: boolean\n private convertUnitsToMm: boolean\n\n private centerX: number\n private centerY: number\n private centerZ: number\n\n private channel: number\n private nonSpatialCoords: Record<string, number> = {}\n\n private isUpdating = false\n private needsUpdate = false\n private currentAbortController: AbortController | null = null\n private runningMin = Infinity\n private runningMax = -Infinity\n\n // Debounce state for batching chunk updates\n private updateDebounceTimer: ReturnType<typeof setTimeout> | null = null\n private readonly UPDATE_DEBOUNCE_MS = 50 // Batch chunks arriving within 50ms\n private pendingChunkCount = 0\n private lastRenderedChunkCount = 0\n\n centerAtDragStart: { x: number; y: number; z: number } | null = null\n\n onChunksUpdated?: () => void\n onAllChunksLoaded?: () => void\n\n private constructor(hostImage: NVImage) {\n this.hostImage = hostImage\n this.chunkClient = null as unknown as ZarrChunkClient\n this.chunkCache = null as unknown as ZarrChunkCache\n this.pyramidInfo = null as unknown as ZarrPyramidInfo\n this.datatypeCode = NiiDataType.DT_UINT8\n this.pyramidLevel = 0\n this.levelDims = { width: 0, height: 0, depth: 0 }\n this.volumeDims = { width: 0, height: 0, depth: 0 }\n this.chunkSize = { width: 0, height: 0, depth: 0 }\n this.voxelScales = { width: 1, height: 1, depth: 1 }\n this.voxelTranslations = { width: 0, height: 0, depth: 0 }\n this.hasTranslations = false\n this.convertUnitsToMm = true\n this.centerX = 0\n this.centerY = 0\n this.centerZ = 0\n this.channel = 0\n }\n\n static async create(hostImage: NVImage, url: string, options: NVZarrHelperOptions): Promise<NVZarrHelper> {\n const helper = new NVZarrHelper(hostImage)\n\n helper.chunkClient = new ZarrChunkClient({ baseUrl: url })\n helper.pyramidInfo = await helper.chunkClient.fetchInfo()\n helper.chunkCache = new ZarrChunkCache(options.cacheSize ?? 500)\n\n const maxTexSize = options.maxTextureSize ?? 2048\n const maxDim = options.maxVolumeSize ?? 256\n\n // Check if level 0 (highest resolution) fits within the max volume size.\n // If so, use the actual dimensions directly instead of a virtual volume.\n const level0 = helper.pyramidInfo.levels[0]\n const level0Shape = level0.shape\n const level0FitsInVolume = level0Shape.every((dim) => dim <= maxDim && dim <= maxTexSize)\n\n if (level0FitsInVolume) {\n // Full dataset fits — use exact dimensions (reversed: depth=dim[0], height=dim[1], width=dim[2])\n if (helper.pyramidInfo.is3D && level0Shape.length >= 3) {\n helper.volumeDims = { depth: level0Shape[0], height: level0Shape[1], width: level0Shape[2] }\n } else {\n helper.volumeDims = { height: level0Shape[0], width: level0Shape[1], depth: 1 }\n }\n } else {\n helper.volumeDims = {\n width: Math.min(maxDim, maxTexSize),\n height: Math.min(maxDim, maxTexSize),\n depth: Math.min(maxDim, maxTexSize)\n }\n }\n\n helper.datatypeCode = zarrDtypeToNifti(helper.pyramidInfo.levels[0].dtype)\n helper.convertUnitsToMm = options.convertUnitsToMm ?? true\n\n helper.channel = options.channel ?? 0\n const axisMapping = helper.pyramidInfo.axisMapping\n for (const nsa of axisMapping.nonSpatialAxes) {\n if (nsa.name === 'c') {\n helper.nonSpatialCoords[nsa.name] = helper.channel\n }\n }\n\n // Set pyramid level\n const level = Math.max(0, Math.min(options.level, helper.pyramidInfo.levels.length - 1))\n helper.pyramidLevel = level\n helper.updateLevelInfo()\n\n // Center on the level\n helper.centerX = helper.levelDims.width / 2\n helper.centerY = helper.levelDims.height / 2\n helper.centerZ = helper.levelDims.depth / 2\n\n // Configure the host NVImage\n helper.configureHostImage()\n\n // Load initial chunks\n await helper.updateVolume()\n\n return helper\n }\n\n private updateLevelInfo(): void {\n const levelInfo = this.pyramidInfo.levels[this.pyramidLevel]\n const shape = levelInfo.shape\n // Map OME spatial dims to NIfTI: dim[0]→depth (slowest), dim[1]→height, dim[2]→width (fastest)\n if (this.pyramidInfo.is3D && shape.length >= 3) {\n this.levelDims = { depth: shape[0], height: shape[1], width: shape[2] }\n } else {\n this.levelDims = { height: shape[0], width: shape[1], depth: 1 }\n }\n\n const chunks = levelInfo.chunks\n if (this.pyramidInfo.is3D && chunks.length >= 3) {\n this.chunkSize = { depth: chunks[0], height: chunks[1], width: chunks[2] }\n } else {\n this.chunkSize = { height: chunks[0], width: chunks[1], depth: 1 }\n }\n\n // Extract physical voxel scales from OME coordinateTransformations (in metadata order)\n if (levelInfo.scales) {\n if (this.pyramidInfo.is3D && levelInfo.scales.length >= 3) {\n this.voxelScales = { depth: levelInfo.scales[0], height: levelInfo.scales[1], width: levelInfo.scales[2] }\n } else if (levelInfo.scales.length >= 2) {\n this.voxelScales = { height: levelInfo.scales[0], width: levelInfo.scales[1], depth: 1 }\n }\n } else {\n this.voxelScales = { width: 1, height: 1, depth: 1 }\n }\n\n // Extract physical voxel translations from OME coordinateTransformations (in metadata order)\n if (levelInfo.translations) {\n if (this.pyramidInfo.is3D && levelInfo.translations.length >= 3) {\n this.voxelTranslations = { depth: levelInfo.translations[0], height: levelInfo.translations[1], width: levelInfo.translations[2] }\n } else if (levelInfo.translations.length >= 2) {\n this.voxelTranslations = { height: levelInfo.translations[0], width: levelInfo.translations[1], depth: 0 }\n }\n this.hasTranslations = true\n } else {\n this.voxelTranslations = { width: 0, height: 0, depth: 0 }\n this.hasTranslations = false\n }\n }\n\n private configureHostImage(): void {\n const { width, height, depth } = this.volumeDims\n const bytesPerVoxel = getBytesPerVoxel(this.datatypeCode)\n\n // Get unit-converted scales for pixDims\n const { scaleW, scaleH, scaleD } = this.getConvertedScales()\n\n const hdr = new NIFTI1()\n hdr.littleEndian = true\n hdr.dims = [3, width, height, depth, 1, 0, 0, 0]\n hdr.pixDims = [1, scaleW, scaleH, scaleD, 1, 0, 0, 0]\n hdr.datatypeCode = this.datatypeCode\n hdr.numBitsPerVoxel = bytesPerVoxel * 8\n hdr.scl_inter = 0\n hdr.scl_slope = 1\n hdr.sform_code = 2\n hdr.magic = 'n+1'\n hdr.vox_offset = 352\n\n // Placeholder affine — updateAffine() will set the real one\n hdr.affine = [\n [1, 0, 0, 0],\n [0, 1, 0, 0],\n [0, 0, 1, 0],\n [0, 0, 0, 1]\n ]\n\n const img = this.hostImage\n img.name = `zarr:${this.pyramidInfo.name}`\n img.id = uuidv4()\n img._colormap = 'gray'\n img._opacity = 1.0\n img.hdr = hdr\n img.nFrame4D = 1\n img.frame4D = 0\n img.nTotalFrame4D = 1\n img.nVox3D = width * height * depth\n img.dims = [width, height, depth]\n img.pixDims = [scaleW, scaleH, scaleD]\n\n img.img = createTypedVoxelArray(this.datatypeCode, width * height * depth)\n\n // Set the affine based on current center position and call calculateRAS()\n this.updateAffine()\n\n // Store original affine so resetVolumeAffine() works on zarr volumes\n img.originalAffine = copyAffine(hdr.affine)\n\n img.cal_min = NaN\n img.cal_max = NaN\n img.robust_min = NaN\n img.robust_max = NaN\n img.global_min = NaN\n img.global_max = NaN\n }\n\n /** Get unit-converted voxel scales */\n private getConvertedScales(): { scaleW: number; scaleH: number; scaleD: number } {\n const units = this.pyramidInfo.spatialUnits\n let scaleD = this.voxelScales.depth\n let scaleH = this.voxelScales.height\n let scaleW = this.voxelScales.width\n if (this.convertUnitsToMm && units) {\n if (units.length >= 3) {\n scaleD = omeUnitToMm(scaleD, units[0])\n scaleH = omeUnitToMm(scaleH, units[1])\n scaleW = omeUnitToMm(scaleW, units[2])\n } else if (units.length >= 2) {\n scaleH = omeUnitToMm(scaleH, units[0])\n scaleW = omeUnitToMm(scaleW, units[1])\n }\n }\n return { scaleW, scaleH, scaleD }\n }\n\n /** Get unit-converted voxel translations */\n private getConvertedTranslations(): { transW: number; transH: number; transD: number } {\n const units = this.pyramidInfo.spatialUnits\n let transD = this.voxelTranslations.depth\n let transH = this.voxelTranslations.height\n let transW = this.voxelTranslations.width\n if (this.convertUnitsToMm && units) {\n if (units.length >= 3) {\n transD = omeUnitToMm(transD, units[0])\n transH = omeUnitToMm(transH, units[1])\n transW = omeUnitToMm(transW, units[2])\n } else if (units.length >= 2) {\n transH = omeUnitToMm(transH, units[0])\n transW = omeUnitToMm(transW, units[1])\n }\n }\n return { transW, transH, transD }\n }\n\n /**\n * Build the NIfTI affine from OME axis names, scales, and translations.\n *\n * NIfTI dimensions map to OME spatial dimensions as:\n * i (dim 1, width) = OME spatial[-1] (last, fastest in C-order)\n * j (dim 2, height) = OME spatial[-2]\n * k (dim 3, depth) = OME spatial[-3] (first, slowest in C-order)\n *\n * The affine maps (i, j, k) → physical (x, y, z):\n * physical_axis = scale * nifti_dim + translation\n * where nifti_dim is the column index (0=i, 1=j, 2=k) and\n * physical_axis row is determined by the OME axis name.\n */\n private updateAffine(): void {\n const hdr = this.hostImage.hdr\n if (!hdr) {\n return\n }\n\n const { width, height, depth } = this.volumeDims\n const axisNames = this.pyramidInfo.axisMapping.spatialAxisNames\n\n if (this.hasTranslations && axisNames.length >= 3) {\n const { scaleW, scaleH, scaleD } = this.getConvertedScales()\n const { transW, transH, transD } = this.getConvertedTranslations()\n\n // Volume window offsets in level coords\n const volStartW = this.centerX - width / 2 // width = last OME dim\n const volStartH = this.centerY - height / 2 // height = middle OME dim\n const volStartD = this.centerZ - depth / 2 // depth = first OME dim\n\n // Scale + offset for each OME spatial dimension, indexed by NIfTI column:\n // column 0 (i=width) → OME dim[-1]: scaleW, transW + volStartW * scaleW\n // column 1 (j=height) → OME dim[-2]: scaleH, transH + volStartH * scaleH\n // column 2 (k=depth) → OME dim[-3]: scaleD, transD + volStartD * scaleD\n const niftiCols = [\n { name: axisNames[axisNames.length - 1], scale: scaleW, trans: transW + volStartW * scaleW },\n { name: axisNames[axisNames.length - 2], scale: scaleH, trans: transH + volStartH * scaleH },\n { name: axisNames[axisNames.length - 3], scale: scaleD, trans: transD + volStartD * scaleD }\n ]\n\n // Map physical axis name → affine row: x→0, y→1, z→2\n const affine: number[][] = [\n [0, 0, 0, 0],\n [0, 0, 0, 0],\n [0, 0, 0, 0],\n [0, 0, 0, 1]\n ]\n for (let col = 0; col < 3; col++) {\n const row = niftiCols[col].name === 'x' ? 0 : niftiCols[col].name === 'y' ? 1 : 2\n affine[row][col] = niftiCols[col].scale\n affine[row][3] = niftiCols[col].trans\n }\n hdr.affine = affine\n } else {\n // No OME translations — calculate offset based on pan position\n const { scaleW, scaleH, scaleD } = this.getConvertedScales()\n\n // Volume window offsets in level coords (same calculation as 3D path)\n const volStartW = this.centerX - width / 2\n const volStartH = this.centerY - height / 2\n const volStartD = this.centerZ - depth / 2\n\n if (axisNames.length >= 2) {\n // Build affine based on axis names for 2D data\n // NIfTI cols: 0=width (last spatial), 1=height (2nd-to-last), 2=depth\n const niftiCols = [\n { name: axisNames[axisNames.length - 1], scale: scaleW, trans: volStartW * scaleW },\n { name: axisNames[axisNames.length - 2], scale: scaleH, trans: volStartH * scaleH },\n { name: 'z', scale: scaleD, trans: volStartD * scaleD }\n ]\n\n const affine: number[][] = [\n [0, 0, 0, 0],\n [0, 0, 0, 0],\n [0, 0, 0, 0],\n [0, 0, 0, 1]\n ]\n for (let col = 0; col < 3; col++) {\n const row = niftiCols[col].name === 'x' ? 0 : niftiCols[col].name === 'y' ? 1 : 2\n affine[row][col] = niftiCols[col].scale\n affine[row][3] = niftiCols[col].trans\n }\n hdr.affine = affine\n } else {\n // Fallback: simple diagonal affine with pan position\n hdr.affine = [\n [scaleW, 0, 0, volStartW * scaleW],\n [0, -scaleH, 0, volStartH * scaleH],\n [0, 0, -scaleD, volStartD * scaleD],\n [0, 0, 0, 1]\n ]\n }\n }\n\n this.hostImage.calculateRAS()\n\n // Keep originalAffine in sync so resetVolumeAffine() restores the\n // current zarr-computed affine (correct for this level/pan), not the\n // one from initial load.\n this.hostImage.originalAffine = copyAffine(this.hostImage.hdr.affine)\n }\n\n beginDrag(): void {\n this.centerAtDragStart = { x: this.centerX, y: this.centerY, z: this.centerZ }\n }\n\n async panBy(dx: number, dy: number, dz: number = 0): Promise<void> {\n this.centerX -= dx\n this.centerY -= dy\n this.centerZ -= dz\n this.clampCenter()\n this.updateAffine()\n await this.updateVolume()\n }\n\n async panTo(newCenterX: number, newCenterY: number, newCenterZ?: number): Promise<void> {\n this.centerX = newCenterX\n this.centerY = newCenterY\n if (newCenterZ !== undefined) {\n this.centerZ = newCenterZ\n }\n this.clampCenter()\n this.updateAffine()\n await this.updateVolume()\n }\n\n async setPyramidLevel(level: number): Promise<void> {\n const newLevel = Math.max(0, Math.min(this.pyramidInfo.levels.length - 1, level))\n if (newLevel === this.pyramidLevel) {\n return\n }\n\n // Convert center from old level coords to new level coords\n const oldDims = this.levelDims\n this.pyramidLevel = newLevel\n this.updateLevelInfo()\n const newDims = this.levelDims\n\n this.centerX = (this.centerX / oldDims.width) * newDims.width\n this.centerY = (this.centerY / oldDims.height) * newDims.height\n this.centerZ = (this.centerZ / oldDims.depth) * newDims.depth\n this.clampCenter()\n this.updateAffine()\n\n await this.updateVolume()\n }\n\n getViewportState(): { centerX: number; centerY: number; centerZ: number; level: number } {\n return {\n centerX: this.centerX,\n centerY: this.centerY,\n centerZ: this.centerZ,\n level: this.pyramidLevel\n }\n }\n\n getPyramidInfo(): ZarrPyramidInfo {\n return this.pyramidInfo\n }\n\n getPyramidLevel(): number {\n return this.pyramidLevel\n }\n\n getLevelDims(): { width: number; height: number; depth: number } {\n return { ...this.levelDims }\n }\n\n getVolumeDims(): { width: number; height: number; depth: number } {\n return { ...this.volumeDims }\n }\n\n private clampCenter(): void {\n const halfW = this.volumeDims.width / 2\n const halfH = this.volumeDims.height / 2\n const halfD = this.volumeDims.depth / 2\n\n this.centerX = Math.max(-halfW, Math.min(this.levelDims.width + halfW, this.centerX))\n this.centerY = Math.max(-halfH, Math.min(this.levelDims.height + halfH, this.centerY))\n this.centerZ = Math.max(-halfD, Math.min(this.levelDims.depth + halfD, this.centerZ))\n }\n\n private getVisibleChunks(): ChunkCoord[] {\n const MAX_CHUNKS = 1000\n const { width, height, depth } = this.volumeDims\n const level = this.pyramidLevel\n\n // Visible region in level coords\n const minX = Math.max(0, Math.floor(this.centerX - width / 2))\n const maxX = Math.min(this.levelDims.width, Math.ceil(this.centerX + width / 2))\n const minY = Math.max(0, Math.floor(this.centerY - height / 2))\n const maxY = Math.min(this.levelDims.height, Math.ceil(this.centerY + height / 2))\n const minZ = Math.max(0, Math.floor(this.centerZ - depth / 2))\n const maxZ = Math.min(this.levelDims.depth, Math.ceil(this.centerZ + depth / 2))\n\n const startCX = Math.max(0, Math.floor(minX / this.chunkSize.width))\n const startCY = Math.max(0, Math.floor(minY / this.chunkSize.height))\n const startCZ = Math.max(0, Math.floor(minZ / this.chunkSize.depth))\n const endCX = Math.min(Math.ceil(this.levelDims.width / this.chunkSize.width), Math.ceil(maxX / this.chunkSize.width))\n const endCY = Math.min(Math.ceil(this.levelDims.height / this.chunkSize.height), Math.ceil(maxY / this.chunkSize.height))\n const endCZ = Math.min(Math.ceil(this.levelDims.depth / this.chunkSize.depth), Math.ceil(maxZ / this.chunkSize.depth))\n\n const chunks: ChunkCoord[] = []\n for (let z = startCZ; z < endCZ; z++) {\n for (let y = startCY; y < endCY; y++) {\n for (let x = startCX; x < endCX; x++) {\n if (this.pyramidInfo.is3D) {\n chunks.push({ level, x, y, z })\n } else {\n chunks.push({ level, x, y })\n }\n if (chunks.length >= MAX_CHUNKS) {\n break\n }\n }\n if (chunks.length >= MAX_CHUNKS) {\n break\n }\n }\n if (chunks.length >= MAX_CHUNKS) {\n break\n }\n }\n\n // Sort by distance from center chunk (closest first)\n const centerCX = (startCX + endCX) / 2\n const centerCY = (startCY + endCY) / 2\n const centerCZ = (startCZ + endCZ) / 2\n chunks.sort((a, b) => {\n const distA = (a.x - centerCX) ** 2 + (a.y - centerCY) ** 2 + ((a.z ?? 0) - centerCZ) ** 2\n const distB = (b.x - centerCX) ** 2 + (b.y - centerCY) ** 2 + ((b.z ?? 0) - centerCZ) ** 2\n return distA - distB\n })\n\n return chunks\n }\n\n private async updateVolume(): Promise<void> {\n if (this.isUpdating) {\n this.needsUpdate = true\n this.currentAbortController?.abort()\n return\n }\n\n this.isUpdating = true\n try {\n do {\n this.needsUpdate = false\n this.currentAbortController?.abort()\n const abortController = new AbortController()\n this.currentAbortController = abortController\n\n this.runningMin = Infinity\n this.runningMax = -Infinity\n\n await new Promise<void>((resolve) => {\n requestAnimationFrame(() => resolve())\n })\n\n if (this.needsUpdate) {\n continue\n }\n\n this.clearVolumeData()\n await this.assembleVisibleChunks(abortController.signal)\n } while (this.needsUpdate)\n } finally {\n this.isUpdating = false\n }\n }\n\n private clearVolumeData(): void {\n const img = this.hostImage.img as TypedArray\n if (img.fill) {\n img.fill(0)\n } else {\n for (let i = 0; i < img.length; i++) {\n img[i] = 0\n }\n }\n\n // Reset chunk counters for the new update cycle\n this.pendingChunkCount = 0\n this.lastRenderedChunkCount = 0\n\n // Clear any pending debounce timer\n if (this.updateDebounceTimer !== null) {\n clearTimeout(this.updateDebounceTimer)\n this.updateDebounceTimer = null\n }\n }\n\n private async assembleVisibleChunks(signal?: AbortSignal): Promise<void> {\n const chunks = this.getVisibleChunks()\n const name = this.pyramidInfo.name\n const level = this.pyramidLevel\n\n const cachedChunks: ChunkCoord[] = []\n const uncachedChunks: ChunkCoord[] = []\n\n for (const chunk of chunks) {\n const key = ZarrChunkCache.getKey(name, chunk.level, chunk.x, chunk.y, chunk.z)\n if (this.chunkCache.has(key)) {\n cachedChunks.push(chunk)\n } else if (!this.chunkCache.isLoading(key)) {\n uncachedChunks.push(chunk)\n }\n }\n\n // Phase 1: assemble cached chunks synchronously\n for (const chunk of cachedChunks) {\n const key = ZarrChunkCache.getKey(name, chunk.level, chunk.x, chunk.y, chunk.z)\n const data = this.chunkCache.get(key)\n if (data) {\n this.assembleChunkIntoVolume(chunk, data)\n this.pendingChunkCount++\n }\n }\n\n if (cachedChunks.length > 0) {\n this.updateCalibration()\n this.onChunksUpdated?.()\n }\n\n // Phase 2: fetch uncached chunks with progressive rendering\n if (uncachedChunks.length > 0) {\n const fetchPromises = uncachedChunks.map(async (chunk) => {\n const key = ZarrChunkCache.getKey(name, chunk.level, chunk.x, chunk.y, chunk.z)\n this.chunkCache.startLoading(key)\n\n try {\n const data = await this.chunkClient.fetchChunk(chunk.level, chunk.x, chunk.y, chunk.z, this.nonSpatialCoords, signal)\n this.chunkCache.doneLoading(key)\n\n if (signal?.aborted) {\n return\n }\n\n if (data) {\n this.chunkCache.set(key, data)\n if (this.pyramidLevel === level) {\n this.assembleChunkIntoVolume(chunk, data)\n this.pendingChunkCount++\n // Use debounced callback to batch multiple chunk updates\n this.scheduleChunksUpdated()\n }\n }\n } catch (err: unknown) {\n this.chunkCache.doneLoading(key)\n if (err instanceof DOMException && err.name === 'AbortError') {\n return\n }\n console.warn(`Failed to fetch chunk ${key}:`, err)\n }\n })\n\n await Promise.all(fetchPromises)\n\n // After all chunks are fetched, clear any pending debounce and trigger final update\n if (this.updateDebounceTimer !== null) {\n clearTimeout(this.updateDebounceTimer)\n this.updateDebounceTimer = null\n }\n // Ensure final calibration and update if any chunks were loaded\n if (this.pendingChunkCount > this.lastRenderedChunkCount) {\n this.lastRenderedChunkCount = this.pendingChunkCount\n this.updateCalibration()\n this.onChunksUpdated?.()\n }\n this.onAllChunksLoaded?.()\n }\n }\n\n private assembleChunkIntoVolume(chunk: ChunkCoord, data: TypedArray): void {\n const { width, height, depth } = this.volumeDims\n const img = this.hostImage.img as TypedArray\n\n // Chunk pixel position in level coords\n const chunkStartX = chunk.x * this.chunkSize.width\n const chunkStartY = chunk.y * this.chunkSize.height\n const chunkStartZ = (chunk.z ?? 0) * this.chunkSize.depth\n\n // Volume starts at center - volumeDims/2\n const volStartX = this.centerX - width / 2\n const volStartY = this.centerY - height / 2\n const volStartZ = this.centerZ - depth / 2\n\n // Destination in volume texture\n const destX = Math.round(chunkStartX - volStartX)\n const destY = Math.round(chunkStartY - volStartY)\n const destZ = Math.round(chunkStartZ - volStartZ)\n\n // Actual chunk size (may be smaller at edges)\n const actualChunkW = Math.min(this.chunkSize.width, this.levelDims.width - chunkStartX)\n const actualChunkH = Math.min(this.chunkSize.height, this.levelDims.height - chunkStartY)\n const actualChunkD = Math.min(this.chunkSize.depth, this.levelDims.depth - chunkStartZ)\n\n if (actualChunkW <= 0 || actualChunkH <= 0 || actualChunkD <= 0) {\n return\n }\n\n // Track min/max for calibration\n const step = Math.max(1, Math.floor(data.length / 2000))\n for (let i = 0; i < data.length; i += step) {\n const val = data[i]\n if (val < this.runningMin) {\n this.runningMin = val\n }\n if (val > this.runningMax) {\n this.runningMax = val\n }\n }\n\n // Copy chunk data into volume (no scaling - 1:1 pixel mapping).\n // Since spatial indices are kept in OME metadata order (not reordered),\n // the zarr C-order layout naturally aligns with NIfTI Fortran-order:\n // - dz iterates depth = OME dim[0] (slowest in both C-order and NIfTI)\n // - dy iterates height = OME dim[1]\n // - dx iterates width = OME dim[2] (fastest in both C-order and NIfTI)\n // srcIdx uses C-order strides based on the actual chunk data dimensions.\n // Edge chunks may be padded (full chunk size) or truncated.\n const fullChunkVol = this.chunkSize.width * this.chunkSize.height * this.chunkSize.depth\n const srcChunkW = data.length >= fullChunkVol ? this.chunkSize.width : actualChunkW\n const srcChunkH = data.length >= fullChunkVol ? this.chunkSize.height : actualChunkH\n for (let dz = 0; dz < actualChunkD; dz++) {\n const vZ = destZ + dz\n if (vZ < 0 || vZ >= depth) {\n continue\n }\n for (let dy = 0; dy < actualChunkH; dy++) {\n const vY = destY + dy\n if (vY < 0 || vY >= height) {\n continue\n }\n for (let dx = 0; dx < actualChunkW; dx++) {\n const vX = destX + dx\n if (vX < 0 || vX >= width) {\n continue\n }\n const srcIdx = dz * srcChunkH * srcChunkW + dy * srcChunkW + dx\n const dstIdx = vX + vY * width + vZ * width * height\n if (srcIdx >= 0 && srcIdx < data.length && dstIdx >= 0 && dstIdx < img.length) {\n img[dstIdx] = data[srcIdx]\n }\n }\n }\n }\n }\n\n private updateCalibration(): void {\n if (this.runningMin < Infinity && this.runningMax > -Infinity) {\n this.hostImage.cal_min = this.runningMin\n this.hostImage.cal_max = this.runningMax\n this.hostImage.robust_min = this.runningMin\n this.hostImage.robust_max = this.runningMax\n this.hostImage.global_min = this.runningMin\n this.hostImage.global_max = this.runningMax\n }\n }\n\n /**\n * Schedule a debounced chunks update callback.\n * Batches multiple chunk arrivals within UPDATE_DEBOUNCE_MS into a single GPU update.\n */\n private scheduleChunksUpdated(): void {\n // Skip if no new chunks since last render\n if (this.pendingChunkCount === this.lastRenderedChunkCount) {\n return\n }\n\n // Clear existing timer and schedule new one\n if (this.updateDebounceTimer !== null) {\n clearTimeout(this.updateDebounceTimer)\n }\n\n this.updateDebounceTimer = setTimeout(() => {\n this.updateDebounceTimer = null\n this.lastRenderedChunkCount = this.pendingChunkCount\n this.updateCalibration()\n this.onChunksUpdated?.()\n }, this.UPDATE_DEBOUNCE_MS)\n }\n\n clearCache(): void {\n this.chunkCache.clear()\n }\n\n async refresh(): Promise<void> {\n await this.updateVolume()\n }\n}\n","import { mat4 } from 'gl-matrix'\n\n/**\n * Represents an affine transformation in decomposed form.\n */\nexport interface AffineTransform {\n translation: [number, number, number] // X, Y, Z in mm\n rotation: [number, number, number] // Euler angles in degrees (X, Y, Z order)\n scale: [number, number, number] // Scale factors\n}\n\n/**\n * Identity transform with no translation, rotation, or scale change.\n */\nexport const identityTransform: AffineTransform = {\n translation: [0, 0, 0],\n rotation: [0, 0, 0],\n scale: [1, 1, 1]\n}\n\n/**\n * Convert degrees to radians.\n */\nexport function degToRad(degrees: number): number {\n return (degrees * Math.PI) / 180\n}\n\n/**\n * Create a rotation matrix from Euler angles (XYZ order).\n * Angles are in degrees.\n */\nexport function eulerToRotationMatrix(rx: number, ry: number, rz: number): mat4 {\n const out = mat4.create()\n\n const radX = degToRad(rx)\n const radY = degToRad(ry)\n const radZ = degToRad(rz)\n\n // Apply rotations in XYZ order\n mat4.rotateX(out, out, radX)\n mat4.rotateY(out, out, radY)\n mat4.rotateZ(out, out, radZ)\n\n return out\n}\n\n/**\n * Create a 4x4 transformation matrix from decomposed transform components.\n * Order: Scale -> Rotate -> Translate\n */\nexport function createTransformMatrix(transform: AffineTransform): mat4 {\n const out = mat4.create()\n\n // Translation\n mat4.translate(out, out, transform.translation)\n\n // Rotation (XYZ Euler angles)\n const rotation = eulerToRotationMatrix(transform.rotation[0], transform.rotation[1], transform.rotation[2])\n mat4.multiply(out, out, rotation)\n\n // Scale\n mat4.scale(out, out, transform.scale)\n\n return out\n}\n\n/**\n * Convert a 2D array (row-major, as used by NIfTI) to gl-matrix mat4 (column-major).\n */\nexport function arrayToMat4(arr: number[][]): mat4 {\n // NIfTI stores as row-major, gl-matrix uses column-major\n // arr[row][col] -> mat4 is column-major (index = col * 4 + row)\n return mat4.fromValues(\n arr[0][0],\n arr[1][0],\n arr[2][0],\n arr[3][0], // column 0\n arr[0][1],\n arr[1][1],\n arr[2][1],\n arr[3][1], // column 1\n arr[0][2],\n arr[1][2],\n arr[2][2],\n arr[3][2], // column 2\n arr[0][3],\n arr[1][3],\n arr[2][3],\n arr[3][3] // column 3\n )\n}\n\n/**\n * Convert gl-matrix mat4 (column-major) to 2D array (row-major, as used by NIfTI).\n */\nexport function mat4ToArray(m: mat4): number[][] {\n // mat4 is column-major: index = col * 4 + row\n // We need row-major: arr[row][col]\n return [\n [m[0], m[4], m[8], m[12]], // row 0\n [m[1], m[5], m[9], m[13]], // row 1\n [m[2], m[6], m[10], m[14]], // row 2\n [m[3], m[7], m[11], m[15]] // row 3\n ]\n}\n\n/**\n * Multiply a transformation matrix by an affine matrix (as 2D array).\n * Returns the result as a 2D array.\n *\n * The transform is applied to the left: result = transform * original\n * This means the transform happens in world coordinate space.\n */\nexport function multiplyAffine(original: number[][], transform: mat4): number[][] {\n const originalMat = arrayToMat4(original)\n const result = mat4.create()\n mat4.multiply(result, transform, originalMat)\n return mat4ToArray(result)\n}\n\n/**\n * Deep copy a 2D affine matrix array.\n */\nexport function copyAffine(affine: number[][]): number[][] {\n return affine.map((row) => [...row])\n}\n\n/**\n * Check if two transforms are approximately equal.\n */\nexport function transformsEqual(a: AffineTransform, b: AffineTransform, epsilon = 0.0001): boolean {\n for (let i = 0; i < 3; i++) {\n if (Math.abs(a.translation[i] - b.translation[i]) > epsilon) {\n return false\n }\n if (Math.abs(a.rotation[i] - b.rotation[i]) > epsilon) {\n return false\n }\n if (Math.abs(a.scale[i] - b.scale[i]) > epsilon) {\n return false\n }\n }\n return true\n}\n","/**\n * ZarrChunkClient - HTTP client for fetching zarr array data using zarrita.js.\n *\n * Handles pyramid discovery and chunk fetching for OME-ZARR and regular zarr stores.\n */\n\nimport * as zarr from 'zarrita'\nimport type { TypedArray } from './ZarrChunkCache.js'\n\nexport interface ZarrChunkClientConfig {\n /** Base URL for zarr store (e.g., \"http://localhost:8090/lightsheet.zarr\") */\n baseUrl: string\n}\n\nexport interface ZarrPyramidLevel {\n /** Level index (0 = highest resolution) */\n index: number\n /** Path to this level in the zarr hierarchy (e.g., \"/0\", \"/1\") */\n path: string\n /** Spatial-only shape in OME metadata order (non-spatial dims stripped) */\n shape: number[]\n /** Spatial-only chunk dimensions matching shape order */\n chunks: number[]\n /** Data type (e.g., \"uint8\", \"uint16\", \"float32\") */\n dtype: string\n /** Physical scale factors per spatial axis in OME metadata order from coordinateTransformations */\n scales?: number[]\n /** Physical translation offsets per spatial axis in OME metadata order from coordinateTransformations */\n translations?: number[]\n}\n\n/**\n * Mapping from spatial chunk coordinates to full zarr array chunk coordinates.\n * Handles non-spatial dimensions like channel (c) and time (t).\n */\nexport interface AxisMapping {\n /** Total number of dimensions in the original zarr array */\n originalNdim: number\n /** Indices of spatial axes in the original array, in OME metadata order */\n spatialIndices: number[]\n /** Names of spatial axes in OME metadata order (e.g., ['x', 'y', 'z'] or ['z', 'y', 'x']) */\n spatialAxisNames: string[]\n /** Non-spatial axes: their index in the original array, chunk size, and default chunk coord */\n nonSpatialAxes: Array<{ index: number; name: string; chunkSize: number; defaultChunkCoord: number }>\n}\n\nexport interface ZarrPyramidInfo {\n /** Name/URL of the zarr store */\n name: string\n /** Pyramid levels (index 0 = highest resolution) */\n levels: ZarrPyramidLevel[]\n /** Whether this is a 3D dataset (based on spatial dimensions) */\n is3D: boolean\n /** Number of spatial dimensions (2 or 3) */\n ndim: number\n /** Mapping from spatial to full array coordinates */\n axisMapping: AxisMapping\n /** Units for spatial axes in OME metadata order (e.g., \"micrometer\", \"millimeter\") */\n spatialUnits?: string[]\n}\n\nexport interface ChunkCoord {\n /** Pyramid level */\n level: number\n /** Chunk X index */\n x: number\n /** Chunk Y index */\n y: number\n /** Chunk Z index (for 3D) */\n z?: number\n}\n\n// Type for zarr Array (simplified for our use)\ntype ZarrArray = zarr.Array<zarr.DataType, zarr.Readable>\n\n// Interface for OME multiscales metadata\ninterface OmeMultiscalesDataset {\n path: string\n coordinateTransformations?: Array<{\n type: string\n scale?: number[]\n translation?: number[]\n }>\n}\n\ninterface OmeMultiscales {\n axes?: Array<{ name: string; type: string; unit?: string }>\n datasets: OmeMultiscalesDataset[]\n name?: string\n}\n\nexport class ZarrChunkClient {\n private store: zarr.FetchStore\n private baseUrl: string\n private arrays: Map<number, ZarrArray> = new Map()\n /** Maps level index to actual path in the zarr store */\n private levelPaths: Map<number, string> = new Map()\n /** Axis mapping for coordinate translation */\n private axisMapping: AxisMapping | null = null\n\n constructor(config: ZarrChunkClientConfig) {\n this.baseUrl = config.baseUrl\n this.store = new zarr.FetchStore(config.baseUrl)\n }\n\n /**\n * Discover pyramid structure by reading OME-ZARR multiscales metadata,\n * or falling back to probing for arrays at /0, /1, /2, etc.\n */\n async fetchInfo(): Promise<ZarrPyramidInfo> {\n const root = zarr.root(this.store)\n\n // Raw levels before axis stripping (with full original shapes)\n const rawLevels: Array<{ index: number; path: string; shape: number[]; chunks: number[]; dtype: string }> = []\n let omeAxes: Array<{ name: string; type: string; unit?: string }> | null = null\n let omeMultiscales: OmeMultiscales | null = null\n\n // Try to open as a single array first (non-pyramidal case)\n try {\n const arr = await zarr.open(root, { kind: 'array' })\n rawLevels.push({\n index: 0,\n path: '/',\n shape: [...arr.shape],\n chunks: [...arr.chunks],\n dtype: arr.dtype\n })\n this.arrays.set(0, arr)\n this.levelPaths.set(0, '/')\n } catch {\n // Not a single array, try to open as a group and read OME metadata\n\n try {\n const group = await zarr.open(root, { kind: 'group' })\n // Look for OME multiscales metadata in attributes\n // Can be at .ome.multiscales (v0.5) or .multiscales (older)\n const attrs = group.attrs as Record<string, unknown>\n if (attrs.ome && typeof attrs.ome === 'object') {\n const ome = attrs.ome as { multiscales?: OmeMultiscales[] }\n if (ome.multiscales && Array.isArray(ome.multiscales) && ome.multiscales.length > 0) {\n omeMultiscales = ome.multiscales[0]\n }\n } else if (attrs.multiscales && Array.isArray(attrs.multiscales) && attrs.multiscales.length > 0) {\n omeMultiscales = attrs.multiscales[0] as OmeMultiscales\n }\n } catch {\n // Could not open as group\n }\n\n // Extract OME axes metadata if available\n if (omeMultiscales?.axes && Array.isArray(omeMultiscales.axes)) {\n omeAxes = omeMultiscales.axes\n }\n\n if (omeMultiscales && omeMultiscales.datasets && omeMultiscales.datasets.length > 0) {\n // Use OME multiscales paths\n for (let i = 0; i < omeMultiscales.datasets.length; i++) {\n const dataset = omeMultiscales.datasets[i]\n const path = dataset.path.startsWith('/') ? dataset.path : `/${dataset.path}`\n try {\n const loc = root.resolve(path)\n const arr = await zarr.open(loc, { kind: 'array' })\n rawLevels.push({\n index: i,\n path,\n shape: [...arr.shape],\n chunks: [...arr.chunks],\n dtype: arr.dtype\n })\n this.arrays.set(i, arr)\n this.levelPaths.set(i, path)\n } catch (err) {\n console.warn(`Failed to open array at path ${path}:`, err)\n }\n }\n } else {\n // No OME metadata, probe for arrays at /0, /1, /2, ... until we get an error\n for (let i = 0; i < 20; i++) {\n const path = `/${i}`\n try {\n const loc = root.resolve(path)\n const arr = await zarr.open(loc, { kind: 'array' })\n rawLevels.push({\n index: i,\n path,\n shape: [...arr.shape],\n chunks: [...arr.chunks],\n dtype: arr.dtype\n })\n this.arrays.set(i, arr)\n this.levelPaths.set(i, path)\n } catch {\n // No more levels\n break\n }\n }\n }\n }\n\n if (rawLevels.length === 0) {\n throw new Error(`No zarr arrays found at ${this.baseUrl}`)\n }\n\n // Build axis mapping: identify spatial vs non-spatial dimensions\n const originalNdim = rawLevels[0].shape.length\n const axisMapping = this.buildAxisMapping(originalNdim, rawLevels[0].chunks, omeAxes)\n this.axisMapping = axisMapping\n\n // Extract spatial units from OME axes metadata (in spatial index order: [Z, Y, X] or [Y, X])\n let spatialUnits: string[] | undefined\n if (omeAxes) {\n spatialUnits = axisMapping.spatialIndices.map((i) => omeAxes[i]?.unit ?? '')\n }\n\n // Strip non-spatial dimensions from shape and chunks, and extract per-level scales\n const levels: ZarrPyramidLevel[] = rawLevels.map((raw) => {\n const level: ZarrPyramidLevel = {\n index: raw.index,\n path: raw.path,\n shape: axisMapping.spatialIndices.map((i) => raw.shape[i]),\n chunks: axisMapping.spatialIndices.map((i) => raw.chunks[i]),\n dtype: raw.dtype\n }\n\n // Extract spatial scale factors and translations from OME coordinateTransformations\n if (omeMultiscales?.datasets?.[raw.index]?.coordinateTransformations) {\n const transforms = omeMultiscales.datasets[raw.index].coordinateTransformations!\n const scaleTransform = transforms.find((t) => t.type === 'scale' && t.scale)\n if (scaleTransform?.scale) {\n // Extract scales for spatial axes only, in the reordered [Z, Y, X] order\n level.scales = axisMapping.spatialIndices.map((i) => scaleTransform.scale![i])\n }\n const translationTransform = transforms.find((t) => t.type === 'translation' && t.translation)\n if (translationTransform?.translation) {\n // Extract translations for spatial axes only, in the reordered [Z, Y, X] order\n level.translations = axisMapping.spatialIndices.map((i) => translationTransform.translation![i])\n }\n }\n\n return level\n })\n\n // Determine dimensionality from spatial axes\n const spatialNdim = axisMapping.spatialIndices.length\n const is3D = spatialNdim >= 3\n\n console.log(\n `Zarr axis mapping: original ndim=${originalNdim}, spatial ndim=${spatialNdim}, ` +\n `spatial indices=${JSON.stringify(axisMapping.spatialIndices)}, ` +\n `non-spatial=${JSON.stringify(axisMapping.nonSpatialAxes.map((a) => a.name))}`\n )\n\n return {\n name: this.baseUrl,\n levels,\n is3D,\n ndim: spatialNdim,\n axisMapping,\n spatialUnits\n }\n }\n\n /**\n * Build axis mapping from OME axes metadata or infer from array dimensions.\n * Identifies spatial (x, y, z) vs non-spatial (c, t) dimensions and returns\n * indices for extracting spatial-only shape/chunks.\n * Spatial indices are kept in the original OME metadata order (NOT reordered).\n */\n private buildAxisMapping(originalNdim: number, originalChunks: number[], omeAxes: Array<{ name: string; type: string; unit?: string }> | null): AxisMapping {\n const spatialIndices: number[] = []\n const spatialAxisNames: string[] = []\n const nonSpatialAxes: AxisMapping['nonSpatialAxes'] = []\n\n if (omeAxes && omeAxes.length === originalNdim) {\n // Use OME axes metadata to identify spatial vs non-spatial\n // Keep spatial axes in their original metadata order\n for (let i = 0; i < omeAxes.length; i++) {\n const axis = omeAxes[i]\n if (axis.type === 'space') {\n spatialIndices.push(i)\n spatialAxisNames.push(axis.name.toLowerCase())\n } else {\n nonSpatialAxes.push({\n index: i,\n name: axis.name,\n chunkSize: originalChunks[i],\n defaultChunkCoord: 0\n })\n }\n }\n } else {\n // No OME axes metadata — infer from ndim.\n // OME convention: leading dims are non-spatial (t, c), trailing are spatial (z, y, x)\n if (omeAxes === null) {\n console.warn('No OME axes metadata found — inferring axis layout from array dimensions')\n }\n if (originalNdim <= 3) {\n // 2D or 3D — all spatial, assume z/y/x names for trailing dims\n const defaultNames3D = ['z', 'y', 'x']\n const defaultNames2D = ['y', 'x']\n const names = originalNdim === 2 ? defaultNames2D : defaultNames3D.slice(3 - originalNdim)\n for (let i = 0; i < originalNdim; i++) {\n spatialIndices.push(i)\n spatialAxisNames.push(names[i])\n }\n } else if (originalNdim === 4) {\n // Assume [C, Z, Y, X]\n nonSpatialAxes.push({ index: 0, name: 'c', chunkSize: originalChunks[0], defaultChunkCoord: 0 })\n spatialIndices.push(1, 2, 3)\n spatialAxisNames.push('z', 'y', 'x')\n } else if (originalNdim === 5) {\n // Assume [T, C, Z, Y, X]\n nonSpatialAxes.push({ index: 0, name: 't', chunkSize: originalChunks[0], defaultChunkCoord: 0 })\n nonSpatialAxes.push({ index: 1, name: 'c', chunkSize: originalChunks[1], defaultChunkCoord: 0 })\n spatialIndices.push(2, 3, 4)\n spatialAxisNames.push('z', 'y', 'x')\n } else {\n // Unknown layout — treat last 3 as spatial, rest as non-spatial\n for (let i = 0; i < originalNdim - 3; i++) {\n nonSpatialAxes.push({ index: i, name: `dim${i}`, chunkSize: originalChunks[i], defaultChunkCoord: 0 })\n }\n for (let i = originalNdim - 3; i < originalNdim; i++) {\n spatialIndices.push(i)\n spatialAxisNames.push(['z', 'y', 'x'][i - (originalNdim - 3)])\n }\n }\n }\n\n return { originalNdim, spatialIndices, spatialAxisNames, nonSpatialAxes }\n }\n\n /**\n * Open a zarr array at a specific pyramid level.\n * Uses cached arrays when available.\n */\n private async openLevel(level: number): Promise<ZarrArray> {\n if (this.arrays.has(level)) {\n return this.arrays.get(level)!\n }\n\n const root = zarr.root(this.store)\n // Use stored path from fetchInfo, or fall back to numbered path\n const path = this.levelPaths.get(level) ?? (level === 0 ? '/' : `/${level}`)\n\n try {\n const loc = root.resolve(path)\n const arr = await zarr.open(loc, { kind: 'array' })\n this.arrays.set(level, arr)\n this.levelPaths.set(level, path)\n return arr\n } catch {\n // Fallback: try root for level 0\n if (level === 0) {\n const arr = await zarr.open(root, { kind: 'array' })\n this.arrays.set(level, arr)\n this.levelPaths.set(level, '/')\n return arr\n }\n throw new Error(`Cannot open zarr array at level ${level}`)\n }\n }\n\n /**\n * Fetch a single chunk by spatial coordinates.\n * Uses the axis mapping to build full chunk coordinates including non-spatial dims.\n * Returns the spatial-only decoded TypedArray data.\n *\n * @param level - Pyramid level\n * @param x - Spatial X chunk index\n * @param y - Spatial Y chunk index\n * @param z - Spatial Z chunk index (for 3D)\n * @param nonSpatialCoords - Optional overrides for non-spatial dimensions (e.g., channel index)\n */\n async fetchChunk(level: number, x: number, y: number, z?: number, nonSpatialCoords?: Record<string, number>, signal?: AbortSignal): Promise<TypedArray | null> {\n try {\n const arr = await this.openLevel(level)\n const mapping = this.axisMapping\n\n let chunkCoords: number[]\n\n if (mapping && mapping.originalNdim > mapping.spatialIndices.length) {\n // Build full chunk coordinates using axis mapping\n chunkCoords = new Array(mapping.originalNdim).fill(0)\n\n // Set non-spatial axes to default values (or overrides)\n for (const nsa of mapping.nonSpatialAxes) {\n chunkCoords[nsa.index] = nonSpatialCoords?.[nsa.name] ?? nsa.defaultChunkCoord\n }\n\n // Set spatial axes — spatialIndices are in [Z, Y, X] or [Y, X] order\n const spatialCoords = z !== undefined && mapping.spatialIndices.length >= 3 ? [z, y, x] : [y, x]\n\n for (let i = 0; i < spatialCoords.length && i < mapping.spatialIndices.length; i++) {\n chunkCoords[mapping.spatialIndices[i]] = spatialCoords[i]\n }\n } else {\n // No non-spatial dims — use spatial coords directly\n if (z !== undefined && arr.shape.length >= 3) {\n chunkCoords = [z, y, x]\n } else {\n chunkCoords = [y, x]\n }\n }\n\n const chunk = await arr.getChunk(chunkCoords, { signal })\n let data = chunk.data as TypedArray\n\n // If there are non-spatial dimensions, extract the spatial-only slice.\n // For non-spatial chunk sizes of 1 (common case), data starts at offset 0\n // and has exactly spatialSize elements — no slicing needed.\n if (mapping && mapping.nonSpatialAxes.length > 0) {\n const spatialSize = mapping.spatialIndices.reduce((acc, idx) => acc * arr.chunks[idx], 1)\n if (data.length > spatialSize) {\n // Compute offset for the requested non-spatial chunk coords\n // Data is in row-major order: leading dims have largest strides\n let offset = 0\n let stride = data.length\n for (let d = 0; d < mapping.originalNdim; d++) {\n stride = Math.floor(stride / arr.chunks[d])\n const isSpatial = mapping.spatialIndices.includes(d)\n if (!isSpatial) {\n // Use 0 within the chunk (we fetched the right chunk coord already)\n // This handles the case where non-spatial chunk size > 1\n offset += 0 * stride\n }\n }\n data = data.subarray(offset, offset + spatialSize) as TypedArray\n }\n }\n\n return data\n } catch (err) {\n console.warn(`Failed to fetch chunk at level ${level}, x=${x}, y=${y}, z=${z}:`, err)\n return null\n }\n }\n\n /**\n * Fetch multiple chunks in parallel.\n * Returns a Map from chunk key to TypedArray.\n */\n async fetchChunks(name: string, level: number, coords: ChunkCoord[]): Promise<Map<string, TypedArray>> {\n const results = new Map<string, TypedArray>()\n\n const promises = coords.map(async (coord) => {\n const data = await this.fetchChunk(level, coord.x, coord.y, coord.z)\n if (data) {\n const key = coord.z !== undefined ? `${name}:${level}/${coord.x}/${coord.y}/${coord.z}` : `${name}:${level}/${coord.x}/${coord.y}`\n results.set(key, data)\n }\n })\n\n await Promise.all(promises)\n return results\n }\n\n /**\n * Fetch a rectangular region using zarr.get with slices.\n * Useful for fetching exact viewport regions rather than whole chunks.\n * Uses axis mapping to handle non-spatial dimensions.\n */\n async fetchRegion(\n level: number,\n region: {\n xStart: number\n xEnd: number\n yStart: number\n yEnd: number\n zStart?: number\n zEnd?: number\n }\n ): Promise<{ data: TypedArray; shape: number[] } | null> {\n try {\n const arr = await this.openLevel(level)\n const mapping = this.axisMapping\n\n // Build slice selections for all dimensions\n const selections: Array<ReturnType<typeof zarr.slice> | number> = []\n\n if (mapping && mapping.originalNdim > mapping.spatialIndices.length) {\n // Build full selections with non-spatial dims fixed to 0\n for (let d = 0; d < mapping.originalNdim; d++) {\n const spatialIdx = mapping.spatialIndices.indexOf(d)\n if (spatialIdx === -1) {\n // Non-spatial dim — select index 0\n selections.push(0)\n } else if (mapping.spatialIndices.length >= 3) {\n // Spatial dim in [Z, Y, X] order\n if (spatialIdx === 0 && region.zStart !== undefined && region.zEnd !== undefined) {\n selections.push(zarr.slice(region.zStart, region.zEnd))\n } else if (spatialIdx === 1) {\n selections.push(zarr.slice(region.yStart, region.yEnd))\n } else if (spatialIdx === 2) {\n selections.push(zarr.slice(region.xStart, region.xEnd))\n } else {\n selections.push(zarr.slice(0, arr.shape[d]))\n }\n } else {\n // 2D spatial: [Y, X]\n if (spatialIdx === 0) {\n selections.push(zarr.slice(region.yStart, region.yEnd))\n } else {\n selections.push(zarr.slice(region.xStart, region.xEnd))\n }\n }\n }\n } else {\n // No non-spatial dims — use spatial coords directly\n if (region.zStart !== undefined && region.zEnd !== undefined && arr.shape.length >= 3) {\n selections.push(zarr.slice(region.zStart, region.zEnd))\n selections.push(zarr.slice(region.yStart, region.yEnd))\n selections.push(zarr.slice(region.xStart, region.xEnd))\n } else {\n selections.push(zarr.slice(region.yStart, region.yEnd))\n selections.push(zarr.slice(region.xStart, region.xEnd))\n }\n }\n\n const result = await zarr.get(arr, selections)\n return {\n data: result.data as TypedArray,\n shape: result.shape\n }\n } catch (err) {\n console.warn(`Failed to fetch region at level ${level}:`, err)\n return null\n }\n }\n\n /**\n * Get the zarr store URL\n */\n getUrl(): string {\n return this.baseUrl\n }\n\n /**\n * Clear cached array references\n */\n clearArrayCache(): void {\n this.arrays.clear()\n this.levelPaths.clear()\n }\n}\n","import { vec3, vec4 } from 'gl-matrix'\nimport { v4 as uuidv4 } from '@lukeed/uuid'\nimport { log } from '@/logger'\nimport { NiivueObject3D } from '@/niivue-object3D'\nimport { ColorMap, LUT, cmapper, COLORMAP_TYPE } from '@/colortables'\nimport { NVMeshUtilities } from '@/nvmesh-utilities'\nimport { NVMeshLoaders } from '@/nvmesh-loaders'\nimport { NVLabel3D, LabelTextAlignment, LabelLineTerminator } from '@/nvlabel'\n\nimport { LegacyConnectome, LegacyNodes, NVConnectomeEdge, NVConnectomeNode, Point } from '@/types'\nimport { DefaultMeshType, GII, MZ3, TCK, TRACT, TRK, TT, TRX, VTK, ValuesArray, X3D, AnyNumberArray } from '@/nvmesh-types'\n\n/** Enum for text alignment\n */\nexport enum MeshType {\n MESH = 'mesh',\n CONNECTOME = 'connectome',\n FIBER = 'fiber'\n}\n\nexport type NVMeshLayer = {\n name?: string\n key?: string\n url?: string\n headers?: Record<string, string>\n opacity: number\n colormap: string\n colormapNegative?: string\n colormapInvert?: boolean\n colormapLabel?: ColorMap | LUT\n useNegativeCmap?: boolean\n global_min?: number\n global_max?: number\n cal_min: number\n cal_max: number\n cal_minNeg: number\n cal_maxNeg: number\n isAdditiveBlend?: boolean\n frame4D: number\n nFrame4D: number\n values: AnyNumberArray // number[] | Float32Array | Uint32Array\n outlineBorder?: number\n isTransparentBelowCalMin?: boolean\n colormapType?: number\n base64?: string\n // TODO referenced in niivue/refreshColormaps\n colorbarVisible?: boolean\n showLegend?: boolean\n labels?: NVLabel3D[]\n atlasValues?: AnyNumberArray\n}\n\nexport const NVMeshLayerDefaults = {\n colormap: 'gray',\n opacity: 0.0,\n nFrame4D: 0,\n frame4D: 0,\n outlineBorder: 0,\n cal_min: 0,\n cal_max: 0,\n cal_minNeg: 0,\n cal_maxNeg: 0,\n colormapType: COLORMAP_TYPE.MIN_TO_MAX,\n values: new Array<number>(),\n useNegativeCmap: false,\n showLegend: true\n}\n\nexport class NVMeshFromUrlOptions {\n url: string\n gl: WebGL2RenderingContext | null\n name: string\n opacity: number\n rgba255: Uint8Array\n visible: boolean\n layers: NVMeshLayer[]\n colorbarVisible: boolean\n meshShaderIndex: number\n\n constructor(url = '', gl = null, name = '', opacity = 1.0, rgba255 = new Uint8Array([255, 255, 255, 255]), visible = true, layers = [], colorbarVisible = true, meshShaderIndex = 0) {\n this.url = url\n this.gl = gl\n this.name = name\n this.opacity = opacity\n this.rgba255 = rgba255\n this.visible = visible\n this.layers = layers\n this.colorbarVisible = colorbarVisible\n this.meshShaderIndex = meshShaderIndex\n }\n}\n\n/**\n * Parameters for loading a base mesh or volume.\n */\ntype BaseLoadParams = {\n /** WebGL rendering context. */\n gl: WebGL2RenderingContext\n /** Name for this image. Default is an empty string. */\n name: string\n /** Opacity for this image. Default is 1. */\n opacity: number\n /** Base color of the mesh in RGBA [0-255]. Default is white. */\n rgba255: number[] | Uint8Array\n /** Whether this image is visible. */\n visible: boolean\n /** Layers of the mesh to load. */\n layers: NVMeshLayer[]\n /** Shader index for mesh rendering. Default is 0 (Phong). */\n meshShaderIndex: number\n}\n\nexport type LoadFromUrlParams = Partial<BaseLoadParams> & {\n // the resolvable URL pointing to a mesh to load\n url: string\n headers?: Record<string, string>\n buffer?: ArrayBuffer\n}\n\ntype LoadFromFileParams = BaseLoadParams & {\n // the file object\n file: Blob\n}\n\ntype LoadFromBase64Params = BaseLoadParams & {\n // the base64 encoded string\n base64: string\n}\n\n/**\n * a NVMesh encapsulates some mesh data and provides methods to query and operate on meshes\n */\nexport class NVMesh {\n id: string\n name: string\n anatomicalStructurePrimary: string\n colorbarVisible: boolean\n furthestVertexFromOrigin: number\n extentsMin: number | number[]\n extentsMax: number | number[]\n opacity: number\n visible: boolean\n meshShaderIndex = 0\n offsetPt0: Uint32Array | null = null\n\n colormapInvert = false\n fiberGroupColormap: ColorMap | null = null\n\n indexBuffer: WebGLBuffer\n vertexBuffer: WebGLBuffer\n vao: WebGLVertexArrayObject\n vaoFiber: WebGLVertexArrayObject\n\n pts: Float32Array\n tris?: Uint32Array\n layers: NVMeshLayer[]\n type = MeshType.MESH\n\n data_type?: string\n rgba255: Uint8Array\n fiberLength?: number\n fiberLengths?: Uint32Array\n fiberDensity?: Float32Array\n fiberDither = 0.1\n fiberColor = 'Global'\n fiberDecimationStride = 1 // e.g. if 2 the 50% of streamlines visible, if 3 then 1/3rd\n fiberSides = 5 // 1=streamline, 2=imposter, >2=mesh(cylinder with fiberSides sides)\n fiberRadius = 0 // in mm, e.g. 3 means 6mm diameter fibers, ignored if fiberSides < 3\n fiberOcclusion = 0 // value 0..1 to simulate ambient occlusion\n f32PerVertex = 5 // MUST be 5 or 7: number of float32s per vertex DEPRECATED, future releases will ALWAYS be 5\n dpsThreshold = NaN\n fiberMask?: unknown[]\n colormap?: ColorMap | LegacyConnectome | string | null\n dpg?: ValuesArray | null\n dps?: ValuesArray | null\n dpv?: ValuesArray | null\n groups?: ValuesArray | null\n hasConnectome = false\n connectome?: LegacyConnectome | string\n\n // TODO this should somehow get aligned with connectome\n indexCount?: number\n vertexCount = 1\n nodeScale = 4\n edgeScale = 1\n legendLineThickness = 0\n showLegend = true\n nodeColormap = 'warm'\n edgeColormap = 'warm'\n nodeColormapNegative?: string\n edgeColormapNegative?: string\n nodeMinColor?: number\n nodeMaxColor?: number\n edgeMin?: number\n edgeMax?: number\n\n nodes?: LegacyNodes | NVConnectomeNode[]\n\n edges?: number[] | NVConnectomeEdge[]\n\n points?: Point[]\n\n /**\n * @param pts - a 3xN array of vertex positions (X,Y,Z coordinates).\n * @param tris - a 3xN array of triangle indices (I,J,K; indexed from zero). Each triangle generated from three vertices.\n * @param name - a name for this image. Default is an empty string\n * @param rgba255 - the base color of the mesh. RGBA values from 0 to 255. Default is white\n * @param opacity - the opacity for this mesh. default is 1\n * @param visible - whether or not this image is to be visible\n * @param gl - WebGL rendering context\n * @param connectome - specify connectome edges and nodes. Default is null (not a connectome).\n * @param dpg - Data per group for tractography, see TRK format. Default is null (not tractograpgy)\n * @param dps - Data per streamline for tractography, see TRK format. Default is null (not tractograpgy)\n * @param dpv - Data per vertex for tractography, see TRK format. Default is null (not tractograpgy)\n * @param groups - Groups for tractography, see TRK format. Default is null (not tractograpgy)\n * @param colorbarVisible - does this mesh display a colorbar\n * @param anatomicalStructurePrimary - region for mesh. Default is an empty string\n */\n constructor(\n pts: Float32Array,\n tris: Uint32Array,\n name = '',\n rgba255 = new Uint8Array([255, 255, 255, 255]),\n opacity = 1.0,\n visible = true,\n gl: WebGL2RenderingContext,\n connectome: LegacyConnectome | string | null = null,\n dpg: ValuesArray | null = null,\n dps: ValuesArray | null = null,\n dpv: ValuesArray | null = null,\n groups: ValuesArray | null = null,\n colorbarVisible = true,\n anatomicalStructurePrimary = ''\n ) {\n this.anatomicalStructurePrimary = anatomicalStructurePrimary\n this.name = name\n this.colorbarVisible = colorbarVisible\n this.id = uuidv4()\n const obj = NVMeshUtilities.getExtents(pts)\n this.furthestVertexFromOrigin = obj.mxDx\n this.extentsMin = obj.extentsMin\n this.extentsMax = obj.extentsMax\n this.opacity = opacity > 1.0 ? 1.0 : opacity // make sure opacity can't be initialized greater than 1 see: #107 and #117 on github\n this.visible = visible\n this.meshShaderIndex = 0\n this.indexBuffer = gl.createBuffer()!\n this.vertexBuffer = gl.createBuffer()!\n this.vao = gl.createVertexArray()!\n // the VAO binds the vertices and indices as well as describing the vertex layout\n gl.bindVertexArray(this.vao)\n gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer)\n gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer)\n // vertex position: 3 floats X,Y,Z\n gl.enableVertexAttribArray(0)\n\n gl.enableVertexAttribArray(1)\n const f32PerVertex = this.f32PerVertex\n if (f32PerVertex !== 7) {\n // n32\n gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 20, 0)\n // vertex surface normal vector: (also three floats)\n gl.vertexAttribPointer(1, 4, gl.BYTE, true, 20, 12)\n // vertex color\n gl.enableVertexAttribArray(2)\n gl.vertexAttribPointer(2, 4, gl.UNSIGNED_BYTE, true, 20, 16)\n } else {\n gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 28, 0)\n // vertex surface normal vector: (also three floats)\n gl.vertexAttribPointer(1, 3, gl.FLOAT, false, 28, 12)\n // vertex color\n gl.enableVertexAttribArray(2)\n gl.vertexAttribPointer(2, 4, gl.UNSIGNED_BYTE, true, 28, 24)\n }\n gl.bindVertexArray(null) // https://stackoverflow.com/questions/43904396/are-we-not-allowed-to-bind-gl-array-buffer-and-vertex-attrib-array-to-0-in-webgl\n\n this.vaoFiber = gl.createVertexArray()!\n this.offsetPt0 = null\n this.hasConnectome = false\n this.colormapInvert = false\n this.fiberGroupColormap = null\n this.pts = pts\n this.layers = []\n this.type = MeshType.MESH\n this.tris = tris\n if (rgba255[3] < 1) {\n this.rgba255 = rgba255\n this.fiberLength = 2\n this.fiberDither = 0.1\n this.fiberColor = 'Global'\n this.fiberDecimationStride = 1 // e.g. if 2 the 50% of streamlines visible, if 3 then 1/3rd\n this.fiberMask = [] // provide method to show/hide specific fibers\n this.colormap = connectome\n this.dpg = dpg\n this.dps = dps\n this.dpv = dpv\n this.groups = groups\n if (dpg) {\n this.initValuesArray(dpg)\n }\n if (dps) {\n this.initValuesArray(dps)\n }\n if (dpv) {\n this.initValuesArray(dpv)\n }\n if (groups) {\n this.initValuesArray(groups)\n }\n this.offsetPt0 = new Uint32Array(tris)\n this.tris = new Uint32Array(0)\n this.updateFibers(gl)\n // define VAO\n gl.bindVertexArray(this.vaoFiber)\n gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer)\n gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer)\n // vertex position: 3 floats X,Y,Z\n gl.enableVertexAttribArray(0)\n gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 16, 0)\n // vertex color\n gl.enableVertexAttribArray(1)\n gl.vertexAttribPointer(1, 4, gl.UNSIGNED_BYTE, true, 16, 12)\n gl.bindVertexArray(null) // https://stackoverflow.com/questions/43904396/are-we-not-allowed-to-bind-gl-array-buffer-and-vertex-attrib-array-to-0-in-webgl\n return\n } // if fiber not mesh\n if (connectome) {\n this.connectome = connectome\n this.hasConnectome = true\n const keysArray = Object.keys(connectome)\n for (let i = 0, len = keysArray.length; i < len; i++) {\n this[keysArray[i]] = connectome[keysArray[i]]\n }\n }\n this.rgba255 = rgba255\n\n this.updateMesh(gl)\n }\n\n initValuesArray(va: ValuesArray): ValuesArray {\n for (let i = 0; i < va.length; i++) {\n const mn = va[i].vals.reduce((acc, current) => Math.min(acc, current))\n const mx = va[i].vals.reduce((acc, current) => Math.max(acc, current))\n va[i].global_min = mn\n va[i].global_max = mx\n va[i].cal_min = mn\n va[i].cal_max = mx\n }\n return va\n }\n\n // given streamlines (which webGL renders as a single pixel), extrude to cylinders\n linesToCylinders(gl: WebGL2RenderingContext, posClrF32: Float32Array, indices: number[]): void {\n // return Float32Array\n // const posClrF32 four 32-bit components X,Y,Z,C where C is Uint32 with RGBA\n function v4ToV3(v4: vec4): vec3 {\n return vec3.fromValues(v4[0], v4[1], v4[2])\n }\n const primitiveRestart = Math.pow(2, 32) - 1 // for gl.UNSIGNED_INT\n const n_count = indices.length\n let n_line_vtx = 0\n let n_streamlines = 0\n // n.b. each streamline terminates with a `primitiveRestart`, even the final one\n for (let i = 0; i < n_count; i++) {\n if (indices[i] === primitiveRestart) {\n n_streamlines++\n continue\n }\n n_line_vtx++\n }\n const cyl_sides = this.fiberSides\n // next: generate extruded cylinders\n // npt is number of points (vertices) for cylinders\n const npt = cyl_sides * n_line_vtx\n const f32PerVertex = this.f32PerVertex // 7 if NormalXYZ is 3 floats, 5 if normalXYZ is packed into rgb32\n if (f32PerVertex !== 5) {\n throw Error('fiberSides > 1 requires f32PerVertex == 5')\n }\n const f32 = new Float32Array(npt * f32PerVertex) // Each vertex has 5 components: PosX, PosY, PosZ, NormalXYZ, RGBA32\n const u8 = new Uint8Array(f32.buffer) // Each vertex has 7 components: PositionXYZ, NormalXYZ, RGBA32\n let vtx = 0\n //\n // previous vector location\n let prevV4 = vec4.create()\n let currV4 = vec4.create()\n let nextV4 = vec4.create()\n const v1 = vec3.create()\n let prevV2 = vec3.create()\n let node = 0\n const radius = this.fiberRadius\n for (let i = 0; i < n_count; i++) {\n const isLineEnd = indices[i] === primitiveRestart\n if (isLineEnd && node < 1) {\n continue\n } // two restarts in a row!\n let idx = indices[i] * 4 // each posClrF32 has 4 elements X,Y,Z,C\n node++\n if (node <= 1) {\n // first vertex in a streamline, no previous vertex\n prevV4 = vec4.fromValues(posClrF32[idx + 0], posClrF32[idx + 1], posClrF32[idx + 2], posClrF32[idx + 3])\n currV4 = vec4.clone(prevV4)\n if (i + 1 < n_count && indices[i + 1] !== primitiveRestart) {\n idx = indices[i + 1] * 4\n nextV4 = vec4.fromValues(posClrF32[idx + 0], posClrF32[idx + 1], posClrF32[idx + 2], posClrF32[idx + 3])\n vec3.subtract(v1, v4ToV3(prevV4), v4ToV3(nextV4))\n vec3.normalize(v1, v1) // principle axis of cylinder\n prevV2 = NiivueObject3D.getFirstPerpVector(v1)\n }\n continue\n }\n if (isLineEnd) {\n // last vertex of streamline, no next vertex\n nextV4 = vec4.clone(currV4)\n } else {\n nextV4 = vec4.fromValues(posClrF32[idx + 0], posClrF32[idx + 1], posClrF32[idx + 2], posClrF32[idx + 3])\n }\n // mean direction at joint\n // n.b. vec4 -> vec3 we ignore 4th dimension (color)\n vec3.subtract(v1, v4ToV3(prevV4), v4ToV3(nextV4))\n vec3.normalize(v1, v1) // principle axis of cylinder\n // avoid twisted cylinders: ensure v2 as closely aligned with previous v2 as possible\n // method simpler than Frenet–Serret apparatus\n // https://math.stackexchange.com/questions/410530/find-closest-vector-to-a-which-is-perpendicular-to-b\n // const v2 = NiivueObject3D.getFirstPerpVector(v1)\n // 𝐷=𝐴×𝐵, and then 𝐶=𝐵×𝐷. 𝐶 is automatically orthogonal to 𝐵\n const D = vec3.create()\n vec3.cross(D, prevV2, v1)\n const v2 = vec3.create()\n vec3.cross(v2, v1, D)\n prevV2 = vec3.clone(prevV2)\n // the next line of code would create arbitrary v2 that might show twisting\n // v2 = NiivueObject3D.getFirstPerpVector(v1)\n // Get the second perp vector by cross product\n const v3 = vec3.create()\n vec3.cross(v3, v1, v2) // a unit length vector orthogonal to v1 and v2\n vec3.normalize(v3, v3)\n const vtxXYZ = vec3.create()\n for (let j = 0; j < cyl_sides; j++) {\n const c = Math.cos((j / cyl_sides) * 2 * Math.PI)\n const s = Math.sin((j / cyl_sides) * 2 * Math.PI)\n vtxXYZ[0] = radius * (c * v2[0] + s * v3[0])\n vtxXYZ[1] = radius * (c * v2[1] + s * v3[1])\n vtxXYZ[2] = radius * (c * v2[2] + s * v3[2])\n vec3.add(vtxXYZ, v4ToV3(currV4), vtxXYZ)\n const fidx = vtx * f32PerVertex\n f32[fidx + 0] = vtxXYZ[0]\n f32[fidx + 1] = vtxXYZ[1]\n f32[fidx + 2] = vtxXYZ[2]\n // compute normal\n const n3 = vec3.create()\n vec3.subtract(n3, vtxXYZ, v4ToV3(currV4))\n vec3.normalize(n3, n3)\n const fidxU8 = (fidx + 3) * 4 // 4 Uint8 per Float32\n u8[fidxU8 + 0] = n3[0] * 127\n u8[fidxU8 + 1] = n3[1] * 127\n u8[fidxU8 + 2] = n3[2] * 127\n // f32[fidx+3] = normal;\n f32[fidx + 4] = currV4[3]\n // u32[fidx+3] = 65555;\n // u32[fidx+4] = 65555;\n vtx++\n }\n prevV4 = vec4.clone(currV4)\n currV4 = vec4.clone(nextV4)\n if (isLineEnd) {\n node = 0\n }\n }\n // ntri = number of triangles\n // each cylinder is composed of 2 * cyl_sides (e.g. triangular cylinder is 6 triangles)\n // each streamline with n nodes has n-1 cylinders (fencepost)\n // each triangle defined by three indices, each referring to a vertex\n const nidx = (n_line_vtx - n_streamlines) * cyl_sides * 2 * 3\n const idxs = new Uint32Array(nidx)\n let idx = 0\n vtx = 0\n for (let i = 1; i < n_count; i++) {\n if (indices[i] === primitiveRestart) {\n vtx += cyl_sides\n continue\n }\n if (indices[i - 1] === primitiveRestart) {\n // fencepost: do not create indices for first node in each streamline\n continue\n }\n let prevStartVtx = vtx // startOfPreviousCylinder\n let startVtx = vtx + cyl_sides // startOfCurrentCylinder\n const prevStartVtxOverflow = startVtx // startOfCurrentCylinder\n const startVtxOverflow = startVtx + cyl_sides // startOfNextCylinder\n for (let j = 0; j < cyl_sides; j++) {\n // emit triangle with one vertex on previous\n idxs[idx++] = prevStartVtx\n idxs[idx++] = startVtx++\n if (startVtx === startVtxOverflow) {\n startVtx = startVtxOverflow - cyl_sides\n }\n idxs[idx++] = startVtx\n // emit triangle with two vertex on previous\n idxs[idx++] = prevStartVtx++\n if (prevStartVtx === prevStartVtxOverflow) {\n prevStartVtx = prevStartVtxOverflow - cyl_sides\n }\n idxs[idx++] = startVtx\n idxs[idx++] = prevStartVtx\n }\n vtx += cyl_sides\n }\n // copy index and vertex buffer to GPU\n // no need to release: https://registry.khronos.org/OpenGL-Refpages/gl4/html/glBufferData.xhtml\n // any pre-existing data store is deleted\n gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer)\n gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, Uint32Array.from(idxs), gl.STATIC_DRAW)\n gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer)\n // issue1129\n // gl.bufferData(gl.ARRAY_BUFFER, Float32Array.from(f32), gl.STATIC_DRAW)\n gl.bufferData(gl.ARRAY_BUFFER, u8, gl.STATIC_DRAW)\n this.indexCount = nidx\n } // linesToCylinders\n\n createFiberDensityMap(): void {\n // generate a fiber density map\n // the array fiberDensity has one element per vertex\n // this provides the normalized (0..1) neighboring vertices\n if (this.fiberDensity) {\n return\n }\n const pts = this.pts\n const npt = pts.length / 3 // each point has three components: X,Y,Z\n let maxExtentsRange = 0\n for (let i = 0; i < 3; i++) {\n const range = this.extentsMax[i] - this.extentsMin[i]\n maxExtentsRange = Math.max(maxExtentsRange, range)\n }\n this.fiberDensity = new Float32Array(npt)\n if (maxExtentsRange === 0) {\n return\n }\n // DSI-Studio counts vertex density per voxel\n // However, some tract formats do not store voxel dimensions\n // therefore, we will create a 3D volume of size bins*bins*bins\n const bins = 64\n const binWidth = maxExtentsRange / (bins - 1)\n const half = binWidth / 2\n const scale = (bins - 1) / maxExtentsRange\n let densityMap = new Float32Array(bins * bins * bins)\n const mn = [this.extentsMin[0] - half, this.extentsMin[1] - half, this.extentsMin[2] - half]\n // sum density map\n const xyz = [0, 0, 0]\n const prevVx = -1\n const binsXbins = bins * bins\n let j = 0\n for (let i = 0; i < npt; i++) {\n xyz[0] = Math.round((pts[j++] - mn[0]) * scale)\n xyz[1] = Math.round((pts[j++] - mn[1]) * scale)\n xyz[2] = Math.round((pts[j++] - mn[2]) * scale)\n const vx = xyz[0] + xyz[1] * bins + xyz[2] * binsXbins\n if (vx === prevVx) {\n // each streamline contributes once per voxel\n continue\n }\n densityMap[vx]++\n }\n function blur3D(vol: Float32Array, dim: number): Float32Array {\n // let raw = vol.slice()\n let raw = vol.slice()\n let v = -1\n const dim1 = dim - 1\n // blur in x\n for (let z = 0; z < dim; z++) {\n for (let y = 0; y < dim; y++) {\n for (let x = 0; x < dim; x++) {\n v++\n if (x < 1 || x >= dim1) {\n continue\n }\n vol[v] = raw[v - 1] + raw[v] + raw[v] + raw[v + 1]\n }\n }\n }\n // blur in y\n v = -1\n raw = vol.slice()\n for (let z = 0; z < dim; z++) {\n for (let y = 0; y < dim; y++) {\n for (let x = 0; x < dim; x++) {\n v++\n if (y < 1 || y >= dim1) {\n continue\n }\n vol[v] = raw[v - dim] + raw[v] + raw[v] + raw[v + dim]\n }\n }\n }\n // blur in z\n const dimXdim = dim * dim\n v = -1\n raw = vol.slice()\n for (let z = 0; z < dim; z++) {\n for (let y = 0; y < dim; y++) {\n for (let x = 0; x < dim; x++) {\n v++\n if (z < 1 || z >= dim1) {\n continue\n }\n vol[v] = raw[v - dimXdim] + raw[v] + raw[v] + raw[dimXdim]\n }\n }\n }\n return vol\n }\n densityMap = blur3D(densityMap, bins)\n densityMap = blur3D(densityMap, bins)\n // let raw = densityMap.slice()\n let mx = 0\n let mn0 = Infinity\n const binsXbinsXbins = bins * bins * bins\n for (let i = 0; i < binsXbinsXbins; i++) {\n if (densityMap[i] <= 0) {\n continue\n }\n mx = Math.max(mx, densityMap[i])\n mn0 = Math.min(mn0, densityMap[i])\n }\n // console.log('Maximum streamlines in a voxel:', mx, mn0)\n if (mx <= 1 || mx <= mn0) {\n // no neighbors: no ambient occlusion\n return\n }\n j = 0\n for (let i = 0; i < binsXbinsXbins; i++) {\n // least occluded vertices should have no occlusion\n densityMap[i] = Math.max(0, densityMap[i] - mn0)\n }\n mx -= mn0\n for (let i = 0; i < npt; i++) {\n xyz[0] = Math.round((pts[j++] - mn[0]) * scale)\n xyz[1] = Math.round((pts[j++] - mn[1]) * scale)\n xyz[2] = Math.round((pts[j++] - mn[2]) * scale)\n const vx = xyz[0] + xyz[1] * bins + xyz[2] * binsXbins\n this.fiberDensity[i] = densityMap[vx] / mx\n }\n }\n\n // not included in public docs\n // internal function filters tractogram to identify which color and visibility of streamlines\n updateFibers(gl: WebGL2RenderingContext): void {\n if (!this.offsetPt0 || !this.fiberLength) {\n return\n }\n const pts = this.pts\n const offsetPt0 = this.offsetPt0\n const n_count = offsetPt0.length - 1\n const npt = pts.length / 3 // each point has three components: X,Y,Z\n // only once: compute length of each streamline\n if (!this.fiberLengths) {\n this.fiberLengths = new Uint32Array(n_count)\n for (let i = 0; i < n_count; i++) {\n // for each streamline\n const vStart3 = offsetPt0[i] * 3 // first vertex in streamline\n const vEnd3 = (offsetPt0[i + 1] - 1) * 3 // last vertex in streamline\n let len = 0\n for (let j = vStart3; j < vEnd3; j += 3) {\n const v = vec3.fromValues(pts[j + 0] - pts[j + 3], pts[j + 1] - pts[j + 4], pts[j + 2] - pts[j + 5])\n len += vec3.len(v)\n }\n this.fiberLengths[i] = len\n }\n } // only once: compute length of each streamline\n // determine fiber colors\n // Each streamline vertex has color and position attributes\n // Interleaved Vertex Data https://developer.apple.com/library/archive/documentation/3DDrawing/Conceptual/OpenGLES_ProgrammingGuide/TechniquesforWorkingwithVertexData/TechniquesforWorkingwithVertexData.html\n const posClrF32 = new Float32Array(npt * 4) // four 32-bit components X,Y,Z,C\n const posClrU32 = new Uint32Array(posClrF32.buffer) // typecast of our X,Y,Z,C array\n // fill XYZ position of XYZC array\n let i3 = 0\n let i4 = 0\n for (let i = 0; i < npt; i++) {\n posClrF32[i4 + 0] = pts[i3 + 0]\n posClrF32[i4 + 1] = pts[i3 + 1]\n posClrF32[i4 + 2] = pts[i3 + 2]\n i3 += 3\n i4 += 4\n }\n // fill fiber Color\n const dither = this.fiberDither\n const ditherHalf = dither * 0.5\n function rgb2int32(r: number, g: number, b: number): number {\n const ditherFrac = dither * Math.random()\n const d = 255.0 * (ditherFrac - ditherHalf)\n r = Math.max(Math.min(r + d, 255.0), 0.0)\n g = Math.max(Math.min(g + d, 255.0), 0.0)\n b = Math.max(Math.min(b + d, 255.0), 0.0)\n return r + (g << 8) + (b << 16)\n }\n function direction2rgb(x1: number, y1: number, z1: number, x2: number, y2: number, z2: number, ditherFrac: number): number {\n // generate color based on direction between two 3D spatial positions\n const v = vec3.fromValues(Math.abs(x1 - x2), Math.abs(y1 - y2), Math.abs(z1 - z2))\n vec3.normalize(v, v)\n const r = ditherFrac - ditherHalf\n for (let j = 0; j < 3; j++) {\n v[j] = 255 * Math.max(Math.min(Math.abs(v[j]) + r, 1.0), 0.0)\n }\n return v[0] + (v[1] << 8) + (v[2] << 16)\n } // direction2rgb()\n // Determine color: local, global, dps0, dpv0, etc.\n const fiberColor = this.fiberColor.toLowerCase()\n let dps: Float32Array | null = null\n let dpv: ValuesArray[0] | null = null\n if (fiberColor.startsWith('dps') && this.dps && this.dps.length > 0) {\n const n = parseInt(fiberColor.substring(3))\n if (n < this.dps.length && this.dps[n].vals.length === n_count) {\n dps = this.dps[n].vals\n }\n }\n if (fiberColor.startsWith('dpv') && this.dpv && this.dpv.length > 0) {\n const n = parseInt(fiberColor.substring(3))\n if (n < this.dpv.length && this.dpv[n].vals.length === npt) {\n dpv = this.dpv[n]\n }\n }\n const streamlineVisible = new Int16Array(n_count)\n if ((this.groups && this.fiberGroupColormap !== null) || (fiberColor.startsWith('dpg') && this.dpg.length > 0)) {\n const lut = new Uint8ClampedArray(this.groups.length * 4) // 4 component RGBA for each group\n const groupVisible = new Array(this.groups.length).fill(false)\n if (this.fiberGroupColormap) {\n const cmap = this.fiberGroupColormap\n if (cmap.A === undefined) {\n cmap.A = Array.from(new Uint8ClampedArray(cmap.I.length).fill(255))\n }\n for (let i = 0; i < cmap.I.length; i++) {\n let idx = cmap.I[i]\n if (idx < 0 || idx >= this.groups.length) {\n continue\n }\n if (cmap.A[i] < 1) {\n continue\n }\n groupVisible[idx] = true\n idx *= 4\n lut[idx] = cmap.R[i]\n lut[idx + 1] = cmap.G[i]\n lut[idx + 2] = cmap.B[i]\n lut[idx + 3] = 255 // opaque\n }\n } else {\n if (fiberColor.startsWith('dpg') && this.dpg.length > 0) {\n const n = parseInt(fiberColor.substring(3))\n const dpg = n < this.dpg.length ? this.dpg[n] : this.dpg[0]\n const lut255 = cmapper.colormap(this.colormap as string, this.colormapInvert)\n const mn = dpg.cal_min\n const mx = dpg.cal_max\n const ngroup = this.groups.length\n for (let i = 0; i < ngroup; i++) {\n const v = dpg.vals[i]\n if (v < mn) {\n continue\n }\n let color255 = Math.round(255 * Math.min(Math.max((v - mn!) / (mx! - mn!), 0), 1))\n groupVisible[i] = true\n const idx = i * 4\n // read RGB values from chosen colormap\n color255 *= 4\n lut[idx] = lut255[color255 + 0]\n lut[idx + 1] = lut255[color255 + 1]\n lut[idx + 2] = lut255[color255 + 2]\n lut[idx + 3] = 255 // opaque\n }\n }\n }\n streamlineVisible.fill(-1) // -1 assume streamline not visible\n for (let i = 0; i < this.groups.length; i++) {\n if (!groupVisible[i]) {\n continue\n } // this group is not visible\n const group = this.groups[i]\n for (let v = 0; v < group.vals.length; v++) {\n streamlineVisible[group.vals[v]] = i\n }\n }\n for (let i = 0; i < n_count; i++) {\n if (streamlineVisible[i] < 0) {\n continue\n } // hidden\n const color = (streamlineVisible[i] % 256) * 4\n // let RGBA = lut[color] + (lut[color + 1] << 8) + (lut[color + 2] << 16);\n const RGBA = rgb2int32(lut[color], lut[color + 1], lut[color + 2])\n const vStart = offsetPt0[i] // first vertex in streamline\n const vEnd = offsetPt0[i + 1] - 1 // last vertex in streamline\n const vStart4 = vStart * 4 + 3 // +3: fill 4th component colors: XYZC = 0123\n const vEnd4 = vEnd * 4 + 3\n for (let j = vStart4; j <= vEnd4; j += 4) {\n posClrU32[j] = RGBA\n }\n }\n } else if (dpv) {\n // color per vertex\n const lut = cmapper.colormap(this.colormap as string, this.colormapInvert)\n const mn = dpv.cal_min\n const mx = dpv.cal_max\n let v4 = 3 // +3: fill 4th component colors: XYZC = 0123\n for (let i = 0; i < npt; i++) {\n let color = Math.min(Math.max((dpv.vals[i] - mn!) / (mx! - mn!), 0), 1)\n color = Math.round(Math.max(Math.min(255, color * 255))) * 4\n const RGBA = lut[color] + (lut[color + 1] << 8) + (lut[color + 2] << 16)\n posClrU32[v4] = RGBA\n v4 += 4\n }\n } else if (dps) {\n const lut = cmapper.colormap(this.colormap as string, this.colormapInvert)\n let mn = dps[0]\n let mx = dps[0]\n for (let i = 0; i < n_count; i++) {\n mn = Math.min(mn, dps[i])\n mx = Math.max(mx, dps[i])\n }\n if (mx === mn) {\n mn -= 1\n } // avoid divide by zero\n for (let i = 0; i < n_count; i++) {\n let color = (dps[i] - mn) / (mx - mn)\n color = Math.round(Math.max(Math.min(255, color * 255))) * 4\n const RGBA = lut[color] + (lut[color + 1] << 8) + (lut[color + 2] << 16)\n const vStart = offsetPt0[i] // first vertex in streamline\n const vEnd = offsetPt0[i + 1] - 1 // last vertex in streamline\n const vStart4 = vStart * 4 + 3 // +3: fill 4th component colors: XYZC = 0123\n const vEnd4 = vEnd * 4 + 3\n for (let j = vStart4; j <= vEnd4; j += 4) {\n posClrU32[j] = RGBA\n }\n }\n } else if (fiberColor.includes('fixed')) {\n if (dither === 0.0) {\n const RGBA = this.rgba255[0] + (this.rgba255[1] << 8) + (this.rgba255[2] << 16)\n let v4 = 3 // +3: fill 4th component colors: XYZC = 0123\n for (let i = 0; i < npt; i++) {\n posClrU32[v4] = RGBA\n v4 += 4\n }\n } else {\n for (let i = 0; i < n_count; i++) {\n const RGBA = rgb2int32(this.rgba255[0], this.rgba255[1], this.rgba255[2])\n const vStart = offsetPt0[i] // first vertex in streamline\n const vEnd = offsetPt0[i + 1] - 1 // last vertex in streamline\n const vStart4 = vStart * 4 + 3 // +3: fill 4th component colors: XYZC = 0123\n const vEnd4 = vEnd * 4 + 3\n for (let j = vStart4; j <= vEnd4; j += 4) {\n posClrU32[j] = RGBA\n }\n }\n } // else fixed with dither\n } else if (fiberColor.includes('local')) {\n for (let i = 0; i < n_count; i++) {\n // for each streamline\n const vStart = offsetPt0[i] // first vertex in streamline\n const vEnd = offsetPt0[i + 1] - 1 // last vertex in streamline\n let v3 = vStart * 3 // pts have 3 components XYZ\n const vEnd3 = vEnd * 3\n const ditherFrac = dither * Math.random() // same dither amount throughout line\n // for first point, we do not have a prior sample\n let RGBA = direction2rgb(pts[v3], pts[v3 + 1], pts[v3 + 2], pts[v3 + 4], pts[v3 + 5], pts[v3 + 6], ditherFrac)\n let v4 = vStart * 4 + 3 // +3: fill 4th component colors: XYZC = 0123\n while (v3 < vEnd3) {\n posClrU32[v4] = RGBA\n v4 += 4 // stride is 4 32-bit values: float32 XYZ and 32-bit rgba\n v3 += 3 // read next vertex\n // direction estimated based on previous and next vertex\n RGBA = direction2rgb(pts[v3 - 3], pts[v3 - 2], pts[v3 - 1], pts[v3 + 3], pts[v3 + 4], pts[v3 + 5], ditherFrac)\n }\n posClrU32[v4] = posClrU32[v4 - 4]\n }\n } else {\n // if color is local direction, else global\n for (let i = 0; i < n_count; i++) {\n // for each streamline\n const vStart = offsetPt0[i] // first vertex in streamline\n const vEnd = offsetPt0[i + 1] - 1 // last vertex in streamline\n const vStart3 = vStart * 3 // pts have 3 components XYZ\n const vEnd3 = vEnd * 3\n const RGBA = direction2rgb(pts[vStart3], pts[vStart3 + 1], pts[vStart3 + 2], pts[vEnd3], pts[vEnd3 + 1], pts[vEnd3 + 2], dither * Math.random())\n const vStart4 = vStart * 4 + 3 // +3: fill 4th component colors: XYZC = 0123\n const vEnd4 = vEnd * 4 + 3\n for (let j = vStart4; j <= vEnd4; j += 4) {\n posClrU32[j] = RGBA\n }\n }\n }\n // SHADING: ambient occlusion\n if (this.fiberOcclusion > 0) {\n this.createFiberDensityMap()\n function shadeRGBA(rgba: number, frac: number): number {\n const r = frac * (rgba & 0xff)\n const g = frac * ((rgba >> 8) & 0xff)\n const b = frac * ((rgba >> 16) & 0xff)\n return r + (g << 8) + (b << 16)\n }\n for (let i = 0; i < n_count; i++) {\n // for each streamline\n const vStart = offsetPt0[i] // first vertex in streamline\n const vEnd = offsetPt0[i + 1] - 1 // last vertex in streamline\n const vStart4 = vStart * 4 + 3 // +3: fill 4th component colors: XYZC = 0123\n const vEnd4 = vEnd * 4 + 3\n let vtx = vStart\n const bias = Math.min(this.fiberOcclusion, 0.99)\n for (let j = vStart4; j <= vEnd4; j += 4) {\n let shade = this.fiberDensity[vtx++]\n if (shade <= 0) {\n continue\n }\n // Schlick's fast bias function\n // https://github.com/ayamflow/schlick-curve\n shade = shade / ((1.0 / bias - 2.0) * (1.0 - shade) + 1.0)\n const frac = 1 - Math.min(shade, 0.9)\n // console.log(shade, frac)\n let RGBA = posClrU32[j]\n RGBA = shadeRGBA(RGBA, frac)\n posClrU32[j] = RGBA\n }\n }\n }\n // issue 1426\n if (Number.isFinite(this.dpsThreshold) && this.dps) {\n if (!dps) {\n // if color is not based on dps, threshold on first dps\n const n = 0\n if (this.dps[n].vals.length === n_count) {\n dps = this.dps[n].vals\n }\n }\n if (dps) {\n for (let i = 0; i < n_count; i++) {\n if (dps[i] < this.dpsThreshold) {\n streamlineVisible[i] = -1\n }\n }\n }\n }\n // INDICES:\n const min_mm = this.fiberLength\n // https://blog.spacepatroldelta.com/a?ID=00950-d878555f-a97a-4e32-9f40-fd9a449cb4fe\n const primitiveRestart = Math.pow(2, 32) - 1 // for gl.UNSIGNED_INT\n const indices: number[] = []\n let stride = -1\n for (let i = 0; i < n_count; i++) {\n // let n_pts = offsetPt0[i + 1] - offsetPt0[i]; //if streamline0 starts at point 0 and streamline1 at point 4, then streamline0 has 4 points: 0,1,2,3\n if (streamlineVisible[i] < 0) {\n continue\n }\n if (this.fiberLengths[i] < min_mm) {\n continue\n }\n stride++\n if (stride % this.fiberDecimationStride !== 0) {\n continue\n } // e.g. if stride is 2 then half culled\n for (let j = offsetPt0[i]; j < offsetPt0[i + 1]; j++) {\n indices.push(j)\n }\n indices.push(primitiveRestart)\n }\n if (this.fiberSides > 2 && this.fiberRadius > 0) {\n this.linesToCylinders(gl, posClrF32, indices)\n } else {\n // copy streamlines to GPU\n this.indexCount = indices.length\n gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer)\n gl.bufferData(gl.ARRAY_BUFFER, Uint32Array.from(posClrU32), gl.STATIC_DRAW)\n gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer)\n gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, Uint32Array.from(indices), gl.STATIC_DRAW)\n }\n } // updateFibers()\n\n // given X,Y,Z coordinates in world space, return index of nearest vertex as well as\n // the distance of this closest vertex to the coordinates\n indexNearestXYZmm(Xmm: number, Ymm: number, Zmm: number): number[] {\n const pts = this.pts\n const nvtx = this.pts.length / 3\n let i = 0\n let mnDx = Infinity\n let mnIdx = 0\n for (let j = 0; j < nvtx; j++) {\n const dx = Math.pow(pts[i] - Xmm, 2) + Math.pow(pts[i + 1] - Ymm, 2) + Math.pow(pts[i + 2] - Zmm, 2)\n if (dx < mnDx) {\n mnDx = dx\n mnIdx = j\n }\n i += 3\n }\n // Pythagorean theorem sqrt(x^2+y^2+z^2)\n // only calculate sqrt once\n mnDx = Math.sqrt(mnDx)\n return [mnIdx, mnDx]\n } // indexNearestXYZmm()\n\n // internal function discards GPU resources\n unloadMesh(gl: WebGL2RenderingContext): void {\n // free WebGL resources\n gl.bindBuffer(gl.ARRAY_BUFFER, null)\n gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null)\n gl.bindVertexArray(null)\n gl.deleteBuffer(this.vertexBuffer)\n gl.deleteBuffer(this.indexBuffer)\n gl.deleteVertexArray(this.vao)\n gl.deleteVertexArray(this.vaoFiber)\n // presumably, if we null the mesh we dereference all the arrays, or do we have to explicitly null arrays\n this.offsetPt0 = null\n this.tris = null\n this.pts = null\n if (this.layers && this.layers.length > 0) {\n for (let i = 0; i < this.layers.length; i++) {\n this.layers[i].values = null\n }\n }\n if (this.dpg && this.dpg.length > 0) {\n for (let i = 0; i < this.dpg.length; i++) {\n this.dpg[i].vals = null\n }\n }\n if (this.dps && this.dps.length > 0) {\n for (let i = 0; i < this.dps.length; i++) {\n this.dps[i].vals = null\n }\n }\n }\n\n // apply color lookup table to convert scalar array to RGBA array\n scalars2RGBA(rgba: Uint8ClampedArray, layer: NVMeshLayer, scalars: AnyNumberArray, isNegativeCmap: boolean = false): Uint8ClampedArray {\n const nValues = scalars.length\n if (4 * nValues < rgba.length) {\n log.error(`colormap2RGBA incorrectly specified ${nValues}*4 != ${rgba.length}`)\n return rgba\n }\n const opa255 = Math.round(layer.opacity * 255)\n let mn = layer.cal_min\n let mx = layer.cal_max\n let lut = cmapper.colormap(layer.colormap as string, this.colormapInvert)\n let flip = 1\n if (isNegativeCmap) {\n if (!layer.useNegativeCmap) {\n return rgba\n }\n flip = -1\n lut = cmapper.colormap(layer.colormapNegative, layer.colormapInvert)\n mn = layer.cal_min\n mx = layer.cal_max\n if (isFinite(layer.cal_minNeg) && isFinite(layer.cal_minNeg)) {\n mn = -layer.cal_minNeg\n mx = -layer.cal_maxNeg\n }\n }\n let mnCal = mn\n if (!layer.isTransparentBelowCalMin) {\n mnCal = Number.NEGATIVE_INFINITY\n }\n const isTranslucentBelowMin = layer.colormapType === COLORMAP_TYPE.ZERO_TO_MAX_TRANSLUCENT_BELOW_MIN\n\n if (layer.colormapType !== COLORMAP_TYPE.MIN_TO_MAX) {\n mn = Math.min(mn, 0.0)\n }\n const scale255 = 255.0 / (mx - mn)\n for (let j = 0; j < nValues; j++) {\n let v = scalars[j] * flip\n if (isNaN(v)) {\n continue\n }\n let opa = opa255\n if (v < mnCal) {\n if (v > 0 && isTranslucentBelowMin) {\n opa = Math.round(layer.opacity * 255 * Math.pow(v / mnCal, 2.0))\n } else {\n continue\n }\n }\n v = (v - mn) * scale255\n if (v < 0 && layer.isTransparentBelowCalMin) {\n continue\n }\n v = Math.min(255, Math.max(0, Math.round(v))) * 4\n const idx = j * 4\n rgba[idx + 0] = lut[v + 0]\n rgba[idx + 1] = lut[v + 1]\n rgba[idx + 2] = lut[v + 2]\n rgba[idx + 3] = opa\n }\n return rgba\n }\n\n blendColormap(u8: Uint8Array, additiveRGBA: Uint8Array, layer: NVMeshLayer, mn: number, mx: number, lut: Uint8ClampedArray, invert: boolean = false): void {\n const nvtx = this.pts.length / 3\n const opacity = Math.min(layer.opacity, 1.0)\n function lerp(x: number, y: number, a: number): number {\n // https://www.khronos.org/registry/OpenGL-Refpages/gl4/html/mix.xhtml\n return x * (1 - a) + y * a\n }\n function additiveBlend(x: number, y: number): number {\n return Math.min(x + y, 255.0)\n }\n const scaleFlip = invert ? -1 : 1\n const frame = Math.min(Math.max(layer.frame4D, 0), layer.nFrame4D - 1)\n const frameOffset = nvtx * frame\n let mnCal = mn\n if (!layer.isTransparentBelowCalMin) {\n mnCal = Number.NEGATIVE_INFINITY\n }\n if (layer.colormapType !== COLORMAP_TYPE.MIN_TO_MAX) {\n mn = Math.min(mn, 0.0)\n }\n const scale255 = 255.0 / (mx - mn)\n // create border map for optional outline\n let borders = new Array(nvtx).fill(false)\n if (layer.outlineBorder !== 0.0) {\n const v255s = new Uint8Array(nvtx).fill(0)\n for (let j = 0; j < nvtx; j++) {\n const v = scaleFlip * layer.values[j + frameOffset]\n if (v >= mnCal) {\n v255s[j] = 1\n }\n }\n borders = NVMeshUtilities.getClusterBoundaryU8(v255s, this.tris)\n for (let j = 0; j < nvtx; j++) {\n const v = scaleFlip * layer.values[j + frameOffset]\n if (v < mnCal) {\n borders[j] = false\n }\n }\n }\n // create lookup table for translucency\n const alphas = new Float32Array(256).fill(opacity)\n if (mnCal > mn && layer.colormapType === COLORMAP_TYPE.ZERO_TO_MAX_TRANSLUCENT_BELOW_MIN) {\n let minOpaque = Math.round((mnCal - mn) * scale255)\n minOpaque = Math.max(minOpaque, 1)\n for (let j = 1; j < minOpaque; j++) {\n alphas[j] = opacity * Math.pow(j / minOpaque, 2.0)\n }\n alphas[0] = 0\n mnCal = mn + Number.EPSILON\n }\n for (let j = 0; j < nvtx; j++) {\n const v = scaleFlip * layer.values[j + frameOffset]\n if (v < mnCal) {\n continue\n }\n let v255 = Math.round((v - mn) * scale255)\n if (v255 < 0 && layer.isTransparentBelowCalMin) {\n continue\n }\n v255 = Math.max(0.0, v255)\n v255 = Math.min(255.0, v255)\n let opa = alphas[v255]\n v255 *= 4\n let vtx = j * 28 + 24 // posNormClr is 28 bytes stride, RGBA color at offset 24,\n if (this.f32PerVertex !== 7) {\n vtx = j * 20 + 16\n }\n if (layer.isAdditiveBlend) {\n const j4 = j * 4\n // sum red, green and blue layers\n additiveRGBA[j4 + 0] = additiveBlend(additiveRGBA[j4 + 0], lut[v255 + 0])\n additiveRGBA[j4 + 1] = additiveBlend(additiveRGBA[j4 + 1], lut[v255 + 1])\n additiveRGBA[j4 + 2] = additiveBlend(additiveRGBA[j4 + 2], lut[v255 + 2])\n additiveRGBA[j4 + 3] = additiveBlend(additiveRGBA[j4 + 3], 255.0)\n } else {\n if (borders[j]) {\n opa = layer.outlineBorder\n if (layer.outlineBorder < 0) {\n u8[vtx + 0] = 0\n u8[vtx + 1] = 0\n u8[vtx + 2] = 0\n continue\n }\n }\n u8[vtx + 0] = lerp(u8[vtx + 0], lut[v255 + 0], opa)\n u8[vtx + 1] = lerp(u8[vtx + 1], lut[v255 + 1], opa)\n u8[vtx + 2] = lerp(u8[vtx + 2], lut[v255 + 2], opa)\n }\n }\n } // blendColormap()\n\n // internal function filters mesh to identify which color of triangulated mesh vertices\n updateMesh(gl: WebGL2RenderingContext): void {\n if (this.offsetPt0) {\n this.updateFibers(gl)\n return // fiber not mesh\n }\n if (this.hasConnectome) {\n // this.updateConnectome(gl)\n return // connectome not mesh\n }\n if (!this.pts || !this.tris || !this.rgba255) {\n log.warn('underspecified mesh')\n return\n }\n function lerp(x: number, y: number, a: number): number {\n // https://www.khronos.org/registry/OpenGL-Refpages/gl4/html/mix.xhtml\n return x * (1 - a) + y * a\n }\n const posNormClr = this.generatePosNormClr(this.pts, this.tris, this.rgba255)\n const nvtx = this.pts.length / 3\n const u8 = new Uint8Array(posNormClr.buffer) // Each vertex has 7 components: PositionXYZ, NormalXYZ, RGBA32\n // create emission values\n // let posNormClrEmission = posNormClr.slice();\n let maxAdditiveBlend = 0\n const additiveRGBA = new Uint8Array(nvtx * 4) // emission\n let tris = this.tris\n if (this.layers && this.layers.length > 0) {\n for (let i = 0; i < this.layers.length; i++) {\n const layer = this.layers[i]\n const opacity = layer.opacity\n if (opacity <= 0.0 || layer.cal_min > layer.cal_max) {\n continue\n }\n if (layer.outlineBorder === undefined) {\n layer.outlineBorder = 0\n }\n if (layer.isAdditiveBlend === undefined) {\n layer.isAdditiveBlend = false\n }\n // build a label colormap\n if (layer.colormapLabel && (layer.colormapLabel as ColorMap).R && !(layer.colormapLabel as LUT).lut) {\n // convert colormap JSON to RGBA LUT\n layer.colormapLabel = cmapper.makeLabelLut(layer.colormapLabel as ColorMap, 255, layer.global_max)\n }\n if (layer.colormapLabel && (layer.colormapLabel as LUT).lut) {\n const colormapLabel = layer.colormapLabel as LUT\n let minv = 0\n if (layer.colormapLabel.min) {\n minv = layer.colormapLabel.min\n }\n let lut = colormapLabel.lut\n const nLabel = Math.floor(lut.length / 4)\n if (layer.atlasValues && nLabel > 0 && nLabel === layer.atlasValues.length && layer.colormap) {\n const atlasValues = layer.atlasValues\n let hasNaN = false\n let onlyNaN = true\n for (let j = 0; j < nLabel; j++) {\n if (isNaN(atlasValues[j])) {\n hasNaN = true\n } else {\n onlyNaN = false\n }\n }\n if (onlyNaN) {\n log.debug(`invisible mesh: all atlasValues are NaN.`)\n return\n }\n if (hasNaN) {\n log.debug(`some vertices have NaN atlasValues (mesh will be decimated).`)\n // First: identify all vertices mapped to NaN\n const nanVtxs = new Array(nvtx).fill(false)\n for (let j = 0; j < nvtx; j++) {\n const v = Math.round(layer.values[j]) - minv\n if (isNaN(atlasValues[v])) {\n nanVtxs[j] = true\n }\n }\n\n // next: find all triangle indices that have NaN vertices\n const nanIdxs = new Array(tris.length).fill(false)\n for (let j = 0; j < tris.length; j++) {\n if (nanVtxs[tris[j]]) {\n nanIdxs[j] = true\n }\n }\n // each triangle has 3 indices\n const trisIn = this.tris\n let nTriOK = 0\n for (let j = 0; j < trisIn.length; j += 3) {\n if (!nanIdxs[j] && !nanIdxs[j + 1] && !nanIdxs[j + 2]) {\n nTriOK++\n }\n }\n if (nTriOK === 0) {\n log.debug(`invisible mesh: all triangles of a vertex with a NaN atlasValue.`)\n }\n tris = new Uint32Array(nTriOK * 3)\n let k = 0\n for (let j = 0; j < trisIn.length; j += 3) {\n if (!nanIdxs[j] && !nanIdxs[j + 1] && !nanIdxs[j + 2]) {\n tris[k++] = trisIn[j]\n tris[k++] = trisIn[j + 1]\n tris[k++] = trisIn[j + 2]\n }\n }\n }\n lut.fill(0) // make all transparent\n lut = this.scalars2RGBA(lut, layer, atlasValues)\n if (layer.useNegativeCmap) {\n lut = this.scalars2RGBA(lut, layer, atlasValues, true)\n }\n } else if (layer.atlasValues) {\n log.warn(`Expected ${nLabel} atlasValues but got ${layer.atlasValues.length} for mesh layer`)\n }\n // create labels for legend\n if (layer.showLegend && nLabel === layer.colormapLabel.labels.length) {\n layer.labels = []\n for (let j = 0; j < nLabel; j++) {\n const rgba = Array.from(lut.slice(j * 4, j * 4 + 4)).map((v) => v / 255)\n const labelName = layer.colormapLabel.labels[j]\n // xyzMM is the center of mass for the label\n // For folded cortical regions, this point often lies within the volume\n const xyzMM = [0, 0, 0]\n let count = 0\n for (let i = 0; i < nvtx; i++) {\n if (layer.values[i] === j) {\n const idx = i * 3\n xyzMM[0] += this.pts[idx]\n xyzMM[1] += this.pts[idx + 1]\n xyzMM[2] += this.pts[idx + 2]\n count++\n }\n }\n if (count > 0) {\n xyzMM[0] /= count\n xyzMM[1] /= count\n xyzMM[2] /= count\n }\n if (\n rgba[3] === 0 ||\n !labelName || // handles empty string, null, undefined\n labelName.startsWith('_')\n ) {\n continue\n }\n rgba[3] = 1\n const label = new NVLabel3D(\n labelName,\n {\n textColor: rgba,\n bulletScale: 1,\n bulletColor: rgba,\n lineWidth: 0,\n lineColor: rgba,\n textScale: 1.0,\n textAlignment: LabelTextAlignment.LEFT,\n lineTerminator: LabelLineTerminator.NONE\n },\n xyzMM\n )\n layer.labels.push(label)\n log.debug('label for mesh layer:', label)\n } // for each label\n } else {\n delete layer.labels\n }\n const frame = Math.min(Math.max(layer.frame4D, 0), layer.nFrame4D - 1)\n const frameOffset = nvtx * frame\n const rgba8 = new Uint8Array(nvtx * 4)\n let k = 0\n for (let j = 0; j < nvtx; j++) {\n // eslint-disable-next-line\n const v = layer.values[j + frameOffset] - minv\n const idx = 4 * Math.min(Math.max(v, 0), nLabel - 1)\n rgba8[k + 0] = lut[idx + 0]\n rgba8[k + 1] = lut[idx + 1]\n rgba8[k + 2] = lut[idx + 2]\n rgba8[k + 3] = Math.round((lut[idx + 3] / 255) * layer.opacity * 255)\n k += 4\n }\n let opaque = new Array(nvtx).fill(false)\n if (layer.outlineBorder !== 0.0) {\n opaque = NVMeshUtilities.getClusterBoundary(rgba8, this.tris)\n }\n k = 0\n for (let j = 0; j < nvtx; j++) {\n let vtx = j * 28 + 24 // posNormClr is 28 bytes stride, RGBA color at offset 24,\n if (this.f32PerVertex !== 7) {\n vtx = j * 20 + 16\n }\n let opa = rgba8[k + 3] / 255\n if (opaque[j]) {\n opa = layer.outlineBorder\n if (layer.outlineBorder < 0) {\n u8[vtx + 0] = 0\n u8[vtx + 1] = 0\n u8[vtx + 2] = 0\n k += 4\n continue\n }\n }\n u8[vtx + 0] = lerp(u8[vtx + 0], rgba8[k + 0], opa)\n u8[vtx + 1] = lerp(u8[vtx + 1], rgba8[k + 1], opa)\n u8[vtx + 2] = lerp(u8[vtx + 2], rgba8[k + 2], opa)\n k += 4\n } // for each vertex\n continue\n } // if colormapLabel\n if (layer.values instanceof Uint8Array) {\n const rgba8 = new Uint8Array(layer.values.buffer)\n let opaque = new Array(nvtx).fill(true)\n if (layer.outlineBorder !== 0) {\n opaque = NVMeshUtilities.getClusterBoundary(rgba8, this.tris)\n }\n let k = 0\n for (let j = 0; j < layer.values.length; j++) {\n let vtx = j * 28 + 24 // posNormClr is 28 bytes stride, RGBA color at offset 24,\n if (this.f32PerVertex !== 7) {\n vtx = j * 20 + 16\n }\n let opa = opacity\n if (opaque[j]) {\n opa = layer.outlineBorder\n if (layer.outlineBorder < 0) {\n u8[vtx + 0] = 0\n u8[vtx + 1] = 0\n u8[vtx + 2] = 0\n k += 4\n continue\n }\n }\n u8[vtx + 0] = lerp(u8[vtx + 0], rgba8[k + 0], opa)\n u8[vtx + 1] = lerp(u8[vtx + 1], rgba8[k + 1], opa)\n u8[vtx + 2] = lerp(u8[vtx + 2], rgba8[k + 2], opa)\n k += 4\n }\n continue\n }\n if (layer.useNegativeCmap) {\n layer.cal_min = Math.max(Number.EPSILON, layer.cal_min)\n layer.cal_max = Math.max(layer.cal_min + 0.000001, layer.cal_max)\n }\n if (layer.isTransparentBelowCalMin === undefined) {\n layer.isTransparentBelowCalMin = true\n }\n const lut = cmapper.colormap(layer.colormap, layer.colormapInvert)\n if (layer.isAdditiveBlend) {\n maxAdditiveBlend++\n }\n this.blendColormap(u8, additiveRGBA, layer, layer.cal_min, layer.cal_max, lut)\n if (layer.useNegativeCmap) {\n const neglut = cmapper.colormap(layer.colormapNegative, layer.colormapInvert)\n let mn = layer.cal_min\n let mx = layer.cal_max\n if (isFinite(layer.cal_minNeg) && isFinite(layer.cal_minNeg)) {\n mn = -layer.cal_minNeg\n mx = -layer.cal_maxNeg\n }\n this.blendColormap(u8, additiveRGBA, layer, mn, mx, neglut, true)\n }\n }\n }\n if (maxAdditiveBlend > 0) {\n for (let j = 0; j < nvtx; j++) {\n let vtx = j * 28 + 24 // posNormClr is 28 bytes stride, RGBA color at offset 24,\n if (this.f32PerVertex !== 7) {\n vtx = j * 20 + 16\n }\n const v = j * 4 // additiveRGBA is 4 bytes stride, RGBA color at offset 0,\n const opacity = Math.min(maxAdditiveBlend, additiveRGBA[v + 3] / 255)\n if (opacity <= 0) {\n continue\n }\n function modulate(x: number, y: number): number {\n return Math.min(x * y * (1 / 255), 255.0)\n }\n u8[vtx + 0] = modulate(u8[vtx + 0], additiveRGBA[v + 0])\n u8[vtx + 1] = modulate(u8[vtx + 1], additiveRGBA[v + 1])\n u8[vtx + 2] = modulate(u8[vtx + 2], additiveRGBA[v + 2])\n u8[vtx + 0] = lerp(u8[vtx + 0], additiveRGBA[v + 0], opacity)\n u8[vtx + 1] = lerp(u8[vtx + 1], additiveRGBA[v + 1], opacity)\n u8[vtx + 2] = lerp(u8[vtx + 2], additiveRGBA[v + 2], opacity)\n }\n } // isAdditiveBlend\n // generate webGL buffers and vao\n gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer)\n gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, Uint32Array.from(tris), gl.STATIC_DRAW)\n gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer)\n // issue1129\n // gl.bufferData(gl.ARRAY_BUFFER, Float32Array.from(posNormClr), gl.STATIC_DRAW)\n gl.bufferData(gl.ARRAY_BUFFER, u8, gl.STATIC_DRAW)\n this.indexCount = tris.length\n this.vertexCount = this.pts.length\n } // updateMesh()\n\n // internal function filters mesh to identify which color of triangulated mesh vertices\n reverseFaces(gl: WebGL2RenderingContext): void {\n if (this.offsetPt0) {\n return\n } // fiber not mesh\n if (this.hasConnectome) {\n return\n } // connectome not mesh\n const tris = this.tris || [] // TODO tris should probably be assigned in the constructor\n for (let j = 0; j < tris.length; j += 3) {\n const tri = tris[j]\n tris[j] = tris[j + 1]\n tris[j + 1] = tri\n }\n this.updateMesh(gl) // apply the new properties...\n }\n\n hierarchicalOrder(): number {\n const V0 = 12\n const F0 = 20\n const nF = this.tris.length / 3\n const order = Math.log(nF / F0) / Math.log(4)\n // Sanity checks\n if (nF !== Math.pow(4, order) * F0) {\n return NaN\n }\n const nV = this.pts.length / 3\n if (nV !== Math.pow(4, order) * (V0 - 2) + 2) {\n return NaN\n }\n // next checks are in case FreeSurfer was optimized with more local face indices\n // for an example see BrainMesh_ICBM152.lh.mz3\n for (let i = 0; i < 15; i += 3) {\n if (this.tris[i] !== 0) {\n return NaN\n }\n }\n for (let i = 15; i < 24; i += 3) {\n if (this.tris[i] !== 3) {\n return NaN\n }\n }\n for (let i = 24; i < 30; i += 3) {\n if (this.tris[i] !== 4) {\n return NaN\n }\n }\n return order\n }\n\n decimateFaces(n: number, ntarget: number): void {\n let fac = this.tris\n // Constants for the icosahedron\n const V0 = 12\n const F0 = 20\n for (let j = n - 1; j >= ntarget; j--) {\n const nVjprev = Math.pow(4, j + 1) * (V0 - 2) + 2\n const nVj = Math.pow(4, j) * (V0 - 2) + 2\n const nFjprev = fac.length / 3 // = 4^(j+1)*F0\n const nFj = Math.pow(4, j) * F0\n\n log.info(`order ${j + 1} -> ${j} vertices ${nVjprev} -> ${nVj} faces ${nFjprev} -> ${nFj}`)\n\n const remap = Array.from({ length: nVjprev }, (_, i) => i + 1)\n\n for (let i = 0; i < nFjprev; i++) {\n const v1 = fac[3 * i]\n const v2 = fac[3 * i + 1]\n const v3 = fac[3 * i + 2]\n remap[v1 - 1] = Math.min(remap[v1 - 1], v2, v3)\n }\n\n const facJ = new Uint32Array(nFj * 3)\n for (let i = 0; i < nFj; i++) {\n facJ[3 * i] = remap[fac[3 * i] - 1]\n facJ[3 * i + 1] = remap[fac[3 * i + 1] - 1]\n facJ[3 * i + 2] = remap[fac[3 * i + 2] - 1]\n }\n fac = facJ\n }\n this.tris = new Uint32Array(fac)\n }\n\n // internal function simplifies FreeSurfer triangulated mesh and overlays\n decimateHierarchicalMesh(gl: WebGL2RenderingContext, order: number = 4): boolean {\n const inputOrder = this.hierarchicalOrder()\n if (isNaN(inputOrder)) {\n log.warn('Unable to decimate mesh: it does not have a hierarchical structure')\n return false\n }\n if (order >= inputOrder) {\n log.warn(`Unable to decimate mesh: input order (${inputOrder}) must be larger than downsampled order (${order})`)\n return false\n }\n const inputVLength = this.pts.length / 3\n const V0 = 12\n const nV = Math.pow(4, order) * (V0 - 2) + 2\n this.pts = new Float32Array(this.pts.slice(0, nV * 3))\n this.decimateFaces(inputOrder, order)\n if (this.layers && this.layers.length > 0) {\n for (let i = 0; i < this.layers.length; i++) {\n const layer = this.layers[i]\n if (layer.values instanceof Float32Array || layer.values.length !== inputVLength) {\n layer.values = new Float32Array(layer.values.slice(0, nV))\n } else {\n log.warn(`decimation logic needs to be updated`)\n }\n }\n }\n this.updateMesh(gl) // apply the new properties...\n return true\n }\n\n // adjust attributes of a mesh layer. invoked by niivue.setMeshLayerProperty()\n // TODO this method is a bit too generic\n async setLayerProperty(id: number, key: keyof NVMeshLayer, val: number | string | boolean, gl: WebGL2RenderingContext): Promise<void> {\n const layer = this.layers[id]\n if (!layer || !(key in layer)) {\n log.warn('mesh does not have property ', key, ' for layer ', layer)\n return\n }\n if (key === 'colormapLabel') {\n if (typeof val === 'object') {\n // assume JSON\n layer[key] = cmapper.makeLabelLut(val, 255, layer.global_max)\n } else if (typeof val === 'string') {\n // assume URL\n const cmap = await cmapper.makeLabelLutFromUrl(val)\n layer[key] = cmap\n this.updateMesh(gl) // apply the new properties...\n return\n } else {\n log.error('colormapLabel requires a string or object')\n }\n } else {\n // @ts-expect-error TODO generic property access\n layer[key] = val\n }\n this.updateMesh(gl) // apply the new properties...\n }\n\n // adjust mesh attributes. invoked by niivue.setMeshProperty(()\n // TODO this method is too generic\n setProperty(key: keyof this, val: number | string | boolean | Uint8Array | number[] | ColorMap | LegacyConnectome | Float32Array, gl: WebGL2RenderingContext): void {\n if (!(key in this)) {\n console.warn('Mesh does not have property:', key, this)\n return\n }\n /* if (typeof val !== 'number' && typeof val !== 'string' && typeof val !== 'boolean' && !Array.isArray(val)) {\n console.warn('Invalid value type. Expected number, numbers, string, or boolean but received:', typeof val)\n return\n } */\n ;(this as any)[key] = val // TypeScript safety workaround\n this.updateMesh(gl) // Apply the new properties\n }\n\n // Each streamline vertex has color, normal and position attributes\n // Interleaved Vertex Data https://developer.apple.com/library/archive/documentation/3DDrawing/Conceptual/OpenGLES_ProgrammingGuide/TechniquesforWorkingwithVertexData/TechniquesforWorkingwithVertexData.html\n generatePosNormClr(pts: Float32Array, tris: Uint32Array, rgba255: Uint8Array): Float32Array {\n if (pts.length < 3 || rgba255.length < 4) {\n log.error('Catastrophic failure generatePosNormClr()')\n log.debug('this', this)\n log.debug('pts', pts)\n log.debug('rgba', rgba255)\n }\n const norms = NVMeshUtilities.generateNormals(pts, tris)\n const npt = pts.length / 3\n const isPerVertexColors = npt === rgba255.length / 4\n // n32\n const f32PerVertex = this.f32PerVertex // 7 if NormalXYZ is 3 floats, 5 if normalXYZ is packed into rgb32\n const f32 = new Float32Array(npt * f32PerVertex) // Each vertex has 7 components: PositionXYZ, NormalXYZ, RGBA32\n const u8 = new Uint8Array(f32.buffer) // Each vertex has 7 components: PositionXYZ, NormalXYZ, RGBA32\n let p = 0 // input position\n let c = 0 // input color\n let f = 0 // output float32 location (position and normals)\n let u = (f32PerVertex - 1) * 4 // output uint8 location (colors), offset 24 as after 3*position+3*normal\n for (let i = 0; i < npt; i++) {\n f32[f + 0] = pts[p + 0]\n f32[f + 1] = pts[p + 1]\n f32[f + 2] = pts[p + 2]\n if (f32PerVertex !== 7) {\n u8[u - 4] = norms[p + 0] * 127\n u8[u - 3] = norms[p + 1] * 127\n u8[u - 2] = norms[p + 2] * 127\n } else {\n f32[f + 3] = norms[p + 0]\n f32[f + 4] = norms[p + 1]\n f32[f + 5] = norms[p + 2]\n }\n u8[u] = rgba255[c + 0]\n u8[u + 1] = rgba255[c + 1]\n u8[u + 2] = rgba255[c + 2]\n u8[u + 3] = rgba255[c + 3]\n if (isPerVertexColors) {\n c += 4\n }\n p += 3 // read 3 input components: XYZ\n f += f32PerVertex // write 7 output components: 3*Position, 3*Normal, 1*RGBA\n u += f32PerVertex * 4 // stride of 28 bytes\n }\n return f32\n }\n\n // wrapper to read meshes, tractograms and connectomes regardless of format\n static async readMesh(buffer: ArrayBuffer, name: string, gl: WebGL2RenderingContext, opacity = 1.0, rgba255 = new Uint8Array([255, 255, 255, 255]), visible = true): Promise<NVMesh> {\n let tris: Uint32Array = new Uint32Array([])\n let pts: Float32Array = new Float32Array([])\n let anatomicalStructurePrimary = ''\n let obj: TCK | TRACT | TT | TRX | TRK | GII | MZ3 | X3D | VTK | DefaultMeshType\n const re = /(?:\\.([^.]+))?$/\n let ext = re.exec(name)![1]\n ext = ext.toUpperCase()\n if (ext === 'GZ') {\n ext = re.exec(name.slice(0, -3))![1] // img.trk.gz -> img.trk\n ext = ext.toUpperCase()\n }\n if (ext === 'JCON') {\n // return NVMesh.loadConnectomeFromJSON(JSON.parse(new TextDecoder().decode(buffer)), gl, name, opacity)\n log.error('you should never see this message: load using nvconnectome not nvmesh')\n }\n if (ext === 'JSON') {\n // return NVMesh.loadConnectomeFromFreeSurfer(JSON.parse(new TextDecoder().decode(buffer)), gl, name, opacity)\n log.error('you should never see this message: load using nvconnectome not nvmesh')\n }\n rgba255[3] = Math.max(1, rgba255[3])\n if (ext === 'TCK' || ext === 'TRK' || ext === 'TT' || ext === 'TRX' || ext === 'TRACT') {\n if (ext === 'TCK') {\n obj = NVMeshLoaders.readTCK(buffer)\n } else if (ext === 'TRACT') {\n obj = NVMeshLoaders.readTRACT(buffer)\n } else if (ext === 'TT') {\n obj = await NVMeshLoaders.readTT(buffer)\n } else if (ext === 'TRX') {\n obj = await NVMeshLoaders.readTRX(buffer)\n } else {\n obj = await NVMeshLoaders.readTRK(buffer)\n }\n if (typeof obj === 'undefined') {\n const pts = new Float32Array([0, 0, 0, 0, 0, 0])\n const offsetPt0 = new Uint32Array([0])\n obj = { pts, offsetPt0 }\n log.error('Creating empty tracts')\n }\n rgba255[3] = 0.0\n return new NVMesh(\n obj.pts,\n obj.offsetPt0,\n name,\n rgba255, // colormap,\n opacity, // opacity,\n visible, // visible,\n gl,\n 'inferno',\n (obj as TRX).dpg || null,\n (obj as TRX).dps || null,\n (obj as TRX).dpv || null,\n (obj as TRX).groups\n )\n } // is fibers\n if (ext === 'GII') {\n obj = await NVMeshLoaders.readGII(buffer)\n } else if (ext === 'MZ3') {\n obj = await NVMeshLoaders.readMZ3(buffer)\n if (!('positions' in obj)) {\n log.warn('MZ3 does not have positions (statistical overlay?)')\n }\n } else if (ext === 'ASC') {\n obj = NVMeshLoaders.readASC(buffer)\n } else if (ext === 'DFS') {\n obj = NVMeshLoaders.readDFS(buffer)\n } else if (ext === 'BYU' || ext === 'G') {\n obj = NVMeshLoaders.readGEO(buffer)\n } else if (ext === 'GEO') {\n obj = NVMeshLoaders.readGEO(buffer, true)\n } else if (ext === 'ICO' || ext === 'TRI') {\n obj = NVMeshLoaders.readICO(buffer)\n } else if (ext === 'OFF') {\n obj = NVMeshLoaders.readOFF(buffer)\n } else if (ext === 'NV') {\n obj = NVMeshLoaders.readNV(buffer)\n } else if (ext === 'OBJ') {\n obj = await NVMeshLoaders.readOBJ(buffer)\n } else if (ext === 'PLY') {\n obj = NVMeshLoaders.readPLY(buffer)\n } else if (ext === 'WRL') {\n obj = NVMeshLoaders.readWRL(buffer)\n } else if (ext === 'X3D') {\n obj = NVMeshLoaders.readX3D(buffer)\n } else if (ext === 'FIB' || ext === 'VTK') {\n obj = NVMeshLoaders.readVTK(buffer)\n if ('offsetPt0' in obj) {\n // VTK files used both for meshes and streamlines\n rgba255[3] = 0.0\n return new NVMesh(\n obj.pts,\n obj.offsetPt0,\n name,\n rgba255, // colormap,\n opacity, // opacity,\n visible, // visible,\n gl,\n 'inferno'\n )\n } // if streamlines, not mesh\n } else if (ext === 'SRF') {\n obj = await NVMeshLoaders.readSRF(buffer)\n } else if (ext === 'STL') {\n obj = NVMeshLoaders.readSTL(buffer)\n } else {\n obj = NVMeshLoaders.readFreeSurfer(buffer)\n } // freesurfer hail mary\n if ((obj as GII).anatomicalStructurePrimary) {\n anatomicalStructurePrimary = (obj as GII).anatomicalStructurePrimary\n }\n if (obj instanceof Float32Array) {\n throw new Error('fatal: unknown mesh type loaded')\n }\n\n if (!('positions' in obj)) {\n throw new Error('positions not loaded')\n }\n if (!obj.indices) {\n throw new Error('indices not loaded')\n }\n\n pts = obj.positions\n tris = obj.indices\n\n if ('rgba255' in obj && obj.rgba255.length > 0) {\n // e.g. x3D format\n // rgba255 = Array.from(obj.rgba255)\n rgba255 = obj.rgba255\n }\n if ('colors' in obj && obj.colors && obj.colors.length === pts.length) {\n const n = pts.length / 3\n rgba255 = new Uint8Array(n * 4)\n let c = 0\n let k = 0\n for (let i = 0; i < n; i++) {\n // convert ThreeJS unit RGB to RGBA255\n rgba255[k++] = obj.colors[c] * 255 // red\n rgba255[k++] = obj.colors[c + 1] * 255 // green\n rgba255[k++] = obj.colors[c + 2] * 255 // blue\n rgba255[k++] = 255 // alpha\n c += 3\n } // for i: each vertex\n } // obj includes colors\n const npt = pts.length / 3\n const ntri = tris.length / 3\n if (ntri < 1 || npt < 3) {\n throw new Error('Mesh should have at least one triangle and three vertices')\n }\n rgba255[3] = Math.max(1, rgba255[3]) // mesh not streamline\n const nvm = new NVMesh(\n pts,\n tris,\n name,\n rgba255, // colormap,\n opacity, // opacity,\n visible, // visible,\n gl,\n null, // connectome\n null, // dpg\n null, // dps\n null, // dpv\n null, // groups\n true, // colorbarVisible\n anatomicalStructurePrimary\n )\n if ('scalars' in obj && obj.scalars.length > 0) {\n const newLayer = await NVMeshLoaders.readLayer(name, buffer, nvm, opacity, 'gray')\n if (typeof newLayer === 'undefined') {\n log.warn('readLayer() failed to convert scalars')\n } else {\n nvm.layers.push(newLayer)\n nvm.updateMesh(gl)\n }\n }\n return nvm\n }\n\n static async loadLayer(layer: NVMeshLayer, nvmesh: NVMesh): Promise<void> {\n let buffer = new Uint8Array().buffer\n\n function base64ToArrayBuffer(base64: string): ArrayBuffer {\n const binary_string = window.atob(base64)\n const len = binary_string.length\n const bytes = new Uint8Array(len)\n for (let i = 0; i < len; i++) {\n bytes[i] = binary_string.charCodeAt(i)\n }\n return bytes.buffer\n }\n\n if (layer.base64 !== undefined) {\n // populate buffer with base64 if exists\n buffer = base64ToArrayBuffer(layer.base64)\n } else {\n if (!layer.url) {\n throw new Error('layer: missing url')\n }\n // fetch url otherwise\n const response = await fetch(layer.url, { headers: layer.headers })\n if (!response.ok) {\n throw Error(response.statusText)\n }\n buffer = await response.arrayBuffer()\n }\n\n let layerName: string\n let urlParts: string[] = []\n if (layer.name && layer.name !== '') {\n layerName = layer.name\n } else {\n if (!layer.url) {\n throw new Error('layer: missing url')\n }\n // urlParts = layer.url.split(\"/\");\n // layerName = urlParts.slice(-1)[0];\n try {\n // if a full url like https://domain/path/file.nii.gz?query=filter\n // parse the url and get the pathname component without the query\n urlParts = new URL(layer.url).pathname.split('/')\n } catch (e) {\n // if a relative url then parse the path (assuming no query)\n urlParts = layer.url.split('/')\n } finally {\n layerName = urlParts.slice(-1)[0]\n }\n }\n if (layerName.indexOf('?') > -1) {\n layerName = layerName.slice(0, layerName.indexOf('?')) // remove query string if any\n }\n\n let opacity = 0.5\n if ('opacity' in layer) {\n opacity = layer.opacity\n }\n let colormap = 'warm'\n if ('colormap' in layer) {\n colormap = layer.colormap!\n }\n let colormapNegative = 'winter'\n if ('colormapNegative' in layer) {\n colormapNegative = layer.colormapNegative!\n }\n let useNegativeCmap = false\n if ('useNegativeCmap' in layer) {\n useNegativeCmap = layer.useNegativeCmap!\n }\n let cal_min: number | null = null\n if ('cal_min' in layer) {\n cal_min = layer.cal_min\n }\n let cal_max: number | null = null\n if ('cal_max' in layer) {\n cal_max = layer.cal_max\n }\n\n const newLayer = await NVMeshLoaders.readLayer(layerName, buffer, nvmesh, opacity, colormap, colormapNegative, useNegativeCmap, cal_min, cal_max)\n if (newLayer) {\n nvmesh.layers.push(newLayer)\n }\n }\n\n /**\n * factory function to load and return a new NVMesh instance from a given URL\n */\n static async loadFromUrl({\n url = '',\n headers = {},\n gl,\n name = '',\n opacity = 1.0,\n rgba255 = [255, 255, 255, 255],\n visible = true,\n layers = [],\n buffer = new ArrayBuffer(0),\n meshShaderIndex = 0\n }: Partial<LoadFromUrlParams> = {}): Promise<NVMesh> {\n let urlParts = url.split('/') // split url parts at slash\n if (name === '') {\n try {\n // if a full url like https://domain/path/file.nii.gz?query=filter\n // parse the url and get the pathname component without the query\n urlParts = new URL(url).pathname.split('/')\n } catch (e) {\n // if a relative url then parse the path (assuming no query)\n urlParts = url.split('/')\n }\n name = urlParts.slice(-1)[0] // name will be last part of url (e.g. some/url/image.nii.gz --> image.nii.gz\n if (name.indexOf('?') > -1) {\n name = name.slice(0, name.indexOf('?')) // remove query string if any\n }\n }\n if (url === '') {\n throw Error('url must not be empty')\n }\n if (!gl) {\n throw Error('gl context is null')\n }\n let buff\n if (buffer.byteLength > 0) {\n buff = buffer\n } else {\n // TRX format is special (its a zip archive of multiple files)\n const response = await fetch(url, { headers })\n if (!response.ok) {\n throw Error(response.statusText)\n }\n buff = await response.arrayBuffer()\n }\n const nvmesh = await this.readMesh(buff, name, gl, opacity, new Uint8Array(rgba255), visible)\n\n // Set mesh shader index if provided\n if (meshShaderIndex !== undefined) {\n nvmesh.meshShaderIndex = meshShaderIndex\n }\n\n if (!layers || layers.length < 1) {\n return nvmesh\n }\n\n for (let i = 0; i < layers.length; i++) {\n await NVMesh.loadLayer(layers[i], nvmesh)\n }\n\n // apply the new properties\n nvmesh.updateMesh(gl)\n return nvmesh\n }\n\n // not included in public docs\n // loading Nifti files\n static async readFileAsync(file: Blob): Promise<ArrayBuffer> {\n return new Promise((resolve, reject) => {\n const reader = new FileReader()\n\n reader.onload = (): void => {\n resolve(reader.result as ArrayBuffer)\n }\n\n reader.onerror = reject\n\n reader.readAsArrayBuffer(file)\n })\n }\n\n /**\n * factory function to load and return a new NVMesh instance from a file in the browser\n *\n * @returns NVMesh instance\n */\n static async loadFromFile({ file, gl, name = '', opacity = 1.0, rgba255 = [255, 255, 255, 255], visible = true, layers = [] }: Partial<LoadFromFileParams> = {}): Promise<NVMesh> {\n if (!file) {\n throw new Error('file must be set')\n }\n if (!gl) {\n throw new Error('rendering context must be set')\n }\n\n const buffer = await NVMesh.readFileAsync(file)\n const nvmesh = await NVMesh.readMesh(buffer, name, gl, opacity, new Uint8Array(rgba255), visible)\n\n if (!layers || layers.length < 1) {\n return nvmesh\n }\n\n for (let i = 0; i < layers.length; i++) {\n await NVMesh.loadLayer(layers[i], nvmesh)\n }\n\n // apply the new properties\n nvmesh.updateMesh(gl)\n return nvmesh\n }\n\n /**\n * load and return a new NVMesh instance from a base64 encoded string\n */\n async loadFromBase64({ base64, gl, name = '', opacity = 1.0, rgba255 = [255, 255, 255, 255], visible = true, layers = [] }: Partial<LoadFromBase64Params> = {}): Promise<NVMesh> {\n if (!base64) {\n throw new Error('base64 must bet set')\n }\n if (!gl) {\n throw new Error('rendering context must be set')\n }\n\n // https://stackoverflow.com/questions/21797299/convert-base64-string-to-arraybuffer\n function base64ToArrayBuffer(base64: string): ArrayBuffer {\n const binary_string = window.atob(base64)\n const len = binary_string.length\n const bytes = new Uint8Array(len)\n for (let i = 0; i < len; i++) {\n bytes[i] = binary_string.charCodeAt(i)\n }\n return bytes.buffer\n }\n\n const buffer = base64ToArrayBuffer(base64)\n const nvmesh = await NVMesh.readMesh(buffer, name, gl, opacity, new Uint8Array(rgba255), visible)\n\n if (!layers || layers.length < 1) {\n return nvmesh\n }\n for (let i = 0; i < layers.length; i++) {\n await NVMesh.loadLayer(layers[i], nvmesh)\n }\n\n // apply new properties\n nvmesh.updateMesh(gl)\n return nvmesh\n }\n}\n","import { mat4, vec3, vec4 } from 'gl-matrix'\n\ntype Geometry = {\n vertexBuffer: WebGLBuffer\n indexBuffer: WebGLBuffer\n indexCount: number\n vao: WebGLVertexArrayObject | null\n}\n\n/**\n * Represents a 3D object rendered with WebGL, including geometry, transformations, and rendering state.\n * Used internally by Niivue for rendering meshes and crosshairs.\n */\nexport class NiivueObject3D {\n static BLEND = 1\n static CULL_FACE = 2\n static CULL_FRONT = 4\n static CULL_BACK = 8\n static ENABLE_DEPTH_TEST = 16\n\n sphereIdx: number[] = []\n sphereVtx: number[] = []\n renderShaders: number[] = []\n isVisible = true\n isPickable = true\n vertexBuffer: WebGLVertexArrayObject\n indexCount: number\n indexBuffer: WebGLVertexArrayObject | null\n vao: WebGLVertexArrayObject | null\n mode: number\n glFlags = 0\n id: number\n colorId: [number, number, number, number]\n\n modelMatrix = mat4.create()\n scale = [1, 1, 1]\n position = [0, 0, 0]\n rotation = [0, 0, 0]\n rotationRadians = 0.0\n\n extentsMin: number[] = []\n extentsMax: number[] = []\n\n // TODO needed through NVImage\n furthestVertexFromOrigin?: number\n originNegate?: vec3\n fieldOfViewDeObliqueMM?: vec3\n\n // TODO needed through crosshairs in NiiVue\n mm?: vec4\n\n constructor(id: number, vertexBuffer: WebGLBuffer, mode: number, indexCount: number, indexBuffer: WebGLVertexArrayObject | null = null, vao: WebGLVertexArrayObject | null = null) {\n this.vertexBuffer = vertexBuffer\n this.indexCount = indexCount\n this.indexBuffer = indexBuffer\n this.vao = vao\n this.mode = mode\n\n this.id = id\n this.colorId = [((id >> 0) & 0xff) / 255.0, ((id >> 8) & 0xff) / 255.0, ((id >> 16) & 0xff) / 255.0, ((id >> 24) & 0xff) / 255.0]\n }\n\n static generateCrosshairs = function (gl: WebGL2RenderingContext, id: number, xyzMM: vec4, xyzMin: vec3, xyzMax: vec3, radius: number, sides = 20, gap = 0): NiivueObject3D {\n const geometry = NiivueObject3D.generateCrosshairsGeometry(gl, xyzMM, xyzMin, xyzMax, radius, sides, gap)\n return new NiivueObject3D(id, geometry.vertexBuffer, gl.TRIANGLES, geometry.indexCount, geometry.indexBuffer, geometry.vao)\n }\n\n // not included in public docs\n static generateCrosshairsGeometry = function (gl: WebGL2RenderingContext, xyzMM: vec4, xyzMin: vec3, xyzMax: vec3, radius: number, sides = 20, gap = 0): Geometry {\n const vertices: number[] = []\n const indices: number[] = []\n const gapX = radius * gap\n if (gapX <= 0) {\n // left-right\n let start = vec3.fromValues(xyzMin[0], xyzMM[1], xyzMM[2])\n let dest = vec3.fromValues(xyzMax[0], xyzMM[1], xyzMM[2])\n NiivueObject3D.makeCylinder(vertices, indices, start, dest, radius, sides)\n // anterior-posterior\n start = vec3.fromValues(xyzMM[0], xyzMin[1], xyzMM[2])\n dest = vec3.fromValues(xyzMM[0], xyzMax[1], xyzMM[2])\n NiivueObject3D.makeCylinder(vertices, indices, start, dest, radius, sides)\n // superior-inferior\n start = vec3.fromValues(xyzMM[0], xyzMM[1], xyzMin[2])\n dest = vec3.fromValues(xyzMM[0], xyzMM[1], xyzMax[2])\n NiivueObject3D.makeCylinder(vertices, indices, start, dest, radius, sides)\n } else {\n // left-right\n let start = vec3.fromValues(xyzMin[0], xyzMM[1], xyzMM[2])\n let dest = vec3.fromValues(xyzMM[0] - gapX, xyzMM[1], xyzMM[2])\n NiivueObject3D.makeCylinder(vertices, indices, start, dest, radius, sides, false)\n start = vec3.fromValues(xyzMM[0] + gapX, xyzMM[1], xyzMM[2])\n dest = vec3.fromValues(xyzMax[0], xyzMM[1], xyzMM[2])\n NiivueObject3D.makeCylinder(vertices, indices, start, dest, radius, sides, false)\n // anterior-posterior\n start = vec3.fromValues(xyzMM[0], xyzMin[1], xyzMM[2])\n dest = vec3.fromValues(xyzMM[0], xyzMM[1] - gapX, xyzMM[2])\n NiivueObject3D.makeCylinder(vertices, indices, start, dest, radius, sides, false)\n start = vec3.fromValues(xyzMM[0], xyzMM[1] + gapX, xyzMM[2])\n dest = vec3.fromValues(xyzMM[0], xyzMax[1], xyzMM[2])\n NiivueObject3D.makeCylinder(vertices, indices, start, dest, radius, sides, false)\n // superior-inferior\n start = vec3.fromValues(xyzMM[0], xyzMM[1], xyzMin[2])\n dest = vec3.fromValues(xyzMM[0], xyzMM[1], xyzMM[2] - gapX)\n NiivueObject3D.makeCylinder(vertices, indices, start, dest, radius, sides, false)\n start = vec3.fromValues(xyzMM[0], xyzMM[1], xyzMM[2] + gapX)\n dest = vec3.fromValues(xyzMM[0], xyzMM[1], xyzMax[2])\n NiivueObject3D.makeCylinder(vertices, indices, start, dest, radius, sides, false)\n }\n // console.log('i:',indices.length / 3, 'v:',vertices.length / 3);\n const vertexBuffer = gl.createBuffer()\n if (vertexBuffer === null) {\n throw new Error('could not instantiate vertex buffer')\n }\n\n gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer)\n gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW)\n\n // index buffer allocated in parent class\n const indexBuffer = gl.createBuffer()\n if (indexBuffer === null) {\n throw new Error('could not instantiate index buffer')\n }\n gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer)\n gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint32Array(indices), gl.STATIC_DRAW)\n\n const vao = gl.createVertexArray()\n gl.bindVertexArray(vao)\n gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer)\n gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer)\n // vertex position: 3 floats X,Y,Z\n gl.enableVertexAttribArray(0)\n gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 0, 0)\n gl.bindVertexArray(null) // https://stackoverflow.com/questions/43904396/are-we-not-allowed-to-bind-gl-array-buffer-and-vertex-attrib-array-to-0-in-webgl\n\n return {\n vertexBuffer,\n indexBuffer,\n indexCount: indices.length,\n vao\n }\n }\n\n static getFirstPerpVector = function (v1: vec3): vec3 {\n const v2 = vec3.fromValues(0.0, 0.0, 0.0)\n if (v1[0] === 0.0) {\n v2[0] = 1.0\n } else if (v1[1] === 0.0) {\n v2[1] = 1.0\n } else if (v1[2] === 0.0) {\n v2[2] = 1.0\n } else {\n // If xyz is all set, we set the z coordinate as first and second argument .\n // As the scalar product must be zero, we add the negated sum of x and y as third argument\n v2[0] = v1[2] // scalp = z*x\n v2[1] = v1[2] // scalp = z*(x+y)\n v2[2] = -(v1[0] + v1[1]) // scalp = z*(x+y)-z*(x+y) = 0\n vec3.normalize(v2, v2)\n }\n return v2\n }\n\n static subdivide = function (verts: number[], faces: number[]): void {\n // Subdivide each triangle into four triangles, pushing verts to the unit sphere\"\"\"\n let nv = verts.length / 3\n let nf = faces.length / 3\n const n = nf\n const vNew = vec3.create()\n const nNew = vec3.create()\n for (let faceIndex = 0; faceIndex < n; faceIndex++) {\n // setlength(verts, nv + 3);\n const fx = faces[faceIndex * 3 + 0]\n const fy = faces[faceIndex * 3 + 1]\n const fz = faces[faceIndex * 3 + 2]\n const vx = vec3.fromValues(verts[fx * 3 + 0], verts[fx * 3 + 1], verts[fx * 3 + 2])\n const vy = vec3.fromValues(verts[fy * 3 + 0], verts[fy * 3 + 1], verts[fy * 3 + 2])\n const vz = vec3.fromValues(verts[fz * 3 + 0], verts[fz * 3 + 1], verts[fz * 3 + 2])\n vec3.add(vNew, vx, vy)\n vec3.normalize(nNew, vNew)\n verts.push(...nNew)\n\n vec3.add(vNew, vy, vz)\n vec3.normalize(nNew, vNew)\n verts.push(...nNew)\n\n vec3.add(vNew, vx, vz)\n vec3.normalize(nNew, vNew)\n verts.push(...nNew)\n // Split the current triangle into four smaller triangles:\n let face = [nv, nv + 1, nv + 2]\n faces.push(...face)\n face = [fx, nv, nv + 2]\n faces.push(...face)\n face = [nv, fy, nv + 1]\n faces.push(...face)\n faces[faceIndex * 3 + 0] = nv + 2\n faces[faceIndex * 3 + 1] = nv + 1\n faces[faceIndex * 3 + 2] = fz\n nf = nf + 3\n nv = nv + 3\n }\n }\n\n static weldVertices = function (verts: number[], faces: number[]): number[] {\n // unify identical vertices\n const nv = verts.length / 3\n // yikes: bubble sort! TO DO: see Surfice for more efficient solution\n let nUnique = 0 // first vertex is unique\n // var remap = new Array();\n const remap = new Int32Array(nv)\n for (let i = 0; i < nv - 1; i++) {\n if (remap[i] !== 0) {\n continue\n } // previously tested\n remap[i] = nUnique\n let v = i * 3\n const x = verts[v]\n const y = verts[v + 1]\n const z = verts[v + 2]\n for (let j = i + 1; j < nv; j++) {\n v += 3\n if (x === verts[v] && y === verts[v + 1] && z === verts[v + 2]) {\n remap[j] = nUnique\n }\n }\n nUnique++ // another new vertex\n } // for i\n if (nUnique === nv) {\n return verts\n }\n // console.log('welding vertices removed redundant positions ', nv, '->', nUnique);\n const nf = faces.length\n for (let f = 0; f < nf; f++) {\n faces[f] = remap[faces[f]]\n }\n const vtx = verts.slice(0, nUnique * 3 - 1)\n for (let i = 0; i < nv - 1; i++) {\n const v = i * 3\n const r = remap[i] * 3\n vtx[r] = verts[v]\n vtx[r + 1] = verts[v + 1]\n vtx[r + 2] = verts[v + 2]\n }\n return vtx\n }\n\n static makeSphere = function (vertices: number[], indices: number[], radius: number, origin: vec3 | vec4 = [0, 0, 0]): void {\n let vtx = [\n 0.0, 0.0, 1.0, 0.894, 0.0, 0.447, 0.276, 0.851, 0.447, -0.724, 0.526, 0.447, -0.724, -0.526, 0.447, 0.276, -0.851, 0.447, 0.724, 0.526, -0.447, -0.276, 0.851, -0.447, -0.894, 0.0, -0.447,\n -0.276, -0.851, -0.447, 0.724, -0.526, -0.447, 0.0, 0.0, -1.0\n ]\n // let idx = new Uint16Array([\n const idx = [\n 0, 1, 2, 0, 2, 3, 0, 3, 4, 0, 4, 5, 0, 5, 1, 7, 6, 11, 8, 7, 11, 9, 8, 11, 10, 9, 11, 6, 10, 11, 6, 2, 1, 7, 3, 2, 8, 4, 3, 9, 5, 4, 10, 1, 5, 6, 7, 2, 7, 8, 3, 8, 9, 4, 9, 10, 5, 10, 6, 1\n ]\n NiivueObject3D.subdivide(vtx, idx)\n NiivueObject3D.subdivide(vtx, idx)\n vtx = NiivueObject3D.weldVertices(vtx, idx)\n\n for (let i = 0; i < vtx.length; i++) {\n vtx[i] = vtx[i] * radius\n }\n const nvtx = vtx.length / 3\n let j = 0\n for (let i = 0; i < nvtx; i++) {\n vtx[j] = vtx[j] + origin[0]\n j++\n vtx[j] = vtx[j] + origin[1]\n j++\n vtx[j] = vtx[j] + origin[2]\n j++\n }\n const idx0 = Math.floor(vertices.length / 3) // first new vertex will be AFTER previous vertices\n for (let i = 0; i < idx.length; i++) {\n idx[i] = idx[i] + idx0\n }\n\n indices.push(...idx)\n vertices.push(...vtx)\n }\n\n static makeCylinder = function (vertices: number[], indices: number[], start: vec3, dest: vec3, radius: number, sides = 20, endcaps = true): void {\n if (sides < 3) {\n sides = 3\n } // prism is minimal 3D cylinder\n const v1 = vec3.create()\n vec3.subtract(v1, dest, start)\n vec3.normalize(v1, v1) // principle axis of cylinder\n const v2 = NiivueObject3D.getFirstPerpVector(v1) // a unit length vector orthogonal to v1\n // Get the second perp vector by cross product\n const v3 = vec3.create()\n vec3.cross(v3, v1, v2) // a unit length vector orthogonal to v1 and v2\n vec3.normalize(v3, v3)\n let num_v = 2 * sides\n let num_f = 2 * sides\n if (endcaps) {\n num_f += 2 * sides\n num_v += 2\n }\n const idx0 = Math.floor(vertices.length / 3) // first new vertex will be AFTER previous vertices\n const idx = new Uint32Array(num_f * 3)\n const vtx = new Float32Array(num_v * 3)\n function setV(i: number, vec3: vec3): void {\n vtx[i * 3 + 0] = vec3[0]\n vtx[i * 3 + 1] = vec3[1]\n vtx[i * 3 + 2] = vec3[2]\n }\n function setI(i: number, a: number, b: number, c: number): void {\n idx[i * 3 + 0] = a + idx0\n idx[i * 3 + 1] = b + idx0\n idx[i * 3 + 2] = c + idx0\n }\n const startPole = 2 * sides\n const destPole = startPole + 1\n if (endcaps) {\n setV(startPole, start)\n setV(destPole, dest)\n }\n const pt1 = vec3.create()\n const pt2 = vec3.create()\n for (let i = 0; i < sides; i++) {\n const c = Math.cos((i / sides) * 2 * Math.PI)\n const s = Math.sin((i / sides) * 2 * Math.PI)\n pt1[0] = radius * (c * v2[0] + s * v3[0])\n pt1[1] = radius * (c * v2[1] + s * v3[1])\n pt1[2] = radius * (c * v2[2] + s * v3[2])\n vec3.add(pt2, start, pt1)\n setV(i, pt2)\n vec3.add(pt2, dest, pt1)\n setV(i + sides, pt2)\n let nxt = 0\n if (i < sides - 1) {\n nxt = i + 1\n }\n setI(i * 2, i, nxt, i + sides)\n setI(i * 2 + 1, nxt, nxt + sides, i + sides)\n if (endcaps) {\n setI(sides * 2 + i, i, startPole, nxt)\n setI(sides * 2 + i + sides, destPole, i + sides, nxt + sides)\n }\n }\n indices.push(...idx)\n vertices.push(...vtx)\n }\n\n static makeColoredCylinder = function (\n vertices: number[],\n indices: number[],\n colors: number[],\n start: vec3,\n dest: vec3,\n radius: number,\n rgba255 = [192, 0, 0, 255],\n sides = 20,\n endcaps = false\n ): void {\n let nv = vertices.length / 3\n NiivueObject3D.makeCylinder(vertices, indices, start, dest, radius, sides, endcaps)\n nv = vertices.length / 3 - nv\n const clrs = []\n for (let i = 0; i < nv * 4 - 1; i += 4) {\n clrs[i] = rgba255[0]\n clrs[i + 1] = rgba255[1]\n clrs[i + 2] = rgba255[2]\n clrs[i + 3] = rgba255[3]\n }\n colors.push(...clrs)\n }\n\n static makeColoredSphere = function (vertices: number[], indices: number[], colors: number[], radius: number, origin: vec3 | vec4 = [0, 0, 0], rgba255 = [0, 0, 192, 255]): void {\n let nv = vertices.length / 3\n NiivueObject3D.makeSphere(vertices, indices, radius, origin)\n nv = vertices.length / 3 - nv\n const clrs = []\n for (let i = 0; i < nv * 4 - 1; i += 4) {\n clrs[i] = rgba255[0]\n clrs[i + 1] = rgba255[1]\n clrs[i + 2] = rgba255[2]\n clrs[i + 3] = rgba255[3]\n }\n colors.push(...clrs)\n }\n}\n","import { vec3 } from 'gl-matrix'\nimport { LUT } from '@/colortables'\n\ntype Extents = {\n mxDx: number\n extentsMin: number | number[]\n extentsMax: number | number[]\n}\n\n/**\n * Utilities class for common mesh functions\n */\nexport class NVMeshUtilities {\n static getClusterBoundaryU8(u8: Uint8Array, faces: number[] | Uint32Array): boolean[] {\n // assume all vertices are not near a border\n const border = new Array(u8.length).fill(false)\n const binary = new Array(u8.length).fill(false)\n for (let i = 0; i < u8.length; i++) {\n if (u8[i] > 0) {\n binary[i] = true\n }\n }\n const nTri = faces.length / 3\n let j = 0\n // interior: a triangle where all three vertices are the same color\n // else, all three vertices are on a border\n for (let i = 0; i < nTri; i++) {\n const v0 = faces[j]\n const v1 = faces[j + 1]\n const v2 = faces[j + 2]\n j += 3\n if (binary[v0] === binary[v1] && binary[v0] === binary[v2] && binary[v1] === binary[v2]) {\n continue\n }\n border[v0] = true\n border[v1] = true\n border[v2] = true\n }\n return border\n }\n\n static async gzip(data: Uint8Array): Promise<Uint8Array> {\n const stream = new CompressionStream('gzip')\n const writer = stream.writable.getWriter()\n writer.write(data).catch(console.error)\n const closePromise = writer.close().catch(console.error)\n const response = new Response(stream.readable)\n const result = new Uint8Array(await response.arrayBuffer())\n await closePromise // Ensure close happens eventually\n return result\n }\n\n static createMZ3(\n vertices: Float32Array,\n indices: Uint32Array,\n compress: boolean = false,\n colors: Uint8Array | null = null,\n scalars: Float32Array | null = null,\n colormapLabel: LUT | null = null\n ): ArrayBuffer {\n // ATTR bit flags (per mz3 spec)\n const IS_FACE = 1 // 0x01\n const IS_VERT = 2 // 0x02\n const IS_RGBA = 4 // 0x04\n const IS_SCALAR = 8 // 0x08\n const IS_LOOKUP = 64 // 0x40\n\n const MAGIC = 23117 // 0x4D5A\n\n const nface = indices ? indices.length / 3 : 0\n const nvert = vertices ? vertices.length / 3 : 0\n\n // Basic validation (keeps behavior similar to original)\n if (!Number.isInteger(nface) || nface < 0) {\n throw new Error('indices length must be a multiple of 3')\n }\n if (!Number.isInteger(nvert) || nvert < 0) {\n throw new Error('vertices length must be a multiple of 3')\n }\n\n const isFace = indices && indices.length > 0\n const isVert = vertices && vertices.length > 0\n const isRGBA = colors instanceof Uint8Array && colors.length === nvert * 4\n\n // Prepare lookup (colormapLabel) bytes if provided\n let lookupBytes: Uint8Array | null = null\n let nskip = 0\n if (colormapLabel != null) {\n const lookupJson = typeof colormapLabel === 'string' ? colormapLabel : JSON.stringify(colormapLabel)\n lookupBytes = new TextEncoder().encode(lookupJson)\n nskip = lookupBytes.byteLength\n }\n\n // scalars: must be Float32Array and its length must be positive multiple of nvert\n let isScalars = false\n let nScalarLayers = 0\n if (scalars != null) {\n if (!(scalars instanceof Float32Array)) {\n throw new Error('scalars must be a Float32Array or null/undefined')\n }\n if (nvert === 0) {\n throw new Error('cannot have scalars without vertices (nvert === 0)')\n }\n if (scalars.length > 0 && scalars.length % nvert === 0) {\n isScalars = true\n nScalarLayers = scalars.length / nvert\n } else {\n throw new Error(`scalars.length (${scalars.length}) must be a positive integer multiple of nvert (${nvert})`)\n }\n }\n\n // Build ATTR bitfield (minimal change from original: use bit flags)\n let attr = 0\n if (isFace) {\n attr |= IS_FACE\n }\n if (isVert) {\n attr |= IS_VERT\n }\n if (isRGBA) {\n attr |= IS_RGBA\n }\n if (isScalars) {\n attr |= IS_SCALAR\n }\n if (lookupBytes) {\n attr |= IS_LOOKUP\n }\n\n // header and block sizes\n const headerSize = 16\n\n const indexSize = isFace ? nface * 3 * 4 : 0 // Uint32 indices\n const vertexSize = isVert ? nvert * 3 * 4 : 0 // Float32 vertices\n const colorSize = isRGBA ? nvert * 4 : 0 // RGBA uint8 per vertex\n const scalarSize = isScalars ? nScalarLayers * nvert * 4 : 0 // NSCALAR * NVERT * 4 bytes (Float32)\n\n const totalSize = headerSize + nskip + indexSize + vertexSize + colorSize + scalarSize\n\n const buffer = new ArrayBuffer(totalSize)\n const writer = new DataView(buffer)\n\n // Write header (all little-endian)\n let pos = 0\n writer.setUint16(pos, MAGIC, true)\n pos += 2\n writer.setUint16(pos, attr, true)\n pos += 2\n writer.setUint32(pos, nface, true)\n pos += 4\n writer.setUint32(pos, nvert, true)\n pos += 4\n writer.setUint32(pos, nskip, true)\n pos += 4\n // pos === headerSize\n\n // Helper cursor\n let offset = headerSize\n\n // Write NSKIP (lookup JSON) directly after header if present\n if (nskip > 0 && lookupBytes) {\n new Uint8Array(buffer, offset, lookupBytes.length).set(lookupBytes)\n offset += nskip\n }\n\n // Write indices\n if (isFace) {\n new Uint32Array(buffer, offset, indices.length).set(indices)\n offset += indexSize\n }\n\n // Write vertices\n if (isVert) {\n new Float32Array(buffer, offset, vertices.length).set(vertices)\n offset += vertexSize\n }\n\n // Write colors (RGBA)\n if (isRGBA) {\n new Uint8Array(buffer, offset, colors!.length).set(colors!)\n offset += colorSize\n }\n\n // Write scalars (if any)\n if (isScalars) {\n new Float32Array(buffer, offset, scalars!.length).set(scalars!)\n offset += scalarSize\n }\n\n // sanity check\n if (offset !== totalSize) {\n throw new Error(`mz3 internal size mismatch: offset ${offset} !== totalSize ${totalSize}`)\n }\n\n if (compress) {\n throw new Error('Call async createMZ3Async() for compression')\n }\n\n return buffer\n }\n\n static async createMZ3Async(\n vertices: Float32Array,\n indices: Uint32Array,\n compress: boolean = false,\n colors: Uint8Array | null = null,\n scalars: Float32Array | null = null,\n colormapLabel: LUT | null = null\n ): Promise<ArrayBuffer> {\n const buffer = this.createMZ3(vertices, indices, compress, colors, scalars, colormapLabel)\n if (compress) {\n return await this.gzip(new Uint8Array(buffer))\n }\n return buffer\n }\n\n static createOBJ(vertices: Float32Array, indices: Uint32Array): ArrayBuffer {\n // generate binary OBJ format mesh\n // n.b. widespread support, but large and slow due to ASCII\n // https://en.wikipedia.org/wiki/Wavefront_.obj_file\n let objContent = ''\n // Add vertices to OBJ content\n for (let i = 0; i < vertices.length; i += 3) {\n objContent += `v ${vertices[i]} ${vertices[i + 1]} ${vertices[i + 2]}\\n`\n }\n // Add faces to OBJ content (OBJ indices start at 1, not 0)\n for (let i = 0; i < indices.length; i += 3) {\n objContent += `f ${indices[i] + 1} ${indices[i + 1] + 1} ${indices[i + 2] + 1}\\n`\n }\n // Encode the OBJ content as an ArrayBuffer\n const encoder = new TextEncoder()\n const arrayBuffer = encoder.encode(objContent).buffer\n return arrayBuffer\n }\n\n static createSTL(vertices: Float32Array, indices: Uint32Array): ArrayBuffer {\n // generate binary STL format mesh\n // n.b. inefficient and slow as vertices are not reused\n // https://en.wikipedia.org/wiki/STL_(file_format)#Binary\n const numTriangles = indices.length / 3\n const bufferLength = 84 + numTriangles * 50\n const arrayBuffer = new ArrayBuffer(bufferLength)\n const dataView = new DataView(arrayBuffer)\n // Write header (80 bytes)\n for (let i = 0; i < 80; i++) {\n dataView.setUint8(i, 0)\n }\n // Write number of triangles (4 bytes)\n dataView.setUint32(80, numTriangles, true)\n let offset = 84\n for (let i = 0; i < indices.length; i += 3) {\n const i0 = indices[i] * 3\n const i1 = indices[i + 1] * 3\n const i2 = indices[i + 2] * 3\n // Normal vector (12 bytes, set to zero)\n dataView.setFloat32(offset, 0, true) // Normal X\n dataView.setFloat32(offset + 4, 0, true) // Normal Y\n dataView.setFloat32(offset + 8, 0, true) // Normal Z\n offset += 12\n // Vertex 1 (12 bytes)\n dataView.setFloat32(offset, vertices[i0], true) // Vertex 1 X\n dataView.setFloat32(offset + 4, vertices[i0 + 1], true) // Vertex 1 Y\n dataView.setFloat32(offset + 8, vertices[i0 + 2], true) // Vertex 1 Z\n offset += 12\n // Vertex 2 (12 bytes)\n dataView.setFloat32(offset, vertices[i1], true) // Vertex 2 X\n dataView.setFloat32(offset + 4, vertices[i1 + 1], true) // Vertex 2 Y\n dataView.setFloat32(offset + 8, vertices[i1 + 2], true) // Vertex 2 Z\n offset += 12\n // Vertex 3 (12 bytes)\n dataView.setFloat32(offset, vertices[i2], true) // Vertex 3 X\n dataView.setFloat32(offset + 4, vertices[i2 + 1], true) // Vertex 3 Y\n dataView.setFloat32(offset + 8, vertices[i2 + 2], true) // Vertex 3 Z\n offset += 12\n // Attribute byte count (2 bytes, set to zero)\n dataView.setUint16(offset, 0, true)\n offset += 2\n }\n return arrayBuffer\n }\n\n static downloadArrayBuffer(buffer: ArrayBuffer, filename: string): void {\n const blob = new Blob([buffer], { type: 'application/octet-stream' })\n const url = URL.createObjectURL(blob)\n const a = document.createElement('a')\n a.href = url\n a.download = filename\n document.body.appendChild(a)\n a.style.display = 'none'\n a.click()\n setTimeout(() => {\n document.body.removeChild(a)\n URL.revokeObjectURL(url)\n }, 0)\n }\n\n static async saveMesh(vertices: Float32Array, indices: Uint32Array, filename: string = '.mz3', compress: boolean = false): Promise<ArrayBuffer> {\n let buff = new ArrayBuffer(0)\n if (/\\.obj$/i.test(filename)) {\n buff = this.createOBJ(vertices, indices)\n } else if (/\\.stl$/i.test(filename)) {\n buff = this.createSTL(vertices, indices)\n } else {\n if (!/\\.mz3$/i.test(filename)) {\n filename += '.mz3'\n }\n buff = await this.createMZ3Async(vertices, indices, compress)\n }\n if (filename.length > 4) {\n this.downloadArrayBuffer(buff, filename)\n }\n return buff\n }\n\n static getClusterBoundary(rgba8: Uint8Array, faces: number[] | Uint32Array): boolean[] {\n const rgba32 = new Uint32Array(rgba8.buffer)\n // assume all vertices are not near a border\n const border = new Array(rgba32.length).fill(false)\n const nTri = faces.length / 3\n let j = 0\n // interior: a triangle where all three vertices are the same color\n // else, all three vertices are on a border\n for (let i = 0; i < nTri; i++) {\n const v0 = faces[j]\n const v1 = faces[j + 1]\n const v2 = faces[j + 2]\n j += 3\n if (rgba32[v0] === rgba32[v1] && rgba32[v0] === rgba32[v2] && rgba32[v1] === rgba32[v2]) {\n continue\n }\n border[v0] = true\n border[v1] = true\n border[v2] = true\n }\n return border\n }\n\n // return spatial extremes for vertices\n static getExtents(pts: number[] | Float32Array): Extents {\n if ((!ArrayBuffer.isView(pts) && !Array.isArray(pts)) || pts.length < 3) {\n return { mxDx: 0.0, extentsMin: 0.0, extentsMax: 0.0 }\n }\n\n // each vertex has 3 coordinates: XYZ\n let mxDx = 0.0\n const mn = vec3.fromValues(pts[0], pts[1], pts[2])\n const mx = vec3.fromValues(pts[0], pts[1], pts[2])\n for (let i = 0; i < pts.length; i += 3) {\n const v = vec3.fromValues(pts[i], pts[i + 1], pts[i + 2])\n mxDx = Math.max(mxDx, vec3.len(v))\n vec3.min(mn, mn, v)\n vec3.max(mx, mx, v)\n }\n const extentsMin = [mn[0], mn[1], mn[2]]\n const extentsMax = [mx[0], mx[1], mx[2]]\n return { mxDx, extentsMin, extentsMax }\n }\n\n // determine vector orthogonal to plane defined by triangle\n // triangle winding determines front/back face\n static generateNormals(pts: number[] | Float32Array, tris: number[] | Uint32Array): Float32Array {\n // from https://github.com/rii-mango/Papaya\n /*\nCopyright (c) 2012-2015, RII-UTHSCSA\nAll rights reserved.\n\nTHIS PRODUCT IS NOT FOR CLINICAL USE.\n\nRedistribution and use in source and binary forms, with or without modification, are permitted provided that the\nfollowing conditions are met:\n\n - Redistributions of source code must retain the above copyright notice, this list of conditions and the following\n disclaimer.\n\n - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following\n disclaimer in the documentation and/or other materials provided with the distribution.\n\n - Neither the name of the RII-UTHSCSA nor the names of its contributors may be used to endorse or promote products\n derived from this software without specific prior written permission.\n\n THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES,\n INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\n DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\n SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,\n WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n*/\n const normal = []\n // nn = [],\n let ctr\n const normalsDataLength = pts.length\n let qx\n let qy\n let qz\n let px\n let py\n let pz\n let index1\n let index2\n let index3\n\n const norms = new Float32Array(normalsDataLength)\n const numIndices = tris.length\n for (ctr = 0; ctr < numIndices; ctr += 3) {\n index1 = tris[ctr] * 3\n index2 = tris[ctr + 1] * 3\n index3 = tris[ctr + 2] * 3\n\n const p1 = [pts[index1], pts[index1 + 1], pts[index1 + 2]]\n const p2 = [pts[index2], pts[index2 + 1], pts[index2 + 2]]\n const p3 = [pts[index3], pts[index3 + 1], pts[index3 + 2]]\n\n qx = p2[0] - p1[0]\n qy = p2[1] - p1[1]\n qz = p2[2] - p1[2]\n px = p3[0] - p1[0]\n py = p3[1] - p1[1]\n pz = p3[2] - p1[2]\n\n normal[0] = py * qz - pz * qy\n normal[1] = pz * qx - px * qz\n normal[2] = px * qy - py * qx\n\n norms[index1] += normal[0]\n norms[index1 + 1] += normal[1]\n norms[index1 + 2] += normal[2]\n\n norms[index2] += normal[0]\n norms[index2 + 1] += normal[1]\n norms[index2 + 2] += normal[2]\n\n norms[index3] += normal[0]\n norms[index3 + 1] += normal[1]\n norms[index3 + 2] += normal[2]\n }\n for (ctr = 0; ctr < normalsDataLength; ctr += 3) {\n normal[0] = -1 * norms[ctr]\n normal[1] = -1 * norms[ctr + 1]\n normal[2] = -1 * norms[ctr + 2]\n let len = normal[0] * normal[0] + normal[1] * normal[1] + normal[2] * normal[2]\n if (len > 0) {\n len = 1.0 / Math.sqrt(len)\n normal[0] *= len\n normal[1] *= len\n normal[2] *= len\n }\n norms[ctr] = normal[0]\n norms[ctr + 1] = normal[1]\n norms[ctr + 2] = normal[2]\n }\n return norms\n }\n}\n","import { mat4, vec4, vec3 } from 'gl-matrix'\nimport { log } from '@/logger'\nimport { NVUtilities, Zip } from '@/nvutilities'\nimport { ColorMap, LUT, cmapper } from '@/colortables'\nimport { NiivueObject3D } from '@/niivue-object3D'\nimport { NVMesh, NVMeshLayer, NVMeshLayerDefaults } from '@/nvmesh'\nimport {\n ANNOT,\n DefaultMeshType,\n GII,\n // Layer,\n MGH,\n MZ3,\n SmpMap,\n TCK,\n TRACT,\n TRK,\n TT,\n TRX,\n VTK,\n X3D,\n XmlTag,\n AnyNumberArray,\n ValuesArray\n} from '@/nvmesh-types'\n\nconst utiltiesLogger = log\n\n/**\n * Class to load different mesh formats\n * @ignore\n */\nexport class NVMeshLoaders {\n // read undocumented AFNI tract.niml format streamlines\n static readTRACT(buffer: ArrayBuffer): TRACT {\n const len = buffer.byteLength\n if (len < 20) {\n throw new Error('File too small to be niml.tract: bytes = ' + len)\n }\n const reader = new DataView(buffer)\n const bytes = new Uint8Array(buffer)\n let pos = 0\n function readStr(): string {\n // read until right angle bracket \">\"\n while (pos < len && bytes[pos] !== 60) {\n pos++\n } // start with \"<\"\n const startPos = pos\n while (pos < len && bytes[pos] !== 62) {\n pos++\n }\n pos++ // skip EOLN\n if (pos - startPos < 1) {\n return ''\n }\n return new TextDecoder().decode(buffer.slice(startPos, pos - 1)).trim()\n }\n\n let line = readStr() // 1st line: signature '<network'\n function readNumericTag(TagName: string): number {\n // Tag 'Dim1' will return 3 for Dim1=\"3\"\n const pos = line.indexOf(TagName)\n if (pos < 0) {\n return 0\n }\n const spos = line.indexOf('\"', pos) + 1\n const epos = line.indexOf('\"', spos)\n const str = line.slice(spos, epos)\n return parseInt(str)\n }\n const n_tracts = readNumericTag('N_tracts=')\n if (!line.startsWith('<network') || n_tracts < 1) {\n log.warn('This is not a valid niml.tract file ' + line)\n }\n let npt = 0\n const offsetPt0 = []\n offsetPt0.push(npt) // 1st streamline starts at 0\n const pts = []\n const bundleTags = []\n for (let t = 0; t < n_tracts; t++) {\n line = readStr() // <tracts ...\n const new_tracts = readNumericTag('ni_dimen=')\n const bundleTag = readNumericTag('Bundle_Tag=')\n const isLittleEndian = line.includes('binary.lsbfirst')\n for (let i = 0; i < new_tracts; i++) {\n // let id = reader.getUint32(pos, isLittleEndian);\n pos += 4\n const new_pts = reader.getUint32(pos, isLittleEndian) / 3\n pos += 4\n for (let j = 0; j < new_pts; j++) {\n pts.push(reader.getFloat32(pos, isLittleEndian))\n pos += 4\n pts.push(-reader.getFloat32(pos, isLittleEndian))\n pos += 4\n pts.push(reader.getFloat32(pos, isLittleEndian))\n pos += 4\n }\n npt += new_pts\n offsetPt0.push(npt)\n bundleTags.push(bundleTag) // each streamline associated with tract\n }\n line = readStr() // </tracts>\n }\n const dps = []\n dps.push({\n id: 'tract',\n vals: Float32Array.from(bundleTags)\n })\n\n return {\n pts: new Float32Array(pts),\n offsetPt0: new Uint32Array(offsetPt0),\n dps\n }\n } // readTRACT()\n\n // https://dsi-studio.labsolver.org/doc/cli_data.html\n // https://brain.labsolver.org/hcp_trk_atlas.html\n static async readTT(buffer: ArrayBuffer): Promise<TT> {\n // Read a Matlab V4 file, n.b. does not support modern versions\n // https://www.mathworks.com/help/pdf_doc/matlab/matfile_format.pdf\n let offsetPt0 = new Uint32Array(0)\n let pts = new Float32Array(0)\n const mat = await NVUtilities.readMatV4(buffer)\n if (!('trans_to_mni' in mat)) {\n throw new Error(\"TT format file must have 'trans_to_mni'\")\n }\n if (!('voxel_size' in mat)) {\n throw new Error(\"TT format file must have 'voxel_size'\")\n }\n if (!('track' in mat)) {\n throw new Error(\"TT format file must have 'track'\")\n }\n let trans_to_mni = mat4.create()\n const m = mat.trans_to_mni\n trans_to_mni = mat4.fromValues(m[0], m[1], m[2], m[3], m[4], m[5], m[6], m[7], m[8], m[9], m[10], m[11], m[12], m[13], m[14], m[15])\n mat4.transpose(trans_to_mni, trans_to_mni)\n // unlike TRK, TT uses voxel centers, not voxel corners\n function parse_tt(track: Float64Array | Float32Array | Uint32Array | Uint16Array | Uint8Array | Int32Array | Int16Array | Int8Array): void {\n const dv = new DataView(track.buffer)\n const pos = []\n let nvert3 = 0\n let i = 0\n while (i < track.length) {\n pos.push(i)\n const newpts = dv.getUint32(i, true)\n i = i + newpts + 13\n nvert3 += newpts\n }\n offsetPt0 = new Uint32Array(pos.length + 1)\n pts = new Float32Array(nvert3)\n let npt = 0\n for (let i = 0; i < pos.length; i++) {\n offsetPt0[i] = npt / 3\n let p = pos[i]\n const sz = dv.getUint32(p, true) / 3\n let x = dv.getInt32(p + 4, true)\n let y = dv.getInt32(p + 8, true)\n let z = dv.getInt32(p + 12, true)\n p += 16\n pts[npt++] = x\n pts[npt++] = y\n pts[npt++] = z\n for (let j = 2; j <= sz; j++) {\n x = x + dv.getInt8(p++)\n y = y + dv.getInt8(p++)\n z = z + dv.getInt8(p++)\n pts[npt++] = x\n pts[npt++] = y\n pts[npt++] = z\n }\n } // for each streamline\n for (let i = 0; i < npt; i++) {\n pts[i] = pts[i] / 32.0\n }\n let v = 0\n for (let i = 0; i < npt / 3; i++) {\n const pos = vec4.fromValues(pts[v], pts[v + 1], pts[v + 2], 1)\n vec4.transformMat4(pos, pos, trans_to_mni)\n pts[v++] = pos[0]\n pts[v++] = pos[1]\n pts[v++] = pos[2]\n }\n offsetPt0[pos.length] = npt / 3 // solve fence post problem, offset for final streamline\n } // parse_tt()\n parse_tt(mat.track)\n return {\n pts,\n offsetPt0\n }\n } // readTT\n\n /**\n * Assemble dpg from a map-of-groups into a ValuesArray ordered by groups[].\n * Missing group data or tags are padded with NaN to maintain group alignment.\n *\n * @param dpgMap - map from groupId -> ValuesArray (entries for that group)\n * @param groups - ValuesArray describing groups; defines the result ordering\n * @returns ValuesArray - one entry per unique tag found across all groups\n * @throws Error if \"groups\" is empty or missing\n */\n static assembleDpgFromMap(dpgMap: Record<string, ValuesArray>, groups: ValuesArray): ValuesArray {\n if (!Array.isArray(groups) || groups.length === 0) {\n throw new Error('assembleDpgFromMap: \"groups\" is empty or missing; cannot assemble dpg.')\n }\n\n // 1. Identify all unique tags across all existing groups in dpgMap\n const allTags = new Set<string>()\n for (const gid in dpgMap) {\n dpgMap[gid].forEach((entry) => allTags.add(entry.id))\n }\n\n const result: ValuesArray = []\n\n // 2. For each unique tag found in the dpgMap...\n for (const tag of allTags) {\n const perGroupArrays: Float32Array[] = []\n let totalLen = 0\n\n // 3. Iterate through every group defined in the TRX header\n for (let gi = 0; gi < groups.length; gi++) {\n const gid = String(groups[gi].id)\n const entries = dpgMap[gid] || [] // Handle missing group in map\n const entry = entries.find((e) => e.id === tag)\n\n if (entry) {\n // Use actual data if it exists\n const vals = entry.vals instanceof Float32Array ? entry.vals : Float32Array.from(entry.vals as Iterable<number>)\n perGroupArrays.push(vals)\n totalLen += vals.length\n } else {\n // If tag is missing for this group, fill with NaN (assuming scalar dpg)\n // Note: TRX dpg is typically 1 value per group.\n const fallback = new Float32Array([NaN])\n perGroupArrays.push(fallback)\n totalLen += fallback.length\n }\n }\n\n // 4. Concatenate results for the tag\n const merged = new Float32Array(totalLen)\n let off = 0\n for (const arr of perGroupArrays) {\n merged.set(arr, off)\n off += arr.length\n }\n\n result.push({ id: tag, vals: merged })\n }\n\n return result\n }\n\n // read TRX format tractogram\n // https://github.com/tee-ar-ex/trx-spec/blob/master/specifications.md\n static async readTRX(buffer: ArrayBuffer): Promise<TRX> {\n // Javascript does not support float16, so we convert to float32\n // https://stackoverflow.com/questions/5678432/decompressing-half-precision-floats-in-javascript\n function decodeFloat16(binary: number): number {\n 'use strict'\n const exponent = (binary & 0x7c00) >> 10\n const fraction = binary & 0x03ff\n return (binary >> 15 ? -1 : 1) * (exponent ? (exponent === 0x1f ? (fraction ? NaN : Infinity) : Math.pow(2, exponent - 15) * (1 + fraction / 0x400)) : 6.103515625e-5 * (fraction / 0x400))\n } // decodeFloat16()\n let noff = 0\n let npt = 0\n let pts = new Float32Array([])\n const offsetPt0: number[] = []\n const dpgMap: Record<string, ValuesArray> = {}\n const groups = []\n const dps = []\n const dpv = []\n let header = []\n let isOverflowUint64 = false\n const zip = new Zip(buffer)\n for (let i = 0; i < zip.entries.length; i++) {\n const entry = zip.entries[i]\n if (entry.uncompressedSize === 0) {\n continue // e.g. folder\n }\n\n const parts = entry.fileName.split('/')\n const fname = parts.slice(-1)[0] // my.trx/dpv/fx.float32 -> fx.float32\n if (fname.startsWith('.')) {\n continue\n }\n const dname = parts.slice(-3)[0] // my.trx/dpg/ARC_L/z.float32 -> dpg\n const pname = parts.slice(-2)[0] // my.trx/dpv/fx.float32 -> dpv\n const tag = fname.split('.')[0] // \"positions.3.float16 -> \"positions\"\n const data = await entry.extract()\n // const data = await NVUtilities.zipInflate(buffer, entry.startsAt, entry.compressedSize, entry.uncompressedSize, entry.compressionMethod )\n // console.log(`entry ${pname} ${fname} ${tag} : ${data.length}`)\n if (fname.includes('header.json')) {\n const jsonString = new TextDecoder().decode(data)\n header = JSON.parse(jsonString)\n continue\n }\n\n // next read arrays for all possible datatypes: int8/16/32/64 uint8/16/32/64 float16/32/64\n let nval = 0\n let vals: AnyNumberArray = []\n if (fname.endsWith('.uint64') || fname.endsWith('.int64')) {\n // javascript does not have 64-bit integers! read lower 32-bits\n // note for signed int64 we only read unsigned bytes\n // for both signed and unsigned, generate an error if any value is out of bounds\n // one alternative might be to convert to 64-bit double that has a flintmax of 2^53.\n nval = data.length / 8 // 8 bytes per 64bit input\n vals = new Uint32Array(nval)\n const u32 = new Uint32Array(data.buffer)\n let j = 0\n for (let i = 0; i < nval; i++) {\n vals[i] = u32[j]\n if (u32[j + 1] !== 0) {\n isOverflowUint64 = true\n }\n j += 2\n }\n } else if (fname.endsWith('.uint32')) {\n vals = new Uint32Array(data.buffer)\n } else if (fname.endsWith('.uint16')) {\n vals = new Uint16Array(data.buffer)\n } else if (fname.endsWith('.uint8')) {\n vals = new Uint8Array(data.buffer)\n } else if (fname.endsWith('.int32')) {\n vals = new Int32Array(data.buffer)\n } else if (fname.endsWith('.int16')) {\n vals = new Int16Array(data.buffer)\n } else if (fname.endsWith('.int8')) {\n vals = new Int8Array(data.buffer)\n } else if (fname.endsWith('.float64')) {\n vals = new Float64Array(data.buffer)\n } else if (fname.endsWith('.float32')) {\n vals = new Float32Array(data.buffer)\n } else if (fname.endsWith('.float16')) {\n // javascript does not have 16-bit floats! Convert to 32-bits\n nval = data.length / 2 // 2 bytes per 16bit input\n vals = new Float32Array(nval)\n const u16 = new Uint16Array(data.buffer)\n const lut = new Float32Array(65536)\n for (let i = 0; i < 65536; i++) {\n lut[i] = decodeFloat16(i)\n }\n for (let i = 0; i < nval; i++) {\n vals[i] = lut[u16[i]]\n }\n } else {\n continue\n } // not a data array\n nval = vals.length\n // next: read data_per_group\n if (pname.includes('groups')) {\n groups.push({\n id: tag,\n vals: Float32Array.from(vals.slice())\n })\n continue\n }\n if (dname.includes('dpg')) {\n const key = String(pname) // be defensive; ensure key is a string\n if (!dpgMap[key]) {\n dpgMap[key] = []\n }\n dpgMap[key].push({\n id: tag,\n vals: Float32Array.from(vals.slice())\n })\n continue\n }\n // next: read data_per_vertex\n if (pname.includes('dpv')) {\n dpv.push({\n id: tag,\n vals: Float32Array.from(vals.slice())\n })\n continue\n }\n // next: read data_per_streamline\n if (pname.includes('dps')) {\n dps.push({\n id: tag,\n vals: Float32Array.from(vals.slice())\n })\n continue\n }\n // Next: read offsets: Always uint64\n if (fname.startsWith('offsets.')) {\n // javascript does not have 64-bit integers! read lower 32-bits\n noff = nval // 8 bytes per 64bit input\n // we need to solve the fence post problem, so we can not use slice\n for (let i = 0; i < nval; i++) {\n offsetPt0[i] = vals[i]\n }\n }\n if (fname.startsWith('positions.3.')) {\n npt = nval // 4 bytes per 32bit input\n pts = new Float32Array(vals)\n }\n }\n if (noff === 0 || npt === 0) {\n throw new Error('Failure reading TRX format (no offsets or points).')\n }\n if (isOverflowUint64) {\n // TODO use BigInt\n throw new Error('Too many vertices: JavaScript does not support 64 bit integers')\n }\n let dpg = []\n if (groups.length > 0 && dpgMap && Object.keys(dpgMap).length > 0) {\n dpg = this.assembleDpgFromMap(dpgMap, groups)\n }\n offsetPt0[noff] = npt / 3 // solve fence post problem, offset for final streamline\n return {\n pts,\n offsetPt0: new Uint32Array(offsetPt0),\n dpg,\n dps,\n dpv,\n groups,\n header\n }\n } // readTRX()\n\n // issue1426 MRtrix data per streamline as ASCII text\n static readTXT(buffer: ArrayBuffer, n_count = 0): Float32Array {\n // Decode ASCII (or UTF-8) bytes into a string\n const text = new TextDecoder('utf-8').decode(buffer)\n // Split into lines, handling any line endings: \\r\\n, \\r, or \\n\n const lines = text.split(/\\r?\\n|\\r/).filter((l) => l.trim().length > 0)\n // If n_count not specified, use number of lines\n if (n_count <= 0) {\n n_count = lines.length\n }\n const vals = new Float32Array(n_count)\n // Parse each line into a float\n for (let i = 0; i < n_count && i < lines.length; i++) {\n const v = parseFloat(lines[i].trim())\n vals[i] = Number.isFinite(v) ? v : 0.0\n }\n\n return vals\n }\n\n // read mrtrix tsf format Track Scalar Files - these are are DPV\n // https://mrtrix.readthedocs.io/en/dev/getting_started/image_data.html#track-scalar-file-format-tsf\n static readTSF(buffer: ArrayBuffer, n_vert = 0): Float32Array {\n const vals = new Float32Array(n_vert)\n const len = buffer.byteLength\n if (len < 20) {\n throw new Error('File too small to be TSF: bytes = ' + len)\n }\n const bytes = new Uint8Array(buffer)\n let pos = 0\n function readStr(): string {\n while (pos < len && bytes[pos] === 10) {\n pos++\n } // skip blank lines\n const startPos = pos\n while (pos < len && bytes[pos] !== 10) {\n pos++\n }\n pos++ // skip EOLN\n if (pos - startPos < 1) {\n return ''\n }\n return new TextDecoder().decode(buffer.slice(startPos, pos - 1))\n }\n let line = readStr() // 1st line: signature 'mrtrix tracks'\n if (!line.includes('mrtrix track scalars')) {\n throw new Error('Not a valid TSF file')\n }\n let offset = -1 // \"file: offset\" is REQUIRED\n while (pos < len && !line.includes('END')) {\n line = readStr()\n if (line.toLowerCase().startsWith('file:')) {\n offset = parseInt(line.split(' ').pop()!)\n }\n if (line.toLowerCase().startsWith('datatype:') && !line.endsWith('Float32LE')) {\n throw new Error('Only supports TSF files with Float32LE')\n }\n }\n if (offset < 20) {\n throw new Error('Not a valid TSF file (missing file offset)')\n }\n pos = offset\n const reader = new DataView(buffer)\n // read and transform vertex positions\n let npt = 0\n while (pos + 4 <= len && npt < n_vert) {\n const ptx = reader.getFloat32(pos, true)\n pos += 4\n if (!isFinite(ptx)) {\n // both NaN and Infinity are not finite\n if (!isNaN(ptx)) {\n // terminate if infinity\n break\n }\n } else {\n vals[npt++] = ptx\n }\n }\n return vals\n } // readTSF\n\n // read mrtrix tck format streamlines\n // https://mrtrix.readthedocs.io/en/latest/getting_started/image_data.html#tracks-file-format-tck\n static readTCK(buffer: ArrayBuffer): TCK {\n const len = buffer.byteLength\n if (len < 20) {\n throw new Error('File too small to be TCK: bytes = ' + len)\n }\n const bytes = new Uint8Array(buffer)\n let pos = 0\n function readStr(): string {\n while (pos < len && bytes[pos] === 10) {\n pos++\n } // skip blank lines\n const startPos = pos\n while (pos < len && bytes[pos] !== 10) {\n pos++\n }\n pos++ // skip EOLN\n if (pos - startPos < 1) {\n return ''\n }\n return new TextDecoder().decode(buffer.slice(startPos, pos - 1))\n }\n let line = readStr() // 1st line: signature 'mrtrix tracks'\n if (!line.includes('mrtrix tracks')) {\n throw new Error('Not a valid TCK file')\n }\n let offset = -1 // \"file: offset\" is REQUIRED\n while (pos < len && !line.includes('END')) {\n line = readStr()\n if (line.toLowerCase().startsWith('file:')) {\n offset = parseInt(line.split(' ').pop()!)\n }\n }\n if (offset < 20) {\n throw new Error('Not a valid TCK file (missing file offset)')\n }\n pos = offset\n const reader = new DataView(buffer)\n // read and transform vertex positions\n let npt = 0\n // over-provision offset array to store number of segments\n let offsetPt0 = new Uint32Array(len / (4 * 4))\n let noffset = 0\n // over-provision points array to store vertex positions\n let npt3 = 0\n let pts = new Float32Array(len / 4)\n offsetPt0[0] = 0 // 1st streamline starts at 0\n while (pos + 12 < len) {\n const ptx = reader.getFloat32(pos, true)\n pos += 4\n const pty = reader.getFloat32(pos, true)\n pos += 4\n const ptz = reader.getFloat32(pos, true)\n pos += 4\n if (!isFinite(ptx)) {\n // both NaN and Infinity are not finite\n offsetPt0[noffset++] = npt\n if (!isNaN(ptx)) {\n // terminate if infinity\n break\n }\n } else {\n pts[npt3++] = ptx\n pts[npt3++] = pty\n pts[npt3++] = ptz\n npt++\n }\n }\n // resize offset/vertex arrays that were initially over-provisioned\n pts = pts.slice(0, npt3)\n offsetPt0 = offsetPt0.slice(0, noffset)\n return {\n pts,\n offsetPt0\n }\n } // readTCK()\n\n // not included in public docs\n // read trackvis trk format streamlines\n // http://trackvis.org/docs/?subsect=fileformat\n static async readTRK(buffer: ArrayBuffer): Promise<TRK> {\n // https://brain.labsolver.org/hcp_trk_atlas.html\n // https://github.com/xtk/X/tree/master/io\n // in practice, always little endian\n let reader = new DataView(buffer)\n let magic = reader.getUint32(0, true) // 'TRAC'\n if (magic !== 1128354388) {\n // e.g. TRK.gz\n let raw\n if (magic === 4247762216) {\n // e.g. TRK.zstd\n // raw = fzstd.decompress(new Uint8Array(buffer));\n // raw = new Uint8Array(raw);\n throw new Error('zstd TRK decompression is not supported')\n } else {\n raw = await NVUtilities.decompress(new Uint8Array(buffer))\n }\n buffer = raw.buffer\n reader = new DataView(buffer)\n magic = reader.getUint32(0, true) // 'TRAC'\n }\n const vers = reader.getUint32(992, true) // 2\n const hdr_sz = reader.getUint32(996, true) // 1000\n if (vers > 2 || hdr_sz !== 1000 || magic !== 1128354388) {\n throw new Error('Not a valid TRK file')\n }\n const n_scalars = reader.getInt16(36, true)\n const dpv = []\n // data_per_vertex\n for (let i = 0; i < n_scalars; i++) {\n const arr = new Uint8Array(buffer.slice(38 + i * 20, 58 + i * 20))\n const str = new TextDecoder().decode(arr).split('\\0').shift()\n dpv.push({\n id: str!.trim(), // TODO can we guarantee this?\n vals: [] as number[]\n })\n }\n const voxel_sizeX = reader.getFloat32(12, true)\n const voxel_sizeY = reader.getFloat32(16, true)\n const voxel_sizeZ = reader.getFloat32(20, true)\n const zoomMat = mat4.fromValues(1 / voxel_sizeX, 0, 0, -0.5, 0, 1 / voxel_sizeY, 0, -0.5, 0, 0, 1 / voxel_sizeZ, -0.5, 0, 0, 0, 1)\n const n_properties = reader.getInt16(238, true)\n const dps = []\n // data_per_streamline\n for (let i = 0; i < n_properties; i++) {\n const arr = new Uint8Array(buffer.slice(240 + i * 20, 260 + i * 20))\n const str = new TextDecoder().decode(arr).split('\\0').shift()\n dps.push({\n id: str!.trim(), // TODO can we guarantee this?\n vals: [] as number[]\n })\n }\n const mat = mat4.create()\n for (let i = 0; i < 16; i++) {\n mat[i] = reader.getFloat32(440 + i * 4, true)\n }\n if (mat[15] === 0.0) {\n // vox_to_ras[3][3] is 0, it means the matrix is not recorded\n log.warn('TRK vox_to_ras not set')\n mat4.identity(mat)\n }\n const vox2mmMat = mat4.create()\n mat4.mul(vox2mmMat, zoomMat, mat)\n let i32 = null\n let f32 = null\n i32 = new Int32Array(buffer.slice(hdr_sz))\n f32 = new Float32Array(i32.buffer)\n const ntracks = i32.length\n if (ntracks < 1) {\n throw new Error('Empty TRK file.')\n }\n // read and transform vertex positions\n let i = 0\n let npt = 0\n // pre-allocate and over-provision offset array\n let offsetPt0 = new Uint32Array(i32.length / 4)\n let noffset = 0\n // pre-allocate and over-provision vertex positions array\n let pts = new Float32Array(i32.length)\n let npt3 = 0\n while (i < ntracks) {\n const n_pts = i32[i]\n i = i + 1 // read 1 32-bit integer for number of points in this streamline\n offsetPt0[noffset++] = npt\n for (let j = 0; j < n_pts; j++) {\n const ptx = f32[i + 0]\n const pty = f32[i + 1]\n const ptz = f32[i + 2]\n i += 3 // read 3 32-bit floats for XYZ position\n pts[npt3++] = ptx * vox2mmMat[0] + pty * vox2mmMat[1] + ptz * vox2mmMat[2] + vox2mmMat[3]\n pts[npt3++] = ptx * vox2mmMat[4] + pty * vox2mmMat[5] + ptz * vox2mmMat[6] + vox2mmMat[7]\n pts[npt3++] = ptx * vox2mmMat[8] + pty * vox2mmMat[9] + ptz * vox2mmMat[10] + vox2mmMat[11]\n if (n_scalars > 0) {\n for (let s = 0; s < n_scalars; s++) {\n dpv[s].vals.push(f32[i])\n i++\n }\n }\n npt++\n } // for j: each point in streamline\n if (n_properties > 0) {\n for (let j = 0; j < n_properties; j++) {\n dps[j].vals.push(f32[i])\n i++\n }\n }\n } // for each streamline: while i < n_count\n // output uses static float32 not dynamic number[]\n const dps32 = []\n // data_per_streamline\n for (let i = 0; i < dps.length; i++) {\n dps32.push({\n id: dps[i].id,\n vals: Float32Array.from(dps[i].vals)\n })\n }\n const dpv32 = []\n for (let i = 0; i < dpv.length; i++) {\n dpv32.push({\n id: dpv[i].id,\n vals: Float32Array.from(dpv[i].vals)\n })\n }\n // add 'first index' as if one more line was added (fence post problem)\n offsetPt0[noffset++] = npt\n // resize offset/vertex arrays that were initially over-provisioned\n pts = pts.slice(0, npt3)\n offsetPt0 = offsetPt0.slice(0, noffset)\n return {\n pts,\n offsetPt0,\n dps: dps32,\n dpv: dpv32\n }\n } // readTRK()\n\n // read legacy VTK text format file\n static readTxtVTK(buffer: ArrayBuffer): VTK {\n const enc = new TextDecoder('utf-8')\n const txt = enc.decode(buffer)\n const lines = txt.split('\\n')\n const n = lines.length\n if (n < 7 || !lines[0].startsWith('# vtk DataFile')) {\n throw new Error('Invalid VTK image')\n }\n if (!lines[2].startsWith('ASCII')) {\n throw new Error('Not ASCII VTK mesh')\n }\n let pos = 3\n while (lines[pos].length < 1) {\n pos++\n } // skip blank lines\n if (!lines[pos].includes('POLYDATA')) {\n throw new Error('Not ASCII VTK polydata')\n }\n pos++\n while (lines[pos].length < 1) {\n pos++\n } // skip blank lines\n if (!lines[pos].startsWith('POINTS')) {\n throw new Error('Not VTK POINTS')\n }\n let items = lines[pos].trim().split(/\\s+/)\n const nvert = parseInt(items[1]) // POINTS 10261 float\n const nvert3 = nvert * 3\n const positions = new Float32Array(nvert * 3)\n let v = 0\n while (v < nvert * 3) {\n pos++\n const str = lines[pos].trim()\n const pts = str.trim().split(/\\s+/)\n for (let i = 0; i < pts.length; i++) {\n if (v >= nvert3) {\n break\n }\n positions[v] = parseFloat(pts[i])\n v++\n }\n }\n const tris = []\n pos++\n while (lines[pos].length < 1) {\n pos++\n } // skip blank lines\n if (lines[pos].startsWith('METADATA')) {\n while (lines[pos].length > 1) {\n pos++\n } // skip until blank line\n pos++\n }\n items = lines[pos].trim().split(/\\s+/)\n pos++\n if (items[0].includes('LINES')) {\n const n_count = parseInt(items[1])\n if (n_count < 1) {\n throw new Error('Corrupted VTK ASCII')\n }\n let str = lines[pos].trim()\n const offsetPt0 = []\n let pts: number[] = []\n if (str.startsWith('OFFSETS')) {\n // 'new' line style https://discourse.vtk.org/t/upcoming-changes-to-vtkcellarray/2066\n pos++\n let c = 0\n while (c < n_count) {\n str = lines[pos].trim()\n pos++\n const items = str.trim().split(/\\s+/)\n for (let i = 0; i < items.length; i++) {\n offsetPt0[c] = parseInt(items[i])\n c++\n if (c >= n_count) {\n break\n }\n } // for each line\n } // while offset array not filled\n pts = Array.from(positions)\n } else {\n // classic line style https://www.visitusers.org/index.php?title=ASCII_VTK_Files\n let npt = 0\n offsetPt0[0] = 0 // 1st streamline starts at 0\n let asciiInts: number[] = []\n let asciiIntsPos = 0\n\n function lineToInts(): void {\n // VTK can save one array across multiple ASCII lines\n str = lines[pos].trim()\n const items = str.trim().split(/\\s+/)\n asciiInts = []\n for (let i = 0; i < items.length; i++) {\n asciiInts.push(parseInt(items[i]))\n }\n asciiIntsPos = 0\n pos++\n }\n\n lineToInts()\n for (let c = 0; c < n_count; c++) {\n if (asciiIntsPos >= asciiInts.length) {\n lineToInts()\n }\n const numPoints = asciiInts[asciiIntsPos++]\n npt += numPoints\n offsetPt0[c + 1] = npt\n for (let i = 0; i < numPoints; i++) {\n if (asciiIntsPos >= asciiInts.length) {\n lineToInts()\n }\n const idx = asciiInts[asciiIntsPos++] * 3\n pts.push(positions[idx + 0]) // X\n pts.push(positions[idx + 1]) // Y\n pts.push(positions[idx + 2]) // Z\n } // for numPoints: number of segments in streamline\n } // for n_count: number of streamlines\n }\n return {\n pts: Float32Array.from(pts),\n offsetPt0: Uint32Array.from(offsetPt0)\n }\n } else if (items[0].includes('TRIANGLE_STRIPS')) {\n const nstrip = parseInt(items[1])\n for (let i = 0; i < nstrip; i++) {\n const str = lines[pos].trim()\n pos++\n const vs = str.trim().split(/\\s+/)\n const ntri = parseInt(vs[0]) - 2 // -2 as triangle strip is creates pts - 2 faces\n let k = 1\n for (let t = 0; t < ntri; t++) {\n if (t % 2) {\n // preserve winding order\n tris.push(parseInt(vs[k + 2]))\n tris.push(parseInt(vs[k + 1]))\n tris.push(parseInt(vs[k]))\n } else {\n tris.push(parseInt(vs[k]))\n tris.push(parseInt(vs[k + 1]))\n tris.push(parseInt(vs[k + 2]))\n }\n k += 1\n } // for each triangle\n } // for each strip\n } else if (items[0].includes('POLYGONS')) {\n const npoly = parseInt(items[1])\n for (let i = 0; i < npoly; i++) {\n const str = lines[pos].trim()\n pos++\n const vs = str.trim().split(/\\s+/)\n const ntri = parseInt(vs[0]) - 2 // e.g. 3 for triangle\n const fx = parseInt(vs[1])\n let fy = parseInt(vs[2])\n for (let t = 0; t < ntri; t++) {\n const fz = parseInt(vs[3 + t])\n tris.push(fx)\n tris.push(fy)\n tris.push(fz)\n fy = fz\n }\n }\n } else {\n throw new Error('Unsupported ASCII VTK datatype ' + items[0])\n }\n const indices = new Uint32Array(tris)\n return {\n positions,\n indices\n }\n } // readTxtVTK()\n\n // read mesh overlay to influence vertex colors\n static async readLayer(\n name: string = '',\n buffer: ArrayBuffer,\n nvmesh: NVMesh,\n opacity = 0.5,\n colormap = 'warm',\n colormapNegative = 'winter',\n useNegativeCmap = false,\n cal_min: number | null = null,\n cal_max: number | null = null,\n outlineBorder = 0\n ): Promise<NVMeshLayer | undefined> {\n const layer: NVMeshLayer = {\n ...NVMeshLayerDefaults,\n colormapInvert: false,\n colormapType: 0, // COLORMAP_TYPE.MIN_TO_MAX\n isTransparentBelowCalMin: true,\n isAdditiveBlend: false,\n colorbarVisible: true,\n colormapLabel: null\n }\n const isReadColortables = true\n const re = /(?:\\.([^.]+))?$/\n let ext = re.exec(name)![1] // TODO can we guarantee this?\n ext = ext.toUpperCase()\n if (ext === 'GZ') {\n ext = re.exec(name.slice(0, -3))![1] // img.trk.gz -> img.trk\n ext = ext.toUpperCase()\n }\n const n_vert = nvmesh.vertexCount / 3 // each vertex has XYZ component\n if (nvmesh.offsetPt0) {\n // for streamlines, we can only read .tsf and txt files\n // typescript hell commences for one liner\n // const tag = name.split('/')!.pop()!.split('.')!.slice(0, -1).join('.')!\n const splitResult = name.split('/')\n let tag = 'Unknown'\n if (splitResult.length > 1) {\n const tag1 = splitResult.pop()\n if (tag1) {\n tag = tag.split('.').slice(0, -1).join('.')\n }\n }\n if (ext === 'TXT') {\n const n_count = nvmesh.offsetPt0.length - 1\n const vals = NVMeshLoaders.readTXT(buffer, n_count)\n if (vals.length !== n_count) {\n throw new Error(`TXT file has ${vals.length} items, expected one per streamline (${n_count}).`)\n }\n if (!nvmesh.dps) {\n nvmesh.dps = []\n }\n const mn = vals.reduce((acc, current) => Math.min(acc, current))\n const mx = vals.reduce((acc, current) => Math.max(acc, current))\n nvmesh.dps.push({\n id: tag,\n vals: Float32Array.from(vals.slice()),\n global_min: mn,\n global_max: mx,\n cal_min: mn,\n cal_max: mx\n })\n return layer\n }\n if (ext === 'NII') {\n const vals = (await NVMeshLoaders.readNII(buffer, nvmesh.pts, '')) as Float32Array\n // const npt = nvmesh.pts.length / 3\n const mn = vals.reduce((acc, current) => Math.min(acc, current))\n const mx = vals.reduce((acc, current) => Math.max(acc, current))\n nvmesh.dpv.push({\n id: tag,\n vals: Float32Array.from(vals.slice()),\n global_min: mn,\n global_max: mx,\n cal_min: mn,\n cal_max: mx\n })\n // console.log(`>>> ${tag} got ${vals.length} expected ${nvmesh.pts.length / 3}`)\n return layer\n }\n if (ext !== 'TSF') {\n throw new Error('readLayer for streamlines only supports TSF and TXT files.')\n }\n const npt = nvmesh.pts.length / 3\n // return to readable javascript\n const vals = NVMeshLoaders.readTSF(buffer, npt)\n if (!nvmesh.dpv) {\n nvmesh.dpv = []\n }\n const mn = vals.reduce((acc, current) => Math.min(acc, current))\n const mx = vals.reduce((acc, current) => Math.max(acc, current))\n nvmesh.dpv.push({\n id: tag,\n vals: Float32Array.from(vals.slice()),\n global_min: mn,\n global_max: mx,\n cal_min: mn,\n cal_max: mx\n })\n return layer\n }\n if (n_vert < 3) {\n log.error('n_vert < 3 in layer')\n return\n }\n if (ext === 'MZ3') {\n const obj = await NVMeshLoaders.readMZ3(buffer, n_vert)\n layer.values = obj.scalars\n if ('colormapLabel' in obj) {\n layer.colormapLabel = obj.colormapLabel\n }\n } else if (ext === 'ANNOT') {\n if (!isReadColortables) {\n // TODO: bogus ANNOT return type\n layer.values = NVMeshLoaders.readANNOT(buffer, n_vert) as unknown as Float32Array\n } else {\n const obj = NVMeshLoaders.readANNOT(buffer, n_vert, true)\n if (!(obj instanceof Uint32Array)) {\n layer.values = obj.scalars\n layer.colormapLabel = obj.colormapLabel\n } // unable to decode colormapLabel\n else {\n layer.values = obj\n }\n }\n } else if (ext === 'CRV' || ext === 'CURV' || ext === 'THICKNESS' || ext === 'AREA') {\n layer.values = NVMeshLoaders.readCURV(buffer, n_vert)\n layer.isTransparentBelowCalMin = false\n } else if (ext === 'GII') {\n const obj = await NVMeshLoaders.readGII(buffer, n_vert)\n layer.values = obj.scalars // colormapLabel\n layer.colormapLabel = obj.colormapLabel\n } else if (ext === 'MGH' || ext === 'MGZ') {\n if (!isReadColortables) {\n layer.values = (await NVMeshLoaders.readMGH(buffer, n_vert)) as number[]\n } else {\n const obj = await NVMeshLoaders.readMGH(buffer, n_vert, true)\n if ('scalars' in obj) {\n layer.values = obj.scalars\n layer.colormapLabel = obj.colormapLabel\n } // unable to decode colormapLabel\n else {\n layer.values = obj\n }\n }\n } else if (ext === 'NII') {\n layer.values = (await NVMeshLoaders.readNII(buffer, nvmesh.pts, nvmesh.anatomicalStructurePrimary)) as Float32Array\n } else if (ext === 'SMP') {\n layer.values = await NVMeshLoaders.readSMP(buffer, n_vert)\n } else if (ext === 'STC') {\n layer.values = NVMeshLoaders.readSTC(buffer, n_vert)\n } else if (NVMeshLoaders.isCurv(buffer)) {\n // Unknown layer overlay format - hail mary assume FreeSurfer\n layer.values = NVMeshLoaders.readCURV(buffer, n_vert)\n layer.isTransparentBelowCalMin = false\n } else {\n log.warn('Unknown layer overlay format ' + name)\n return layer\n }\n if (!layer.values) {\n log.error('no values in layer')\n return\n }\n layer.nFrame4D = layer.values.length / n_vert\n layer.frame4D = 0\n layer.outlineBorder = outlineBorder\n // determine global min..max\n let mn = layer.values[0]\n let mx = layer.values[0]\n for (let i = 0; i < layer.values.length; i++) {\n mn = Math.min(mn, layer.values[i])\n mx = Math.max(mx, layer.values[i])\n }\n layer.global_min = mn\n layer.global_max = mx\n layer.cal_min = cal_min || 0\n if (!cal_min) {\n layer.cal_min = mn\n }\n layer.cal_max = cal_max || 0\n if (!cal_max) {\n layer.cal_max = mx\n }\n layer.cal_minNeg = NaN\n layer.cal_maxNeg = NaN\n layer.opacity = opacity\n layer.colormap = colormap\n layer.colormapNegative = colormapNegative\n layer.useNegativeCmap = useNegativeCmap\n return layer\n } // readLayer()\n\n // read brainvoyager smp format file\n // https://support.brainvoyager.com/brainvoyager/automation-development/84-file-formats/40-the-format-of-smp-files\n static async readSMP(buffer: ArrayBuffer, n_vert: number): Promise<Float32Array> {\n const len = buffer.byteLength\n let reader = new DataView(buffer)\n let vers = reader.getUint16(0, true)\n if (vers > 5) {\n // assume gzip\n const raw = await NVUtilities.decompress(new Uint8Array(buffer))\n reader = new DataView(raw.buffer)\n vers = reader.getUint16(0, true)\n buffer = raw.buffer\n }\n if (vers > 5) {\n log.error('Unsupported or invalid BrainVoyager SMP version ' + vers)\n }\n const nvert = reader.getUint32(2, true)\n if (nvert !== n_vert) {\n log.error('SMP file has ' + nvert + ' vertices, background mesh has ' + n_vert)\n }\n const nMaps = reader.getUint16(6, true)\n\n const scalars = new Float32Array(nvert * nMaps)\n const maps = []\n let pos = 9\n function readStr(): string {\n const startPos = pos\n while (pos < len && reader.getUint8(pos) !== 0) {\n pos++\n }\n pos++ // skip null termination\n return new TextDecoder().decode(buffer.slice(startPos, pos - 1))\n } // readStr: read variable length string\n\n // read Name of SRF\n const _filenameSRF = readStr()\n\n for (let i = 0; i < nMaps; i++) {\n const m: Partial<SmpMap> = {}\n m.mapType = reader.getUint32(pos, true)\n pos += 4\n // Read additional values only if a lag map\n if (vers >= 3 && m.mapType === 3) {\n m.nLags = reader.getUint32(pos, true)\n pos += 4\n m.mnLag = reader.getUint32(pos, true)\n pos += 4\n m.mxLag = reader.getUint32(pos, true)\n pos += 4\n m.ccOverlay = reader.getUint32(pos, true)\n pos += 4\n }\n m.clusterSize = reader.getUint32(pos, true)\n pos += 4\n m.clusterCheck = reader.getUint8(pos)\n pos += 1\n m.critThresh = reader.getFloat32(pos, true)\n pos += 4\n m.maxThresh = reader.getFloat32(pos, true)\n pos += 4\n if (vers >= 4) {\n m.includeValuesGreaterThreshMax = reader.getUint32(pos, true)\n pos += 4\n }\n m.df1 = reader.getUint32(pos, true)\n pos += 4\n m.df2 = reader.getUint32(pos, true)\n pos += 4\n if (vers >= 5) {\n m.posNegFlag = reader.getUint32(pos, true)\n pos += 4\n } else {\n m.posNegFlag = 3\n }\n m.cortexBonferroni = reader.getUint32(pos, true)\n pos += 4\n m.posMinRGB = [0, 0, 0]\n m.posMaxRGB = [0, 0, 0]\n m.negMinRGB = [0, 0, 0]\n m.negMaxRGB = [0, 0, 0]\n if (vers >= 2) {\n m.posMinRGB[0] = reader.getUint8(pos)\n pos++\n m.posMinRGB[1] = reader.getUint8(pos)\n pos++\n m.posMinRGB[2] = reader.getUint8(pos)\n pos++\n m.posMaxRGB[0] = reader.getUint8(pos)\n pos++\n m.posMaxRGB[1] = reader.getUint8(pos)\n pos++\n m.posMaxRGB[2] = reader.getUint8(pos)\n pos++\n if (vers >= 4) {\n m.negMinRGB[0] = reader.getUint8(pos)\n pos++\n m.negMinRGB[1] = reader.getUint8(pos)\n pos++\n m.negMinRGB[2] = reader.getUint8(pos)\n pos++\n m.negMaxRGB[0] = reader.getUint8(pos)\n pos++\n m.negMaxRGB[1] = reader.getUint8(pos)\n pos++\n m.negMaxRGB[2] = reader.getUint8(pos)\n pos++\n } // vers >= 4\n m.enableSMPColor = reader.getUint8(pos)\n pos++\n if (vers >= 4) {\n m.lut = readStr()\n }\n m.colorAlpha = reader.getFloat32(pos, true)\n pos += 4\n } // vers >= 2\n m.name = readStr()\n const scalarsNew = new Float32Array(buffer, pos, nvert)\n scalars.set(scalarsNew, i * nvert)\n pos += nvert * 4\n maps.push(m)\n } // for i to nMaps\n return scalars\n } // readSMP()\n\n // read mne stc format file, not to be confused with brainvoyager stc format\n // https://github.com/mne-tools/mne-python/blob/main/mne/source_estimate.py#L211-L365\n static readSTC(buffer: ArrayBuffer, n_vert: number): Float32Array {\n // https://github.com/fahsuanlin/fhlin_toolbox/blob/400cb73cda4880d9ad7841d9dd68e4e9762976bf/codes/inverse_read_stc.m\n // let len = buffer.byteLength;\n const reader = new DataView(buffer)\n // first 12 bytes are header\n // let epoch_begin_latency = reader.getFloat32(0, false);\n // let sample_period = reader.getFloat32(4, false);\n const n_vertex = reader.getInt32(8, false)\n if (n_vertex !== n_vert) {\n throw new Error('Overlay has ' + n_vertex + ' vertices, expected ' + n_vert)\n }\n // next 4*n_vertex bytes are vertex IDS\n let pos = 12 + n_vertex * 4\n // next 4 bytes reports number of volumes/time points\n const n_time = reader.getUint32(pos, false)\n pos += 4\n const f32 = new Float32Array(n_time * n_vertex)\n // reading all floats with .slice() would be faster, but lets handle endian-ness\n for (let i = 0; i < n_time * n_vertex; i++) {\n f32[i] = reader.getFloat32(pos, false)\n pos += 4\n }\n return f32\n } // readSTC()\n\n static isCurv(buffer: ArrayBuffer): boolean {\n const view = new DataView(buffer) // ArrayBuffer to dataview\n // ALWAYS big endian\n const sig0 = view.getUint8(0)\n const sig1 = view.getUint8(1)\n const sig2 = view.getUint8(2)\n if (sig0 !== 255 || sig1 !== 255 || sig2 !== 255) {\n utiltiesLogger.debug('Unable to recognize file type: does not appear to be FreeSurfer format.')\n return false\n }\n return true\n }\n\n // read freesurfer curv big-endian format\n // https://github.com/bonilhamusclab/MRIcroS/blob/master/%2BfileUtils/%2Bpial/readPial.m\n // http://www.grahamwideman.com/gw/brain/fs/surfacefileformats.htm\n static readCURV(buffer: ArrayBuffer, n_vert: number): Float32Array {\n const view = new DataView(buffer) // ArrayBuffer to dataview\n // ALWAYS big endian\n const sig0 = view.getUint8(0)\n const sig1 = view.getUint8(1)\n const sig2 = view.getUint8(2)\n const n_vertex = view.getUint32(3, false)\n // let num_f = view.getUint32(7, false);\n const n_time = view.getUint32(11, false)\n if (sig0 !== 255 || sig1 !== 255 || sig2 !== 255) {\n utiltiesLogger.debug('Unable to recognize file type: does not appear to be FreeSurfer format.')\n }\n if (n_vert !== n_vertex) {\n throw new Error('CURV file has different number of vertices ( ' + n_vertex + ')than mesh (' + n_vert + ')')\n }\n if (buffer.byteLength < 15 + 4 * n_vertex * n_time) {\n throw new Error('CURV file smaller than specified')\n }\n const f32 = new Float32Array(n_time * n_vertex)\n let pos = 15\n // reading all floats with .slice() would be faster, but lets handle endian-ness\n for (let i = 0; i < n_time * n_vertex; i++) {\n f32[i] = view.getFloat32(pos, false)\n pos += 4\n }\n let mn = f32[0]\n let mx = f32[0]\n for (let i = 0; i < f32.length; i++) {\n mn = Math.min(mn, f32[i])\n mx = Math.max(mx, f32[i])\n }\n // normalize\n const scale = 1.0 / (mx - mn)\n for (let i = 0; i < f32.length; i++) {\n f32[i] = 1.0 - (f32[i] - mn) * scale\n }\n return f32\n } // readCURV()\n\n // read freesurfer Annotation file provides vertex colors\n // https://surfer.nmr.mgh.harvard.edu/fswiki/LabelsClutsAnnotationFiles\n static readANNOT(buffer: ArrayBuffer, n_vert: number, isReadColortables = false): ANNOT {\n const view = new DataView(buffer) // ArrayBuffer to dataview\n // ALWAYS big endian\n const n_vertex = view.getUint32(0, false)\n const n_vertexDecimated = this.decimateLayerVertices(n_vertex, n_vert)\n if (n_vert !== n_vertexDecimated) {\n throw new Error('ANNOT file has different number of vertices than mesh')\n }\n if (buffer.byteLength < 4 + 8 * n_vertex) {\n throw new Error('ANNOT file smaller than specified')\n }\n let pos = 0\n // reading all floats with .slice() would be faster, but lets handle endian-ness\n const rgba32 = new Uint32Array(n_vertex)\n for (let i = 0; i < n_vertex; i++) {\n const idx = view.getUint32((pos += 4), false)\n rgba32[idx] = view.getUint32((pos += 4), false)\n }\n if (!isReadColortables) {\n // only read label colors, ignore labels\n return rgba32\n }\n let tag = 0\n try {\n tag = view.getInt32((pos += 4), false)\n } catch (error) {\n return rgba32\n }\n const TAG_OLD_COLORTABLE = 1\n if (tag !== TAG_OLD_COLORTABLE) {\n // undocumented old format\n return rgba32\n }\n const ctabversion = view.getInt32((pos += 4), false)\n if (ctabversion > 0) {\n // undocumented old format\n return rgba32\n }\n const maxstruc = view.getInt32((pos += 4), false)\n const len = view.getInt32((pos += 4), false)\n pos += len\n const num_entries = view.getInt32((pos += 4), false)\n if (num_entries < 1) {\n // undocumented old format\n return rgba32\n }\n // preallocate lookuptable\n const LUT = {\n R: Array(maxstruc).fill(0),\n G: Array(maxstruc).fill(0),\n B: Array(maxstruc).fill(0),\n A: Array(maxstruc).fill(0),\n I: Array(maxstruc).fill(0),\n labels: Array(maxstruc).fill('')\n }\n for (let i = 0; i < num_entries; i++) {\n const struc = view.getInt32((pos += 4), false)\n const labelLen = view.getInt32((pos += 4), false)\n pos += 4\n let txt = ''\n for (let c = 0; c < labelLen; c++) {\n const val = view.getUint8(pos++)\n if (val === 0) {\n break\n }\n txt += String.fromCharCode(val)\n }\n pos -= 4\n const R = view.getInt32((pos += 4), false)\n const G = view.getInt32((pos += 4), false)\n const B = view.getInt32((pos += 4), false)\n const A = view.getInt32((pos += 4), false)\n if (struc < 0 || struc >= maxstruc) {\n log.warn('annot entry out of range')\n continue\n }\n LUT.R[struc] = R\n LUT.G[struc] = G\n LUT.B[struc] = B\n LUT.A[struc] = A\n LUT.I[struc] = (A << 24) + (B << 16) + (G << 8) + R\n LUT.labels[struc] = txt\n }\n // start issue1455: if ALL labels are transparent assume ALL opaque\n let isAllAlphaZero = true\n for (let i = 0; i < maxstruc; i++) {\n if (LUT.A[i] > 0) {\n isAllAlphaZero = false\n }\n }\n if (isAllAlphaZero) {\n for (let i = 0; i < maxstruc; i++) {\n LUT.A[i] = 255\n }\n }\n // end issue1455\n const scalars = new Float32Array(n_vertex)\n scalars.fill(-1)\n let nError = 0\n for (let i = 0; i < n_vert; i++) {\n const RGB = rgba32[i]\n for (let c = 0; c < maxstruc; c++) {\n if (LUT.I[c] === RGB) {\n scalars[i] = c\n break\n }\n } // for c\n if (scalars[i] < 0) {\n nError++\n scalars[i] = 0\n }\n }\n if (nError > 0) {\n log.error(`annot vertex colors do not match ${nError} of ${n_vertex} vertices.`)\n }\n for (let i = 0; i < maxstruc; i++) {\n LUT.I[i] = i\n }\n const colormapLabel = cmapper.makeLabelLut(LUT)\n return {\n scalars,\n colormapLabel\n }\n } // readANNOT()\n\n // read BrainNet viewer format\n // https://www.nitrc.org/projects/bnv/\n static readNV(buffer: ArrayBuffer): DefaultMeshType {\n // n.b. clockwise triangle winding, indexed from 1\n const len = buffer.byteLength\n const bytes = new Uint8Array(buffer)\n let pos = 0\n function readStr(): string {\n while (pos < len && bytes[pos] === 10) {\n pos++\n } // skip blank lines\n const startPos = pos\n while (pos < len && bytes[pos] !== 10) {\n pos++\n }\n pos++ // skip EOLN\n if (pos - startPos < 1) {\n return ''\n }\n return new TextDecoder().decode(buffer.slice(startPos, pos - 1))\n }\n let nvert = 0 // 173404 346804\n let ntri = 0\n let v = 0\n let t = 0\n let positions: Float32Array\n let indices: Uint32Array\n while (pos < len) {\n const line = readStr()\n if (line.startsWith('#')) {\n continue\n }\n const items = line.trim().split(/\\s+/)\n if (nvert < 1) {\n nvert = parseInt(items[0])\n positions = new Float32Array(nvert * 3)\n continue\n }\n if (v < nvert * 3) {\n positions![v] = parseFloat(items[0])\n positions![v + 1] = parseFloat(items[1])\n positions![v + 2] = parseFloat(items[2])\n v += 3\n continue\n }\n if (ntri < 1) {\n ntri = parseInt(items[0])\n indices = new Uint32Array(ntri * 3)\n continue\n }\n if (t >= ntri * 3) {\n break\n }\n indices![t + 2] = parseInt(items[0]) - 1\n indices![t + 1] = parseInt(items[1]) - 1\n indices![t + 0] = parseInt(items[2]) - 1\n t += 3\n }\n return {\n positions: positions!,\n indices: indices!\n }\n } // readNV()\n\n // read ASCII Patch File format\n // https://afni.nimh.nih.gov/pub/dist/doc/htmldoc/demos/Bootcamp/CD.html#cd\n // http://www.grahamwideman.com/gw/brain/fs/surfacefileformats.htm\n static readASC(buffer: ArrayBuffer): DefaultMeshType {\n const len = buffer.byteLength\n const bytes = new Uint8Array(buffer)\n let pos = 0\n function readStr(): string {\n while (pos < len && bytes[pos] === 10) {\n pos++\n } // skip blank lines\n const startPos = pos\n while (pos < len && bytes[pos] !== 10) {\n pos++\n }\n pos++ // skip EOLN\n if (pos - startPos < 1) {\n return ''\n }\n return new TextDecoder().decode(buffer.slice(startPos, pos - 1))\n }\n let line = readStr() // 1st line: '#!ascii version of lh.pial'\n if (!line.startsWith('#!ascii')) {\n log.warn('Invalid ASC mesh')\n }\n line = readStr() // 1st line: signature\n let items = line.trim().split(/\\s+/)\n const nvert = parseInt(items[0]) // 173404 346804\n const ntri = parseInt(items[1])\n const positions = new Float32Array(nvert * 3)\n let j = 0\n for (let i = 0; i < nvert; i++) {\n line = readStr() // 1st line: signature\n items = line.trim().split(/\\s+/)\n positions[j] = parseFloat(items[0])\n positions[j + 1] = parseFloat(items[1])\n positions[j + 2] = parseFloat(items[2])\n j += 3\n }\n const indices = new Uint32Array(ntri * 3)\n j = 0\n for (let i = 0; i < ntri; i++) {\n line = readStr() // 1st line: signature\n items = line.trim().split(/\\s+/)\n indices[j] = parseInt(items[0])\n indices[j + 1] = parseInt(items[1])\n indices[j + 2] = parseInt(items[2])\n j += 3\n }\n return {\n positions,\n indices\n }\n } // readASC()\n\n // read legacy VTK format\n static readVTK(buffer: ArrayBuffer): VTK {\n const len = buffer.byteLength\n if (len < 20) {\n throw new Error('File too small to be VTK: bytes = ' + buffer.byteLength)\n }\n const bytes = new Uint8Array(buffer)\n let pos = 0\n function readStr(isSkipBlank = true): string {\n if (isSkipBlank) {\n while (pos < len && bytes[pos] === 10) {\n pos++\n }\n } // skip blank lines\n const startPos = pos\n while (pos < len && bytes[pos] !== 10) {\n pos++\n }\n pos++ // skip EOLN\n if (pos - startPos < 1) {\n return ''\n }\n return new TextDecoder().decode(buffer.slice(startPos, pos - 1))\n }\n let line = readStr() // 1st line: signature\n if (!line.startsWith('# vtk DataFile')) {\n throw new Error('Invalid VTK mesh')\n }\n line = readStr(false) // 2nd line comment, n.b. MRtrix stores empty line\n line = readStr() // 3rd line ASCII/BINARY\n if (line.startsWith('ASCII')) {\n return NVMeshLoaders.readTxtVTK(buffer)\n } else if (!line.startsWith('BINARY')) {\n throw new Error('Invalid VTK image, expected ASCII or BINARY ' + line)\n }\n line = readStr() // 5th line \"DATASET POLYDATA\"\n if (!line.includes('POLYDATA')) {\n throw new Error('Only able to read VTK POLYDATA ' + line)\n }\n line = readStr() // 6th line \"POINTS 10261 float\"\n if (!line.includes('POINTS') || (!line.includes('double') && !line.includes('float'))) {\n log.warn('Only able to read VTK float or double POINTS' + line)\n }\n const isFloat64 = line.includes('double')\n let items = line.trim().split(/\\s+/)\n const nvert = parseInt(items[1]) // POINTS 10261 float\n const nvert3 = nvert * 3\n const positions = new Float32Array(nvert3)\n const reader = new DataView(buffer)\n if (isFloat64) {\n for (let i = 0; i < nvert3; i++) {\n positions[i] = reader.getFloat64(pos, false)\n pos += 8\n }\n } else {\n for (let i = 0; i < nvert3; i++) {\n positions[i] = reader.getFloat32(pos, false)\n pos += 4\n }\n }\n line = readStr() // Type, \"LINES 11885 \"\n items = line.trim().split(/\\s+/)\n const tris = []\n if (items[0].includes('LINES')) {\n const n_count = parseInt(items[1])\n // tractogaphy data: detect if borked by DiPy\n const posOK = pos\n line = readStr() // borked files \"OFFSETS vtktypeint64\"\n if (line.startsWith('OFFSETS')) {\n let isInt64 = false\n if (line.includes('int64')) {\n isInt64 = true\n }\n const offsetPt0 = new Uint32Array(n_count)\n if (isInt64) {\n let isOverflowInt32 = false\n for (let c = 0; c < n_count; c++) {\n let idx = reader.getInt32(pos, false)\n if (idx !== 0) {\n isOverflowInt32 = true\n }\n pos += 4\n idx = reader.getInt32(pos, false)\n pos += 4\n offsetPt0[c] = idx\n }\n if (isOverflowInt32) {\n log.warn('int32 overflow: JavaScript does not support int64')\n }\n } else {\n for (let c = 0; c < n_count; c++) {\n const idx = reader.getInt32(pos, false)\n pos += 4\n offsetPt0[c] = idx\n }\n }\n const pts = positions\n return {\n pts,\n offsetPt0\n }\n }\n pos = posOK // valid VTK file\n let npt = 0\n const offsetPt0 = []\n const pts = []\n offsetPt0.push(npt) // 1st streamline starts at 0\n for (let c = 0; c < n_count; c++) {\n const numPoints = reader.getInt32(pos, false)\n pos += 4\n npt += numPoints\n offsetPt0.push(npt)\n for (let i = 0; i < numPoints; i++) {\n const idx = reader.getInt32(pos, false) * 3\n pos += 4\n pts.push(positions[idx + 0])\n pts.push(positions[idx + 1])\n pts.push(positions[idx + 2])\n } // for numPoints: number of segments in streamline\n } // for n_count: number of streamlines\n return {\n pts: Float32Array.from(pts),\n offsetPt0: Uint32Array.from(offsetPt0)\n }\n } else if (items[0].includes('TRIANGLE_STRIPS')) {\n const nstrip = parseInt(items[1])\n for (let i = 0; i < nstrip; i++) {\n const ntri = reader.getInt32(pos, false) - 2 // -2 as triangle strip is creates pts - 2 faces\n pos += 4\n for (let t = 0; t < ntri; t++) {\n if (t % 2) {\n // preserve winding order\n tris.push(reader.getInt32(pos + 8, false))\n tris.push(reader.getInt32(pos + 4, false))\n tris.push(reader.getInt32(pos, false))\n } else {\n tris.push(reader.getInt32(pos, false))\n tris.push(reader.getInt32(pos + 4, false))\n tris.push(reader.getInt32(pos + 8, false))\n }\n pos += 4\n } // for each triangle\n pos += 8\n } // for each strip\n } else if (items[0].includes('POLYGONS')) {\n const npoly = parseInt(items[1])\n const byteOffsetAfterPoly = pos\n const maybeOffsetsLine = readStr()\n if (maybeOffsetsLine.startsWith('OFFSETS')) {\n let isInt64 = maybeOffsetsLine.includes('int64')\n const offset = new Uint32Array(npoly)\n let is32bitOverflow = false\n for (let i = 0; i < npoly; i++) {\n if (isInt64) {\n if (reader.getInt32(pos, false) !== 0) {\n is32bitOverflow = true\n }\n pos += 4\n } // skip high 32 bits\n offset[i] = reader.getInt32(pos, false)\n pos += 4\n }\n if (!Number.isSafeInteger(npoly) || npoly >= 2147483648 || is32bitOverflow) {\n throw new Error(`values exceed 2GB limit`)\n }\n const connLine = readStr()\n if (!connLine.startsWith('CONNECTIVITY')) {\n throw new Error('Expected CONNECTIVITY after OFFSETS')\n }\n isInt64 = connLine.includes('int64')\n const numIndices = offset[npoly - 1]\n const connectivity = new Uint32Array(numIndices)\n for (let i = 0; i < numIndices; i++) {\n if (isInt64) {\n pos += 4\n }\n connectivity[i] = reader.getInt32(pos, false)\n pos += 4\n }\n\n for (let i = 0; i < npoly; i++) {\n const start = i === 0 ? 0 : offset[i - 1]\n const end = offset[i]\n for (let t = 1; t < end - start - 1; t++) {\n tris.push(connectivity[start])\n tris.push(connectivity[start + t])\n tris.push(connectivity[start + t + 1])\n }\n }\n } else {\n // Classic binary VTK format: rewind and parse as before\n pos = byteOffsetAfterPoly\n for (let i = 0; i < npoly; i++) {\n const ntri = reader.getInt32(pos, false) - 2\n if (i === 0 && ntri > 65535) {\n throw new Error('Invalid VTK binary polygons using little-endian data (MRtrix)')\n }\n pos += 4\n const fx = reader.getInt32(pos, false)\n pos += 4\n let fy = reader.getInt32(pos, false)\n pos += 4\n for (let t = 0; t < ntri; t++) {\n const fz = reader.getInt32(pos, false)\n pos += 4\n tris.push(fx, fy, fz)\n fy = fz\n }\n }\n }\n } else {\n throw new Error('Unsupported binary VTK datatype ' + items[0])\n }\n const indices = new Uint32Array(tris)\n return {\n positions,\n indices\n }\n } // readVTK()\n\n static readWRL(buffer: ArrayBuffer): DefaultMeshType {\n const wrlText = new TextDecoder('utf-8').decode(buffer)\n const coordRegex = /coord\\s+Coordinate\\s*\\{\\s*point\\s*\\[([\\s\\S]*?)\\]/\n const indexRegex = /coordIndex\\s*\\[([\\s\\S]*?)\\]/\n const colorRegex = /color\\s+Color\\s*\\{\\s*color\\s*\\[([\\s\\S]*?)\\]/\n const coordMatch = coordRegex.exec(wrlText)\n const indexMatch = indexRegex.exec(wrlText)\n const colorMatch = colorRegex.exec(wrlText)\n\n if (!coordMatch || !indexMatch) {\n throw new Error('Invalid WRL file: Could not find vertices or indices.')\n }\n // Extract vertex positions\n const positions = new Float32Array(\n coordMatch[1]\n .trim()\n .split(/[\\s,]+/)\n .map(Number)\n )\n // Extract per-vertex colors (if they exist)\n let colors: Float32Array | null = null\n if (colorMatch) {\n colors = new Float32Array(\n colorMatch[1]\n .trim()\n .split(/[\\s,]+/)\n .map(Number)\n )\n const nVert = positions.length / 3\n if (colors.length !== nVert * 3) {\n console.warn(`Unexpected color count: expected ${nVert * 3}, got ${colors.length}`)\n colors = null // Ignore malformed color data\n }\n }\n // Extract triangle indices (ignoring `-1` separators)\n const indices = new Uint32Array(\n indexMatch[1]\n .trim()\n .split(/[\\s,]+/)\n .map(Number)\n .filter((v) => v !== -1)\n )\n return { positions, indices, colors }\n } // readWRL()\n\n // read brainsuite DFS format\n // http://brainsuite.org/formats/dfs/\n static readDFS(buffer: ArrayBuffer): DefaultMeshType {\n // Does not play with other formats: vertex positions do not use Aneterior Commissure as origin\n const reader = new DataView(buffer)\n const magic = reader.getUint32(0, true) // \"DFS_\"\n const LE = reader.getUint16(4, true) // \"LE\"\n if (magic !== 1599292996 || LE !== 17740) {\n log.warn('Not a little-endian brainsuite DFS mesh')\n }\n const hdrBytes = reader.getUint32(12, true)\n // var mdoffset = reader.getUint32(16, true);\n // var pdoffset = reader.getUint32(20, true);\n const nface = reader.getUint32(24, true) // number of triangles\n const nvert = reader.getUint32(28, true)\n // var nStrips = reader.getUint32(32, true); //deprecated\n // var stripSize = reader.getUint32(36, true); //deprecated\n // var normals = reader.getUint32(40, true);\n // var uvStart = reader.getUint32(44, true);\n const vcoffset = reader.getUint32(48, true) // vertexColor offset\n // var precision = reader.getUint32(52, true);\n // float64 orientation[4][4]; //4x4 matrix, affine transformation to world coordinates*)\n let pos = hdrBytes\n const indices = new Uint32Array(buffer, pos, nface * 3)\n pos += nface * 3 * 4\n const positions = new Float32Array(buffer, pos, nvert * 3)\n // oops, triangle winding opposite of CCW convention\n for (let i = 0; i < nvert * 3; i += 3) {\n const tmp = positions[i]\n positions[i] = positions[i + 1]\n positions[i + 1] = tmp\n }\n let colors\n if (vcoffset >= 0) {\n colors = new Float32Array(buffer, vcoffset, nvert * 3)\n }\n return {\n positions,\n indices,\n colors\n }\n }\n\n // read surfice MZ3 format\n // https://github.com/neurolabusc/surf-ice/tree/master/mz3\n static async readMZ3(buffer: ArrayBuffer, n_vert = 0): Promise<MZ3> {\n if (buffer.byteLength < 20) {\n throw new Error('File too small to be mz3: bytes = ' + buffer.byteLength)\n }\n let reader = new DataView(buffer)\n let _buffer = buffer\n\n // Check for gzip\n let magic = reader.getUint16(0, true)\n if (magic === 35615 || magic === 8075) {\n const raw = await NVUtilities.decompress(new Uint8Array(buffer))\n reader = new DataView(raw.buffer)\n magic = reader.getUint16(0, true)\n _buffer = raw.buffer\n }\n const attr = reader.getUint16(2, true)\n const nface = reader.getUint32(4, true)\n let nvert = reader.getUint32(8, true)\n const nskip = reader.getUint32(12, true)\n utiltiesLogger.debug('MZ3 magic %d attr %d face %d vert %d skip %d', magic, attr, nface, nvert, nskip)\n if (magic !== 23117) {\n throw new Error('Invalid MZ3 file')\n }\n const isFace = (attr & 1) !== 0\n const isVert = (attr & 2) !== 0\n const isRGBA = (attr & 4) !== 0\n let isSCALAR = (attr & 8) !== 0\n const isDOUBLE = (attr & 16) !== 0\n const isAOMAP = (attr & 32) !== 0\n const isLOOKUP = (attr & 64) !== 0\n utiltiesLogger.debug(`isFace=${isFace} isVert=${isVert} isRGBA=${isRGBA} isSCALAR=${isSCALAR} isDOUBLE=${isDOUBLE} isAOMAP=${isAOMAP} isLOOKUP=${isLOOKUP}`)\n if (attr > 127) {\n throw new Error('Unsupported future version of MZ3 file')\n }\n\n let bytesPerScalar = 4\n if (isDOUBLE) {\n bytesPerScalar = 8\n }\n\n let NSCALAR = 0\n if (n_vert > 0 && !isFace && nface < 1 && !isRGBA) {\n isSCALAR = true\n }\n if (isSCALAR) {\n const nv = n_vert || nvert\n const FSizeWoScalars = 16 + nskip + (isFace ? nface * 12 : 0) + (isVert ? nv * 12 : 0) + (isRGBA ? nv * 4 : 0)\n const scalarFloats = Math.floor((_buffer.byteLength - FSizeWoScalars) / bytesPerScalar)\n if (nvert !== n_vert && scalarFloats % n_vert === 0) {\n nvert = n_vert\n }\n NSCALAR = Math.floor(scalarFloats / nvert)\n if (NSCALAR < 1) {\n log.warn('Corrupt MZ3: file reports NSCALAR but not enough bytes')\n isSCALAR = false\n }\n }\n\n if (nvert < 3 && n_vert < 3) {\n throw new Error('Not a mesh MZ3 file (maybe scalar)')\n }\n if (n_vert > 0 && n_vert !== nvert) {\n log.warn('Layer has ' + nvert + 'vertices, but background mesh has ' + n_vert)\n }\n\n let filepos = 16 + nskip\n const view = new DataView(_buffer)\n\n let indices: Uint32Array | null = null\n if (isFace) {\n indices = new Uint32Array(nface * 3)\n for (let i = 0; i < nface * 3; i++) {\n indices[i] = view.getUint32(filepos, true)\n filepos += 4\n }\n }\n\n let positions: Float32Array | null = null\n if (isVert) {\n positions = new Float32Array(nvert * 3)\n for (let i = 0; i < nvert * 3; i++) {\n positions[i] = view.getFloat32(filepos, true)\n filepos += 4\n }\n }\n let colors: Float32Array | null = null\n if (isRGBA) {\n colors = new Float32Array(nvert * 3)\n for (let i = 0; i < nvert; i++) {\n for (let j = 0; j < 3; j++) {\n colors[i * 3 + j] = view.getUint8(filepos++) / 255\n }\n filepos++ // skip Alpha\n }\n }\n let scalars = new Float32Array()\n if (isSCALAR && NSCALAR > 0) {\n if (isDOUBLE) {\n const flt64 = new Float64Array(NSCALAR * nvert)\n for (let i = 0; i < NSCALAR * nvert; i++) {\n flt64[i] = view.getFloat64(filepos, true)\n filepos += 8\n }\n scalars = Float32Array.from(flt64)\n } else {\n scalars = new Float32Array(NSCALAR * nvert)\n for (let i = 0; i < NSCALAR * nvert; i++) {\n scalars[i] = view.getFloat32(filepos, true)\n filepos += 4\n }\n }\n }\n if (n_vert > 0 && isLOOKUP && isSCALAR) {\n // Read NSKIP bytes after 16-byte header\n const decoder = new TextDecoder('utf-8')\n const jsonBytes = new Uint8Array(_buffer, 16, nskip)\n const jsonText = decoder.decode(jsonBytes)\n const colormap = JSON.parse(jsonText)\n const colormapLabel = cmapper.makeLabelLut(colormap)\n return { scalars, colormapLabel }\n }\n if (n_vert > 0 && isRGBA && isSCALAR) {\n let mx = scalars[0]\n for (let i = 0; i < nvert; i++) {\n mx = Math.max(mx, scalars[i])\n }\n const Labels: ColorMap = { R: [], G: [], B: [], A: [], I: [], labels: [] }\n for (let i = 0; i <= mx; i++) {\n for (let v = 0; v < nvert; v++) {\n if (i === scalars[v]) {\n const v3 = v * 3\n Labels.I.push(i)\n Labels.R.push(colors![v3] * 255)\n Labels.G.push(colors![v3 + 1] * 255)\n Labels.B.push(colors![v3 + 2] * 255)\n Labels.A.push(255)\n Labels.labels!.push(`${i}`)\n break\n }\n }\n }\n const colormapLabel = cmapper.makeLabelLut(Labels)\n return { scalars, colormapLabel }\n }\n if (n_vert > 0) {\n return { scalars }\n }\n return { positions, indices, scalars, colors }\n }\n\n // read PLY format\n // https://en.wikipedia.org/wiki/PLY_(file_format)\n static readPLY(buffer: ArrayBuffer): DefaultMeshType {\n const len = buffer.byteLength\n const bytes = new Uint8Array(buffer)\n let pos = 0\n function readStr(): string {\n while (pos < len && bytes[pos] === 10) {\n pos++\n } // skip blank lines\n const startPos = pos\n while (pos < len && bytes[pos] !== 10) {\n pos++\n }\n pos++ // skip EOLN\n if (pos - startPos < 1) {\n return ''\n }\n return new TextDecoder().decode(buffer.slice(startPos, pos - 1))\n }\n let line = readStr() // 1st line: magic 'ply'\n if (!line.startsWith('ply')) {\n throw new Error('Not a valid PLY file')\n }\n line = readStr() // 2nd line: format 'format binary_little_endian 1.0'\n const isAscii = line.includes('ascii')\n function dataTypeBytes(str: string): number {\n if (str === 'char' || str === 'uchar' || str === 'int8' || str === 'uint8') {\n return 1\n }\n if (str === 'short' || str === 'ushort' || str === 'int16' || str === 'uint16') {\n return 2\n }\n if (str === 'int' || str === 'uint' || str === 'int32' || str === 'uint32' || str === 'float' || str === 'float32') {\n return 4\n }\n if (str === 'double') {\n return 8\n }\n throw new Error('Unknown data type: ' + str)\n }\n const isLittleEndian = line.includes('binary_little_endian')\n let nvert = 0\n let vertIsDouble = false\n let vertStride = 0 // e.g. if each vertex stores xyz as float32 and rgb as uint8, stride is 15\n let indexStrideBytes = 0 // \"list uchar int vertex_indices\" has stride 1 + 3 * 4\n let indexCountBytes = 0 // if \"property list uchar int vertex_index\" this is 1 (uchar)\n let indexBytes = 0 // if \"property list uchar int vertex_index\" this is 4 (int)\n let indexPaddingBytes = 0\n let nIndexPadding = 0\n let nface = 0\n const vertexProps: string[] = []\n let currVertPropOffset = 0\n let redOffset = -1\n let greenOffset = -1\n let blueOffset = -1\n let hasColors = false\n while (pos < len && !line.startsWith('end_header')) {\n line = readStr()\n if (line.startsWith('comment')) {\n continue\n }\n // line = line.replaceAll('\\t', ' '); // ?are tabs valid white space?\n let items = line.split(/\\s/)\n if (line.startsWith('element vertex')) {\n nvert = parseInt(items[items.length - 1])\n // read vertex properties:\n line = readStr()\n items = line.split(/\\s/)\n currVertPropOffset = 0\n vertexProps.length = 0\n while (line.startsWith('property')) {\n const datatype = items[1]\n const propName = items[2]\n // record property name (for ASCII token mapping)\n vertexProps.push(propName)\n // record offsets for color components (for binary parsing)\n if (propName === 'red') {\n redOffset = currVertPropOffset\n hasColors = true\n } else if (propName === 'green') {\n greenOffset = currVertPropOffset\n hasColors = true\n } else if (propName === 'blue') {\n blueOffset = currVertPropOffset\n hasColors = true\n }\n if (propName === 'x' && datatype.startsWith('double')) {\n vertIsDouble = true\n } else if (propName === 'x' && !datatype.startsWith('float')) {\n log.error('Error: expect ply xyz to be float or double: ' + line)\n }\n const bytes = dataTypeBytes(datatype)\n vertStride += bytes\n currVertPropOffset += bytes\n line = readStr()\n items = line.split(/\\s/)\n }\n }\n if (line.startsWith('element face')) {\n nface = parseInt(items[items.length - 1])\n // read face properties:\n line = readStr()\n items = line.split(/\\s/)\n while (line.startsWith('property')) {\n if (items[1] === 'list') {\n indexCountBytes = dataTypeBytes(items[2])\n indexBytes = dataTypeBytes(items[3])\n indexStrideBytes += indexCountBytes + 3 * indexBytes // e.g. \"uchar int\" is 1 + 3 * 4 bytes\n } else {\n const bytes = dataTypeBytes(items[1])\n indexStrideBytes += bytes\n if (indexBytes === 0) {\n // this index property is BEFORE the list\n indexPaddingBytes += bytes\n nIndexPadding++\n }\n }\n line = readStr()\n items = line.split(/\\s/)\n }\n }\n } // while reading all lines of header\n if (isAscii) {\n if (nface < 1) {\n log.error(`Malformed ply format: faces ${nface} `)\n }\n const positions = new Float32Array(nvert * 3)\n let colors: Float32Array | undefined\n // find ascii token indices for x,y,z and rgb if present\n const idxX = vertexProps.indexOf('x')\n const idxY = vertexProps.indexOf('y')\n const idxZ = vertexProps.indexOf('z')\n const idxR = vertexProps.indexOf('red')\n const idxG = vertexProps.indexOf('green')\n const idxB = vertexProps.indexOf('blue')\n if (idxR !== -1 && idxG !== -1 && idxB !== -1) {\n colors = new Float32Array(nvert * 3)\n hasColors = true\n }\n let v = 0\n for (let i = 0; i < nvert; i++) {\n line = readStr()\n const items = line.split(/\\s/)\n // read xyz using property indices (fallback to 0,1,2 if not found)\n const tx = idxX >= 0 ? parseFloat(items[idxX]) : parseFloat(items[0])\n const ty = idxY >= 0 ? parseFloat(items[idxY]) : parseFloat(items[1])\n const tz = idxZ >= 0 ? parseFloat(items[idxZ]) : parseFloat(items[2])\n positions[v] = tx\n positions[v + 1] = ty\n positions[v + 2] = tz\n if (hasColors && colors) {\n const rr = idxR >= 0 ? parseInt(items[idxR]) : 0\n const gg = idxG >= 0 ? parseInt(items[idxG]) : 0\n const bb = idxB >= 0 ? parseInt(items[idxB]) : 0\n const vi = i * 3\n colors[vi] = rr / 255.0\n colors[vi + 1] = gg / 255.0\n colors[vi + 2] = bb / 255.0\n }\n v += 3\n }\n let indices = new Uint32Array(nface * 3)\n let f = 0\n for (let i = 0; i < nface; i++) {\n line = readStr()\n const items = line.split(/\\s/)\n const nTri = parseInt(items[nIndexPadding]) - 2\n if (nTri < 1) {\n break\n } // error\n if (f + nTri * 3 > indices.length) {\n const c = new Uint32Array(indices.length + indices.length)\n c.set(indices)\n indices = c.slice()\n }\n const idx0 = parseInt(items[nIndexPadding + 1])\n let idx1 = parseInt(items[nIndexPadding + 2])\n for (let j = 0; j < nTri; j++) {\n const idx2 = parseInt(items[nIndexPadding + 3 + j])\n indices[f + 0] = idx0\n indices[f + 1] = idx1\n indices[f + 2] = idx2\n idx1 = idx2\n f += 3\n }\n }\n if (indices.length !== f) {\n indices = indices.slice(0, f)\n }\n const out: DefaultMeshType = {\n positions,\n indices\n }\n if (hasColors) {\n // colors was created only when we detected rgb properties\n out.colors = typeof colors !== 'undefined' ? colors : undefined\n }\n return out\n } // if isAscii\n if (vertStride < 12 || indexCountBytes < 1 || indexBytes < 1 || nface < 1) {\n log.warn(`Malformed ply format: stride ${vertStride} count ${indexCountBytes} iBytes ${indexBytes} iStrideBytes ${indexStrideBytes} iPadBytes ${indexPaddingBytes} faces ${nface}`)\n }\n const reader = new DataView(buffer)\n let positions\n let colors: Float32Array | undefined\n if (hasColors) {\n colors = new Float32Array(nvert * 3)\n }\n if (pos % 4 === 0 && vertStride === 12 && isLittleEndian) {\n // optimization: vertices only store xyz position as float\n // n.b. start offset of Float32Array must be a multiple of 4\n positions = new Float32Array(buffer, pos, nvert * 3)\n pos += nvert * vertStride\n // if colors are present they wouldn't be in this optimized path (vertStride would be >12)\n } else {\n positions = new Float32Array(nvert * 3)\n let v = 0\n for (let i = 0; i < nvert; i++) {\n if (vertIsDouble) {\n positions[v] = reader.getFloat64(pos, isLittleEndian)\n positions[v + 1] = reader.getFloat64(pos + 8, isLittleEndian)\n positions[v + 2] = reader.getFloat64(pos + 16, isLittleEndian)\n } else {\n positions[v] = reader.getFloat32(pos, isLittleEndian)\n positions[v + 1] = reader.getFloat32(pos + 4, isLittleEndian)\n positions[v + 2] = reader.getFloat32(pos + 8, isLittleEndian)\n }\n if (hasColors && colors) {\n // read uchar rgb at recorded offsets (if set)\n const base = pos\n const r = redOffset >= 0 ? reader.getUint8(base + redOffset) : 0\n const g = greenOffset >= 0 ? reader.getUint8(base + greenOffset) : 0\n const b = blueOffset >= 0 ? reader.getUint8(base + blueOffset) : 0\n const vi = i * 3\n colors[vi] = r / 255.0\n colors[vi + 1] = g / 255.0\n colors[vi + 2] = b / 255.0\n }\n v += 3\n pos += vertStride\n }\n }\n const indices = new Uint32Array(nface * 3) // assume triangular mesh: pre-allocation optimization\n let isTriangular = true\n let j = 0\n if (indexCountBytes === 1 && indexBytes === 4 && indexStrideBytes === 13) {\n // default mode: \"list uchar int vertex_indices\" without other properties\n for (let i = 0; i < nface; i++) {\n const nIdx = reader.getUint8(pos)\n pos += indexCountBytes\n if (nIdx !== 3) {\n isTriangular = false\n }\n indices[j] = reader.getUint32(pos, isLittleEndian)\n pos += 4\n indices[j + 1] = reader.getUint32(pos, isLittleEndian)\n pos += 4\n indices[j + 2] = reader.getUint32(pos, isLittleEndian)\n pos += 4\n j += 3\n }\n } else {\n // not 1:4 index data\n let startPos = pos\n for (let i = 0; i < nface; i++) {\n pos = startPos + indexPaddingBytes\n let nIdx = 0\n if (indexCountBytes === 1) {\n nIdx = reader.getUint8(pos)\n } else if (indexCountBytes === 2) {\n nIdx = reader.getUint16(pos, isLittleEndian)\n } else if (indexCountBytes === 4) {\n nIdx = reader.getUint32(pos, isLittleEndian)\n }\n pos += indexCountBytes\n if (nIdx !== 3) {\n isTriangular = false\n }\n for (let k = 0; k < 3; k++) {\n if (indexBytes === 1) {\n indices[j] = reader.getUint8(pos)\n } else if (indexBytes === 2) {\n indices[j] = reader.getUint16(pos, isLittleEndian)\n } else if (indexBytes === 4) {\n indices[j] = reader.getUint32(pos, isLittleEndian)\n }\n j++\n pos += indexBytes\n }\n startPos += indexStrideBytes\n } // for each face\n } // if not 1:4 datatype\n if (!isTriangular) {\n log.warn('Only able to read PLY meshes limited to triangles.')\n }\n const out: DefaultMeshType = {\n positions,\n indices\n }\n if (hasColors && typeof colors !== 'undefined') {\n out.colors = colors\n }\n return out\n } // readPLY()\n\n // FreeSurfer can convert meshes to ICO/TRI format text files\n // https://github.com/dfsp-spirit/freesurferformats/blob/434962608108c75d4337d5e7a5096e3bd4ee6ee6/R/read_fs_surface.R#L1090\n // detect TRI format that uses same extension\n // http://paulbourke.net/dataformats/tri/\n static readICO(buffer: ArrayBuffer): DefaultMeshType {\n const enc = new TextDecoder('utf-8')\n const txt = enc.decode(buffer)\n const lines = txt.split('\\n')\n let header = lines[0].trim().split(/\\s+/)\n // read line 0: header\n // FreeSurfer header has one item: [0]'num_verts'\n // Bourke header has 2 items: [0]'num_verts', [1]'num_faces'\n if (header.length > 1) {\n log.warn('This is not a valid FreeSurfer ICO/TRI mesh.')\n }\n const num_v = parseInt(header[0])\n // read vertices: each line has 4 values: index, x, y, z\n const positions = new Float32Array(num_v * 3)\n // let v = 0;\n let line = 1 // line 0 is header\n for (let i = 0; i < num_v; i++) {\n const items = lines[line].trim().split(/\\s+/)\n line++\n // idx is indexed from 1, not 0\n let idx = parseInt(items[0]) - 1\n const x = parseFloat(items[1])\n const y = parseFloat(items[2])\n const z = parseFloat(items[3])\n if (idx < 0 || idx >= num_v) {\n log.error('ICO vertices corrupted')\n break\n }\n idx *= 3\n positions[idx] = x\n positions[idx + 1] = y\n positions[idx + 2] = z\n } // read all vertices\n // read faces\n header = lines[line].trim().split(/\\s+/)\n line++\n const num_f = parseInt(header[0])\n const indices = new Uint32Array(num_f * 3)\n for (let i = 0; i < num_f; i++) {\n const items = lines[line].trim().split(/\\s+/)\n line++\n // all values indexed from 1, not 0\n let idx = parseInt(items[0]) - 1\n const x = parseInt(items[1]) - 1\n const y = parseInt(items[2]) - 1\n const z = parseInt(items[3]) - 1\n if (idx < 0 || idx >= num_f) {\n log.error('ICO indices corrupted')\n break\n }\n idx *= 3\n indices[idx] = x\n indices[idx + 1] = y\n indices[idx + 2] = z\n } // read all faces\n // FreeSurfer seems to enforce clockwise winding: reverse to CCW\n for (let j = 0; j < indices.length; j += 3) {\n const tri = indices[j]\n indices[j] = indices[j + 1]\n indices[j + 1] = tri\n }\n return {\n positions,\n indices\n }\n } // readICO()\n\n // While BYU and FreeSurfer GEO are related\n // - BYU can have multiple parts\n // - BYU faces not always triangular\n // http://www.grahamwideman.com/gw/brain/fs/surfacefileformats.htm#GeoFile\n // http://www.eg-models.de/formats/Format_Byu.html\n // https://github.com/dfsp-spirit/freesurferformats/blob/dafaf88a601dac90fa3c9aae4432f003f5344546/R/read_fs_surface.R#L924\n // https://github.com/dfsp-spirit/freesurferformats/blob/434962608108c75d4337d5e7a5096e3bd4ee6ee6/R/read_fs_surface.R#L1144\n // n.b. AFNI uses the '.g' extension for this format 'ConvertSurface -i_gii L.surf.gii -o_byu L'\n static readGEO(buffer: ArrayBuffer, isFlipWinding = false): DefaultMeshType {\n const enc = new TextDecoder('utf-8')\n const txt = enc.decode(buffer)\n const lines = txt.split('\\n')\n const header = lines[0].trim().split(/\\s+/)\n // read line 0: header\n // header[0]='nparts', [1]'npoints/vertices', [2]'npolys/faces', [3]'nconnects'\n const num_p = parseInt(header[0])\n let num_v = parseInt(header[1])\n let num_f = parseInt(header[2])\n const num_c = parseInt(header[3])\n if (num_p > 1 || num_c !== num_f * 3) {\n log.warn('Multi-part BYU/GEO header or not a triangular mesh.')\n }\n // skip line 1: it is redundant (contains number of faces once more)\n // next read the vertices (points)\n const pts = []\n num_v *= 3 // each vertex has three components (x,y,z)\n let v = 0\n let line = 2 // line 0 and 1 are header\n while (v < num_v) {\n const items = lines[line].trim().split(/\\s+/)\n line++\n for (let i = 0; i < items.length; i++) {\n pts.push(parseFloat(items[i]))\n v++\n if (v >= num_v) {\n break\n }\n } // for each item\n } // read all vertices\n // next read faces (triangles)\n const t: number[] = []\n num_f *= 3 // each triangle has three vertices (i,j,k)\n let f = 0\n while (f < num_f) {\n const items = lines[line].trim().split(/\\s+/)\n line++\n for (let i = 0; i < items.length; i++) {\n t.push(Math.abs(parseInt(items[i])) - 1)\n f++\n if (f >= num_f) {\n break\n }\n } // for each item\n } // read all faces\n // FreeSurfer seems to enforce clockwise winding: reverse to CCW\n if (isFlipWinding) {\n for (let j = 0; j < t.length; j += 3) {\n const tri = t[j]\n t[j] = t[j + 1]\n t[j + 1] = tri\n }\n }\n // return results\n const positions = new Float32Array(pts)\n const indices = new Uint32Array(t)\n return {\n positions,\n indices\n }\n } // readGEO()\n\n // read OFF format\n // https://en.wikipedia.org/wiki/OFF_(file_format)\n static readOFF(buffer: ArrayBuffer): DefaultMeshType {\n const enc = new TextDecoder('utf-8')\n const txt = enc.decode(buffer)\n const lines = txt.split('\\n')\n // var n = lines.length;\n const pts = []\n const t = []\n let i = 0\n // first line signature \"OFF\", but R freesurfer package uses \"# OFF\"\n if (!lines[i].includes('OFF')) {\n log.warn('File does not start with OFF')\n } else {\n i++\n }\n let items = lines[i].trim().split(/\\s+/)\n const num_v = parseInt(items[0])\n const num_f = parseInt(items[1])\n i++\n for (let j = 0; j < num_v; j++) {\n const str = lines[i]\n items = str.trim().split(/\\s+/)\n pts.push(parseFloat(items[0]))\n pts.push(parseFloat(items[1]))\n pts.push(parseFloat(items[2]))\n i++\n }\n for (let j = 0; j < num_f; j++) {\n const str = lines[i]\n items = str.trim().split(/\\s+/)\n const n = parseInt(items[0])\n if (n !== 3) {\n log.warn('Only able to read OFF files with triangular meshes')\n }\n t.push(parseInt(items[1]))\n t.push(parseInt(items[2]))\n t.push(parseInt(items[3]))\n i++\n }\n const positions = new Float32Array(pts)\n const indices = new Uint32Array(t)\n return {\n positions,\n indices\n }\n } // readOFF()\n\n static readOBJMNI(buffer: ArrayBuffer): DefaultMeshType {\n // Support MNI 'P'olygon mesh format\n // n.b. uses same .obj extension as WaveFront OBJ meshes\n // https://bigbrain.loris.ca/main.php?test_name=brainsurfaces\n // http://www.bic.mni.mcgill.ca/users/mishkin/mni_obj_format.pdf\n // https://pages.stat.wisc.edu/~mchung/softwares/mesh/mesh.html\n // https://github.com/aces/brainbrowser/tree/master\n const enc = new TextDecoder('utf-8')\n const txt = enc.decode(buffer)\n const items = txt.trim().split(/\\s*,\\s*|\\s+/)\n if (items.length < 1 || items[0] !== 'P') {\n log.warn('This is not a valid MNI OBJ mesh.')\n }\n let j = 6\n const nVert = parseInt(items[j++])\n const nVertX3 = nVert * 3\n const positions = new Float32Array(nVertX3)\n for (let i = 0; i < nVertX3; i++) {\n positions[i] = parseFloat(items[j++])\n }\n j += nVertX3\n const nTri = parseInt(items[j++])\n const colour_flag = parseInt(items[j++])\n if (nTri < 1 || colour_flag < 0 || colour_flag > 2) {\n log.warn('This is not a valid MNI OBJ mesh.')\n }\n let num_c = 1\n if (colour_flag === 1) {\n num_c = nTri\n } else if (colour_flag === 1) {\n num_c = nVert\n }\n j += num_c * 4\n j += nTri\n const nTriX3 = nTri * 3\n const indices = new Uint32Array(nTriX3)\n for (let i = 0; i < nTriX3; i++) {\n indices[i] = parseInt(items[j++])\n }\n return {\n positions,\n indices\n }\n } // readOBJMNI()\n\n static async readOBJ(buffer: ArrayBuffer): Promise<DefaultMeshType> {\n // WaveFront OBJ format\n const headerBytes = new Uint8Array(buffer, 0, 2)\n if (headerBytes[0] === 0x1f && headerBytes[1] === 0x8b) {\n // gzip signature 0x1F8B in little and big endian\n buffer = await NVUtilities.decompressToBuffer(new Uint8Array(buffer))\n }\n const enc = new TextDecoder('utf-8')\n const txt = enc.decode(buffer)\n if (txt[0] === 'P') {\n return this.readOBJMNI(buffer)\n }\n const lines = txt.split('\\n')\n const n = lines.length\n const pts = []\n const tris = []\n for (let i = 0; i < n; i++) {\n const str = lines[i]\n if (str[0] === 'v' && str[1] === ' ') {\n // 'v ' but not 'vt' or 'vn'\n const items = str.trim().split(/\\s+/)\n pts.push(parseFloat(items[1]))\n pts.push(parseFloat(items[2]))\n pts.push(parseFloat(items[3]))\n // v 0 -0.5 -0\n }\n if (str[0] === 'f') {\n const items = str.trim().split(/\\s+/)\n const new_t = items.length - 3 // number of new triangles created\n if (new_t < 1) {\n break\n } // error\n let tn = items[1].split('/')\n const t0 = parseInt(tn[0]) - 1 // first vertex\n tn = items[2].split('/')\n let tprev = parseInt(tn[0]) - 1 // previous vertex\n for (let j = 0; j < new_t; j++) {\n tn = items[3 + j].split('/')\n const tcurr = parseInt(tn[0]) - 1 // current vertex\n tris.push(t0)\n tris.push(tprev)\n tris.push(tcurr)\n tprev = tcurr\n }\n }\n } // for all lines\n const positions = new Float32Array(pts)\n const indices = new Uint32Array(tris)\n // next vertex indices are supposed to be from 1, not 0\n let min = indices[0]\n let max = indices[0]\n for (let i = 1; i < indices.length; i++) {\n if (indices[i] < min) {\n min = indices[i]\n }\n if (indices[i] > max) {\n max = indices[i]\n }\n }\n if (max - min + 1 > positions.length / 3) {\n throw new Error('Not a valid OBJ file')\n }\n for (let i = 0; i < indices.length; i++) {\n indices[i] -= min\n }\n return {\n positions,\n indices\n }\n } // readOBJ()\n\n // read FreeSurfer big endian format\n static readFreeSurfer(buffer: ArrayBuffer): DefaultMeshType {\n const bytes = new Uint8Array(buffer)\n if (bytes[0] === 35 && bytes[1] === 33 && bytes[2] === 97) {\n return NVMeshLoaders.readASC(buffer) // \"#!ascii version\"\n }\n const view = new DataView(buffer) // ArrayBuffer to dataview\n const sig0 = view.getUint32(0, false)\n const sig1 = view.getUint32(4, false)\n if (sig0 !== 4294966883 || sig1 !== 1919246708) {\n utiltiesLogger.debug('Unable to recognize file type: does not appear to be FreeSurfer format.')\n }\n let offset = 0\n while (view.getUint8(offset) !== 10) {\n offset++\n }\n offset += 2\n let nv = view.getUint32(offset, false) // number of vertices\n offset += 4\n let nf = view.getUint32(offset, false) // number of faces\n offset += 4\n nv *= 3 // each vertex has 3 positions: XYZ\n const positions = new Float32Array(nv)\n for (let i = 0; i < nv; i++) {\n positions[i] = view.getFloat32(offset, false)\n offset += 4\n }\n nf *= 3 // each triangle face indexes 3 triangles\n const indices = new Uint32Array(nf)\n for (let i = 0; i < nf; i++) {\n indices[i] = view.getUint32(offset, false)\n offset += 4\n }\n // read undocumented footer\n // https://github.com/nipy/nibabel/blob/8fea2a8e50aaf4d8b0d4bfff7a21b132914120ee/nibabel/freesurfer/io.py#L58C5-L58C9\n const head0 = view.getUint32(offset, false)\n offset += 4\n let isHeadOK = head0 === 20\n if (!isHeadOK) {\n // read two more int32s\n const head1 = view.getUint32(offset, false)\n offset += 4\n const head2 = view.getUint32(offset, false)\n offset += 4\n isHeadOK = head0 === 2 && head1 === 0 && head2 === 20\n }\n if (!isHeadOK) {\n log.warn('Unknown FreeSurfer Mesh extension code.')\n } else {\n const footer = new TextDecoder().decode(buffer.slice(offset)).trim()\n const strings = footer.split('\\n')\n for (let s = 0; s < strings.length; s++) {\n if (!strings[s].startsWith('cras')) {\n continue\n }\n const cras = strings[s].split('=')[1].trim()\n const FreeSurferTranlate = cras.split(' ').map(Number)\n const nvert = Math.floor(positions.length / 3)\n let i = 0\n for (let v = 0; v < nvert; v++) {\n positions[i] += FreeSurferTranlate[0]\n i++\n positions[i] += FreeSurferTranlate[1]\n i++\n positions[i] += FreeSurferTranlate[2]\n i++\n }\n }\n }\n return {\n positions,\n indices\n }\n } // readFreeSurfer()\n\n // read brainvoyager SRF format\n // https://support.brainvoyager.com/brainvoyager/automation-development/84-file-formats/344-users-guide-2-3-the-format-of-srf-files\n static async readSRF(buffer: ArrayBuffer): Promise<DefaultMeshType> {\n const bytes = new Uint8Array(buffer)\n if (bytes[0] === 35 && bytes[1] === 33 && bytes[2] === 97) {\n // .srf also used for freesurfer https://brainder.org/research/brain-for-blender/\n return NVMeshLoaders.readASC(buffer) // \"#!ascii version\"\n }\n if (bytes[0] === 31 && bytes[1] === 139) {\n // handle .srf.gz\n const raw = await NVUtilities.decompress(new Uint8Array(buffer))\n buffer = raw.buffer\n }\n const reader = new DataView(buffer)\n const ver = reader.getFloat32(0, true)\n const nVert = reader.getUint32(8, true)\n const nTri = reader.getUint32(12, true)\n const oriX = reader.getFloat32(16, true)\n const oriY = reader.getFloat32(20, true)\n const oriZ = reader.getFloat32(24, true)\n const positions = new Float32Array(nVert * 3)\n // BrainVoyager does not use Talairach coordinates for XYZ!\n // read X component of each vertex\n let pos = 28\n let j = 1 // BrainVoyager X is Talairach Y\n for (let i = 0; i < nVert; i++) {\n positions[j] = -reader.getFloat32(pos, true) + oriX\n j += 3 // read one of 3 components: XYZ\n pos += 4 // read one float32\n }\n // read Y component of each vertex\n j = 2 // BrainVoyager Y is Talairach Z\n for (let i = 0; i < nVert; i++) {\n positions[j] = -reader.getFloat32(pos, true) + oriY\n j += 3 // read one of 3 components: XYZ\n pos += 4 // read one float32\n }\n // read Z component of each vertex\n j = 0 // BrainVoyager Z is Talairach X\n for (let i = 0; i < nVert; i++) {\n positions[j] = -reader.getFloat32(pos, true) + oriZ\n j += 3 // read one of 3 components: XYZ\n pos += 4 // read one float32\n }\n // not sure why normals are stored, does bulk up file size\n pos = 28 + 4 * 6 * nVert // each vertex has 6 float32s: XYZ for position and normal\n // read concave and convex colors:\n const rVex = reader.getFloat32(pos, true)\n const gVex = reader.getFloat32(pos + 4, true)\n const bVex = reader.getFloat32(pos + 8, true)\n const rCave = reader.getFloat32(pos + 16, true)\n const gCave = reader.getFloat32(pos + 20, true)\n const bCave = reader.getFloat32(pos + 24, true)\n pos += 8 * 4 // skip 8 floats (RGBA convex/concave)\n // read per-vertex colors\n const colors = new Float32Array(nVert * 3)\n const colorsIdx = new Uint32Array(buffer, pos, nVert)\n j = 0 // convert RGBA -> RGB\n for (let i = 0; i < nVert; i++) {\n const c = colorsIdx[i]\n if (c > 1056964608) {\n colors[j + 0] = ((c >> 16) & 0xff) / 255\n colors[j + 1] = ((c >> 8) & 0xff) / 255\n colors[j + 2] = (c & 0xff) / 255\n }\n if (c === 0) {\n // convex\n colors[j + 0] = rVex\n colors[j + 1] = gVex\n colors[j + 2] = bVex\n }\n if (c === 1) {\n // concave\n colors[j + 0] = rCave\n colors[j + 1] = gCave\n colors[j + 2] = bCave\n }\n j += 3\n }\n pos += nVert * 4 // MeshColor, sequence of color indices\n // not sure why nearest neighbors are stored, slower and bigger files\n for (let i = 0; i < nVert; i++) {\n const nNearest = reader.getUint32(pos, true)\n pos += 4 + 4 * nNearest\n }\n const indices = new Uint32Array(nTri * 3)\n for (let i = 0; i < nTri * 3; i++) {\n indices[i] = reader.getInt32(pos, true)\n pos += 4\n }\n if (ver !== 4) {\n log.warn('Not valid SRF')\n }\n\n return {\n positions,\n indices,\n colors\n }\n } // readSRF()\n\n // read STL ASCII format file\n // http://paulbourke.net/dataformats/stl/\n static readTxtSTL(buffer: ArrayBuffer): DefaultMeshType {\n const enc = new TextDecoder('utf-8')\n const txt = enc.decode(buffer)\n const lines = txt.split('\\n')\n if (!lines[0].startsWith('solid')) {\n throw new Error('Not a valid STL file')\n }\n const pts = []\n for (let i = 1; i < lines.length; i++) {\n if (!lines[i].includes('vertex')) {\n continue\n }\n const items = lines[i].trim().split(/\\s+/)\n for (let j = 1; j < items.length; j++) {\n pts.push(parseFloat(items[j]))\n }\n }\n const npts = Math.floor(pts.length / 3) // each vertex has x,y,z\n if (npts * 3 !== pts.length) {\n throw new Error('Unable to parse ASCII STL file.')\n }\n const positions = new Float32Array(pts)\n const indices = new Uint32Array(npts)\n for (let i = 0; i < npts; i++) {\n indices[i] = i\n }\n return {\n positions,\n indices\n }\n } // readTxtSTL()\n\n // read STL format, nb this format does not reuse vertices\n // https://en.wikipedia.org/wiki/STL_(file_format)\n static readSTL(buffer: ArrayBuffer): DefaultMeshType {\n if (buffer.byteLength < 80 + 4 + 50) {\n throw new Error('File too small to be STL: bytes = ' + buffer.byteLength)\n }\n const reader = new DataView(buffer)\n const sig = reader.getUint32(0, true)\n if (sig === 1768714099) {\n return NVMeshLoaders.readTxtSTL(buffer)\n }\n const ntri = reader.getUint32(80, true)\n const ntri3 = 3 * ntri\n if (buffer.byteLength < 80 + 4 + ntri * 50) {\n throw new Error('STL file too small to store triangles = ' + ntri)\n }\n const indices = new Uint32Array(ntri3)\n const positions = new Float32Array(ntri3 * 3)\n let pos = 80 + 4 + 12\n let v = 0 // vertex\n for (let i = 0; i < ntri; i++) {\n for (let j = 0; j < 9; j++) {\n positions[v] = reader.getFloat32(pos, true)\n v += 1\n pos += 4\n }\n pos += 14 // 50 bytes for triangle, only 36 used for position\n }\n for (let i = 0; i < ntri3; i++) {\n indices[i] = i\n }\n return {\n positions,\n indices\n }\n } // readSTL()\n\n static decimateLayerVertices(nVertLayer: number, nVertMesh: number): number {\n // downsample layer vertices if the mesh has been decimated\n if (nVertLayer % nVertMesh === 0) {\n return nVertLayer\n }\n const V0 = 12\n const orderLayer = Math.round(Math.log((nVertLayer - 2) / (V0 - 2)) / Math.log(4))\n const orderMesh = Math.round(Math.log((nVertMesh - 2) / (V0 - 2)) / Math.log(4))\n // sanity check\n const nVLayer = Math.pow(4, orderLayer) * (V0 - 2) + 2\n const nVMesh = Math.pow(4, orderMesh) * (V0 - 2) + 2\n if (nVLayer !== nVertLayer || nVMesh !== nVertMesh) {\n return nVertLayer\n }\n return nVertMesh\n }\n\n // read NIfTI2 format with embedded CIfTI\n // this variation very specific to connectome workbench\n // https://brainder.org/2015/04/03/the-nifti-2-file-format/\n static async readNII2(buffer: ArrayBuffer, n_vert = 0, anatomicalStructurePrimary = ''): Promise<Int32Array | Float32Array | Int16Array | Uint8Array> {\n let scalars: Float32Array | Int32Array | Int16Array | Uint8Array = new Float32Array()\n const len = buffer.byteLength\n let isLittleEndian = true\n const reader = new DataView(buffer)\n let magic = reader.getUint16(0, isLittleEndian)\n if (magic === 469893120) {\n isLittleEndian = false\n magic = reader.getUint16(0, isLittleEndian)\n }\n if (magic !== 540) {\n throw new Error('Not a valid NIfTI-2 dataset')\n }\n const voxoffset = Number(reader.getBigInt64(168, isLittleEndian))\n const scl_slope = reader.getFloat64(176, isLittleEndian)\n const scl_inter = reader.getFloat64(184, isLittleEndian)\n if (scl_slope !== 1 || scl_inter !== 0) {\n log.warn('ignoring scale slope and intercept')\n }\n const intent_code = reader.getUint32(504, isLittleEndian)\n const datatype = reader.getUint16(12, isLittleEndian)\n if (datatype !== 2 && datatype !== 4 && datatype !== 8 && datatype !== 16) {\n throw new Error('Unsupported NIfTI datatype ' + datatype)\n }\n let nvert = 1\n const dim = [1, 1, 1, 1, 1, 1, 1, 1]\n for (let i = 1; i < 8; i++) {\n dim[i] = Math.max(Number(reader.getBigInt64(16 + i * 8, isLittleEndian)), 1)\n nvert *= dim[i]\n }\n if (intent_code >= 3000 && intent_code <= 3099 && voxoffset > 580) {\n // CIFTI ConnDenseScalar\n let indexOffset = 0\n let indexCount = 0\n let surfaceNumberOfVertices = 0\n let brainStructure = ''\n let vertexIndices: Uint32Array = new Uint32Array()\n const bytes = new Uint8Array(buffer)\n let pos = 552\n\n function readStrX(): string {\n while (pos < len && bytes[pos] === 10) {\n pos++\n } // skip blank lines\n const startPos = pos\n while (pos < len && bytes[pos] !== 10) {\n pos++\n }\n pos++ // skip EOLN\n if (pos - startPos < 1) {\n return ''\n }\n return new TextDecoder().decode(buffer.slice(startPos, pos - 1)).trim()\n }\n\n function readStr(): string {\n // concatenate lines to return tag <...>\n let line = readStrX()\n if (!line.startsWith('<') || line.endsWith('>')) {\n return line\n }\n while (pos < len && !line.endsWith('>')) {\n line += readStrX()\n }\n return line\n }\n let line: string\n\n function readNumericTag(TagName: string, asString = false): string | number {\n // Tag 'Dim1' will return 3 for Dim1=\"3\"\n const tpos = line.indexOf(TagName)\n if (tpos < 0) {\n return 1\n }\n const spos = line.indexOf('\"', tpos) + 1\n const epos = line.indexOf('\"', spos)\n const str = line.slice(spos, epos)\n if (asString) {\n return str\n }\n return parseInt(str)\n } // readNumericTag\n\n const nFrame4D = dim[5] // number of timepoints/frames per vertex\n const scalars = new Float32Array(n_vert * nFrame4D)\n\n // eslint-disable-next-line no-unmodified-loop-condition -- pos is modified within readStr\n while (pos < len) {\n line = readStr()\n if (line.includes('</CIFTI>')) {\n break\n }\n if (line.includes('<BrainModel')) {\n const nv = readNumericTag('SurfaceNumberOfVertices=') as number\n const bStruct = (readNumericTag('BrainStructure=', true) as string).toUpperCase()\n if (nv % n_vert !== 0) {\n continue\n }\n // a single CIfTI file can contain multiple structures, but only one structure per mesh\n // The big kludge: try to find CIfTI structure that matches GIfTI mesh\n let isMatch = false\n if (anatomicalStructurePrimary.includes('CORTEX') && bStruct.includes('CORTEX')) {\n isMatch = true\n }\n // to do: other anatomy: cerebellum\n if (!isMatch) {\n continue\n }\n isMatch = false\n if (anatomicalStructurePrimary.includes('LEFT') && bStruct.includes('LEFT')) {\n isMatch = true\n }\n if (anatomicalStructurePrimary.includes('RIGHT') && bStruct.includes('RIGHT')) {\n isMatch = true\n }\n if (!isMatch) {\n continue\n }\n surfaceNumberOfVertices = nv\n indexOffset = readNumericTag('IndexOffset=') as number\n indexCount = readNumericTag('IndexCount=') as number\n brainStructure = bStruct\n if (!line.includes('<VertexIndices>')) {\n line = readStr()\n }\n if (!line.startsWith('<VertexIndices>') || !line.endsWith('</VertexIndices>')) {\n log.warn('Unable to find CIfTI <VertexIndices>')\n return scalars\n }\n line = line.slice(15, -16)\n const items = line.trim().split(/\\s+/)\n if (items.length < indexCount) {\n log.error('Error parsing VertexIndices')\n }\n vertexIndices = new Uint32Array(indexCount)\n for (let i = 0; i < indexCount; i++) {\n vertexIndices[i] = parseInt(items[i])\n }\n } // read <BrainModel\n } // while (pos < len) or reached </CIFTI>\n\n if (surfaceNumberOfVertices === 0 || vertexIndices.length === 0) {\n log.warn('Unable to find CIfTI structure that matches the mesh.')\n return scalars\n }\n if (datatype !== 16) {\n log.warn('Only able to read float32 CIfTI (only known datatype).')\n return scalars\n }\n\n const vals = new Float32Array(indexCount * nFrame4D)\n const off = voxoffset + nFrame4D * indexOffset * 4\n for (let i = 0; i < indexCount * nFrame4D; i++) {\n vals[i] = reader.getFloat32(off + i * 4, isLittleEndian)\n }\n // }\n let j = 0\n\n for (let i = 0; i < indexCount; i++) {\n for (let f = 0; f < nFrame4D; f++) {\n scalars[vertexIndices[i] + f * n_vert] = vals[j]\n j++\n }\n }\n log.debug('CIfTI diagnostics', surfaceNumberOfVertices, brainStructure, indexOffset, indexCount, indexOffset, anatomicalStructurePrimary)\n //\n return scalars\n } // is CIfTI\n nvert = this.decimateLayerVertices(nvert, n_vert)\n if (nvert % n_vert !== 0) {\n throw new Error('Vertices in layer (' + nvert + ') is not a multiple of number of vertices (' + n_vert + ')')\n }\n if (isLittleEndian) {\n // block read native endian\n if (datatype === 16) {\n scalars = new Float32Array(buffer, voxoffset, nvert)\n } else if (datatype === 8) {\n scalars = new Int32Array(buffer, voxoffset, nvert)\n } else if (datatype === 4) {\n scalars = new Int16Array(buffer, voxoffset, nvert)\n }\n } else {\n // if isLittleEndian\n if (datatype === 16) {\n scalars = new Float32Array(nvert)\n for (let i = 0; i < nvert; i++) {\n scalars[i] = reader.getFloat32(voxoffset + i * 4, isLittleEndian)\n }\n } else if (datatype === 8) {\n scalars = new Int32Array(nvert)\n for (let i = 0; i < nvert; i++) {\n scalars[i] = reader.getInt32(voxoffset + i * 4, isLittleEndian)\n }\n } else if (datatype === 4) {\n scalars = new Int16Array(nvert)\n for (let i = 0; i < nvert; i++) {\n scalars[i] = reader.getInt16(voxoffset + i * 2, isLittleEndian)\n }\n }\n } // if isLittleEndian else big end\n if (datatype === 2) {\n scalars = new Uint8Array(buffer, voxoffset, nvert)\n }\n return scalars\n } // readNII2()\n\n // read NIfTI1/2 as vertex colors\n // https://brainder.org/2012/09/23/the-nifti-file-format/#:~:text=In%20the%20nifti%20format%2C%20the,seventh%2C%20are%20for%20other%20uses.\n static async readNII(buffer: ArrayBuffer, pts: Float32Array, anatomicalStructurePrimary = ''): Promise<Float32Array | Uint8Array | Int32Array | Int16Array> {\n const n_mesh_vert = pts.length / 3\n // TODO clean up number types\n let scalars: Float32Array | Int32Array | Int16Array | Uint8Array = new Float32Array()\n let isLittleEndian = true\n let reader = new DataView(buffer)\n let magic = reader.getUint16(0, isLittleEndian)\n if (magic === 540 || magic === 469893120) {\n return NVMeshLoaders.readNII2(buffer, n_mesh_vert, anatomicalStructurePrimary)\n }\n if (magic === 23553) {\n isLittleEndian = false\n magic = reader.getUint16(0, isLittleEndian)\n }\n if (magic !== 348) {\n // gzip signature 0x1F8B in little and big endian\n const raw = await NVUtilities.decompress(new Uint8Array(buffer))\n reader = new DataView(raw.buffer)\n buffer = raw.buffer\n magic = reader.getUint16(0, isLittleEndian)\n if (magic === 540 || magic === 469893120) {\n return NVMeshLoaders.readNII2(buffer, n_mesh_vert, anatomicalStructurePrimary)\n }\n if (magic === 23553) {\n isLittleEndian = false\n magic = reader.getUint16(0, isLittleEndian)\n }\n }\n if (magic !== 348) {\n log.error('Not a valid NIfTI image.')\n }\n const voxoffset = reader.getFloat32(108, isLittleEndian)\n const scl_slope = reader.getFloat32(112, isLittleEndian)\n const scl_inter = reader.getFloat32(116, isLittleEndian)\n const qform_code = reader.getUint16(252, isLittleEndian)\n const sform_code = reader.getUint16(254, isLittleEndian)\n const datatype = reader.getUint16(70, isLittleEndian)\n if (datatype !== 2 && datatype !== 4 && datatype !== 8 && datatype !== 16) {\n throw new Error('Unsupported NIfTI datatype ' + datatype)\n }\n const sform = mat4.create()\n for (let i = 0; i < 12; i++) {\n sform[i] = reader.getFloat32(280 + i * 4, isLittleEndian)\n }\n let n_vox = 1\n const dim = new Array(8)\n for (let i = 0; i < 8; i++) {\n dim[i] = reader.getUint16(40 + i * 2, isLittleEndian)\n if (i < 1) {\n continue\n }\n n_vox *= dim[i] || 1\n }\n let is3D = false\n let nvertD = this.decimateLayerVertices(n_vox, n_mesh_vert)\n if (nvertD % n_mesh_vert !== 0) {\n if (dim[0] >= 3 && dim[1] > 1 && dim[2] > 1 && dim[3] > 1) {\n is3D = true\n nvertD = dim[1] * dim[2] * dim[3]\n } else {\n throw new Error('Voxels in layer (' + n_vox + ') is not a multiple of number of vertices (' + n_mesh_vert + ')')\n }\n }\n n_vox = nvertD\n if (isLittleEndian) {\n // block read native endian\n if (datatype === 16) {\n scalars = new Float32Array(buffer, voxoffset, n_vox)\n } else if (datatype === 8) {\n scalars = new Int32Array(buffer, voxoffset, n_vox)\n } else if (datatype === 4) {\n scalars = new Int16Array(buffer, voxoffset, n_vox)\n }\n } else {\n // if isLittleEndian\n if (datatype === 16) {\n scalars = new Float32Array(n_vox)\n for (let i = 0; i < n_vox; i++) {\n scalars[i] = reader.getFloat32(voxoffset + i * 4, isLittleEndian)\n }\n } else if (datatype === 8) {\n scalars = new Int32Array(n_vox)\n for (let i = 0; i < n_vox; i++) {\n scalars[i] = reader.getInt32(voxoffset + i * 4, isLittleEndian)\n }\n } else if (datatype === 4) {\n scalars = new Int16Array(n_vox)\n for (let i = 0; i < n_vox; i++) {\n scalars[i] = reader.getInt16(voxoffset + i * 2, isLittleEndian)\n }\n }\n } // if isLittleEndian else big end\n if (datatype === 2) {\n scalars = new Uint8Array(buffer, voxoffset, n_vox)\n }\n if (Number.isFinite(scl_slope) && Number.isFinite(scl_inter) && scl_slope !== 0 && (scl_slope !== 1 || scl_inter !== 0)) {\n const f32 = new Float32Array(n_vox)\n for (let i = 0; i < n_vox; i++) {\n f32[i] = scalars[i] * scl_slope + scl_inter\n }\n scalars = f32\n }\n let mn = scalars[0]\n let mx = mn\n for (let i = 0; i < n_vox; i++) {\n if (isNaN(scalars[i])) {\n scalars[i] = 0.0\n }\n mn = Math.min(mn, scalars[i])\n mx = Math.max(mx, scalars[i])\n }\n log.debug(`Layer Range ${mn}..${mx} slope ${scl_slope} inter ${scl_inter}`)\n if (is3D) {\n const f32 = new Float32Array(n_mesh_vert)\n // Sample voxel intensities at mesh vertices\n log.warn('Sampling voxel intensities at mesh vertices (assumes precise alignment).')\n if (qform_code > sform_code || sform_code <= 0) {\n log.warn(`Requires valid sform (sform_code = ${sform_code})`)\n }\n const sformInv = mat4.create()\n mat4.invert(sformInv, sform)\n function transformMat4homogeneous(xyz: number[], m: mat4): number[] {\n const out = [0, 0, 0]\n out[0] = m[0] * xyz[0] + m[1] * xyz[1] + m[2] * xyz[2] + m[3]\n out[1] = m[4] * xyz[0] + m[5] * xyz[1] + m[6] * xyz[2] + m[7]\n out[2] = m[8] * xyz[0] + m[9] * xyz[1] + m[10] * xyz[2] + m[11]\n return out\n }\n const nx = dim[1]\n const ny = dim[2]\n const nz = dim[3]\n const nxy = nx * ny\n let j = 0\n let k = 0\n let nOK = 0\n while (k < n_mesh_vert) {\n const xyz = [pts[j], pts[j + 1], pts[j + 2]]\n const ijk = transformMat4homogeneous(xyz, sformInv)\n j += 3\n k += 1\n // todo read voxel scalar\n const ixf = Math.floor(ijk[0])\n const iyf = Math.floor(ijk[1])\n const izf = Math.floor(ijk[2])\n const ixc = Math.ceil(ijk[0])\n const iyc = Math.ceil(ijk[1])\n const izc = Math.ceil(ijk[2])\n // bounds check: valid ranges are 0..(nx-1), 0..(ny-1), 0..(nz-1)\n if (ixf < 0 || ixc >= nx || iyf < 0 || iyc >= ny || izf < 0 || izc >= nz) {\n continue\n }\n const base = ixf + iyf * nx + izf * nxy\n const vs = new Float32Array(8)\n vs[0] = scalars[base]\n vs[1] = scalars[base + 1]\n vs[2] = scalars[base + nx]\n vs[3] = scalars[base + nx + 1]\n vs[4] = scalars[base + nxy]\n vs[5] = scalars[base + nxy + 1]\n vs[6] = scalars[base + nxy + nx]\n vs[7] = scalars[base + nxy + nx + 1]\n // choose max (unrolled for speed)\n let maxv = vs[0]\n let minv = maxv\n for (let i = 1; i < 8; i++) {\n maxv = Math.max(maxv, vs[i])\n minv = Math.min(minv, vs[i])\n }\n if (Math.abs(minv) > maxv) {\n maxv = minv\n }\n f32[k - 1] = maxv\n nOK++\n }\n\n const fractionOK = nOK / n_mesh_vert\n if (fractionOK < 0.1) {\n log.warn(`${nOK} of ${n_mesh_vert} vertices in range (${(fractionOK * 100).toFixed(1)}%)`)\n }\n scalars = f32\n }\n return scalars\n } // readNII();\n\n // read MGH format as vertex colors (not voxel-based image)\n // https://surfer.nmr.mgh.harvard.edu/fswiki/FsTutorial/MghFormat\n static async readMGH(buffer: ArrayBuffer, n_vert = 0, isReadColortables = false): Promise<MGH> {\n let reader = new DataView(buffer)\n let raw = buffer\n if (reader.getUint8(0) === 31 && reader.getUint8(1) === 139) {\n const decompressed = await NVUtilities.decompress(new Uint8Array(buffer))\n raw = new ArrayBuffer(decompressed.byteLength)\n new Uint8Array(raw).set(new Uint8Array(decompressed))\n reader = new DataView(decompressed.buffer)\n }\n const version = reader.getInt32(0, false)\n const width = Math.max(1, reader.getInt32(4, false))\n const height = Math.max(1, reader.getInt32(8, false))\n const depth = Math.max(1, reader.getInt32(12, false))\n const nframes = Math.max(1, reader.getInt32(16, false))\n const mtype = reader.getInt32(20, false)\n let voxoffset = 284 // ALWAYS fixed header size\n const isLittleEndian = false // ALWAYS byte order is BIG ENDIAN\n if (version !== 1 || mtype < 0 || mtype > 4) {\n log.warn('Not a valid MGH file')\n }\n let nvert = width * height * depth * nframes\n let scalars: AnyNumberArray = []\n nvert = this.decimateLayerVertices(nvert, n_vert)\n if (nvert % n_vert !== 0) {\n log.warn('Vertices in layer (' + nvert + ') is not a multiple of number of vertices (' + n_vert + ')')\n return scalars\n }\n if (mtype === 3) {\n scalars = new Float32Array(nvert)\n for (let i = 0; i < nvert; i++) {\n scalars[i] = reader.getFloat32(voxoffset + i * 4, isLittleEndian)\n }\n } else if (mtype === 1) {\n scalars = new Int32Array(nvert)\n for (let i = 0; i < nvert; i++) {\n scalars[i] = reader.getInt32(voxoffset + i * 4, isLittleEndian)\n }\n } else if (mtype === 4) {\n scalars = new Int16Array(nvert)\n for (let i = 0; i < nvert; i++) {\n scalars[i] = reader.getInt16(voxoffset + i * 2, isLittleEndian)\n }\n } else if (mtype === 0) {\n scalars = new Uint8Array(buffer, voxoffset, nvert)\n }\n if (!isReadColortables) {\n return scalars\n }\n // next: read footer\n let bytesPerVertex = 4\n if (mtype === 4) {\n bytesPerVertex = 2\n }\n if (mtype === 0) {\n bytesPerVertex = 1\n }\n voxoffset += bytesPerVertex * nvert\n voxoffset += 4 * 4 // skip TR, FlipAngle, TE, TI, FOV\n const TAG_OLD_COLORTABLE = 1\n const TAG_OLD_USEREALRAS = 2\n // const TAG_CMDLINE = 3;\n // const TAG_USEREALRAS = 4;\n // const TAG_COLORTABLE = 5;\n // const TAG_GCAMORPH_GEOM = 10;\n // const TAG_GCAMORPH_TYPE = 11;\n // const TAG_GCAMORPH_LABELS = 12;\n const TAG_OLD_SURF_GEOM = 20\n // const TAG_SURF_GEOM = 21;\n const TAG_OLD_MGH_XFORM = 30\n // const TAG_MGH_XFORM = 31;\n // const TAG_GROUP_AVG_SURFACE_AREA = 32;\n // const TAG_AUTO_ALIGN = 33;\n // const TAG_SCALAR_DOUBLE = 40;\n // const TAG_PEDIR = 41;\n // const TAG_MRI_FRAME = 42;\n // const TAG_FIELDSTRENGTH = 43;\n // const TAG_ORIG_RAS2VOX = 44;\n const nBytes = raw.byteLength\n let colormapLabel: LUT\n\n while (voxoffset < nBytes - 8) {\n // let vx = voxoffset;\n const tagType = reader.getInt32((voxoffset += 4), isLittleEndian)\n let plen = 0\n switch (tagType) {\n case TAG_OLD_MGH_XFORM:\n // doesn't include null\n plen = reader.getInt32((voxoffset += 4), isLittleEndian) - 1\n break\n case TAG_OLD_SURF_GEOM: // these don't take lengths at all\n case TAG_OLD_USEREALRAS:\n plen = 0\n break\n case TAG_OLD_COLORTABLE:\n plen = 0\n // CTABreadFromBinary()\n {\n let version = reader.getInt32((voxoffset += 4), isLittleEndian)\n if (version > 0) {\n log.warn('unsupported CTABreadFromBinaryV1')\n return scalars\n }\n version = -version\n if (version !== 2) {\n log.warn('CTABreadFromBinary: unknown version')\n return scalars\n }\n // CTABreadFromBinaryV2() follows\n const nentries = reader.getInt32((voxoffset += 4), isLittleEndian)\n if (nentries < 0) {\n log.warn('CTABreadFromBinaryV2: nentries was ', nentries)\n return scalars\n }\n // skip the file name\n const len = reader.getInt32((voxoffset += 4), isLittleEndian)\n voxoffset += len\n const num_entries_to_read = reader.getInt32((voxoffset += 4), isLittleEndian)\n if (num_entries_to_read < 0) {\n return scalars\n }\n // Allocate our table.\n const Labels: ColorMap = { R: [], G: [], B: [], A: [], I: [], labels: [] }\n for (let i = 0; i < num_entries_to_read; i++) {\n const structure = reader.getInt32((voxoffset += 4), isLittleEndian)\n const labelLen = reader.getInt32((voxoffset += 4), isLittleEndian)\n let pos = voxoffset + 4\n let txt = ''\n for (let c = 0; c < labelLen; c++) {\n const val = reader.getUint8(pos++)\n if (val === 0) {\n break\n }\n txt += String.fromCharCode(val)\n } // for labelLen\n voxoffset += labelLen\n const R = reader.getInt32((voxoffset += 4), isLittleEndian)\n const G = reader.getInt32((voxoffset += 4), isLittleEndian)\n const B = reader.getInt32((voxoffset += 4), isLittleEndian)\n const A = 255 - reader.getInt32((voxoffset += 4), isLittleEndian)\n Labels.I.push(structure)\n Labels.R.push(R)\n Labels.G.push(G)\n Labels.B.push(B)\n Labels.A.push(A)\n Labels.labels!.push(txt)\n // break\n } // for num_entries_to_read\n colormapLabel = cmapper.makeLabelLut(Labels)\n }\n break\n default:\n plen = reader.getInt32((voxoffset += 8), isLittleEndian)\n }\n voxoffset += plen\n }\n return {\n scalars,\n colormapLabel: colormapLabel! // TODO can we guarantee this?\n }\n } // readMGH()\n\n // read X3D format mesh\n // https://en.wikipedia.org/wiki/X3D\n static readX3D(buffer: ArrayBuffer): X3D {\n // n.b. only plain text \".x3d\", not binary \".x3db\"\n // beware: The values of XML attributes are delimited by either single or double quotes\n const len = buffer.byteLength\n if (len < 20) {\n throw new Error('File too small to be X3D: bytes = ' + len)\n }\n const bytes = new Uint8Array(buffer)\n let pos = 0\n function readStr(): string {\n while (pos < len && bytes[pos] !== 60) {\n pos++\n }\n const startP = pos\n while (pos < len && bytes[pos] !== 62) {\n pos++\n }\n const endP = pos\n return new TextDecoder().decode(buffer.slice(startP, endP + 1)).trim()\n }\n let line = readStr() // detect XML signature: '<?xml version=...'\n function readStringTag(TagName: string): string {\n // Tag 'DEF' will return l3 for DEF='l3'\n const fpos = line.indexOf(TagName + '=')\n if (fpos < 0) {\n return ''\n }\n const delimiter = line[fpos + TagName.length + 1]\n const spos = line.indexOf(delimiter, fpos) + 1\n const epos = line.indexOf(delimiter, spos)\n return line.slice(spos, epos)\n }\n function readNumericTag(TagName: string): number | number[] {\n // Tag 'Dim1' will return 3 for Dim1=\"3\"\n const fpos = line.indexOf(TagName + '=')\n if (fpos < 0) {\n return 1\n }\n const delimiter = line[fpos + TagName.length + 1]\n const spos = line.indexOf(delimiter, fpos) + 1\n const epos = line.indexOf(delimiter, spos)\n let str = line.slice(spos, epos).trim()\n str = str.replace(/,\\s*$/, '')\n const items = str.trim().split(/\\s*,\\s*|\\s+/)\n if (items.length < 2) {\n return parseFloat(str)\n }\n let ret = new Array(items.length)\n let j = 0\n for (let i = 0; i < items.length; i++) {\n const v = parseFloat(items[i])\n if (!isFinite(v)) {\n continue\n }\n ret[j] = v\n j++\n }\n ret = ret.slice(0, j)\n return ret\n }\n if (!line.includes('xml version')) {\n log.warn('Not a X3D image')\n }\n let positions: number[] = []\n let indices: number[] = []\n let rgba255: number[] = []\n let color: number[] = []\n let translation: vec4 = [0, 0, 0, 0]\n let rotation = [0, 0, 0, 0]\n let rgba = [255, 255, 255, 255]\n let rgbaGlobal = [255, 255, 255, 255]\n const appearanceStyles: Record<string, number[]> = {}\n function readAppearance(): void {\n if (!line.endsWith('/>')) {\n if (line.startsWith('<Appearance>')) {\n // eslint-disable-next-line no-unmodified-loop-condition -- modified within readStr\n while (pos < len && !line.endsWith('</Appearance>')) {\n line += readStr()\n }\n } else {\n // eslint-disable-next-line no-unmodified-loop-condition -- modified within readStr\n while (pos < len && !line.endsWith('/>')) {\n line += readStr()\n }\n }\n }\n const ref = readStringTag('USE')\n if (ref.length > 1) {\n if (ref in appearanceStyles) {\n rgba = appearanceStyles[ref as keyof typeof appearanceStyles] as number[]\n } else {\n log.warn('Unable to find DEF for ' + ref)\n }\n return\n }\n const diffuseColor = readNumericTag('diffuseColor') as number[]\n if (diffuseColor.length < 3) {\n return\n }\n rgba[0] = Math.round(diffuseColor[0] * 255)\n rgba[1] = Math.round(diffuseColor[1] * 255)\n rgba[2] = Math.round(diffuseColor[2] * 255)\n const def = readStringTag('DEF')\n if (def.length < 1) {\n return\n }\n appearanceStyles[def] = rgba\n }\n // eslint-disable-next-line no-unmodified-loop-condition -- modified within readStr\n while (pos < len) {\n line = readStr()\n rgba = rgbaGlobal.slice()\n if (line.startsWith('<Transform')) {\n translation = readNumericTag('translation') as vec4\n rotation = readNumericTag('rotation') as number[]\n }\n if (line.startsWith('<Appearance')) {\n readAppearance()\n rgbaGlobal = rgba.slice()\n }\n if (line.startsWith('<Shape')) {\n let radius = 1.0\n let height = 1.0\n let coordIndex: number[] = []\n let point: number[] = []\n\n // eslint-disable-next-line no-unmodified-loop-condition -- modified within readAppearance\n while (pos < len) {\n line = readStr()\n if (line.startsWith('<Appearance')) {\n readAppearance()\n }\n if (line.startsWith('</Shape')) {\n break\n }\n if (line.startsWith('<Sphere')) {\n radius = readNumericTag('radius') as number\n height = -1.0\n }\n if (line.startsWith('<Cylinder')) {\n radius = readNumericTag('radius') as number\n height = readNumericTag('height') as number\n }\n if (line.startsWith('<IndexedFaceSet')) {\n height = -2\n // https://www.web3d.org/specifications/X3Dv4Draft/ISO-IEC19775-1v4-CD/Part01/components/geometry3D.html#IndexedFaceSet\n coordIndex = readNumericTag('coordIndex') as number[]\n }\n if (line.startsWith('<IndexedTriangleSet')) {\n height = -7\n coordIndex = readNumericTag('index') as number[]\n }\n if (line.startsWith('<IndexedTriangleStripSet')) {\n height = -3\n // https://www.web3d.org/specifications/X3Dv4Draft/ISO-IEC19775-1v4-CD/Part01/components/geometry3D.html#IndexedFaceSet\n coordIndex = readNumericTag('index') as number[]\n }\n if (line.startsWith('<Coordinate')) {\n point = readNumericTag('point') as number[]\n const rem = point.length % 3\n if (rem !== 0) {\n point = point.slice(0, -rem)\n }\n } // Coordinate point\n if (line.startsWith('<Color')) {\n color = readNumericTag('color') as number[]\n }\n if (line.startsWith('<Box')) {\n height = -4\n log.warn('Unsupported x3d shape: Box')\n }\n if (line.startsWith('<Cone')) {\n height = -5\n log.warn('Unsupported x3d shape: Cone')\n }\n if (line.startsWith('<ElevationGrid')) {\n height = -6\n log.warn('Unsupported x3d shape: ElevationGrid')\n }\n }\n if (height < -3 && height !== -7) {\n // cone, box, elevation grid\n // unsupported\n } else if (height < -1) {\n // indexed triangle mesh or strip\n if (coordIndex.length < 1 || point.length < 3 || point.length === undefined) {\n log.warn('Indexed mesh must specify indices and points')\n break\n }\n const idx0 = Math.floor(positions.length / 3) // first new vertex will be AFTER previous vertices\n let j = 2\n if (height === -7) {\n indices = [...indices, ...coordIndex]\n } else if (height === -2) {\n let triStart = 0\n while (j < coordIndex.length) {\n if (coordIndex[j] >= 0) {\n indices.push(coordIndex[triStart] + idx0)\n indices.push(coordIndex[j - 1] + idx0)\n indices.push(coordIndex[j - 0] + idx0)\n j += 1\n } else {\n j += 3\n triStart = j - 2\n }\n }\n } else {\n while (j < coordIndex.length) {\n if (coordIndex[j] >= 0) {\n indices.push(coordIndex[j - 2] + idx0)\n indices.push(coordIndex[j - 1] + idx0)\n indices.push(coordIndex[j - 0] + idx0)\n j += 1\n } else {\n // coordIndex[j] === -1, next polygon\n j += 3\n }\n }\n }\n // n.b. positions.push(...point) can generate \"Maximum call stack size exceeded\"\n positions = [...positions, ...point]\n const npt = Math.floor(point.length / 3)\n const rgbas = Array(npt).fill(rgba).flat()\n if (color.length === npt * 3) {\n // colors are rgb 0..1, rgbas are RGBA 0..255\n let c3 = 0\n let c4 = 0\n for (let i = 0; i < npt; i++) {\n for (let j2 = 0; j2 < 3; j2++) {\n rgbas[c4] = Math.round(color[c3] * 255.0)\n c3++\n c4++\n }\n c4++\n }\n }\n rgba255 = [...rgba255, ...rgbas]\n } else if (height < 0.0) {\n // sphere\n NiivueObject3D.makeColoredSphere(positions, indices, rgba255, radius, translation, rgba)\n } else {\n // https://www.andre-gaschler.com/rotationconverter/\n const r = mat4.create() // rotation mat4x4\n mat4.fromRotation(r, rotation[3], [rotation[0], rotation[1], rotation[2]])\n const pti = vec4.fromValues(0, -height * 0.5, 0, 1)\n const ptj = vec4.fromValues(0, +height * 0.5, 0, 1)\n vec4.transformMat4(pti, pti, r)\n vec4.transformMat4(ptj, ptj, r)\n vec4.add(pti, pti, translation)\n vec4.add(ptj, ptj, translation)\n const pti3 = vec3.fromValues(pti[0], pti[1], pti[2])\n const ptj3 = vec3.fromValues(ptj[0], ptj[1], ptj[2])\n // https://www.web3d.org/specifications/X3Dv4Draft/ISO-IEC19775-1v4-CD/Part01/components/geometry3D.html#Cylinder\n NiivueObject3D.makeColoredCylinder(positions, indices, rgba255, pti3, ptj3, radius, rgba)\n }\n } // while <shape\n }\n return {\n positions: Float32Array.from(positions),\n indices: Uint32Array.from(indices),\n rgba255: Uint8Array.from(rgba255)\n }\n } // readX3D()\n\n // read GIfTI format mesh\n // https://www.nitrc.org/projects/gifti/\n static async readGII(buffer: ArrayBuffer, n_vert = 0): Promise<GII> {\n let len = buffer.byteLength\n if (len < 20) {\n throw new Error('File too small to be GII: bytes = ' + len)\n }\n let chars = new TextDecoder('ascii').decode(buffer)\n if (chars[0].charCodeAt(0) === 31) {\n // raw GIFTI saved as .gii.gz is smaller than gz GIFTI due to base64 overhead\n const raw = await NVUtilities.decompress(new Uint8Array(buffer))\n buffer = raw.buffer\n chars = new TextDecoder('ascii').decode(raw.buffer)\n }\n let pos = 0\n function readXMLtag(): XmlTag {\n let isEmptyTag = true\n let startPos = pos\n while (isEmptyTag) {\n // while (pos < len && chars[pos] === 10) pos++; //skip blank lines\n while (pos < len && chars[pos] !== '<') {\n pos++\n } // find tag start symbol: '<' e.g. \"<tag>\"\n startPos = pos\n while (pos < len && chars[pos] !== '>') {\n pos++\n } // find tag end symbol: '>' e.g. \"<tag>\"\n isEmptyTag = chars[pos - 1] === '/' // empty tag ends \"/>\" e.g. \"<br/>\"\n if (startPos + 1 < len && chars[startPos + 1] === '/') {\n // skip end tag \"</\"\n pos += 1\n isEmptyTag = true\n }\n // let endTagPos = pos;\n if (pos >= len) {\n break\n }\n }\n const tagString = new TextDecoder().decode(buffer.slice(startPos + 1, pos)).trim()\n const startTag = tagString.split(' ')[0].trim()\n // ignore declarations https://stackoverflow.com/questions/60801060/what-does-mean-in-xml\n const contentStartPos = pos\n let contentEndPos = pos\n let endPos = pos\n if (chars[startPos + 1] !== '?' && chars[startPos + 1] !== '!') {\n // ignore declarations \"<?\" and \"<!\"\n const endTag = '</' + startTag + '>'\n contentEndPos = chars.indexOf(endTag, contentStartPos)\n endPos = contentEndPos + endTag.length - 1\n }\n // <name>content</name>\n // a b c d\n // a: startPos\n // b: contentStartPos\n // c: contentEndPos\n // d: endPos\n return {\n name: tagString,\n startPos,\n contentStartPos,\n contentEndPos,\n endPos\n } //, 'startTagLastPos': startTagLastPos, 'endTagFirstPos': endTagFirstPos, 'endTagLastPos': endTagLastPos];\n }\n let tag = readXMLtag()\n if (!tag.name.startsWith('?xml')) {\n throw new Error('readGII: Invalid XML file')\n }\n while (!tag.name.startsWith('GIFTI') && tag.endPos < len) {\n tag = readXMLtag()\n }\n if (!tag.name.startsWith('GIFTI') || tag.contentStartPos === tag.contentEndPos) {\n throw new Error('readGII: XML file does not include GIFTI tag')\n }\n len = tag.contentEndPos // only read contents of GIfTI tag\n let positions = new Float32Array()\n let indices = new Uint32Array()\n let scalars = new Float32Array()\n let anatomicalStructurePrimary = ''\n let isIdx = false\n let isPts = false\n let isVectors = false\n let isColMajor = false\n let Dims = [1, 1, 1]\n const FreeSurferTranlate = [0, 0, 0] // https://gist.github.com/alexisthual/f0b2f9eb2a67b8f61798f2c138dda981\n let dataType = 0\n // let isLittleEndian = true;\n let isGzip = false\n let isASCII = false\n let nvert = 0\n // FreeSurfer versions after 20221225 disambiguate if transform has been applied\n // \"./mris_convert --to-scanner\" store raw vertex positions in scanner space, so transforms should be ignored.\n // FreeSurfer versions after 20221225 report that the transform is applied by reporting:\n // <DataSpace><![CDATA[NIFTI_XFORM_SCANNER_ANAT\n let isDataSpaceScanner = false\n tag.endPos = tag.contentStartPos // read the children of the 'GIFTI' tag\n let line = ''\n function readNumericTag(TagName: string, isFloat = false): number {\n // Tag 'Dim1' will return 3 for Dim1=\"3\"\n const pos = line.indexOf(TagName)\n if (pos < 0) {\n return 1\n }\n const spos = line.indexOf('\"', pos) + 1\n const epos = line.indexOf('\"', spos)\n const str = line.slice(spos, epos)\n if (isFloat) {\n return parseFloat(str)\n } else {\n return parseInt(str)\n }\n }\n function readBracketTag(TagName: string): string {\n const pos = line.indexOf(TagName)\n if (pos < 0) {\n return ''\n }\n const spos = pos + TagName.length\n const epos = line.indexOf(']', spos)\n return line.slice(spos, epos)\n }\n const Labels: ColorMap = { R: [], G: [], B: [], A: [], I: [], labels: [] }\n while (tag.endPos < len && tag.name.length > 1) {\n tag = readXMLtag()\n if (tag.name.startsWith('Label Key')) {\n line = tag.name\n Labels.I.push(readNumericTag('Key='))\n Labels.R.push(Math.round(255 * readNumericTag('Red=', true)))\n Labels.G.push(Math.round(255 * readNumericTag('Green=', true)))\n Labels.B.push(Math.round(255 * readNumericTag('Blue=', true)))\n Labels.A.push(Math.round(255 * readNumericTag('Alpha', true)))\n line = new TextDecoder().decode(buffer.slice(tag.contentStartPos + 1, tag.contentEndPos)).trim()\n Labels.labels!.push(readBracketTag('<![CDATA['))\n }\n if (tag.name.trim() === 'Data') {\n if (isVectors) {\n continue\n }\n line = new TextDecoder().decode(buffer.slice(tag.contentStartPos + 1, tag.contentEndPos)).trim()\n // Data can be on one to three lines...\n let datBin: Int32Array | Float32Array | Uint8Array\n if (isASCII) {\n const nvert = Dims[0] * Dims[1] * Dims[2]\n const lines = line.split(/\\s+/) // .split(/[ ,]+/);\n if (nvert !== lines.length) {\n throw new Error('Unable to parse ASCII GIfTI')\n }\n if (dataType === 2) {\n dataType = 8\n } // UInt8 -> Int32\n if (dataType === 32) {\n dataType = 16\n } // float64 -> float32\n if (dataType === 8) {\n datBin = new Int32Array(nvert)\n for (let v = 0; v < nvert; v++) {\n datBin[v] = parseInt(lines[v])\n }\n }\n if (dataType === 16) {\n datBin = new Float32Array(nvert)\n for (let v = 0; v < nvert; v++) {\n datBin[v] = parseFloat(lines[v])\n }\n }\n } else if (typeof Buffer === 'undefined') {\n // raw.gii\n function base64ToUint8(base64: string): Uint8Array {\n const binary_string = atob(base64)\n const len = binary_string.length\n const bytes = new Uint8Array(len)\n for (let i = 0; i < len; i++) {\n bytes[i] = binary_string.charCodeAt(i)\n }\n return bytes\n }\n if (isGzip) {\n const datZ = base64ToUint8(line.slice())\n datBin = await NVUtilities.decompress(new Uint8Array(datZ))\n } else {\n datBin = base64ToUint8(line.slice())\n }\n } else {\n // if Buffer not defined\n if (isGzip) {\n const datZ = Buffer.from(line.slice(), 'base64')\n datBin = await NVUtilities.decompress(new Uint8Array(datZ))\n } else {\n datBin = Buffer.from(line.slice(), 'base64')\n }\n }\n if (isPts) {\n if (dataType !== 16) {\n log.warn('expect positions as FLOAT32')\n }\n positions = new Float32Array(datBin!.buffer) // TODO can we guarantee this?\n if (isColMajor) {\n const tmp = positions.slice()\n const np = tmp.length / 3\n let j = 0\n for (let p = 0; p < np; p++) {\n for (let i = 0; i < 3; i++) {\n positions[j] = tmp[i * np + p]\n j++\n }\n }\n } // isColMajor\n } else if (isIdx) {\n if (dataType !== 8) {\n log.warn('expect indices as INT32')\n }\n indices = new Uint32Array(datBin!.buffer)\n if (isColMajor) {\n const tmp = indices.slice()\n const np = tmp.length / 3\n let j = 0\n for (let p = 0; p < np; p++) {\n for (let i = 0; i < 3; i++) {\n indices[j] = tmp[i * np + p]\n j++\n }\n }\n } // isColMajor\n } else {\n // not position or indices: assume scalars NIFTI_INTENT_NONE\n nvert = Dims[0] * Dims[1] * Dims[2]\n if (n_vert !== 0) {\n if (nvert % n_vert !== 0) {\n log.warn('Number of vertices in scalar overlay (' + nvert + ') does not match mesh (' + n_vert + ')')\n }\n }\n function Float32Concat(first: Float32Array, second: Float32Array): Float32Array {\n const firstLength = first.length\n const result = new Float32Array(firstLength + second.length)\n result.set(first)\n result.set(second, firstLength)\n return result\n } // Float32Concat()\n let scalarsNew\n if (dataType === 2) {\n const scalarsInt = new Uint8Array(datBin!.buffer)\n scalarsNew = Float32Array.from(scalarsInt)\n } else if (dataType === 8) {\n const scalarsInt = new Int32Array(datBin!.buffer)\n scalarsNew = Float32Array.from(scalarsInt)\n } else if (dataType === 16) {\n scalarsNew = new Float32Array(datBin!.buffer)\n } else if (dataType === 32) {\n const scalarFloat = new Float64Array(datBin!.buffer)\n scalarsNew = Float32Array.from(scalarFloat)\n } else {\n throw new Error(`Invalid dataType: ${dataType}`)\n }\n scalars = Float32Concat(scalars as Float32Array, scalarsNew)\n }\n continue\n }\n if (tag.name.trim() === 'DataSpace') {\n line = new TextDecoder().decode(buffer.slice(tag.contentStartPos + 1, tag.contentEndPos)).trim()\n if (line.includes('NIFTI_XFORM_SCANNER_ANAT')) {\n isDataSpaceScanner = true\n }\n }\n if (tag.name.trim() === 'MD') {\n line = new TextDecoder().decode(buffer.slice(tag.contentStartPos + 1, tag.contentEndPos)).trim()\n if (line.includes('AnatomicalStructurePrimary') && line.includes('CDATA[')) {\n anatomicalStructurePrimary = readBracketTag('<Value><![CDATA[').toUpperCase()\n // this.AnatomicalStructurePrimary = anatomicalStructurePrimary\n }\n if (line.includes('VolGeom') && line.includes('CDATA[')) {\n let e = -1\n if (line.includes('VolGeomC_R')) {\n e = 0\n }\n if (line.includes('VolGeomC_A')) {\n e = 1\n }\n if (line.includes('VolGeomC_S')) {\n e = 2\n }\n if (e < 0) {\n continue\n }\n FreeSurferTranlate[e] = parseFloat(readBracketTag('<Value><![CDATA['))\n }\n }\n // read DataArray properties\n if (!tag.name.startsWith('DataArray')) {\n continue\n }\n line = tag.name\n Dims = [1, 1, 1]\n isGzip = line.includes('Encoding=\"GZipBase64Binary\"')\n isASCII = line.includes('Encoding=\"ASCII\"')\n isIdx = line.includes('Intent=\"NIFTI_INTENT_TRIANGLE\"')\n isPts = line.includes('Intent=\"NIFTI_INTENT_POINTSET\"')\n isVectors = line.includes('Intent=\"NIFTI_INTENT_VECTOR\"')\n isColMajor = line.includes('ArrayIndexingOrder=\"ColumnMajorOrder\"')\n // isLittleEndian = line.includes('Endian=\"LittleEndian\"');\n if (line.includes('DataType=\"NIFTI_TYPE_UINT8\"')) {\n dataType = 2\n } // DT_UINT8\n if (line.includes('DataType=\"NIFTI_TYPE_INT32\"')) {\n dataType = 8\n } // DT_INT32\n if (line.includes('DataType=\"NIFTI_TYPE_FLOAT32\"')) {\n dataType = 16\n } // DT_FLOAT32\n if (line.includes('DataType=\"NIFTI_TYPE_FLOAT64\"')) {\n dataType = 32\n } // DT_FLOAT64\n Dims[0] = readNumericTag('Dim0=')\n Dims[1] = readNumericTag('Dim1=')\n Dims[2] = readNumericTag('Dim2=')\n }\n let colormapLabel\n if (Labels.I.length > 1) {\n // issue1441\n const hasAlpha = Labels.A.some((a) => a > 0)\n if (!hasAlpha) {\n Labels.A.fill(255)\n }\n colormapLabel = cmapper.makeLabelLut(Labels)\n }\n if (n_vert > 0) {\n return { scalars, colormapLabel, anatomicalStructurePrimary }\n }\n if (positions!.length > 2 && !isDataSpaceScanner && (FreeSurferTranlate[0] !== 0 || FreeSurferTranlate[1] !== 0 || FreeSurferTranlate[2] !== 0)) {\n nvert = Math.floor(positions!.length / 3)\n let i = 0\n for (let v = 0; v < nvert; v++) {\n positions![i] += FreeSurferTranlate[0]\n i++\n positions[i] += FreeSurferTranlate[1]\n i++\n positions[i] += FreeSurferTranlate[2]\n i++\n }\n } // issue416: apply FreeSurfer translation\n return {\n positions,\n indices,\n scalars,\n colormapLabel,\n anatomicalStructurePrimary\n } // MatrixData\n } // readGII()\n}\n","// src/utils/legacy-migrate.ts\n/**\n * Migration utility for older nvdocument shapes.\n *\n * Exports:\n * - migrateLegacyDocument(documentData) -> normalized DocumentData\n * - normalizeMeshForRehydrate(mesh) -> normalized mesh object suitable for rehydrateMeshes\n */\n\nimport { deserialize } from '@ungap/structured-clone'\nimport { DocumentData } from '@/nvdocument' // keep types aligned with your project\n\n// --- helpers --------------------------------------------------------------\n\nfunction isNumeric(v: any): boolean {\n return typeof v === 'number' && Number.isFinite(v)\n}\n\nfunction stabilizeNumber(n: number, decimals = 12): number {\n if (!isNumeric(n)) {\n return n\n }\n const mag = Math.max(1, Math.abs(n))\n const eps = 1e-9 * mag\n const rounded = Number(n.toFixed(decimals))\n if (Math.abs(n - rounded) <= eps) {\n return rounded\n }\n return n\n}\n\nfunction stabilizeArray(arr: any[], decimals = 12): any[] {\n return arr.map((v) => {\n if (Array.isArray(v)) {\n return stabilizeArray(v, decimals)\n }\n if (isNumeric(v)) {\n return stabilizeNumber(v, decimals)\n }\n return v\n })\n}\n\n// Encode special numeric values into strings for safe JSON roundtrip\nfunction specialNumberReplacer(_key: string, value: any): any {\n if (typeof value === 'number') {\n if (Number.isNaN(value)) {\n return 'NaN'\n }\n if (value === Infinity) {\n return 'infinity'\n }\n if (value === -Infinity) {\n return '-infinity'\n }\n return value\n }\n return value\n}\n\n/**\n * Try to extract an Array<number> from a value produced by structured-clone\n * or various typed-array serializations.\n */\nexport function extractNumberArray(maybe: any): number[] | undefined {\n if (maybe === undefined || maybe === null) {\n return undefined\n }\n if (Array.isArray(maybe)) {\n return maybe.slice()\n }\n // typed arrays / ArrayBufferView\n if (ArrayBuffer.isView(maybe)) {\n try {\n return Array.from(maybe as unknown as Iterable<number>)\n } catch (_) {\n /* fallthrough */\n }\n }\n if (typeof maybe === 'object') {\n if (Array.isArray((maybe as any).data)) {\n return (maybe as any).data.slice()\n }\n if (maybe.buffer && Array.isArray(maybe.buffer.data)) {\n return maybe.buffer.data.slice()\n }\n // numeric-keyed object like { \"0\": v0, \"1\": v1, length: N }\n const numericKeys = Object.keys(maybe)\n .filter((k) => /^\\d+$/.test(k))\n .map((k) => Number(k))\n .sort((a, b) => a - b)\n if (numericKeys.length > 0) {\n try {\n return numericKeys.map((i) => maybe[String(i)])\n } catch (_) {\n /* ignore */\n }\n }\n for (const candidateKey of ['_data', 'source', 'values', 'elements', 'items']) {\n if (Array.isArray((maybe as any)[candidateKey])) {\n return (maybe as any)[candidateKey].slice()\n }\n }\n }\n try {\n const arr = Array.from(maybe as unknown as Iterable<number>)\n if (Array.isArray(arr)) {\n return arr\n }\n } catch (_) {\n /* ignore */\n }\n return undefined\n}\n\n// --- per-mesh normalization -----------------------------------------------\n\n/**\n * Convert a single parsed mesh object from legacy shapes to the modern shape\n * used by rehydrateMeshes. This is intentionally defensive and non-destructive.\n */\nexport function normalizeMeshForRehydrate(meshIn: any): any {\n if (!meshIn || typeof meshIn !== 'object') {\n return meshIn\n }\n const m: any = { ...(meshIn || {}) }\n\n // 1) pts (vertex positions)\n if ((m.pts === undefined || m.pts === null) && (m.vertices !== undefined || m.positions !== undefined || m.verts !== undefined)) {\n const candidates = [m.vertices, m.positions, m.verts]\n for (const c of candidates) {\n const arr = extractNumberArray(c)\n if (arr && arr.length > 0) {\n m.pts = stabilizeArray(arr)\n break\n }\n }\n } else if (m.pts != null && !Array.isArray(m.pts) && ArrayBuffer.isView(m.pts)) {\n const arr = extractNumberArray(m.pts)\n if (arr) {\n m.pts = stabilizeArray(arr)\n }\n } else if (Array.isArray(m.pts)) {\n m.pts = stabilizeArray(m.pts)\n }\n\n // 2) tris (triangle indices)\n if ((m.tris === undefined || m.tris === null) && (m.indices !== undefined || m.faces !== undefined || m.cells !== undefined)) {\n const candidates = [m.indices, m.faces, m.cells]\n for (const c of candidates) {\n const arr = extractNumberArray(c)\n if (arr && arr.length > 0) {\n m.tris = arr\n break\n }\n }\n } else if (m.tris != null && !Array.isArray(m.tris) && ArrayBuffer.isView(m.tris)) {\n const arr = extractNumberArray(m.tris)\n if (arr) {\n m.tris = arr\n }\n }\n\n // 3) rgba255 (colors)\n if ((m.rgba255 === undefined || m.rgba255 === null) && (m.rgba !== undefined || m.color !== undefined || m.colors !== undefined)) {\n const candidates = [m.rgba, m.color, m.colors]\n for (const c of candidates) {\n const arr = extractNumberArray(c)\n if (arr && arr.length > 0) {\n m.rgba255 = arr\n break\n }\n }\n } else if (m.rgba255 != null && !Array.isArray(m.rgba255) && ArrayBuffer.isView(m.rgba255)) {\n const arr = extractNumberArray(m.rgba255)\n if (arr) {\n m.rgba255 = arr\n }\n }\n\n // 4) offsetPt0 (legacy fiber storage), fiber metadata left intact\n if (m.offsetPt0 != null && !Array.isArray(m.offsetPt0)) {\n const arr = extractNumberArray(m.offsetPt0)\n if (arr) {\n m.offsetPt0 = arr\n }\n }\n\n // 5) nodes/edges to plain arrays with shallow clones for objects\n if (Array.isArray(m.nodes)) {\n m.nodes = m.nodes.length > 0 && typeof m.nodes[0] === 'object' ? m.nodes.map((n: any) => ({ ...n })) : m.nodes.slice()\n }\n if (Array.isArray(m.edges)) {\n m.edges = m.edges.length > 0 && typeof m.edges[0] === 'object' ? m.edges.map((e: any) => ({ ...e })) : m.edges.slice()\n }\n\n // 6) normalize layers: rename legacy keys and extract values arrays\n if (Array.isArray(m.layers)) {\n m.layers = m.layers.map((layer: any) => {\n if (!layer || typeof layer !== 'object') {\n return layer\n }\n const lcopy: any = { ...(layer || {}) }\n\n // Normalize legacy colormap keys to modern names, but prefer any already-present modern keys.\n if ('colorMap' in lcopy) {\n if (!('colormap' in lcopy)) {\n lcopy.colormap = lcopy.colorMap\n }\n // remove legacy key\n delete lcopy.colorMap\n }\n if ('colorMapNegative' in lcopy) {\n if (!('colormapNegative' in lcopy)) {\n lcopy.colormapNegative = lcopy.colorMapNegative\n }\n // remove legacy key\n delete lcopy.colorMapNegative\n }\n\n // decode numeric strings if present (lightweight)\n for (const k of ['global_min', 'global_max', 'cal_min', 'cal_max', 'cal_minNeg', 'cal_maxNeg']) {\n if (typeof lcopy[k] === 'string') {\n const v = lcopy[k]\n if (v === 'infinity') {\n lcopy[k] = Infinity\n } else if (v === '-infinity') {\n lcopy[k] = -Infinity\n } else if (v === 'NaN') {\n lcopy[k] = NaN\n } else {\n const n = Number(v)\n if (!Number.isNaN(n)) {\n lcopy[k] = n\n }\n }\n }\n }\n\n if (lcopy.values != null && !Array.isArray(lcopy.values)) {\n const ev = extractNumberArray(lcopy.values)\n if (ev) {\n lcopy.values = stabilizeArray(ev)\n }\n } else if (Array.isArray(lcopy.values)) {\n lcopy.values = stabilizeArray(lcopy.values)\n }\n\n if (lcopy.atlasValues != null && !Array.isArray(lcopy.atlasValues)) {\n const ev = extractNumberArray(lcopy.atlasValues)\n if (ev) {\n lcopy.atlasValues = ev\n }\n }\n\n return lcopy\n })\n }\n\n return m\n}\n\n// --- meshesString normalization ------------------------------------------\n\nfunction normalizeMeshesString(meshesString: any): string | undefined {\n if (typeof meshesString !== 'string') {\n return undefined\n }\n const trimmed = meshesString.trim()\n if (trimmed.length === 0) {\n return meshesString\n }\n try {\n const parsed = JSON.parse(meshesString)\n\n // Detect structured-clone serialized form: array of small objects with numeric keys 0 and 1\n const looksLikeStructuredClone = Array.isArray(parsed) && parsed.length > 0 && typeof parsed[0] === 'object' && parsed[0] !== null && '0' in parsed[0] && '1' in parsed[0]\n\n if (looksLikeStructuredClone) {\n // Use deserialize to reconstruct original objects, then normalize them\n try {\n const des = deserialize(parsed as unknown as any) as any[] // reconstructs typed arrays / objects\n if (Array.isArray(des)) {\n const normalized = des.map((mx: any) => normalizeMeshForRehydrate(mx))\n return JSON.stringify(normalized, specialNumberReplacer)\n }\n // fallback: if deserialize produced something unexpected, continue below\n } catch (deserErr) {\n // if deserialize fails, fall through to legacy parsing below\n console.warn('legacy-migrate: structured-clone deserialize failed', deserErr)\n }\n }\n\n // If not structured-clone or deserialize didn't work, handle the usual JSON array case\n if (!Array.isArray(parsed)) {\n // try to locate inner array if serialize() produced wrapper\n const maybeArrayCandidates = ['value', 'data', '0']\n for (const c of maybeArrayCandidates) {\n if (parsed && Array.isArray((parsed as any)[c])) {\n const arr = (parsed as any)[c]\n const norm = arr.map((mx: any) => normalizeMeshForRehydrate(mx))\n return JSON.stringify(norm, specialNumberReplacer)\n }\n }\n return meshesString\n }\n const normalized = parsed.map((mx: any) => normalizeMeshForRehydrate(mx))\n return JSON.stringify(normalized, specialNumberReplacer)\n } catch (e) {\n return meshesString\n }\n}\n\n// --- document-level migration -------------------------------------------\n\n/**\n * Top-level migrateLegacyDocument function.\n * Takes a DocumentData-ish object and returns a normalized DocumentData object (shallow clone).\n */\nexport function migrateLegacyDocument(input: DocumentData | any | undefined): DocumentData {\n if (!input || typeof input !== 'object') {\n return input as DocumentData\n }\n\n const doc: any = { ...(input || {}) }\n\n // normalize root-level legacy colormap names\n if ('colorMap' in doc && !('colormap' in doc)) {\n doc.colormap = doc.colorMap\n }\n if ('colorMapNegative' in doc && !('colormapNegative' in doc)) {\n doc.colormapNegative = doc.colorMapNegative\n }\n\n // imageOptionsArray: convert typed-array-ish colormap fields and decode numeric strings\n if (Array.isArray(doc.imageOptionsArray)) {\n doc.imageOptionsArray = doc.imageOptionsArray.map((io: any) => {\n if (!io || typeof io !== 'object') {\n return io\n }\n const copy = { ...(io || {}) }\n for (const k of ['cal_min', 'cal_max', 'cal_minNeg', 'cal_maxNeg']) {\n if (typeof copy[k] === 'string') {\n if (copy[k] === 'infinity') {\n copy[k] = Infinity\n } else if (copy[k] === '-infinity') {\n copy[k] = -Infinity\n } else if (copy[k] === 'NaN') {\n copy[k] = NaN\n } else {\n const n = Number(copy[k])\n if (!Number.isNaN(n)) {\n copy[k] = n\n }\n }\n }\n }\n if (copy.colormap && !Array.isArray(copy.colormap)) {\n const arr = extractNumberArray(copy.colormap)\n if (arr) {\n copy.colormap = arr\n }\n }\n if (copy.colormapNegative && !Array.isArray(copy.colormapNegative)) {\n const arr = extractNumberArray(copy.colormapNegative)\n if (arr) {\n copy.colormapNegative = arr\n }\n }\n return copy\n })\n }\n\n // meshesString normalization\n if (typeof doc.meshesString === 'string' && doc.meshesString.trim().length > 0) {\n const normalized = normalizeMeshesString(doc.meshesString)\n if (typeof normalized === 'string') {\n doc.meshesString = normalized\n }\n }\n\n // Ensure labels/connectomes/encodedImageBlobs are plain arrays where possible\n if (doc.labels && !Array.isArray(doc.labels)) {\n try {\n doc.labels = Array.from(doc.labels)\n } catch (_) {\n /* leave as-is */\n }\n }\n if (doc.connectomes && !Array.isArray(doc.connectomes)) {\n try {\n doc.connectomes = Array.from(doc.connectomes)\n } catch (_) {\n /* leave as-is */\n }\n }\n if (doc.encodedImageBlobs && !Array.isArray(doc.encodedImageBlobs)) {\n try {\n doc.encodedImageBlobs = Array.from(doc.encodedImageBlobs)\n } catch (_) {\n /* leave as-is */\n }\n }\n\n return doc as DocumentData\n}\n\nexport default migrateLegacyDocument\n","import { SLICE_TYPE } from '@/nvdocument'\nimport { TypedVoxelArray } from '@/nvimage'\n\ninterface DrawingDimensions {\n dimX: number\n dimY: number\n dimZ: number\n}\n\ninterface InterpolationOptions {\n intensityWeight?: number\n binaryThreshold?: number\n intensitySigma?: number\n applySmoothingToSlices?: boolean\n useIntensityGuided?: boolean\n sliceType?: SLICE_TYPE\n}\n\n/**\n * Find the first and last slices containing drawing data along a given axis\n * @param sliceType - The slice orientation (AXIAL, CORONAL, or SAGITTAL)\n * @param drawBitmap - The drawing bitmap data\n * @param dims - The volume dimensions\n * @returns Object containing first and last slice indices, or null if no data found\n */\nexport function findBoundarySlices(sliceType: SLICE_TYPE, drawBitmap: Uint8Array, dims: DrawingDimensions): { first: number; last: number } | null {\n const { dimX, dimY, dimZ } = dims\n\n let axisSize: number\n if (sliceType === SLICE_TYPE.AXIAL) {\n axisSize = dimZ\n } else if (sliceType === SLICE_TYPE.CORONAL) {\n axisSize = dimY\n } else if (sliceType === SLICE_TYPE.SAGITTAL) {\n axisSize = dimX\n } else {\n return null\n }\n\n let firstSliceWithData = -1\n let lastSliceWithData = -1\n\n for (let slice = 0; slice < axisSize; slice++) {\n let hasData = false\n\n if (sliceType === SLICE_TYPE.AXIAL) {\n // Check axial slice (XY plane at Z=slice)\n const offset = slice * dimX * dimY\n for (let i = 0; i < dimX * dimY; i++) {\n if (drawBitmap[offset + i] > 0) {\n hasData = true\n break\n }\n }\n } else if (sliceType === SLICE_TYPE.CORONAL) {\n // Check coronal slice (XZ plane at Y=slice)\n for (let z = 0; z < dimZ; z++) {\n for (let x = 0; x < dimX; x++) {\n const idx = x + slice * dimX + z * dimX * dimY\n if (drawBitmap[idx] > 0) {\n hasData = true\n break\n }\n }\n if (hasData) {\n break\n }\n }\n } else if (sliceType === SLICE_TYPE.SAGITTAL) {\n // Check sagittal slice (YZ plane at X=slice)\n for (let z = 0; z < dimZ; z++) {\n for (let y = 0; y < dimY; y++) {\n const idx = slice + y * dimX + z * dimX * dimY\n if (drawBitmap[idx] > 0) {\n hasData = true\n break\n }\n }\n if (hasData) {\n break\n }\n }\n }\n\n if (hasData) {\n if (firstSliceWithData === -1) {\n firstSliceWithData = slice\n }\n lastSliceWithData = slice\n }\n }\n\n if (firstSliceWithData === -1 || lastSliceWithData === -1) {\n return null\n }\n\n return { first: firstSliceWithData, last: lastSliceWithData }\n}\n\n/**\n * Extract a single slice from 3D volume data\n * @param sliceIndex - Index of the slice to extract\n * @param sliceType - The slice orientation (AXIAL, CORONAL, or SAGITTAL)\n * @param drawBitmap - The drawing bitmap data\n * @param dims - The volume dimensions\n * @returns Float32Array containing the slice data\n */\nexport function extractSlice(sliceIndex: number, sliceType: SLICE_TYPE, drawBitmap: Uint8Array, dims: DrawingDimensions): Float32Array {\n const { dimX, dimY, dimZ } = dims\n\n let sliceData: Float32Array\n if (sliceType === SLICE_TYPE.AXIAL) {\n // Extract axial slice (XY plane at Z=sliceIndex)\n sliceData = new Float32Array(dimX * dimY)\n const offset = sliceIndex * dimX * dimY\n for (let i = 0; i < dimX * dimY; i++) {\n sliceData[i] = drawBitmap[offset + i]\n }\n } else if (sliceType === SLICE_TYPE.CORONAL) {\n // Extract coronal slice (XZ plane at Y=sliceIndex)\n sliceData = new Float32Array(dimX * dimZ)\n for (let z = 0; z < dimZ; z++) {\n for (let x = 0; x < dimX; x++) {\n const srcIdx = x + sliceIndex * dimX + z * dimX * dimY\n const dstIdx = x + z * dimX\n sliceData[dstIdx] = drawBitmap[srcIdx]\n }\n }\n } else if (sliceType === SLICE_TYPE.SAGITTAL) {\n // Extract sagittal slice (YZ plane at X=sliceIndex)\n sliceData = new Float32Array(dimY * dimZ)\n for (let z = 0; z < dimZ; z++) {\n for (let y = 0; y < dimY; y++) {\n const srcIdx = sliceIndex + y * dimX + z * dimX * dimY\n const dstIdx = y + z * dimY\n sliceData[dstIdx] = drawBitmap[srcIdx]\n }\n }\n } else {\n throw new Error('Invalid slice type')\n }\n\n return sliceData\n}\n\n/**\n * Extract intensity slice from image data\n * @param sliceIndex - Index of the slice to extract\n * @param sliceType - The slice orientation\n * @param imageData - The image intensity data\n * @param dims - The volume dimensions\n * @param maxVal - Maximum value for normalization\n * @returns Float32Array containing normalized intensity values\n */\nexport function extractIntensitySlice(sliceIndex: number, sliceType: SLICE_TYPE, imageData: TypedVoxelArray, dims: DrawingDimensions, maxVal: number): Float32Array {\n const { dimX, dimY, dimZ } = dims\n\n let sliceData: TypedVoxelArray\n if (sliceType === SLICE_TYPE.AXIAL) {\n // Extract axial slice (XY plane at Z=sliceIndex)\n sliceData = new Float32Array(dimX * dimY)\n const offset = sliceIndex * dimX * dimY\n for (let i = 0; i < dimX * dimY; i++) {\n sliceData[i] = imageData[offset + i] / maxVal\n }\n } else if (sliceType === SLICE_TYPE.CORONAL) {\n // Extract coronal slice (XZ plane at Y=sliceIndex)\n sliceData = new Float32Array(dimX * dimZ)\n for (let z = 0; z < dimZ; z++) {\n for (let x = 0; x < dimX; x++) {\n const srcIdx = x + sliceIndex * dimX + z * dimX * dimY\n const dstIdx = x + z * dimX\n sliceData[dstIdx] = imageData[srcIdx] / maxVal\n }\n }\n } else if (sliceType === SLICE_TYPE.SAGITTAL) {\n // Extract sagittal slice (YZ plane at X=sliceIndex)\n sliceData = new Float32Array(dimY * dimZ)\n for (let z = 0; z < dimZ; z++) {\n for (let y = 0; y < dimY; y++) {\n const srcIdx = sliceIndex + y * dimX + z * dimX * dimY\n const dstIdx = y + z * dimY\n sliceData[dstIdx] = imageData[srcIdx] / maxVal\n }\n }\n } else {\n throw new Error('Invalid slice type')\n }\n\n return sliceData\n}\n\n/**\n * Insert a color mask into the drawing bitmap at a specific slice\n * @param mask - The mask data to insert\n * @param sliceIndex - Index of the slice\n * @param sliceType - The slice orientation\n * @param drawBitmap - The drawing bitmap to modify\n * @param dims - The volume dimensions\n * @param binaryThreshold - Threshold for binary conversion\n * @param color - Color value to use\n */\nexport function insertColorMask(mask: TypedVoxelArray, sliceIndex: number, sliceType: SLICE_TYPE, drawBitmap: Uint8Array, dims: DrawingDimensions, binaryThreshold: number, color: number): void {\n const { dimX, dimY, dimZ } = dims\n\n if (sliceType === SLICE_TYPE.AXIAL) {\n // Insert axial slice (XY plane at Z=sliceIndex)\n const offset = sliceIndex * dimX * dimY\n for (let i = 0; i < mask.length; i++) {\n if (mask[i] >= binaryThreshold) {\n drawBitmap[offset + i] = color\n }\n }\n } else if (sliceType === SLICE_TYPE.CORONAL) {\n // Insert coronal slice (XZ plane at Y=sliceIndex)\n for (let z = 0; z < dimZ; z++) {\n for (let x = 0; x < dimX; x++) {\n const srcIdx = x + z * dimX\n const dstIdx = x + sliceIndex * dimX + z * dimX * dimY\n if (mask[srcIdx] >= binaryThreshold) {\n drawBitmap[dstIdx] = color\n }\n }\n }\n } else if (sliceType === SLICE_TYPE.SAGITTAL) {\n // Insert sagittal slice (YZ plane at X=sliceIndex)\n for (let z = 0; z < dimZ; z++) {\n for (let y = 0; y < dimY; y++) {\n const srcIdx = y + z * dimY\n const dstIdx = sliceIndex + y * dimX + z * dimX * dimY\n if (mask[srcIdx] >= binaryThreshold) {\n drawBitmap[dstIdx] = color\n }\n }\n }\n } else {\n throw new Error('Invalid slice type')\n }\n}\n\n/**\n * Smooth a 2D slice using a simple 3x3 kernel\n * @param slice - The slice data to smooth\n * @param width - Width of the slice\n * @param height - Height of the slice\n */\nexport function smoothSlice(slice: Float32Array, width: number, height: number): void {\n if (width < 3 || height < 3) {\n return\n }\n\n const temp = new Float32Array(slice.length)\n\n // Smooth in X direction\n for (let y = 0; y < height; y++) {\n for (let x = 0; x < width; x++) {\n const idx = x + y * width\n if (x === 0 || x === width - 1) {\n temp[idx] = slice[idx]\n } else {\n temp[idx] = (slice[idx - 1] + 2 * slice[idx] + slice[idx + 1]) * 0.25\n }\n }\n }\n\n // Smooth in Y direction\n for (let y = 0; y < height; y++) {\n for (let x = 0; x < width; x++) {\n const idx = x + y * width\n if (y === 0 || y === height - 1) {\n slice[idx] = temp[idx]\n } else {\n slice[idx] = (temp[idx - width] + 2 * temp[idx] + temp[idx + width]) * 0.25\n }\n }\n }\n}\n\n/**\n * Calculate intensity-based weight for interpolation\n * @param intensity1 - Intensity from first slice\n * @param intensity2 - Intensity from second slice\n * @param targetIntensity - Target intensity to compare against\n * @param intensitySigma - Sigma parameter for weight calculation\n * @returns Weight value between 0 and 1\n */\nexport function calculateIntensityWeight(intensity1: number, intensity2: number, targetIntensity: number, intensitySigma: number): number {\n const diff1 = Math.abs(targetIntensity - intensity1)\n const diff2 = Math.abs(targetIntensity - intensity2)\n\n const weight1 = Math.exp((-diff1 * diff1) / (2 * intensitySigma * intensitySigma))\n const weight2 = Math.exp((-diff2 * diff2) / (2 * intensitySigma * intensitySigma))\n\n const totalWeight = weight1 + weight2\n if (totalWeight < 1e-6) {\n return 0.5\n }\n\n return weight1 / totalWeight\n}\n\n/**\n * Perform geometric interpolation between two slices\n * @param sliceLow - Lower slice data\n * @param sliceHigh - Higher slice data\n * @param z - Current slice index\n * @param sliceIndexLow - Lower slice index\n * @param sliceIndexHigh - Higher slice index\n * @param interpolatedSlice - Output array for interpolated values\n */\nexport function doGeometricInterpolation(sliceLow: Float32Array, sliceHigh: Float32Array, z: number, sliceIndexLow: number, sliceIndexHigh: number, interpolatedSlice: Float32Array): void {\n const fracHigh = (z - sliceIndexLow) / (sliceIndexHigh - sliceIndexLow)\n const fracLow = 1 - fracHigh\n\n for (let i = 0; i < sliceLow.length; i++) {\n interpolatedSlice[i] = sliceLow[i] * fracLow + sliceHigh[i] * fracHigh\n }\n}\n\n/**\n * Perform intensity-guided interpolation between two slices\n * @param sliceLow - Lower slice data\n * @param sliceHigh - Higher slice data\n * @param z - Current slice index\n * @param sliceIndexLow - Lower slice index\n * @param sliceIndexHigh - Higher slice index\n * @param interpolatedSlice - Output array for interpolated values\n * @param opts - Interpolation options\n * @param intensityLow - Intensity data for lower slice\n * @param intensityHigh - Intensity data for higher slice\n * @param targetIntensity - Target intensity data\n */\nexport function doIntensityGuidedInterpolation(\n sliceLow: Float32Array,\n sliceHigh: Float32Array,\n z: number,\n sliceIndexLow: number,\n sliceIndexHigh: number,\n interpolatedSlice: Float32Array,\n opts: { intensityWeight: number; intensitySigma: number },\n intensityLow: Float32Array,\n intensityHigh: Float32Array,\n targetIntensity: Float32Array\n): void {\n const baseFracHigh = (z - sliceIndexLow) / (sliceIndexHigh - sliceIndexLow)\n const baseFracLow = 1 - baseFracHigh\n\n for (let i = 0; i < sliceLow.length; i++) {\n if (sliceLow[i] > 0 || sliceHigh[i] > 0) {\n const intensityWeight = calculateIntensityWeight(intensityLow[i], intensityHigh[i], targetIntensity[i], opts.intensitySigma)\n\n const alpha = opts.intensityWeight\n const combinedWeightLow = alpha * intensityWeight + (1 - alpha) * baseFracLow\n const combinedWeightHigh = 1 - combinedWeightLow\n\n interpolatedSlice[i] = sliceLow[i] * combinedWeightLow + sliceHigh[i] * combinedWeightHigh\n } else {\n interpolatedSlice[i] = sliceLow[i] * baseFracLow + sliceHigh[i] * baseFracHigh\n }\n }\n}\n\n/**\n * Main interpolation function for mask slices\n * @param drawBitmap - The drawing bitmap to modify\n * @param dims - The volume dimensions\n * @param imageData - Optional image intensity data for guided interpolation\n * @param maxVal - Maximum value for intensity normalization\n * @param sliceIndexLow - Lower slice index (optional)\n * @param sliceIndexHigh - Higher slice index (optional)\n * @param options - Interpolation options\n * @param refreshDrawingCallback - Callback to refresh the drawing\n */\nexport function interpolateMaskSlices(\n drawBitmap: Uint8Array,\n dims: DrawingDimensions,\n imageData: TypedVoxelArray,\n maxVal: number,\n sliceIndexLow: number | undefined,\n sliceIndexHigh: number | undefined,\n options: InterpolationOptions,\n refreshDrawingCallback: () => void\n): void {\n const { dimX, dimY, dimZ } = dims\n\n // Determine slice type (default to axial)\n const sliceType = options.sliceType ?? SLICE_TYPE.AXIAL\n\n // Get dimensions based on slice type\n let sliceWidth: number, sliceHeight: number, maxSliceIndex: number\n if (sliceType === SLICE_TYPE.AXIAL) {\n sliceWidth = dimX\n sliceHeight = dimY\n maxSliceIndex = dimZ - 1\n } else if (sliceType === SLICE_TYPE.CORONAL) {\n sliceWidth = dimX\n sliceHeight = dimZ\n maxSliceIndex = dimY - 1\n } else if (sliceType === SLICE_TYPE.SAGITTAL) {\n sliceWidth = dimY\n sliceHeight = dimZ\n maxSliceIndex = dimX - 1\n } else {\n throw new Error('Invalid slice type. Must be AXIAL, CORONAL, or SAGITTAL')\n }\n\n // Set default options\n const opts = {\n intensityWeight: options.intensityWeight ?? 0.7,\n binaryThreshold: options.binaryThreshold ?? 0.375,\n intensitySigma: options.intensitySigma ?? 0.1,\n applySmoothingToSlices: options.applySmoothingToSlices ?? true,\n useIntensityGuided: options.useIntensityGuided ?? true\n }\n\n // If slice indices are provided, validate them\n if (sliceIndexLow !== undefined && sliceIndexHigh !== undefined) {\n if (sliceIndexLow >= sliceIndexHigh) {\n throw new Error('Low slice index must be less than high slice index')\n }\n if (sliceIndexLow < 0 || sliceIndexHigh > maxSliceIndex) {\n throw new Error(`Slice indices out of bounds [0, ${maxSliceIndex}]`)\n }\n }\n\n // Find all unique colors across all slices\n const colorRanges = new Map<number, { min: number; max: number }>()\n\n // Scan through all slices to find color ranges\n for (let sliceIdx = 0; sliceIdx <= maxSliceIndex; sliceIdx++) {\n const slice = extractSlice(sliceIdx, sliceType, drawBitmap, dims)\n\n for (let i = 0; i < slice.length; i++) {\n const color = slice[i]\n if (color > 0) {\n if (!colorRanges.has(color)) {\n colorRanges.set(color, { min: sliceIdx, max: sliceIdx })\n } else {\n const range = colorRanges.get(color)!\n range.min = Math.min(range.min, sliceIdx)\n range.max = Math.max(range.max, sliceIdx)\n }\n }\n }\n }\n\n // Process each color independently with its own slice range\n for (const [color, range] of colorRanges) {\n // Use provided indices or color-specific range\n const colorSliceLow = sliceIndexLow !== undefined ? Math.max(sliceIndexLow, range.min) : range.min\n const colorSliceHigh = sliceIndexHigh !== undefined ? Math.min(sliceIndexHigh, range.max) : range.max\n\n // Skip if range is invalid or too small\n if (colorSliceLow >= colorSliceHigh || colorSliceHigh - colorSliceLow < 2) {\n continue\n }\n\n // Extract boundary slices for this color\n const sliceLow = extractSlice(colorSliceLow, sliceType, drawBitmap, dims)\n const sliceHigh = extractSlice(colorSliceHigh, sliceType, drawBitmap, dims)\n\n // Create binary masks for this color\n const colorMaskLow = new Float32Array(sliceLow.length)\n const colorMaskHigh = new Float32Array(sliceHigh.length)\n\n for (let i = 0; i < sliceLow.length; i++) {\n colorMaskLow[i] = sliceLow[i] === color ? 1 : 0\n colorMaskHigh[i] = sliceHigh[i] === color ? 1 : 0\n }\n\n // Apply smoothing if enabled\n if (opts.applySmoothingToSlices) {\n smoothSlice(colorMaskLow, sliceWidth, sliceHeight)\n smoothSlice(colorMaskHigh, sliceWidth, sliceHeight)\n }\n\n // Interpolate between boundary slices for this color\n for (let z = colorSliceLow + 1; z < colorSliceHigh; z++) {\n const colorInterpolated = new Float32Array(sliceWidth * sliceHeight)\n\n if (opts.useIntensityGuided && imageData) {\n // Intensity-guided interpolation\n const intensityLow = extractIntensitySlice(colorSliceLow, sliceType, imageData, dims, maxVal)\n const intensityHigh = extractIntensitySlice(colorSliceHigh, sliceType, imageData, dims, maxVal)\n const targetIntensity = extractIntensitySlice(z, sliceType, imageData, dims, maxVal)\n\n doIntensityGuidedInterpolation(colorMaskLow, colorMaskHigh, z, colorSliceLow, colorSliceHigh, colorInterpolated, opts, intensityLow, intensityHigh, targetIntensity)\n } else {\n // Geometric interpolation\n doGeometricInterpolation(colorMaskLow, colorMaskHigh, z, colorSliceLow, colorSliceHigh, colorInterpolated)\n }\n\n // Insert interpolated values for this color\n insertColorMask(colorInterpolated, z, sliceType, drawBitmap, dims, opts.binaryThreshold, color)\n }\n }\n\n // Update the drawing texture\n refreshDrawingCallback()\n}\n"],"mappings":"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AAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IACxE;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACxE;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IACxE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACxE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IACrE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAG;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IACrE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IACvE;AAAA,EACF;AAAA,EACA,GAAK;AAAA,IACH;AAAA,IAAG;AAAA,IAAG;AAAA,IAAG;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IACpE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAI;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACxE;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAG;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IACvE;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IACxE;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IACpE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,EAC5E;AAAA,EACA,GAAK;AAAA,IACH;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACpE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,EACpD;AAAA,EACA,GAAK;AAAA,IACH;AAAA,IAAG;AAAA,IAAG;AAAA,IAAG;AAAA,IAAG;AAAA,IAAG;AAAA,IAAG;AAAA,IAAG;AAAA,IAAG;AAAA,IAAG;AAAA,IAAG;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACtE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACxE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,EACZ;AACF;;;ACtDA;AAAA,EACE,QAAU;AAAA,IACR;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAAA,EACA,GAAK;AAAA,IACH;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACxE;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAG;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IACpE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACxE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACtE;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAG;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACrE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACpE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACvE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IACxE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACxE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACrE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IACpE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,EAC3B;AAAA,EACA,GAAK;AAAA,IACH;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACxE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IACrE;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACxE;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACrE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACrE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACrE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACpE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACrE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,EAC5D;AAAA,EACA,GAAK;AAAA,IACH;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAG;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IACvE;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACvE;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACvE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACzE;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACvE;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IACtE;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACpE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAG;AAAA,IAAG;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IACpE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACxE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACpE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IACxE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACxE;AAAA,EACF;AAAA,EACA,GAAK;AAAA,IACH;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACpE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,EACF;AAAA,EACA,GAAK;AAAA,IACH;AAAA,IAAG;AAAA,IAAG;AAAA,IAAG;AAAA,IAAG;AAAA,IAAG;AAAA,IAAG;AAAA,IAAG;AAAA,IAAG;AAAA,IAAG;AAAA,IAAG;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACtE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACxE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AA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AK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,EACzD;AAAA,EACA,GAAK;AAAA,IACH;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,EAC3B;AAAA,EACA,GAAK;AAAA,IACH;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,EACF;AAAA,EACA,GAAK;AAAA,IACH;AAAA,IAAG;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACvE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,EAClC;AAAA,EACA,GAAK;AAAA,IACH;AAAA,IAAG;AAAA,IAAG;AAAA,IAAG;AAAA,IAAG;AAAA,IAAG;AAAA,IAAG;AAAA,IAAG;AAAA,IAAG;AAAA,IAAG;AAAA,IAAG;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACtE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACxE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA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IAAI;AAAA,IACtE;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IACzE;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IACtE;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACxE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IACrE;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAG;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IACtE;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACpE;AAAA,IAAK;AAAA,IAAI;AAAA,IAAG;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IACzE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACxE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IACpE;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAG;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IACpE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAG;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,EAChD;AAAA,EACA,GAAK;AAAA,IACH;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IACvE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IACrE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACrE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IACvE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IACzE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACpE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IACtE;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IACxE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IACvE;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IACtE;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,EACvC;AAAA,EACA,GAAK;AAAA,IACH;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACvE;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAG;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IACvE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACxE;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAI;AAAA,IACxE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAG;AAAA,IAAG;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IACvE;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IACxE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IACzE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAG;AAAA,IAAG;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IACvE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,EACtC;AAAA,EACA,GAAK;AAAA,IACH;AAAA,IAAG;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACvE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,EAClC;AAAA,EACA,GAAK;AAAA,IACH;AAAA,IAAG;AAAA,IAAG;AAAA,IAAG;AAAA,IAAG;AAAA,IAAG;AAAA,IAAG;AAAA,IAAG;AAAA,IAAG;AAAA,IAAG;AAAA,IAAG;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACtE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACxE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAA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AA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IACvE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAG;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IACrE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAG;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IACvE;AAAA,IAAG;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IACzE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACvE;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IACpE;AAAA,IAAK;AAAA,IAAG;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAG;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACzE;AAAA,IAAG;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,EAC3B;AAAA,EACA,GAAK;AAAA,IACH;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAG;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAG;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAI;AAAA,IAAG;AAAA,IAAI;AAAA,IAAK;AAAA,IACtE;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IACvE;AAAA,IAAI;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IACrE;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IACxE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IACxE;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAG;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IACpE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAG;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAG;AAAA,IACxE;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAG;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACxE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IACrE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAG;AAAA,IACxE;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAC1E;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,EAChB;AAAA,EACA,GAAK;AAAA,IACH;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAI;AAAA,IACvE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAG;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IACzE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IACtE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAG;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAC1E;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IACzE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IACrE;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IACzE;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IACrE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAG;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IACtE;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IACvE;AAAA,IAAK;AAAA,IAAG;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IACtE;AAAA,IAAG;AAAA,IAAI;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,EACtB;AAAA,EACA,GAAK;AAAA,IACH;AAAA,IAAG;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACvE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,EAClC;AAAA,EACA,GAAK;AAAA,IACH;AAAA,IAAG;AAAA,IAAG;AAAA,IAAG;AAAA,IAAG;AAAA,IAAG;AAAA,IAAG;AAAA,IAAG;AAAA,IAAG;AAAA,IAAG;AAAA,IAAG;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACtE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACxE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;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","names":["vec3","vec4","vec3","readHeaderAsync","uuidv4","vec3","ImageType","vec4","NIFTI1","mat4","vec3","vec4","NIFTI1","NIFTI1","NIFTI1","NIFTI1","NIFTI1","vec3","NIFTI1","mat4","NIFTI1","NIFTI1","mat3","mat4","vec3","NIFTI1","NIFTI1","mat4","vec3","vec4","mat3","mat4","vec3","vec4","readHeaderAsync","readHeaderAsync","NIFTI1","mat4","zarr","vec3","vec4","uuidv4","mat4","vec3","vec3","mat4","vec4","vec3","vec3","vec4","slice"]}
|
|
1
|
+
{"version":3,"sources":["../../src/logger.ts","../../src/drawing/rle.ts","../../src/drawing/undo.ts","../../src/nvdocument.ts","../../src/nvserializer.ts","../../src/nvutilities.ts","../../src/nvimage/index.ts","../../src/nvimage/utils.ts","../../src/nvimage/ImageWriter.ts","../../src/nvimage/VolumeUtils.ts","../../src/nvimage/ImageReaders/afni.ts","../../src/nvimage/ImageReaders/brainvoyager.ts","../../src/nvimage/ImageReaders/dsistudio.ts","../../src/nvimage/ImageReaders/ecat.ts","../../src/nvimage/ImageReaders/image.ts","../../src/nvimage/ImageReaders/itk.ts","../../src/nvimage/ImageReaders/mgh.ts","../../src/nvimage/ImageReaders/mrtrix.ts","../../src/nvimage/ImageReaders/nii.ts","../../src/nvimage/ImageReaders/nrrd.ts","../../src/nvimage/ImageReaders/numpy.ts","../../src/nvimage/ImageReaders/zarr.ts","../../src/nvimage/CoordinateTransform.ts","../../src/nvimage/ImageOrientation.ts","../../src/cmaps/index.ts","../../src/cmaps/_itksnap.json","../../src/cmaps/_slicer3d.json","../../src/cmaps/actc.json","../../src/cmaps/batlow.json","../../src/cmaps/bcgwhw_dark.json","../../src/cmaps/bcgwhw.json","../../src/cmaps/blue2red.json","../../src/cmaps/blue2magenta.json","../../src/cmaps/blue2cyan.json","../../src/cmaps/bluegrn.json","../../src/cmaps/blue.json","../../src/cmaps/bone.json","../../src/cmaps/bronze.json","../../src/cmaps/cet_l17.json","../../src/cmaps/cividis.json","../../src/cmaps/cool.json","../../src/cmaps/copper2.json","../../src/cmaps/copper.json","../../src/cmaps/ct_airways.json","../../src/cmaps/ct_artery.json","../../src/cmaps/ct_bones.json","../../src/cmaps/ct_brain_gray.json","../../src/cmaps/ct_brain.json","../../src/cmaps/ct_cardiac.json","../../src/cmaps/ct_head.json","../../src/cmaps/ct_kidneys.json","../../src/cmaps/ct_liver.json","../../src/cmaps/ct_muscles.json","../../src/cmaps/ct_scalp.json","../../src/cmaps/ct_skull.json","../../src/cmaps/ct_soft.json","../../src/cmaps/ct_soft_tissue.json","../../src/cmaps/ct_surface.json","../../src/cmaps/ct_vessels.json","../../src/cmaps/ct_w_contrast.json","../../src/cmaps/cubehelix.json","../../src/cmaps/electric_blue.json","../../src/cmaps/freesurfer.json","../../src/cmaps/ge_color.json","../../src/cmaps/gold.json","../../src/cmaps/gray.json","../../src/cmaps/green.json","../../src/cmaps/green2cyan.json","../../src/cmaps/green2orange.json","../../src/cmaps/hotiron.json","../../src/cmaps/hot.json","../../src/cmaps/hsv.json","../../src/cmaps/inferno.json","../../src/cmaps/jet.json","../../src/cmaps/kry.json","../../src/cmaps/linspecer.json","../../src/cmaps/lipari.json","../../src/cmaps/magma.json","../../src/cmaps/mako.json","../../src/cmaps/navia.json","../../src/cmaps/nih.json","../../src/cmaps/plasma.json","../../src/cmaps/random.json","../../src/cmaps/red.json","../../src/cmaps/redyell.json","../../src/cmaps/rocket.json","../../src/cmaps/surface.json","../../src/cmaps/thermal.json","../../src/cmaps/turbo.json","../../src/cmaps/violet.json","../../src/cmaps/viridis.json","../../src/cmaps/warm.json","../../src/cmaps/winter.json","../../src/cmaps/x_rain.json","../../src/cmaps/afni_blues_inv.json","../../src/cmaps/afni_reds_inv.json","../../src/cmaps/roi_i256.json","../../src/colortables.ts","../../src/nvimage/ImageFactory.ts","../../src/nvimage/ZarrProcessor.ts","../../src/nvimage/zarr/NVZarrHelper.ts","../../src/nvimage/affineUtils.ts","../../src/nvimage/zarr/ZarrChunkClient.ts","../../src/nvmesh.ts","../../src/niivue-object3D.ts","../../src/nvmesh-utilities.ts","../../src/nvmesh-loaders.ts","../../src/utils/legacy-migrate.ts","../../src/drawing/masks.ts"],"sourcesContent":["interface LogLevelMap {\n [key: string]: number\n}\nclass Log {\n level: string\n name: string\n constructor({ name = 'niivue', level = 'info' } = {}) {\n this.name = `${name}`\n this.level = level\n }\n\n // map 'debug' 'info' 'warn' 'error' 'fatal' 'silent' to numbers\n // for comparison\n static levels: LogLevelMap = {\n debug: 0,\n info: 1,\n warn: 2,\n error: 3,\n fatal: 4,\n silent: Infinity\n }\n\n debug(...args: unknown[]): void {\n if (Log.levels[this.level] > Log.levels.debug) {\n return\n }\n // eslint-disable-next-line\n console.debug(`${this.name}-debug`, ...args)\n }\n\n info(...args: unknown[]): void {\n if (Log.levels[this.level] > Log.levels.info) {\n return\n }\n // eslint-disable-next-line\n console.info(`${this.name}-info`, ...args)\n }\n\n warn(...args: unknown[]): void {\n if (Log.levels[this.level] > Log.levels.warn) {\n return\n }\n // eslint-disable-next-line\n console.warn(`${this.name}-warn`, ...args)\n }\n\n error(...args: unknown[]): void {\n if (Log.levels[this.level] > Log.levels.error) {\n return\n }\n // eslint-disable-next-line\n console.error(`${this.name}-error`, ...args)\n }\n\n fatal(...args: unknown[]): void {\n if (Log.levels[this.level] > Log.levels.fatal) {\n return\n }\n // eslint-disable-next-line\n console.error(`${this.name}-fatal`, ...args)\n }\n\n setLogLevel(level: string): void {\n this.level = level\n }\n\n setName(name: string): void {\n this.name = name\n }\n}\n\n// make a log instance and export it\nconst log = new Log({ name: 'niivue', level: 'info' })\nexport { log }\n","import { log } from '@/logger'\n// Internal function to compress drawing using run length encoding\n// inputs\n// data: Uint8Array to compress\n// output\n// returns rle compressed Uint8Array\nexport function encodeRLE(data: Uint8Array): Uint8Array {\n // https://en.wikipedia.org/wiki/PackBits\n // run length encoding\n // input and output are Uint8Array\n // Will compress data with long runs up to x64\n // Worst case encoded size is ~1% larger than input\n const dl = data.length // input length\n let dp = 0 // input position\n // worst case: run length encoding (1+1/127) times larger than input\n const r = new Uint8Array(dl + Math.ceil(0.01 * dl))\n const rI = new Int8Array(r.buffer) // typecast as header can be negative\n let rp = 0 // run length position\n while (dp < dl) {\n // for each byte in input\n let v = data[dp]\n dp++\n let rl = 1 // run length\n while (rl < 129 && dp < dl && data[dp] === v) {\n dp++\n rl++\n }\n if (rl > 1) {\n // header\n rI[rp] = -rl + 1\n rp++\n r[rp] = v\n rp++\n continue\n }\n // count literal length\n while (dp < dl) {\n if (rl > 127) {\n break\n }\n if (dp + 2 < dl) {\n if (v !== data[dp] && data[dp + 2] === data[dp] && data[dp + 1] === data[dp]) {\n break\n }\n }\n v = data[dp]\n dp++\n rl++\n }\n // write header\n r[rp] = rl - 1\n rp++\n for (let i = 0; i < rl; i++) {\n r[rp] = data[dp - rl + i]\n rp++\n }\n }\n log.debug('PackBits ' + dl + ' -> ' + rp + ' bytes (x' + dl / rp + ')')\n return r.slice(0, rp)\n}\n\n// Internal function to decompress drawing using run length encoding\n// inputs\n// rle: packbits compressed stream\n// decodedlen: size of uncompressed data\n// output\n// returns Uint8Array of decodedlen bytes\nexport function decodeRLE(rle: Uint8Array, decodedlen: number): Uint8Array {\n const r = new Uint8Array(rle.buffer)\n const rI = new Int8Array(r.buffer) // typecast as header can be negative\n let rp = 0 // input position in rle array\n // d: output uncompressed data array\n const d = new Uint8Array(decodedlen)\n let dp = 0 // output position in decoded array\n while (rp < r.length) {\n // read header\n const hdr = rI[rp]\n rp++\n if (hdr < 0) {\n // write run\n const v = rI[rp]\n rp++\n for (let i = 0; i < 1 - hdr; i++) {\n d[dp] = v\n dp++\n }\n } else {\n // write literal\n for (let i = 0; i < hdr + 1; i++) {\n d[dp] = rI[rp]\n rp++\n dp++\n }\n }\n }\n return d\n}\n","import { log } from '@/logger'\nimport { decodeRLE } from '@/drawing/rle'\n\ninterface DrawUndoArgs {\n drawUndoBitmaps: Uint8Array[]\n currentDrawUndoBitmap: number\n drawBitmap: Uint8Array\n}\nexport const drawUndo = ({ drawUndoBitmaps, currentDrawUndoBitmap, drawBitmap }: DrawUndoArgs): { drawBitmap: Uint8Array; currentDrawUndoBitmap: number } | undefined => {\n const len = drawUndoBitmaps.length\n if (len < 1) {\n log.debug('undo bitmaps not loaded')\n return\n }\n currentDrawUndoBitmap--\n if (currentDrawUndoBitmap < 0) {\n currentDrawUndoBitmap = len - 1\n }\n if (currentDrawUndoBitmap >= len) {\n currentDrawUndoBitmap = 0\n }\n if (drawUndoBitmaps[currentDrawUndoBitmap].length < 2) {\n log.debug('drawUndo is misbehaving')\n return\n }\n drawBitmap = decodeRLE(drawUndoBitmaps[currentDrawUndoBitmap], drawBitmap.length)\n return { drawBitmap, currentDrawUndoBitmap }\n}\n","// nvdocument.ts (updated)\n// --- imports\nimport { vec3, vec4 } from 'gl-matrix'\nimport { NVSerializer } from '@/nvserializer' // adjust path if needed\n\nimport { NVUtilities } from '@/nvutilities'\nimport { ImageFromUrlOptions, NVIMAGE_TYPE, NVImage } from '@/nvimage'\nimport { NVMesh } from '@/nvmesh'\nimport { NVLabel3D } from '@/nvlabel'\nimport { NVConnectome } from '@/nvconnectome'\n\n/**\n * Represents a completed measurement between two points\n */\nexport interface CompletedMeasurement {\n startMM: vec3 // World coordinates in mm for start point\n endMM: vec3 // World coordinates in mm for end point\n distance: number // Distance between points in mm\n sliceIndex: number\n sliceType: SLICE_TYPE\n slicePosition: number\n}\n\n/**\n * Represents a completed angle measurement between two lines\n */\nexport interface CompletedAngle {\n firstLineMM: { start: vec3; end: vec3 } // World coordinates in mm for first line\n secondLineMM: { start: vec3; end: vec3 } // World coordinates in mm for second line\n angle: number // Angle in degrees\n sliceIndex: number\n sliceType: SLICE_TYPE\n slicePosition: number\n}\n\n/**\n * Slice Type\n * @ignore\n */\nexport enum SLICE_TYPE {\n AXIAL = 0,\n CORONAL = 1,\n SAGITTAL = 2,\n MULTIPLANAR = 3,\n RENDER = 4\n}\n\nexport enum PEN_TYPE {\n PEN = 0,\n RECTANGLE = 1,\n ELLIPSE = 2\n}\n\nexport enum SHOW_RENDER {\n NEVER = 0,\n ALWAYS = 1,\n AUTO = 2\n}\n\n/**\n * Multi-planar layout\n * @ignore\n */\nexport enum MULTIPLANAR_TYPE {\n AUTO = 0,\n COLUMN = 1,\n GRID = 2,\n ROW = 3\n}\n\n/**\n * Drag mode\n * @ignore\n */\nexport enum DRAG_MODE {\n none = 0,\n contrast = 1,\n measurement = 2,\n pan = 3,\n slicer3D = 4,\n callbackOnly = 5,\n roiSelection = 6,\n angle = 7,\n crosshair = 8,\n windowing = 9\n}\n\nexport interface MouseEventConfig {\n leftButton: {\n primary: DRAG_MODE\n withShift?: DRAG_MODE\n withCtrl?: DRAG_MODE\n }\n rightButton: DRAG_MODE\n centerButton: DRAG_MODE\n}\n\nexport interface TouchEventConfig {\n singleTouch: DRAG_MODE\n doubleTouch: DRAG_MODE\n}\n\n/**\n * NVConfigOptions\n */\nexport type NVConfigOptions = {\n // ... (kept unchanged for brevity — same as your original file)\n textHeight: number\n fontSizeScaling: number\n fontMinPx: number\n colorbarHeight: number\n colorbarWidth: number\n showColorbarBorder: boolean\n crosshairWidth: number\n crosshairWidthUnit: 'voxels' | 'mm' | 'percent'\n crosshairGap: number\n rulerWidth: number\n show3Dcrosshair: boolean\n backColor: number[]\n crosshairColor: number[]\n fontColor: Float32List\n selectionBoxColor: number[]\n clipPlaneColor: number[]\n isClipPlanesCutaway: boolean\n paqdUniforms: number[]\n rulerColor: number[]\n colorbarMargin: number\n trustCalMinMax: boolean\n clipPlaneHotKey: string\n cycleClipPlaneHotKey: string\n viewModeHotKey: string\n doubleTouchTimeout: number\n longTouchTimeout: number\n keyDebounceTime: number\n isNearestInterpolation: boolean\n atlasOutline: number\n atlasActiveIndex: number\n isRuler: boolean\n isColorbar: boolean\n isOrientCube: boolean\n tileMargin: number\n multiplanarPadPixels: number\n multiplanarForceRender: boolean\n multiplanarEqualSize: boolean\n multiplanarShowRender: SHOW_RENDER\n isRadiologicalConvention: boolean\n meshThicknessOn2D: number | string\n dragMode: DRAG_MODE\n dragModePrimary: DRAG_MODE\n mouseEventConfig?: MouseEventConfig\n touchEventConfig?: TouchEventConfig\n yoke3Dto2DZoom: boolean\n isDepthPickMesh: boolean\n isCornerOrientationText: boolean\n isOrientationTextVisible: boolean\n showAllOrientationMarkers: boolean\n heroImageFraction: number\n heroSliceType: SLICE_TYPE\n sagittalNoseLeft: boolean\n isSliceMM: boolean\n isV1SliceShader: boolean\n forceDevicePixelRatio: number\n logLevel: 'debug' | 'info' | 'warn' | 'error' | 'fatal' | 'silent'\n loadingText: string\n isForceMouseClickToVoxelCenters: boolean\n dragAndDropEnabled: boolean\n drawingEnabled: boolean\n penValue: number\n penType: PEN_TYPE\n floodFillNeighbors: number\n isFilledPen: boolean\n thumbnail: string\n maxDrawUndoBitmaps: number\n sliceType: SLICE_TYPE\n isAntiAlias: boolean | null\n isAdditiveBlend: boolean\n isResizeCanvas: boolean\n meshXRay: number\n limitFrames4D: number\n showLegend: boolean\n legendBackgroundColor: number[]\n legendTextColor: number[]\n multiplanarLayout: MULTIPLANAR_TYPE\n renderOverlayBlend: number\n sliceMosaicString: string\n centerMosaic: boolean\n interactive: boolean\n penSize: number\n clickToSegment: boolean\n clickToSegmentRadius: number\n clickToSegmentBright: boolean\n clickToSegmentAutoIntensity: boolean\n clickToSegmentIntensityMax: number\n clickToSegmentIntensityMin: number\n clickToSegmentPercent: number\n clickToSegmentMaxDistanceMM: number\n clickToSegmentIs2D: boolean\n selectionBoxLineThickness: number\n selectionBoxIsOutline: boolean\n scrollRequiresFocus: boolean\n showMeasureUnits: boolean\n measureTextJustify: 'start' | 'center' | 'end'\n measureTextColor: number[]\n measureLineColor: number[]\n measureTextHeight: number\n isAlphaClipDark: boolean\n gradientOrder: number\n gradientOpacity: number\n renderSilhouette: number\n gradientAmount: number\n invertScrollDirection: boolean\n is2DSliceShader: boolean\n bounds: [[number, number], [number, number]] | null\n showBoundsBorder?: boolean\n boundsBorderColor?: number[] // [r,g,b,a]\n // Zarr options\n /** Chunk cache size for zarr viewing (default 500) */\n zarrCacheSize: number\n /** Number of chunk rings to prefetch around the visible region for zarr viewing (0 disables, default 1) */\n zarrPrefetchRings: number\n}\n\nexport const DEFAULT_OPTIONS: NVConfigOptions = {\n // ... (same defaults as your original file)\n textHeight: -1.0,\n fontSizeScaling: 0.4,\n fontMinPx: 13,\n colorbarHeight: 0.05,\n colorbarWidth: -1,\n showColorbarBorder: true,\n crosshairWidth: 1,\n crosshairWidthUnit: 'voxels',\n crosshairGap: 0,\n rulerWidth: 4,\n show3Dcrosshair: false,\n backColor: [0, 0, 0, 1],\n crosshairColor: [1, 0, 0, 1],\n fontColor: [0.5, 0.5, 0.5, 1],\n selectionBoxColor: [1, 1, 1, 0.5],\n clipPlaneColor: [0.7, 0, 0.7, 0.5],\n isClipPlanesCutaway: false,\n paqdUniforms: [0.3, 0.5, 0.5, 1.0],\n rulerColor: [1, 0, 0, 0.8],\n colorbarMargin: 0.05,\n trustCalMinMax: true,\n clipPlaneHotKey: 'KeyC',\n cycleClipPlaneHotKey: 'KeyP',\n viewModeHotKey: 'KeyV',\n doubleTouchTimeout: 500,\n longTouchTimeout: 1000,\n keyDebounceTime: 50,\n isNearestInterpolation: false,\n isResizeCanvas: true,\n atlasOutline: 0,\n atlasActiveIndex: 0,\n isRuler: false,\n isColorbar: false,\n isOrientCube: false,\n tileMargin: 0,\n multiplanarPadPixels: 0,\n multiplanarForceRender: false,\n multiplanarEqualSize: false,\n multiplanarShowRender: SHOW_RENDER.AUTO,\n isRadiologicalConvention: false,\n meshThicknessOn2D: Infinity,\n dragMode: DRAG_MODE.contrast,\n dragModePrimary: DRAG_MODE.crosshair,\n mouseEventConfig: undefined,\n touchEventConfig: undefined,\n yoke3Dto2DZoom: false,\n isDepthPickMesh: false,\n isCornerOrientationText: false,\n isOrientationTextVisible: true,\n showAllOrientationMarkers: false,\n heroImageFraction: 0,\n heroSliceType: SLICE_TYPE.RENDER,\n sagittalNoseLeft: false,\n isSliceMM: false,\n isV1SliceShader: false,\n forceDevicePixelRatio: 0,\n logLevel: 'info',\n loadingText: 'loading ...',\n isForceMouseClickToVoxelCenters: false,\n dragAndDropEnabled: true,\n drawingEnabled: false,\n penValue: 1,\n penType: PEN_TYPE.PEN,\n floodFillNeighbors: 6,\n isFilledPen: false,\n thumbnail: '',\n maxDrawUndoBitmaps: 8,\n sliceType: SLICE_TYPE.MULTIPLANAR,\n meshXRay: 0.0,\n isAntiAlias: null,\n limitFrames4D: NaN,\n isAdditiveBlend: false,\n showLegend: true,\n legendBackgroundColor: [0.3, 0.3, 0.3, 0.5],\n legendTextColor: [1.0, 1.0, 1.0, 1.0],\n multiplanarLayout: MULTIPLANAR_TYPE.AUTO,\n renderOverlayBlend: 1.0,\n sliceMosaicString: '',\n centerMosaic: false,\n penSize: 1,\n interactive: true,\n clickToSegment: false,\n clickToSegmentRadius: 3,\n clickToSegmentBright: true,\n clickToSegmentAutoIntensity: false,\n clickToSegmentIntensityMax: NaN,\n clickToSegmentIntensityMin: NaN,\n clickToSegmentPercent: 0,\n clickToSegmentMaxDistanceMM: Number.POSITIVE_INFINITY,\n clickToSegmentIs2D: false,\n selectionBoxLineThickness: 4,\n selectionBoxIsOutline: false,\n scrollRequiresFocus: false,\n showMeasureUnits: true,\n measureTextJustify: 'center',\n measureTextColor: [1, 0, 0, 1],\n measureLineColor: [1, 0, 0, 1],\n measureTextHeight: 0.06,\n isAlphaClipDark: false,\n gradientOrder: 1,\n gradientOpacity: 0.0,\n renderSilhouette: 0.0,\n gradientAmount: 0.0,\n invertScrollDirection: false,\n is2DSliceShader: false,\n bounds: null,\n showBoundsBorder: false,\n boundsBorderColor: [1, 1, 1, 1], // white border by default\n // Zarr options\n zarrCacheSize: 1000,\n zarrPrefetchRings: 10\n}\n\n//\n// -- NEW: Recursive encoded type for NVConfigOptions JSON-safe form\n//\ntype EncodeNumbersIn<T> = T extends number ? number | string : T extends Array<infer U> ? Array<EncodeNumbersIn<U>> : T extends object ? { [K in keyof T]: EncodeNumbersIn<T[K]> } : T\n\ntype EncodedNVConfigOptions = EncodeNumbersIn<NVConfigOptions>\n\n//\n// Utility encode/decode helpers\n//\n\nexport const DEFAULT_SCENE_DATA = {} // placeholder if needed elsewhere (kept for completeness)\n\ntype SceneData = {\n gamma: number\n azimuth: number\n elevation: number\n crosshairPos: vec3\n clipPlanes: number[][]\n clipPlaneDepthAziElevs: number[][]\n volScaleMultiplier: number\n pan2Dxyzmm: vec4\n}\n\nexport const INITIAL_SCENE_DATA = {\n gamma: 1.0,\n azimuth: 110,\n elevation: 10,\n crosshairPos: vec3.fromValues(0.5, 0.5, 0.5),\n clipPlanes: [[0, 0, 0, 0]],\n clipPlaneDepthAziElevs: [[2, 0, 0]],\n volScaleMultiplier: 1.0,\n pan2Dxyzmm: vec4.fromValues(0, 0, 0, 1)\n}\n\nexport type Scene = {\n onAzimuthElevationChange: (azimuth: number, elevation: number) => void\n onZoom3DChange: (scale: number) => void\n sceneData: SceneData\n renderAzimuth: number\n renderElevation: number\n volScaleMultiplier: number\n crosshairPos: vec3\n clipPlane: number[]\n clipPlanes: number[][]\n clipPlaneDepthAziElevs: number[][]\n pan2Dxyzmm: vec4\n _elevation?: number\n _azimuth?: number\n gamma?: number\n}\n\n/**\n * DocumentData / ExportDocumentData types (kept minimal here)\n */\nexport type DocumentData = {\n title?: string\n imageOptionsArray?: ImageFromUrlOptions[]\n meshOptionsArray?: unknown[]\n opts?: Partial<EncodedNVConfigOptions> | Partial<NVConfigOptions>\n previewImageDataURL?: string\n labels?: NVLabel3D[]\n encodedImageBlobs?: string[]\n encodedDrawingBlob?: string\n meshesString?: string\n sceneData?: Partial<SceneData>\n connectomes?: string[]\n customData?: string\n completedMeasurements?: CompletedMeasurement[]\n completedAngles?: CompletedAngle[]\n}\n\nexport type ExportDocumentData = {\n title?: string\n encodedImageBlobs: string[]\n encodedDrawingBlob: string\n previewImageDataURL: string\n imageOptionsMap: Map<string, number>\n imageOptionsArray: ImageFromUrlOptions[]\n sceneData: Partial<SceneData>\n opts: EncodedNVConfigOptions | Partial<EncodedNVConfigOptions>\n meshesString: string\n meshOptionsArray?: unknown[]\n labels: NVLabel3D[]\n connectomes: string[]\n customData: string\n completedMeasurements: CompletedMeasurement[]\n completedAngles: CompletedAngle[]\n}\n\n/**\n * Returns a partial configuration object containing only the fields in the provided\n * options that differ from the DEFAULT_OPTIONS.\n */\n// function diffOptions(opts: NVConfigOptions, defaults: NVConfigOptions): Partial<NVConfigOptions> {\n// const diff: Partial<NVConfigOptions> = {}\n// for (const key in opts) {\n// const value = opts[key]\n// const def = defaults[key]\n// const isArray = Array.isArray(value) && Array.isArray(def)\n\n// if ((isArray && value.some((v, i) => v !== def[i])) || (!isArray && value !== def)) {\n// diff[key] = value\n// }\n// }\n// return diff\n// }\n\n/**\n * NVDocument class (main)\n */\nexport class NVDocument {\n data: DocumentData = {\n title: 'Untitled document',\n imageOptionsArray: [],\n meshOptionsArray: [],\n opts: { ...DEFAULT_OPTIONS } as any,\n previewImageDataURL: '',\n labels: [],\n encodedImageBlobs: [],\n encodedDrawingBlob: ''\n }\n\n scene: Scene\n\n volumes: NVImage[] = []\n meshDataObjects?: Array<NVMesh | NVConnectome>\n meshes: Array<NVMesh | NVConnectome> = []\n drawBitmap: Uint8Array | null = null\n imageOptionsMap = new Map()\n meshOptionsMap = new Map()\n completedMeasurements: CompletedMeasurement[] = []\n completedAngles: CompletedAngle[] = []\n\n private _optsProxy: NVConfigOptions | null = null\n private _optsChangeCallback: ((propertyName: keyof NVConfigOptions, newValue: NVConfigOptions[keyof NVConfigOptions], oldValue: NVConfigOptions[keyof NVConfigOptions]) => void) | null = null\n\n constructor() {\n this.scene = {\n onAzimuthElevationChange: (): void => {},\n onZoom3DChange: (): void => {},\n sceneData: {\n ...INITIAL_SCENE_DATA,\n pan2Dxyzmm: vec4.fromValues(0, 0, 0, 1),\n crosshairPos: vec3.fromValues(0.5, 0.5, 0.5)\n },\n\n get renderAzimuth(): number {\n return this.sceneData.azimuth\n },\n set renderAzimuth(azimuth: number) {\n this.sceneData.azimuth = azimuth\n if (this.onAzimuthElevationChange) {\n this.onAzimuthElevationChange(this.sceneData.azimuth, this.sceneData.elevation)\n }\n },\n\n get renderElevation(): number {\n return this.sceneData.elevation\n },\n set renderElevation(elevation: number) {\n this.sceneData.elevation = elevation\n if (this.onAzimuthElevationChange) {\n this.onAzimuthElevationChange(this.sceneData.azimuth, this.sceneData.elevation)\n }\n },\n\n get volScaleMultiplier(): number {\n return this.sceneData.volScaleMultiplier\n },\n set volScaleMultiplier(scale: number) {\n this.sceneData.volScaleMultiplier = scale\n this.onZoom3DChange(scale)\n },\n\n get crosshairPos(): vec3 {\n return this.sceneData.crosshairPos\n },\n set crosshairPos(crosshairPos: vec3) {\n this.sceneData.crosshairPos = crosshairPos\n },\n\n get clipPlane(): number[] {\n return this.sceneData.clipPlanes[0] ?? []\n },\n set clipPlane(clipPlane) {\n this.sceneData.clipPlanes[0] = clipPlane\n },\n\n get clipPlanes(): number[][] {\n return this.sceneData.clipPlanes\n },\n set clipPlanes(planes: number[][]) {\n this.sceneData.clipPlanes = planes\n },\n\n get clipPlaneDepthAziElevs(): number[][] {\n return this.sceneData.clipPlaneDepthAziElevs\n },\n set clipPlaneDepthAziElevs(values: number[][]) {\n this.sceneData.clipPlaneDepthAziElevs = values\n },\n\n get pan2Dxyzmm(): vec4 {\n return this.sceneData.pan2Dxyzmm\n },\n\n set pan2Dxyzmm(pan2Dxyzmm) {\n this.sceneData.pan2Dxyzmm = pan2Dxyzmm\n },\n\n get gamma(): number {\n return this.sceneData.gamma\n },\n\n set gamma(newGamma) {\n this.sceneData.gamma = newGamma\n }\n }\n }\n\n /**\n * Title of the document\n */\n get title(): string {\n return this.data.title\n }\n\n /**\n * Gets preview image blob\n * @returns dataURL of preview image\n */\n get previewImageDataURL(): string {\n return this.data.previewImageDataURL\n }\n\n /**\n * Sets preview image blob\n * @param dataURL - encoded preview image\n */\n set previewImageDataURL(dataURL: string) {\n this.data.previewImageDataURL = dataURL\n }\n\n /**\n * @param title - title of document\n */\n set title(title: string) {\n this.data.title = title\n }\n\n get imageOptionsArray(): ImageFromUrlOptions[] {\n return this.data.imageOptionsArray\n }\n\n /**\n * Gets the base 64 encoded blobs of associated images\n */\n get encodedImageBlobs(): string[] {\n return this.data.encodedImageBlobs\n }\n\n /**\n * Gets the base 64 encoded blob of the associated drawing\n */\n get encodedDrawingBlob(): string {\n return this.data.encodedDrawingBlob\n }\n\n /**\n * Gets the options of the {@link Niivue} instance\n */\n get opts(): NVConfigOptions {\n if (!this._optsProxy) {\n this._createOptsProxy()\n }\n return this._optsProxy as NVConfigOptions\n }\n\n /**\n * Sets the options of the {@link Niivue} instance\n */\n set opts(opts) {\n this.data.opts = { ...opts } as any\n this._optsProxy = null // Force recreation of proxy\n }\n\n /**\n * Gets the 3D labels of the {@link Niivue} instance\n */\n get labels(): NVLabel3D[] {\n return this.data.labels\n }\n\n /**\n * Sets the 3D labels of the {@link Niivue} instance\n */\n set labels(labels: NVLabel3D[]) {\n this.data.labels = labels\n }\n\n get customData(): string | undefined {\n return this.data.customData\n }\n\n set customData(data: string) {\n this.data.customData = data\n }\n\n /**\n * Checks if document has an image by id\n */\n hasImage(image: NVImage): boolean {\n return this.volumes.find((i) => i.id === image.id) !== undefined\n }\n\n /**\n * Checks if document has an image by url\n */\n hasImageFromUrl(url: string): boolean {\n return this.data.imageOptionsArray.find((i) => i.url === url) !== undefined\n }\n\n /**\n * Adds an image and the options an image was created with\n */\n addImageOptions(image: NVImage, imageOptions: ImageFromUrlOptions): void {\n if (!this.hasImage(image)) {\n if (!imageOptions.name) {\n if (imageOptions.url) {\n const absoluteUrlRE = /^(?:[a-z+]+:)?\\/\\//i\n const url = absoluteUrlRE.test(imageOptions.url) ? new URL(imageOptions.url) : new URL(imageOptions.url, window.location.href)\n\n imageOptions.name = url.pathname.split('/').pop()! // TODO guaranteed?\n if (imageOptions.name.toLowerCase().endsWith('.gz')) {\n imageOptions.name = imageOptions.name.slice(0, -3)\n }\n\n if (!imageOptions.name.toLowerCase().endsWith('.nii')) {\n imageOptions.name += '.nii'\n }\n } else {\n imageOptions.name = 'untitled.nii'\n }\n }\n }\n\n imageOptions.imageType = NVIMAGE_TYPE.NII\n\n this.data.imageOptionsArray.push(imageOptions)\n this.imageOptionsMap.set(image.id, this.data.imageOptionsArray.length - 1)\n }\n\n /**\n * Removes image from the document as well as its options\n */\n removeImage(image: NVImage): void {\n if (this.imageOptionsMap.has(image.id)) {\n const index = this.imageOptionsMap.get(image.id)\n if (this.data.imageOptionsArray.length > index) {\n this.data.imageOptionsArray.splice(index, 1)\n }\n this.imageOptionsMap.delete(image.id)\n }\n this.volumes = this.volumes.filter((i) => i.id !== image.id)\n }\n\n /**\n * Fetch any image data that is missing from this document.\n */\n async fetchLinkedData(): Promise<void> {\n this.data.encodedImageBlobs = []\n if (!this.imageOptionsArray?.length) {\n return\n }\n\n for (const imgOpt of this.imageOptionsArray) {\n if (!imgOpt.url) {\n continue\n }\n\n try {\n const response = await fetch(imgOpt.url)\n if (!response.ok) {\n console.warn('Failed to fetch image:', imgOpt.url)\n continue\n }\n\n const buffer = await response.arrayBuffer()\n const uint8Array = new Uint8Array(buffer)\n const b64 = NVUtilities.uint8tob64(uint8Array)\n this.data.encodedImageBlobs.push(b64)\n\n console.info('fetch linked data fetched from ', imgOpt.url)\n } catch (err) {\n console.warn(`Failed to fetch/encode image from ${imgOpt.url}:`, err)\n }\n }\n }\n\n /**\n * Returns the options for the image if it was added by url\n */\n getImageOptions(image: NVImage): ImageFromUrlOptions | null {\n return this.imageOptionsMap.has(image.id) ? this.data.imageOptionsArray[this.imageOptionsMap.get(image.id)] : null\n }\n\n /**\n * Serialise the document by delegating to NVSerializer.\n */\n json(embedImages = true, embedDrawing = true): ExportDocumentData {\n // NVSerializer is responsible for converting typed arrays, encoding special numbers,\n // producing meshesString, and returning an ExportDocumentData object.\n return NVSerializer.serializeDocument(this, embedImages, embedDrawing)\n }\n\n async download(fileName: string, compress: boolean, opts: { embedImages: boolean } = { embedImages: true }): Promise<void> {\n const data = this.json(opts.embedImages)\n const jsonTxt = JSON.stringify(data)\n const mime = compress ? 'application/gzip' : 'application/json'\n const payload = compress ? await NVUtilities.compressStringToArrayBuffer(jsonTxt) : jsonTxt\n\n NVUtilities.download(payload, fileName, mime)\n }\n\n /**\n * Factory method to return an instance of NVDocument from a URL\n */\n static async loadFromUrl(url: string): Promise<NVDocument> {\n const response = await fetch(url)\n const buffer = await response.arrayBuffer()\n let documentData: DocumentData\n\n if (NVUtilities.isArrayBufferCompressed(buffer)) {\n const documentText = await NVUtilities.decompressArrayBuffer(buffer)\n documentData = JSON.parse(documentText)\n } else {\n const utf8decoder = new TextDecoder()\n documentData = JSON.parse(utf8decoder.decode(buffer))\n }\n\n return NVDocument.loadFromJSON(documentData)\n }\n\n static async loadFromFile(file: Blob): Promise<NVDocument> {\n const arrayBuffer = await NVUtilities.readFileAsync(file)\n let dataString: string\n\n if (NVUtilities.isArrayBufferCompressed(arrayBuffer)) {\n dataString = await NVUtilities.decompressArrayBuffer(arrayBuffer)\n } else {\n const utf8decoder = new TextDecoder()\n dataString = utf8decoder.decode(arrayBuffer)\n }\n\n const documentData = JSON.parse(dataString) as DocumentData\n return NVDocument.loadFromJSON(documentData)\n }\n\n /**\n * Factory method to return an instance of NVDocument from JSON.\n * Delegates the main parsing to NVSerializer, then applies NVDocument-specific\n * post-processing (opts decode, scene defaults, clone measurements/angles).\n */\n static async loadFromJSON(data: DocumentData): Promise<NVDocument> {\n return await NVSerializer.deserializeDocument(data)\n }\n\n /**\n * Sets the callback function to be called when opts properties change\n */\n setOptsChangeCallback(callback: (propertyName: keyof NVConfigOptions, newValue: NVConfigOptions[keyof NVConfigOptions], oldValue: NVConfigOptions[keyof NVConfigOptions]) => void): void {\n this._optsChangeCallback = callback\n this._optsProxy = null // Force recreation with new callback\n }\n\n /**\n * Removes the opts change callback\n */\n removeOptsChangeCallback(): void {\n this._optsChangeCallback = null\n this._optsProxy = null // Force recreation without callback\n }\n\n /**\n * Creates a Proxy wrapper around the opts object to detect changes\n */\n private _createOptsProxy(): void {\n const target = this.data.opts as NVConfigOptions\n\n this._optsProxy = new Proxy(target, {\n set: (obj: any, prop: string | symbol, value: any): boolean => {\n const oldValue = obj[prop]\n\n // Only proceed if the value actually changed\n if (oldValue !== value) {\n obj[prop] = value\n\n // Call the change callback if one is registered\n if (this._optsChangeCallback && typeof prop === 'string' && prop in DEFAULT_OPTIONS) {\n this._optsChangeCallback(prop as keyof NVConfigOptions, value, oldValue)\n }\n }\n\n return true\n },\n\n get: (obj: any, prop: string | symbol): any => {\n return obj[prop]\n }\n })\n }\n}\n","// src/nvserializer.ts\nimport { vec3 } from 'gl-matrix'\nimport { NVUtilities } from '@/nvutilities'\nimport { NVImage, NVIMAGE_TYPE } from '@/nvimage'\nimport { NVMesh, MeshType } from '@/nvmesh'\nimport { NVConnectome } from '@/nvconnectome'\nimport { log } from '@/logger'\nimport { NVDocument, DEFAULT_OPTIONS, DocumentData, ExportDocumentData, INITIAL_SCENE_DATA } from '@/nvdocument'\nimport { migrateLegacyDocument, normalizeMeshForRehydrate } from '@/utils/legacy-migrate' // shared migration utility\n\n/** Helpers for special numeric encoding */\nfunction encodeNumberForJSON(v: number | null | undefined): number | string | undefined {\n if (v === undefined) {\n return undefined\n }\n if (v === null) {\n return null\n }\n if (Number.isFinite(v)) {\n return v\n }\n if (v === Infinity) {\n return 'infinity'\n }\n if (v === -Infinity) {\n return '-infinity'\n }\n return 'NaN'\n}\nfunction decodeNumberFromJSON(v: any): number | null | undefined {\n if (v === undefined) {\n return undefined\n }\n if (v === null) {\n return null\n }\n if (typeof v === 'number') {\n return v\n }\n if (v === 'NaN') {\n return NaN\n }\n if (v === 'infinity') {\n return Infinity\n }\n if (v === '-infinity') {\n return -Infinity\n }\n const n = Number(v)\n return Number.isNaN(n) ? NaN : n\n}\n\n/** Small safe conversion helper that handles arrays, typed arrays, iterables. */\nfunction toPlainArray<T>(maybe: any): T[] | undefined {\n if (maybe === undefined || maybe === null) {\n return undefined\n }\n if (Array.isArray(maybe)) {\n return maybe.slice()\n }\n try {\n // cast via unknown to satisfy TS when given typed arrays / arraybufferviews\n return Array.from(maybe as unknown as Iterable<T>)\n } catch (e) {\n return undefined\n }\n}\n\n/**\n * Convert array-like to Float32Array.\n * Returns undefined if input is null/undefined.\n */\nfunction toFloat32Array(maybe: any): Float32Array | undefined {\n if (maybe === undefined || maybe === null) {\n return undefined\n }\n if (maybe instanceof Float32Array) {\n return maybe\n }\n if (Array.isArray(maybe)) {\n return new Float32Array(maybe)\n }\n if (ArrayBuffer.isView(maybe)) {\n return new Float32Array(Array.from(maybe as unknown as Iterable<number>))\n }\n try {\n return new Float32Array(Array.from(maybe as unknown as Iterable<number>))\n } catch (e) {\n return undefined\n }\n}\n\n/**\n * Convert array-like to Uint32Array.\n * Returns undefined if input is null/undefined.\n */\nfunction toUint32Array(maybe: any): Uint32Array | undefined {\n if (maybe === undefined || maybe === null) {\n return undefined\n }\n if (maybe instanceof Uint32Array) {\n return maybe\n }\n if (Array.isArray(maybe)) {\n return new Uint32Array(maybe)\n }\n if (ArrayBuffer.isView(maybe)) {\n return new Uint32Array(Array.from(maybe as unknown as Iterable<number>))\n }\n try {\n return new Uint32Array(Array.from(maybe as unknown as Iterable<number>))\n } catch (e) {\n return undefined\n }\n}\n\n/**\n * Convert array-like to Uint8Array.\n * Returns undefined if input is null/undefined.\n */\nfunction toUint8Array(maybe: any): Uint8Array | undefined {\n if (maybe === undefined || maybe === null) {\n return undefined\n }\n if (maybe instanceof Uint8Array) {\n return maybe\n }\n if (Array.isArray(maybe)) {\n return new Uint8Array(maybe)\n }\n if (ArrayBuffer.isView(maybe)) {\n return new Uint8Array(Array.from(maybe as unknown as Iterable<number>))\n }\n try {\n return new Uint8Array(Array.from(maybe as unknown as Iterable<number>))\n } catch (e) {\n return undefined\n }\n}\n\n/** replacer to convert typed arrays / array buffers to plain arrays for JSON.stringify */\nfunction jsonReplacer(_key: string, value: any): any {\n if (typeof value === 'function') {\n return undefined\n }\n if (typeof value === 'symbol') {\n return undefined\n }\n\n // Typed arrays & DataView -> array of numbers\n if (ArrayBuffer.isView(value)) {\n if (value instanceof DataView) {\n return Array.from(new Uint8Array((value as DataView).buffer, (value as DataView).byteOffset, (value as DataView).byteLength))\n }\n // cast through unknown to avoid TS complaining about missing iterator\n return Array.from(value as unknown as Iterable<number>)\n }\n\n if (value instanceof ArrayBuffer) {\n return Array.from(new Uint8Array(value))\n }\n\n return value\n}\n\nexport class NVSerializer {\n /**\n * Decode/normalize \"opts\" encoded form into NVConfigOptions shape.\n * Move the decodeOptsFromJSON logic here (serializer owns encoding/decoding).\n */\n static decodeOptsFromJSON(opts: any): any {\n if (opts === undefined || opts === null) {\n return undefined\n }\n\n const decodeRecursive = (v: any): any => {\n if (v === undefined) {\n return undefined\n }\n if (v === null) {\n return null\n }\n if (typeof v === 'number') {\n return v\n }\n if (typeof v === 'string') {\n // try to decode special numeric strings\n if (v === 'NaN') {\n return NaN\n }\n if (v === 'infinity') {\n return Infinity\n }\n if (v === '-infinity') {\n return -Infinity\n }\n return v\n }\n if (Array.isArray(v)) {\n return v.map((el) => decodeRecursive(el))\n }\n if (typeof v === 'object') {\n const out: any = {}\n for (const k of Object.keys(v)) {\n out[k] = decodeRecursive(v[k])\n }\n return out\n }\n return v\n }\n\n const out: any = {}\n for (const k of Object.keys(opts)) {\n const val = opts[k]\n if (val === undefined) {\n continue\n }\n out[k] = decodeRecursive(val)\n }\n return out\n }\n\n /**\n * Serialize an NVDocument-like object into ExportDocumentData (same shape as NVDocument.json)\n * (This function mirrors the responsibilities of NVDocument.json in your code).\n */\n static serializeDocument(document: any, embedImages = true, embedDrawing = true): ExportDocumentData {\n const out: Partial<ExportDocumentData> = {\n encodedImageBlobs: [],\n previewImageDataURL: document.previewImageDataURL ?? '',\n imageOptionsMap: new Map<string, number>()\n }\n\n out.sceneData = { ...(document.scene?.sceneData ?? {}) }\n out.opts = document.opts ?? {}\n out.labels = Array.isArray(document.labels)\n ? document.labels.map((l: any) => {\n const copy = { ...(l || {}) }\n delete copy.onClick\n return copy\n })\n : []\n\n out.customData = document.customData ?? ''\n out.completedMeasurements = document.completedMeasurements ? document.completedMeasurements.slice() : []\n out.completedAngles = document.completedAngles ? document.completedAngles.slice() : []\n\n // images\n const imageOptionsArray: any[] = []\n const volumes = document.volumes ?? []\n if (volumes.length) {\n for (let i = 0; i < volumes.length; i++) {\n const v = volumes[i]\n let imageOptions = document.getImageOptions ? document.getImageOptions(v) : null\n if (imageOptions === null) {\n // fallback minimal options\n imageOptions = {\n name: v?.name ?? '',\n colormap: v?._colormap ?? 'gray',\n opacity: v?._opacity ?? 1.0,\n cal_min: v?.cal_min ?? NaN,\n cal_max: v?.cal_max ?? NaN,\n trustCalMinMax: v?.trustCalMinMax ?? true,\n percentileFrac: v?.percentileFrac ?? 0.02,\n ignoreZeroVoxels: v?.ignoreZeroVoxels ?? false,\n imageType: v?.imageType ?? NVIMAGE_TYPE.NII,\n frame4D: v?.frame4D ?? 0,\n limitFrames4D: v?.limitFrames4D ?? NaN,\n url: v?.url ?? '',\n urlImageData: v?.urlImgData ?? '',\n cal_minNeg: v?.cal_minNeg ?? NaN,\n cal_maxNeg: v?.cal_maxNeg ?? NaN,\n colorbarVisible: v?.colorbarVisible ?? true\n }\n } else {\n if (!('imageType' in imageOptions)) {\n imageOptions.imageType = NVIMAGE_TYPE.NII\n }\n }\n\n imageOptions.colormap = v.colormap\n imageOptions.colormapLabel = v.colormapLabel\n imageOptions.opacity = v.opacity\n imageOptions.cal_max = v.cal_max ?? NaN\n imageOptions.cal_min = v.cal_min ?? NaN\n\n imageOptionsArray.push(imageOptions)\n\n if (embedImages) {\n const blob = NVUtilities.uint8tob64(v.toUint8Array())\n out.encodedImageBlobs!.push(blob)\n }\n out.imageOptionsMap!.set(v.id, i)\n }\n }\n out.imageOptionsArray = imageOptionsArray\n\n // meshes: build safe clones and convert typed arrays to plain arrays\n const meshesForExport: any[] = []\n const meshes = document.meshes || []\n out.connectomes = []\n for (const mesh of meshes) {\n if (!mesh || typeof mesh !== 'object') {\n continue\n }\n if (mesh.type === MeshType.CONNECTOME) {\n try {\n out.connectomes!.push(JSON.stringify((mesh as NVConnectome).json()))\n } catch (e) {\n log.warn('serializeDocument: failed connectome export', e)\n }\n continue\n }\n\n // build layers\n const layersForExport = (mesh.layers || []).map((layer: any) => {\n const exported: any = {\n name: layer?.name,\n key: layer?.key,\n url: layer?.url,\n headers: layer?.headers,\n opacity: layer?.opacity,\n colormap: layer?.colorMap ?? layer?.colormap,\n colormapNegative: layer?.colorMapNegative ?? layer?.colormapNegative,\n colormapInvert: layer?.colormapInvert,\n colormapLabel: layer?.colormapLabel,\n useNegativeCmap: layer?.useNegativeCmap,\n // numeric meta\n global_min: encodeNumberForJSON(layer?.global_min),\n global_max: encodeNumberForJSON(layer?.global_max),\n cal_min: encodeNumberForJSON(layer?.cal_min),\n cal_max: encodeNumberForJSON(layer?.cal_max),\n cal_minNeg: encodeNumberForJSON(layer?.cal_minNeg),\n cal_maxNeg: encodeNumberForJSON(layer?.cal_maxNeg),\n isAdditiveBlend: layer?.isAdditiveBlend,\n frame4D: layer?.frame4D,\n nFrame4D: layer?.nFrame4D,\n outlineBorder: layer?.outlineBorder,\n isTransparentBelowCalMin: layer?.isTransparentBelowCalMin,\n colormapType: layer?.colormapType,\n base64: layer?.base64,\n colorbarVisible: layer?.colorbarVisible,\n showLegend: layer?.showLegend\n }\n\n if (layer?.values != null) {\n const vals = Array.isArray(layer.values) ? layer.values.slice() : Array.from((layer.values as unknown as Iterable<number>) || [])\n exported.values = vals.map((v: any) => {\n const num = Number(v)\n return Number.isFinite(num) ? Math.round(num * 1e12) / 1e12 : num\n })\n }\n if (layer?.atlasValues != null) {\n exported.atlasValues = Array.isArray(layer.atlasValues) ? layer.atlasValues.slice() : Array.from((layer.atlasValues as unknown as Iterable<number>) || [])\n }\n if (Array.isArray(layer?.labels)) {\n exported.labels = layer.labels.map((l: any) => ({ ...(l || {}) }))\n } else if (layer?.labels != null) {\n exported.labels = Array.from((layer.labels as unknown as Iterable<any>) || [])\n }\n return exported\n })\n\n const meshCopy: any = {\n ...Object.assign({}, mesh || {}),\n rgba255: Array.from(mesh.rgba255 || []),\n layers: layersForExport,\n edges: Array.isArray(mesh?.edges) ? mesh.edges.map((e: any) => ({ ...(e || {}) })) : [],\n nodes: Array.isArray(mesh?.nodes) ? mesh.nodes.map((n: any) => ({ ...(n || {}) })) : mesh?.nodes,\n offsetPt0: Array.isArray(mesh?.offsetPt0) ? [...mesh.offsetPt0] : mesh?.offsetPt0\n }\n\n // fiber fields\n if (Array.isArray(mesh.offsetPt0) && mesh.offsetPt0.length > 0) {\n meshCopy.offsetPt0 = Array.isArray(mesh.offsetPt0) ? [...mesh.offsetPt0] : mesh.offsetPt0\n if (Array.isArray(mesh.fiberGroupColormap)) {\n meshCopy.fiberGroupColormap = [...mesh.fiberGroupColormap]\n }\n if (Array.isArray(mesh.fiberColor)) {\n meshCopy.fiberColor = Array.from(mesh.fiberColor)\n }\n meshCopy.fiberDither = mesh.fiberDither\n meshCopy.fiberRadius = mesh.fiberRadius\n meshCopy.colormap = mesh.colormap\n }\n\n meshesForExport.push(meshCopy)\n }\n\n try {\n out.meshesString = JSON.stringify(meshesForExport, jsonReplacer)\n } catch (err) {\n log.warn('serializeDocument: JSON.stringify failed for meshes', err)\n // fallback: per-mesh serialization\n const per: string[] = []\n for (let i = 0; i < meshesForExport.length; i++) {\n try {\n per.push(JSON.stringify(meshesForExport[i], jsonReplacer))\n } catch (e) {\n log.error('serializeDocument: failed to serialize mesh at index', i, e, meshesForExport[i])\n per.push(JSON.stringify({ name: meshesForExport[i]?.name ?? 'unknown', id: meshesForExport[i]?.id ?? null }))\n }\n }\n out.meshesString = `[${per.join(',')}]`\n }\n\n if (embedDrawing && document.drawBitmap) {\n out.encodedDrawingBlob = NVUtilities.uint8tob64(document.drawBitmap)\n } else {\n out.encodedDrawingBlob = document.encodedDrawingBlob ?? ''\n }\n\n out.title = document.title ?? 'untitled'\n out.customData = document.customData ?? ''\n out.labels = Array.isArray(document.labels)\n ? document.labels.map((l: any) => {\n const copy = { ...(l || {}) }\n delete copy.onClick\n return copy\n })\n : []\n\n // Export completed measurements & angles (clone vectors/objects so they survive JSON roundtrip)\n out.completedMeasurements = (document.completedMeasurements || []).map((m: any) => ({\n ...m,\n startMM: Array.isArray(m.startMM) ? [...m.startMM] : m.startMM,\n endMM: Array.isArray(m.endMM) ? [...m.endMM] : m.endMM\n }))\n\n out.completedAngles = (document.completedAngles || []).map((a: any) => ({\n ...a,\n firstLineMM: {\n start: Array.isArray(a.firstLineMM?.start) ? [...a.firstLineMM.start] : a.firstLineMM?.start,\n end: Array.isArray(a.firstLineMM?.end) ? [...a.firstLineMM.end] : a.firstLineMM?.end\n },\n secondLineMM: {\n start: Array.isArray(a.secondLineMM?.start) ? [...a.secondLineMM.start] : a.secondLineMM?.start,\n end: Array.isArray(a.secondLineMM?.end) ? [...a.secondLineMM.end] : a.secondLineMM?.end\n }\n }))\n\n // Make sure sceneData and opts are included (you already had these, but double-check)\n out.sceneData = out.sceneData ?? { ...(document.scene?.sceneData ?? {}) }\n out.opts = out.opts ?? (document.opts ? { ...(document.opts as any) } : {})\n\n return out as ExportDocumentData\n }\n\n /**\n * Rehydrate images into NVImage instances using NVImage.new\n */\n static async rehydrateImages(documentData: DocumentData): Promise<NVImage[]> {\n const imgs: NVImage[] = []\n\n // Normalize legacy shapes first\n const migrated = await migrateLegacyDocument(documentData)\n\n const encoded = migrated.encodedImageBlobs ?? []\n const optsArr = migrated.imageOptionsArray ?? []\n\n for (let i = 0; i < optsArr.length; i++) {\n const imageOptions = { ...(optsArr[i] || {}) } as any\n try {\n const b64 = encoded[i]\n if (!b64) {\n log.debug('NVSerializer.rehydrateImages: no blob for index', i)\n continue\n }\n const u8 = await NVUtilities.b64toUint8(b64)\n if (!u8) {\n log.warn('NVSerializer.rehydrateImages: b64toUint8 returned empty for index', i)\n continue\n }\n\n const name = imageOptions.name ?? imageOptions.url ?? `image-${i}`\n // Ensure colormap keys are strings when we call color-table lookups.\n // Legacy data sometimes stores colormaps in typed-array / object shapes —\n // guard against that so colormapFromKey(name) doesn't throw.\n let colormap: string | any[] = imageOptions.colormap ?? imageOptions.colorMap ?? ''\n if (typeof colormap !== 'string') {\n // Prefer the explicitly provided colorMap string if present\n if (typeof imageOptions.colorMap === 'string') {\n colormap = imageOptions.colorMap\n } else {\n // otherwise treat as \"no named colormap\" so colour-table lookup won't be attempted\n colormap = ''\n }\n }\n\n const opacity = imageOptions.opacity ?? 1.0\n\n const pairedImgData = imageOptions.pairedImgData\n ? typeof imageOptions.pairedImgData === 'string'\n ? (await NVUtilities.b64toUint8(imageOptions.pairedImgData)).buffer\n : imageOptions.pairedImgData\n : null\n const cal_min = decodeNumberFromJSON(imageOptions.cal_min)\n const cal_max = decodeNumberFromJSON(imageOptions.cal_max)\n const trustCalMinMax = imageOptions.trustCalMinMax ?? true\n const percentileFrac = imageOptions.percentileFrac ?? 0.02\n const ignoreZeroVoxels = imageOptions.ignoreZeroVoxels ?? false\n const useQFormNotSForm = imageOptions.useQFormNotSForm ?? false\n const colormapNegative = imageOptions.colormapNegative ?? ''\n const frame4D = imageOptions.frame4D ?? 0\n const imageType = 'imageType' in imageOptions && typeof imageOptions.imageType === 'number' ? imageOptions.imageType : (imageOptions.imageType ?? NVIMAGE_TYPE.UNKNOWN)\n const cal_minNeg = decodeNumberFromJSON(imageOptions.cal_minNeg)\n const cal_maxNeg = decodeNumberFromJSON(imageOptions.cal_maxNeg)\n const colorbarVisible = imageOptions.colorbarVisible ?? true\n const colormapLabel = imageOptions.colormapLabel ?? null\n const colormapType = imageOptions.colormapType ?? 0\n const zarrData = imageOptions.zarrData ?? null\n\n // ensure ArrayBuffer argument\n console.log(`Image ${i} buffer length: ${u8.byteLength}`)\n\n // Check if the data is actually all zeros before Niivue gets it\n const isAllZeros = !u8.some((val) => val !== 0)\n if (isAllZeros) {\n console.error(`CRITICAL: Base64 decoding for ${name} resulted in an all-zero array!`)\n }\n\n const bufferArg = u8.slice().buffer\n\n const img = await NVImage.new(\n bufferArg,\n name,\n colormap as string,\n opacity,\n pairedImgData,\n cal_min as any,\n cal_max as any,\n trustCalMinMax,\n percentileFrac,\n ignoreZeroVoxels,\n useQFormNotSForm,\n colormapNegative,\n frame4D,\n imageType,\n cal_minNeg as any,\n cal_maxNeg as any,\n colorbarVisible,\n colormapLabel,\n colormapType,\n zarrData\n )\n imgs.push(img)\n } catch (err) {\n log.warn('NVSerializer.rehydrateImages: failed for index', i, err)\n }\n }\n return imgs\n }\n\n /**\n * Rehydrate meshes into runtime NVMesh instances.\n * If gl is provided and callUpdateMesh=true, updateMesh(gl) will be called.\n *\n * CRITICAL FIX: Properly convert arrays to TypedArrays before passing to NVMesh constructor\n * to avoid \"underspecified mesh\" warnings.\n */\n static async rehydrateMeshes(documentData: DocumentData, gl?: WebGL2RenderingContext, callUpdateMesh = false): Promise<Array<NVMesh | any>> {\n const out: Array<NVMesh | any> = []\n\n // Normalize legacy shapes first\n const migrated = await migrateLegacyDocument(documentData)\n\n const meshesString = migrated.meshesString ?? '[]'\n\n let parsed: any[] = []\n try {\n parsed = JSON.parse(meshesString)\n } catch (e) {\n console.warn('NVSerializer.rehydrateMeshes: failed to parse meshesString', e)\n parsed = []\n }\n\n for (let i = 0; i < parsed.length; i++) {\n let m = parsed[i] || {}\n try {\n m = normalizeMeshForRehydrate(m)\n // Normalize layer keys and numeric encodings (colorMap -> colormap etc.)\n if (Array.isArray(m.layers)) {\n for (const layer of m.layers) {\n if (!layer) {\n continue\n }\n if ('colorMap' in layer && !('colormap' in layer)) {\n layer.colormap = layer.colorMap\n delete layer.colorMap\n }\n if ('colorMapNegative' in layer && !('colormapNegative' in layer)) {\n layer.colormapNegative = layer.colorMapNegative\n delete layer.colorMapNegative\n }\n\n // decode special numeric strings (in case encodeNumberForJSON used)\n layer.global_min = decodeNumberFromJSON(layer.global_min)\n layer.global_max = decodeNumberFromJSON(layer.global_max)\n layer.cal_min = decodeNumberFromJSON(layer.cal_min)\n layer.cal_max = decodeNumberFromJSON(layer.cal_max)\n layer.cal_minNeg = decodeNumberFromJSON(layer.cal_minNeg)\n layer.cal_maxNeg = decodeNumberFromJSON(layer.cal_maxNeg)\n\n // ensure values/atlasValues are plain arrays (migration already attempted to normalize)\n if (layer.values != null && !Array.isArray(layer.values)) {\n try {\n layer.values = Array.from(layer.values)\n } catch (e) {\n /* leave as-is */\n }\n }\n if (layer.atlasValues != null && !Array.isArray(layer.atlasValues)) {\n try {\n layer.atlasValues = Array.from(layer.atlasValues)\n } catch (e) {\n /* leave as-is */\n }\n }\n }\n }\n\n // CRITICAL: Convert arrays to proper TypedArrays BEFORE creating NVMesh\n // This prevents the \"underspecified mesh\" warning in updateMesh\n\n // Convert rgba255 to Uint8Array\n const rgba255 = toUint8Array(m.rgba255) ?? new Uint8Array([255, 255, 255, 255])\n\n // Convert pts to Float32Array (vertex positions)\n const pts = toFloat32Array(m.pts)\n\n // Convert tris to Uint32Array (triangle indices)\n let tris = toUint32Array(m.tris)\n\n // ensure nodes/edges plain arrays\n if (Array.isArray(m.nodes)) {\n m.nodes = m.nodes.length > 0 && typeof m.nodes[0] === 'object' ? m.nodes.map((n: any) => ({ ...n })) : m.nodes.slice()\n }\n if (Array.isArray(m.edges)) {\n m.edges = m.edges.length > 0 && typeof m.edges[0] === 'object' ? m.edges.map((e: any) => ({ ...e })) : m.edges.slice()\n }\n\n // preserve id if present (important)\n const persistedId = m.id !== undefined ? m.id : null\n\n // If no GL provided, keep as plain object (tests may parse/expect object form)\n if (!gl) {\n // For test environments, keep arrays as-is\n out.push(m)\n continue\n }\n\n // Handle fiber meshes: offsetPt0 becomes tris\n if (Array.isArray(m.offsetPt0) && m.offsetPt0.length > 0) {\n tris = new Uint32Array(m.offsetPt0)\n // Set alpha to 0 for fibers\n if (rgba255) {\n rgba255[3] = 0\n }\n }\n\n // produce a plain Uint8Array backed by ArrayBuffer\n const finalizedRgba = new Uint8Array(Array.isArray(rgba255) ? rgba255 : Array.from(rgba255 as Uint8Array))\n\n const meshInstance = new NVMesh(pts, tris, m.name, finalizedRgba, m.opacity, m.visible, gl, m.connectome, m.dpg, m.dps, m.dpv)\n\n // restore preserved id (overwrite generated id)\n if (persistedId !== null) {\n ;(meshInstance as any).id = persistedId\n }\n\n // fiber metadata\n if (Array.isArray(m.offsetPt0) && m.offsetPt0.length > 0) {\n meshInstance.fiberGroupColormap = m.fiberGroupColormap\n meshInstance.fiberColor = m.fiberColor ? m.fiberColor : undefined\n meshInstance.fiberDither = m.fiberDither\n meshInstance.fiberRadius = m.fiberRadius\n meshInstance.colormap = m.colormap\n }\n\n // layers & shader index\n meshInstance.meshShaderIndex = m.meshShaderIndex\n meshInstance.layers = m.layers || []\n\n if (callUpdateMesh) {\n try {\n meshInstance.updateMesh(gl)\n } catch (e) {\n console.warn('NVSerializer.rehydrateMeshes: updateMesh failed for idx', i, e)\n }\n }\n\n out.push(meshInstance)\n } catch (err) {\n console.warn('NVSerializer.rehydrateMeshes: failed idx', i, err)\n // push the plain object as fallback so lengths/indices remain stable\n out.push(m)\n }\n }\n\n // Defensive: guarantee an array is returned for all (possibly-legacy) inputs.\n try {\n // Defensive: guarantee an array is returned for all (possibly-legacy) inputs.\n if (!Array.isArray(out)) {\n console.warn('NVSerializer.rehydrateMeshes: unexpected non-array output, coercing to array', {\n typeofOut: typeof out,\n isArray: Array.isArray(out),\n outPreview: out && (typeof out === 'object' ? Object.keys(out).slice(0, 10) : out)\n })\n return out ? [out] : []\n }\n\n return out\n } catch (err) {\n console.error('NVSerializer.rehydrateMeshes: final-return error', err)\n return []\n }\n }\n\n static async deserializeDocument(documentData: DocumentData): Promise<NVDocument> {\n // run migration to normalize legacy shapes up-front\n const migrated = await migrateLegacyDocument(documentData)\n\n // Create a fresh NVDocument\n const document = new NVDocument()\n\n // Basic top-level fields (preserve defaults where missing)\n Object.assign(document.data, {\n ...migrated,\n imageOptionsArray: migrated.imageOptionsArray ?? [],\n encodedImageBlobs: migrated.encodedImageBlobs ?? [],\n labels: migrated.labels ?? [],\n meshOptionsArray: migrated.meshOptionsArray ?? [],\n connectomes: migrated.connectomes ?? [],\n encodedDrawingBlob: migrated.encodedDrawingBlob ?? '',\n previewImageDataURL: migrated.previewImageDataURL ?? '',\n customData: migrated.customData ?? '',\n title: migrated.title ?? 'untitled'\n })\n\n // decode opts and merge with defaults\n const decodedOpts = NVSerializer.decodeOptsFromJSON((migrated as any).opts as any)\n document.data.opts = {\n ...DEFAULT_OPTIONS,\n ...(decodedOpts || {})\n } as any\n\n if ((document.data.opts as any).meshThicknessOn2D === 'infinity') {\n ;(document.data.opts as any).meshThicknessOn2D = Infinity\n }\n\n // scene data (merge with initial scene)\n document.scene.sceneData = {\n ...INITIAL_SCENE_DATA,\n ...(migrated.sceneData || {})\n }\n\n // back-compat: single clipPlane fields -> arrays\n const sceneDataAny: any = migrated.sceneData || {}\n if (sceneDataAny.clipPlane && !sceneDataAny.clipPlanes) {\n document.scene.sceneData.clipPlanes = [sceneDataAny.clipPlane]\n }\n if (sceneDataAny.clipPlaneDepthAziElev && !sceneDataAny.clipPlaneDepthAziElevs) {\n document.scene.sceneData.clipPlaneDepthAziElevs = [sceneDataAny.clipPlaneDepthAziElev]\n }\n\n // restore completed measurements & angles (clone vectors)\n if (migrated.completedMeasurements) {\n document.completedMeasurements = migrated.completedMeasurements.map((m: any) => ({\n ...m,\n startMM: vec3.clone(m.startMM),\n endMM: vec3.clone(m.endMM)\n }))\n }\n if (migrated.completedAngles) {\n document.completedAngles = migrated.completedAngles.map((a: any) => ({\n ...a,\n firstLineMM: {\n start: vec3.clone(a.firstLineMM.start),\n end: vec3.clone(a.firstLineMM.end)\n },\n secondLineMM: {\n start: vec3.clone(a.secondLineMM.start),\n end: vec3.clone(a.secondLineMM.end)\n }\n }))\n }\n\n // keep other runtime lists in sync\n // preserve connectome strings (Niivue.loadDocument adds them as runtime meshes later)\n document.data.connectomes = migrated.connectomes ?? []\n\n // drawing blob stays as encoded base64 (consumer will decode)\n document.data.encodedDrawingBlob = migrated.encodedDrawingBlob ?? ''\n\n // preview image\n document.data.previewImageDataURL = migrated.previewImageDataURL ?? ''\n\n return document\n }\n}\n\nexport default NVSerializer\n","import arrayEqual from 'array-equal'\nimport { mat4, vec3, vec4 } from 'gl-matrix'\n\n// TODO: TypedNumberArray also in nvmesh-types.ts\ntype TypedNumberArray = Float64Array | Float32Array | Uint32Array | Uint16Array | Uint8Array | Int32Array | Int16Array | Int8Array\n\n/**\n * Namespace for utility functions\n * @ignore\n */\n\n/**\n * Read ZIP files using asynchronous compression streams API, supports data descriptors\n * todo: check ZIP64 support\n * https://github.com/libyal/assorted/blob/main/documentation/ZIP%20archive%20format.asciidoc\n * https://en.wikipedia.org/wiki/ZIP_(file_format)\n * https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT\n * https://dev.to/ndesmic/writing-a-simple-browser-zip-file-decompressor-with-compressionstreams-5che\n */\n\ninterface Entry {\n signature: string\n version: number\n generalPurpose: number\n compressionMethod: number\n lastModifiedTime: number\n lastModifiedDate: number\n crc: number\n compressedSize: number\n uncompressedSize: number\n fileNameLength: number\n extraLength: number\n fileName: string\n extra: string\n startsAt?: number\n extract?: () => Promise<Uint8Array>\n}\n\ninterface CentralDirectoryEntry {\n versionCreated: number\n versionNeeded: number\n fileCommentLength: number\n diskNumber: number\n internalAttributes: number\n externalAttributes: number\n offset: number\n comments: string\n fileNameLength: number\n extraLength: number\n}\n\ninterface EndOfCentralDirectory {\n numberOfDisks: number\n centralDirectoryStartDisk: number\n numberCentralDirectoryRecordsOnThisDisk: number\n numberCentralDirectoryRecords: number\n centralDirectorySize: number\n centralDirectoryOffset: number\n commentLength: number\n comment: string\n}\n\nexport class Zip {\n #dataView: DataView\n #index: number = 0\n #localFiles: Entry[] = []\n #centralDirectories: CentralDirectoryEntry[] = []\n #endOfCentralDirectory?: EndOfCentralDirectory\n\n constructor(arrayBuffer: ArrayBuffer) {\n this.#dataView = new DataView(arrayBuffer)\n this.read()\n }\n\n async extract(entry: Entry): Promise<Uint8Array> {\n const buffer = new Uint8Array(this.#dataView.buffer.slice(entry.startsAt!, entry.startsAt! + entry.compressedSize))\n if (entry.compressionMethod === 0x00) {\n return buffer\n } else if (entry.compressionMethod === 0x08) {\n const stream = new DecompressionStream('deflate-raw')\n const writer = stream.writable.getWriter()\n writer.write(buffer).catch(console.error)\n const closePromise = writer.close().catch(console.error)\n const response = new Response(stream.readable)\n const result = new Uint8Array(await response.arrayBuffer())\n await closePromise\n return result\n }\n throw new Error(`Unsupported compression method: ${entry.compressionMethod}`)\n }\n\n private read(): void {\n while (!this.#endOfCentralDirectory && this.#index < this.#dataView.byteLength) {\n const signature = this.#dataView.getUint32(this.#index, true)\n if (signature === 0x04034b50) {\n const entry = this.readLocalFile(this.#index)\n entry.extract = this.extract.bind(this, entry)\n this.#localFiles.push(entry)\n const hasDataDescriptor = (entry.generalPurpose & 0x0008) !== 0\n entry.startsAt = this.#index + 30 + entry.fileNameLength + entry.extraLength\n if (entry.compressedSize === 0 && hasDataDescriptor) {\n let scanIndex = entry.startsAt\n while (scanIndex! + 20 <= this.#dataView.byteLength) {\n const possibleSignature = this.#dataView.getUint32(scanIndex!, true)\n if (possibleSignature === 0x08074b50) {\n const nextPK = this.#dataView.getUint16(scanIndex! + 16, true) === 0x4b50\n if (nextPK) {\n scanIndex! += 4\n break\n }\n }\n scanIndex!++\n }\n entry.crc = this.#dataView.getUint32(scanIndex!, true)\n entry.compressedSize = this.#dataView.getUint32(scanIndex! + 4, true)\n entry.uncompressedSize = this.#dataView.getUint32(scanIndex! + 8, true)\n this.#index = scanIndex! + 12\n } else {\n this.#index = entry.startsAt + entry.compressedSize\n }\n } else if (signature === 0x02014b50) {\n const entry = this.readCentralDirectory(this.#index)\n this.#centralDirectories.push(entry)\n this.#index += 46 + entry.fileNameLength + entry.extraLength + entry.fileCommentLength\n } else if (signature === 0x06054b50) {\n this.#endOfCentralDirectory = this.readEndCentralDirectory(this.#index)\n break\n } else if (signature === 0x06064b50) {\n this.#endOfCentralDirectory = this.readEndCentralDirectory64(this.#index)\n break\n } else {\n console.error(`Unexpected ZIP signature 0x${signature.toString(16).padStart(8, '0')} at index ${this.#index}`)\n break\n }\n }\n }\n\n private readLocalFile(offset: number): Entry {\n let compressedSize = this.#dataView.getUint32(offset + 18, true)\n let uncompressedSize = this.#dataView.getUint32(offset + 22, true)\n const fileNameLength = this.#dataView.getUint16(offset + 26, true)\n const extraLength = this.#dataView.getUint16(offset + 28, true)\n const extraOffset = offset + 30 + fileNameLength\n const extra = this.readString(extraOffset, extraLength)\n if (compressedSize === 0xffffffff && uncompressedSize === 0xffffffff) {\n let zip64Offset = extraOffset\n let foundZip64 = false\n while (zip64Offset < extraOffset + extraLength - 4) {\n const fieldSignature = this.#dataView.getUint16(zip64Offset, true)\n const fieldLength = this.#dataView.getUint16(zip64Offset + 2, true)\n zip64Offset += 4 // Move past signature and length\n if (fieldSignature === 0x0001) {\n // ZIP64 Extended Information Extra Field\n if (fieldLength >= 16) {\n // Ensure we have enough bytes\n uncompressedSize = Number(this.#dataView.getBigUint64(zip64Offset, true))\n zip64Offset += 8\n compressedSize = Number(this.#dataView.getBigUint64(zip64Offset, true))\n foundZip64 = true\n break\n } else {\n throw new Error(`ZIP64 extra field found but is too small (expected at least 16 bytes, got ${fieldLength}).`)\n }\n }\n zip64Offset += fieldLength // Move to the next extra field\n }\n if (!foundZip64) {\n throw new Error('ZIP64 format missing extra field with signature 0x0001.')\n }\n }\n return {\n signature: this.readString(offset, 4),\n version: this.#dataView.getUint16(offset + 4, true),\n generalPurpose: this.#dataView.getUint16(offset + 6, true),\n compressionMethod: this.#dataView.getUint16(offset + 8, true),\n lastModifiedTime: this.#dataView.getUint16(offset + 10, true),\n lastModifiedDate: this.#dataView.getUint16(offset + 12, true),\n crc: this.#dataView.getUint32(offset + 14, true),\n compressedSize,\n uncompressedSize,\n fileNameLength,\n extraLength,\n fileName: this.readString(offset + 30, fileNameLength),\n extra: this.readString(offset + 30 + fileNameLength, extraLength)\n }\n }\n\n private readCentralDirectory(offset: number): CentralDirectoryEntry {\n return {\n versionCreated: this.#dataView.getUint16(offset + 4, true),\n versionNeeded: this.#dataView.getUint16(offset + 6, true),\n fileNameLength: this.#dataView.getUint16(offset + 28, true),\n extraLength: this.#dataView.getUint16(offset + 30, true),\n fileCommentLength: this.#dataView.getUint16(offset + 32, true),\n diskNumber: this.#dataView.getUint16(offset + 34, true),\n internalAttributes: this.#dataView.getUint16(offset + 36, true),\n externalAttributes: this.#dataView.getUint32(offset + 38, true),\n offset: this.#dataView.getUint32(offset + 42, true),\n comments: this.readString(offset + 46, this.#dataView.getUint16(offset + 32, true))\n }\n }\n\n private readEndCentralDirectory(offset: number): EndOfCentralDirectory {\n const commentLength = this.#dataView.getUint16(offset + 20, true)\n return {\n numberOfDisks: this.#dataView.getUint16(offset + 4, true),\n centralDirectoryStartDisk: this.#dataView.getUint16(offset + 6, true),\n numberCentralDirectoryRecordsOnThisDisk: this.#dataView.getUint16(offset + 8, true),\n numberCentralDirectoryRecords: this.#dataView.getUint16(offset + 10, true),\n centralDirectorySize: this.#dataView.getUint32(offset + 12, true),\n centralDirectoryOffset: this.#dataView.getUint32(offset + 16, true),\n commentLength,\n comment: this.readString(offset + 22, commentLength)\n }\n }\n\n private readEndCentralDirectory64(offset: number): EndOfCentralDirectory {\n const commentLength = Number(this.#dataView.getBigUint64(offset + 0, true))\n return {\n numberOfDisks: this.#dataView.getUint32(offset + 16, true),\n centralDirectoryStartDisk: this.#dataView.getUint32(offset + 20, true),\n numberCentralDirectoryRecordsOnThisDisk: Number(this.#dataView.getBigUint64(offset + 24, true)),\n numberCentralDirectoryRecords: Number(this.#dataView.getBigUint64(offset + 32, true)),\n centralDirectorySize: Number(this.#dataView.getBigUint64(offset + 40, true)),\n centralDirectoryOffset: Number(this.#dataView.getBigUint64(offset + 48, true)),\n commentLength,\n comment: ''\n }\n }\n\n private readString(offset: number, length: number): string {\n return Array.from({ length }, (_, i) => String.fromCharCode(this.#dataView.getUint8(offset + i))).join('')\n }\n\n get entries(): Entry[] {\n return this.#localFiles\n }\n}\n\nexport class NVUtilities {\n static arrayBufferToBase64(arrayBuffer: ArrayBuffer): string {\n const bytes = new Uint8Array(arrayBuffer)\n return NVUtilities.uint8tob64(bytes)\n }\n\n static async decompress(data: Uint8Array): Promise<Uint8Array> {\n const format =\n data[0] === 31 && data[1] === 139 && data[2] === 8 ? 'gzip' : data[0] === 120 && (data[1] === 1 || data[1] === 94 || data[1] === 156 || data[1] === 218) ? 'deflate' : 'deflate-raw'\n const stream = new DecompressionStream(format)\n const writer = stream.writable.getWriter()\n writer.write(data).catch(console.error) // Do not await this\n // Close without awaiting directly, preventing the hang issue\n const closePromise = writer.close().catch(console.error)\n const response = new Response(stream.readable)\n const result = new Uint8Array(await response.arrayBuffer())\n await closePromise // Ensure close happens eventually\n return result\n }\n\n static async decompressToBuffer(data: Uint8Array): Promise<ArrayBuffer> {\n const decompressed = await NVUtilities.decompress(data)\n return decompressed.buffer.slice(decompressed.byteOffset, decompressed.byteOffset + decompressed.byteLength)\n }\n\n static async readMatV4(buffer: ArrayBuffer, isReplaceDots: boolean = false): Promise<Record<string, TypedNumberArray>> {\n let len = buffer.byteLength\n if (len < 40) {\n throw new Error('File too small to be MAT v4: bytes = ' + buffer.byteLength)\n }\n let reader = new DataView(buffer)\n let magic = reader.getUint16(0, true)\n let _buffer = buffer\n if (magic === 35615 || magic === 8075) {\n // gzip signature 0x1F8B in little and big endian\n const raw = await this.decompress(new Uint8Array(buffer))\n reader = new DataView(raw.buffer)\n magic = reader.getUint16(0, true)\n _buffer = raw.buffer\n len = _buffer.byteLength\n }\n const textDecoder = new TextDecoder('utf-8')\n const bytes = new Uint8Array(_buffer)\n let pos = 0\n const mat: Record<string, TypedNumberArray> = {}\n function getTensDigit(v: number): number {\n return Math.floor(v / 10) % 10\n }\n function readArray(tagDataType: number, tagBytesStart: number, tagBytesEnd: number): TypedNumberArray {\n const byteArray = new Uint8Array(bytes.subarray(tagBytesStart, tagBytesEnd))\n if (tagDataType === 1) {\n return new Float32Array(byteArray.buffer)\n }\n if (tagDataType === 2) {\n return new Int32Array(byteArray.buffer)\n }\n if (tagDataType === 3) {\n return new Int16Array(byteArray.buffer)\n }\n if (tagDataType === 4) {\n return new Uint16Array(byteArray.buffer)\n }\n if (tagDataType === 5) {\n return new Uint8Array(byteArray.buffer)\n }\n return new Float64Array(byteArray.buffer)\n }\n function readTag(): void {\n const mtype = reader.getUint32(pos, true)\n const mrows = reader.getUint32(pos + 4, true)\n const ncols = reader.getUint32(pos + 8, true)\n const imagf = reader.getUint32(pos + 12, true)\n const namlen = reader.getUint32(pos + 16, true)\n pos += 20 // skip header\n if (imagf !== 0) {\n throw new Error('Matlab V4 reader does not support imaginary numbers')\n }\n const tagArrayItems = mrows * ncols\n if (tagArrayItems < 1) {\n throw new Error('mrows * ncols must be greater than one')\n }\n const byteArray = new Uint8Array(bytes.subarray(pos, pos + namlen))\n let tagName = textDecoder.decode(byteArray).trim().replaceAll('\\x00', '')\n // n.b. DSI studio have array mat.dti_fa[] and mat.dti_fa.slope\n if (isReplaceDots) {\n // kludge for invalid DSIstudio FZ files\n tagName = tagName.replaceAll('.', '_')\n }\n const tagDataType = getTensDigit(mtype)\n // 0 double-precision (64-bit) floating-point numbers\n // 1 single-precision (32-bit) floating-point numbers\n // 2 32-bit signed integers\n // 3 16-bit signed integers\n // 4 16-bit unsigned integers\n // 5 8-bit unsigned integers\n let tagBytesPerItem = 8\n if (tagDataType >= 1 && tagDataType <= 2) {\n tagBytesPerItem = 4\n } else if (tagDataType >= 3 && tagDataType <= 4) {\n tagBytesPerItem = 2\n } else if (tagDataType === 5) {\n tagBytesPerItem = 1\n } else if (tagDataType !== 0) {\n throw new Error('impossible Matlab v4 datatype')\n }\n pos += namlen // skip name\n if (mtype > 50) {\n throw new Error('Does not appear to be little-endian V4 Matlab file')\n }\n const posEnd = pos + tagArrayItems * tagBytesPerItem\n mat[tagName] = readArray(tagDataType, pos, posEnd)\n pos = posEnd\n }\n while (pos + 20 < len) {\n readTag()\n }\n return mat\n } // readMatV4()\n\n static b64toUint8(base64: string): Uint8Array {\n const binaryString = atob(base64)\n const length = binaryString.length\n const bytes = new Uint8Array(length)\n for (let i = 0; i < length; i++) {\n bytes[i] = binaryString.charCodeAt(i)\n }\n return bytes\n }\n\n /*\nhttps://gist.github.com/jonleighton/958841\nMIT LICENSE\nCopyright 2011 Jon Leighton\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n*/\n static uint8tob64(bytes: Uint8Array): string {\n // TODO: use TextDecoder instead of shipping own implementation\n\n let base64 = ''\n const encodings = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'\n const byteLength = bytes.byteLength\n const byteRemainder = byteLength % 3\n const mainLength = byteLength - byteRemainder\n\n let a, b, c, d\n let chunk\n\n // Main loop deals with bytes in chunks of 3\n for (let i = 0; i < mainLength; i = i + 3) {\n // Combine the three bytes into a single integer\n chunk = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2]\n\n // Use bitmasks to extract 6-bit segments from the triplet\n a = (chunk & 16515072) >> 18 // 16515072 = (2^6 - 1) << 18\n b = (chunk & 258048) >> 12 // 258048 = (2^6 - 1) << 12\n c = (chunk & 4032) >> 6 // 4032 = (2^6 - 1) << 6\n d = chunk & 63 // 63 = 2^6 - 1\n\n // Convert the raw binary segments to the appropriate ASCII encoding\n base64 += encodings[a] + encodings[b] + encodings[c] + encodings[d]\n }\n\n // Deal with the remaining bytes and padding\n if (byteRemainder === 1) {\n chunk = bytes[mainLength]\n\n a = (chunk & 252) >> 2 // 252 = (2^6 - 1) << 2\n\n // Set the 4 least significant bits to zero\n b = (chunk & 3) << 4 // 3 = 2^2 - 1\n\n base64 += encodings[a] + encodings[b] + '=='\n } else if (byteRemainder === 2) {\n chunk = (bytes[mainLength] << 8) | bytes[mainLength + 1]\n\n a = (chunk & 64512) >> 10 // 64512 = (2^6 - 1) << 10\n b = (chunk & 1008) >> 4 // 1008 = (2^6 - 1) << 4\n\n // Set the 2 least significant bits to zero\n c = (chunk & 15) << 2 // 15 = 2^4 - 1\n\n base64 += encodings[a] + encodings[b] + encodings[c] + '='\n }\n\n return base64\n }\n\n // https://stackoverflow.com/questions/34156282/how-do-i-save-json-to-local-text-file\n static download(content: string | ArrayBuffer, fileName: string, contentType: string): void {\n const a = document.createElement('a')\n const contentArray = Array.isArray(content) ? content : [content]\n const file = new Blob(contentArray, { type: contentType })\n a.href = URL.createObjectURL(file)\n a.download = fileName\n a.click()\n }\n\n static readFileAsync(file: Blob): Promise<ArrayBuffer> {\n return new Promise((resolve, reject) => {\n const reader = new FileReader()\n reader.onload = (): void => {\n resolve(reader.result as ArrayBuffer)\n }\n\n reader.onerror = reject\n\n reader.readAsArrayBuffer(file)\n })\n }\n\n static blobToBase64(blob: Blob): Promise<string> {\n return new Promise((resolve) => {\n const reader = new FileReader()\n reader.onloadend = (): void => resolve(reader.result as string)\n reader.readAsDataURL(blob)\n })\n }\n\n static async decompressBase64String(base64: string): Promise<string> {\n const compressed = atob(base64)\n // convert to an array buffer\n const compressedBuffer = new ArrayBuffer(compressed.length)\n const compressedView = new Uint8Array(compressedBuffer)\n for (let i = 0; i < compressed.length; i++) {\n compressedView[i] = compressed.charCodeAt(i)\n }\n return NVUtilities.decompressArrayBuffer(compressedView)\n }\n\n static async compressToBase64String(string: string): Promise<string> {\n const buf = await NVUtilities.compressStringToArrayBuffer(string)\n return NVUtilities.uint8tob64(new Uint8Array(buf))\n }\n\n /**\n * Converts a string into a Uint8Array for use with compression/decompression methods (101arrowz/fflate: MIT License)\n * @param str The string to encode\n * @param latin1 Whether or not to interpret the data as Latin-1. This should\n * not need to be true unless decoding a binary string.\n * @returns The string encoded in UTF-8/Latin-1 binary\n */\n\n static strToU8(str: string, latin1?: boolean): Uint8Array {\n if (latin1) {\n const ar = new Uint8Array(str.length)\n for (let i = 0; i < str.length; ++i) {\n ar[i] = str.charCodeAt(i)\n }\n return ar\n }\n const l = str.length\n // TODO: strToU8 and strFromU8 both define slc\n // typed array slice - allows garbage collector to free original reference,\n // while being more compatible than .slice\n const slc = (v: Uint8Array, s: number, e?: number): Uint8Array => {\n if (s == null || s < 0) {\n s = 0\n }\n if (e == null || e > v.length) {\n e = v.length\n }\n // can't use .constructor in case user-supplied\n return new Uint8Array(v.subarray(s, e))\n }\n let ar = new Uint8Array(str.length + (str.length >> 1))\n let ai = 0\n const w = (v: number): void => {\n ar[ai++] = v\n }\n for (let i = 0; i < l; ++i) {\n if (ai + 5 > ar.length) {\n const n = new Uint8Array(ai + 8 + ((l - i) << 1))\n n.set(ar)\n ar = n\n }\n let c = str.charCodeAt(i)\n if (c < 128 || latin1) {\n w(c)\n } else if (c < 2048) {\n w(192 | (c >> 6))\n w(128 | (c & 63))\n } else if (c > 55295 && c < 57344) {\n c = (65536 + (c & (1023 << 10))) | (str.charCodeAt(++i) & 1023)\n w(240 | (c >> 18))\n w(128 | ((c >> 12) & 63))\n w(128 | ((c >> 6) & 63))\n w(128 | (c & 63))\n } else {\n c = (65536 + (c & (1023 << 10))) | (str.charCodeAt(++i) & 1023)\n w(240 | (c >> 18))\n w(128 | ((c >> 12) & 63))\n w(128 | ((c >> 6) & 63))\n w(128 | (c & 63))\n }\n }\n return slc(ar, 0, ai)\n }\n\n static async compress(data: Uint8Array, format: CompressionFormat = 'gzip'): Promise<ArrayBuffer> {\n // mimics fflate, use 'deflate-raw' 'deflate' or 'gzip' if needed\n // const format = 'deflate-raw'\n const stream = new CompressionStream(format)\n const writer = stream.writable.getWriter()\n\n writer.write(data).catch(console.error) // Do not await this\n const closePromise = writer.close().catch(console.error)\n\n const response = new Response(stream.readable)\n const result = await response.arrayBuffer()\n\n await closePromise // Ensure close happens eventually\n return result\n }\n\n static async compressStringToArrayBuffer(input: string): Promise<ArrayBuffer> {\n const uint8Array = this.strToU8(input)\n return await this.compress(uint8Array)\n }\n\n static isArrayBufferCompressed(buffer: ArrayBuffer): boolean {\n if (buffer && buffer.byteLength) {\n const arr = new Uint8Array(buffer)\n const magicNumber = (arr[0] << 8) | arr[1]\n return magicNumber === 0x1f8b\n } else {\n return false\n }\n }\n\n /**\n * Converts a Uint8Array to a string (101arrowz/fflate: MIT License)\n * @param dat The data to decode to string\n * @param latin1 Whether or not to interpret the data as Latin-1. This should\n * not need to be true unless encoding to binary string.\n * @returns The original UTF-8/Latin-1 string\n */\n static strFromU8(dat: Uint8Array, latin1?: boolean): string {\n if (latin1) {\n let r = ''\n for (let i = 0; i < dat.length; i += 16384) {\n r += String.fromCharCode.apply(null, dat.subarray(i, i + 16384))\n }\n return r\n } else {\n // typed array slice - allows garbage collector to free original reference,\n // while being more compatible than .slice\n const slc = (v: Uint8Array, s: number, e?: number): Uint8Array => {\n if (s == null || s < 0) {\n s = 0\n }\n if (e == null || e > v.length) {\n e = v.length\n }\n // can't use .constructor in case user-supplied\n return new Uint8Array(v.subarray(s, e))\n }\n // decode UTF8\n const dutf8 = (d: Uint8Array): { s: string; r: Uint8Array } => {\n for (let r = '', i = 0; ; ) {\n let c = d[i++]\n const eb = ((c > 127) as unknown as number) + ((c > 223) as unknown as number) + ((c > 239) as unknown as number)\n if (i + eb > d.length) {\n return { s: r, r: slc(d, i - 1) }\n }\n if (!eb) {\n r += String.fromCharCode(c)\n } else if (eb === 3) {\n c = (((c & 15) << 18) | ((d[i++] & 63) << 12) | ((d[i++] & 63) << 6) | (d[i++] & 63)) - 65536\n r += String.fromCharCode(55296 | (c >> 10), 56320 | (c & 1023))\n } else if (eb & 1) {\n r += String.fromCharCode(((c & 31) << 6) | (d[i++] & 63))\n } else {\n r += String.fromCharCode(((c & 15) << 12) | ((d[i++] & 63) << 6) | (d[i++] & 63))\n }\n }\n }\n const { s, r } = dutf8(dat)\n if (r.length) {\n throw new Error('Unexpected trailing bytes in UTF-8 decoding')\n }\n return s\n }\n }\n\n static async decompressArrayBuffer(buffer: ArrayBuffer): Promise<string> {\n const decompressed = await this.decompress(new Uint8Array(buffer))\n return this.strFromU8(decompressed)\n }\n\n static arraysAreEqual(a: unknown[], b: unknown[]): boolean {\n return arrayEqual(a, b)\n }\n\n /**\n * Generate a pre-filled number array.\n *\n * @param start - start value\n * @param stop - stop value\n * @param step - step value\n * @returns filled number array\n */\n static range(start: number, stop: number, step: number): number[] {\n return Array.from({ length: (stop - start) / step + 1 }, (_, i) => start + i * step)\n }\n\n /**\n * convert spherical AZIMUTH, ELEVATION to Cartesian\n * @param azimuth - azimuth number\n * @param elevation - elevation number\n * @returns the converted [x, y, z] coordinates\n * @example\n * xyz = NVUtilities.sph2cartDeg(42, 42)\n */\n static sph2cartDeg(azimuth: number, elevation: number): number[] {\n // convert spherical AZIMUTH,ELEVATION,RANGE to Cartesian\n // see Matlab's [x,y,z] = sph2cart(THETA,PHI,R)\n // reverse with cart2sph\n const Phi = -elevation * (Math.PI / 180)\n const Theta = ((azimuth - 90) % 360) * (Math.PI / 180)\n const ret = [Math.cos(Phi) * Math.cos(Theta), Math.cos(Phi) * Math.sin(Theta), Math.sin(Phi)]\n const len = Math.sqrt(ret[0] * ret[0] + ret[1] * ret[1] + ret[2] * ret[2])\n if (len <= 0.0) {\n return ret\n }\n ret[0] /= len\n ret[1] /= len\n ret[2] /= len\n return ret\n }\n\n static vox2mm(XYZ: number[], mtx: mat4): vec3 {\n const sform = mat4.clone(mtx)\n mat4.transpose(sform, sform)\n const pos = vec4.fromValues(XYZ[0], XYZ[1], XYZ[2], 1)\n vec4.transformMat4(pos, pos, sform)\n const pos3 = vec3.fromValues(pos[0], pos[1], pos[2])\n return pos3\n }\n}\n","import { NIFTI1, NIFTI2, NIFTIEXTENSION, readHeaderAsync } from 'nifti-reader-js'\nimport { mat4, vec3, vec4 } from 'gl-matrix'\nimport { v4 as uuidv4 } from '@lukeed/uuid'\nimport { ColorMap, LUT } from '@/colortables'\nimport { log } from '@/logger'\nimport { ImageFromBase64, ImageFromFileOptions, ImageFromUrlOptions, ImageMetadata, ImageType, NVIMAGE_TYPE, NiiDataType, NiiIntentCode } from '@/nvimage/utils'\nimport * as ImageWriter from '@/nvimage/ImageWriter'\nimport * as VolumeUtils from '@/nvimage/VolumeUtils'\nimport * as ImageReaders from '@/nvimage/ImageReaders'\nimport * as CoordinateTransform from '@/nvimage/CoordinateTransform'\nimport * as ImageOrientation from '@/nvimage/ImageOrientation'\nimport * as TensorProcessing from '@/nvimage/TensorProcessing'\nimport * as IntensityCalibration from '@/nvimage/IntensityCalibration'\nimport * as ColormapManager from '@/nvimage/ColormapManager'\nimport * as ImageFactory from '@/nvimage/ImageFactory'\nimport * as ImageMetadataModule from '@/nvimage/ImageMetadata'\nimport * as ImageDataProcessor from '@/nvimage/ImageDataProcessor'\nimport * as AffineProcessor from '@/nvimage/AffineProcessor'\nimport * as ZarrProcessor from '@/nvimage/ZarrProcessor'\nimport * as StreamingLoader from '@/nvimage/StreamingLoader'\nimport { NVZarrHelper } from '@/nvimage/zarr/NVZarrHelper'\nimport { AffineTransform, copyAffine, createTransformMatrix, multiplyAffine } from '@/nvimage/affineUtils'\n\nexport * from '@/nvimage/utils'\nexport type TypedVoxelArray = Float32Array | Uint8Array | Int16Array | Float64Array | Uint16Array | Int32Array | Uint32Array\n\n/**\n * a NVImage encapsulates some image data and provides methods to query and operate on images\n */\nexport class NVImage {\n name: string\n id: string\n url?: string\n headers?: Record<string, string>\n _colormap: string\n _opacity: number\n percentileFrac: number\n ignoreZeroVoxels: boolean\n trustCalMinMax: boolean\n colormapNegative: string\n // TODO see niivue/loadDocument\n colormapLabel: LUT | null\n colormapInvert?: boolean\n nFrame4D?: number\n frame4D: number // indexed from 0!\n nTotalFrame4D?: number\n cal_minNeg: number\n cal_maxNeg: number\n colorbarVisible = true\n modulationImage: number | null = null\n modulateAlpha = 0 // if !=0, mod transparency with expon power |Alpha|\n // TODO this is some Daikon internal thing\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n series: any = [] // for concatenating dicom images\n nVox3D?: number\n oblique_angle?: number\n maxShearDeg?: number\n useQFormNotSForm: boolean\n\n colormapType?: number\n\n pixDims?: number[]\n matRAS?: mat4\n pixDimsRAS?: number[]\n obliqueRAS?: mat4\n dimsRAS?: number[]\n permRAS?: number[]\n img2RASstep?: number[]\n img2RASstart?: number[]\n toRAS?: mat4\n toRASvox?: mat4\n\n frac2mm?: mat4\n frac2mmOrtho?: mat4\n extentsMinOrtho?: number[]\n extentsMaxOrtho?: number[]\n mm2ortho?: mat4\n\n hdr: NIFTI1 | NIFTI2 | null = null\n extensions?: NIFTIEXTENSION[]\n imageType?: ImageType\n img?: TypedVoxelArray\n imaginary?: Float32Array // only for complex data\n v1?: Float32Array // only for FIB files\n fileObject?: File | File[]\n dims?: number[]\n\n onColormapChange: (img: NVImage) => void = () => {}\n onOpacityChange: (img: NVImage) => void = () => {}\n\n zarrHelper: NVZarrHelper | null = null\n _hasExplicitZarrCenter = false\n\n mm000?: vec3\n mm100?: vec3\n mm010?: vec3\n mm001?: vec3\n\n cal_min?: number\n cal_max?: number\n robust_min?: number\n robust_max?: number\n global_min?: number\n global_max?: number\n\n // TODO referenced by niivue/loadVolumes\n urlImgData?: string\n isManifest?: boolean\n limitFrames4D?: number\n\n // Original affine matrix stored at load time for reset functionality\n originalAffine?: number[][]\n\n constructor(\n // can be an array of Typed arrays or just a typed array. If an array of Typed arrays then it is assumed you are loading DICOM (perhaps the only real use case?)\n dataBuffer: ArrayBuffer | ArrayBuffer[] | ArrayBufferLike | null = null,\n name = '',\n colormap = 'gray',\n opacity = 1.0,\n pairedImgData: ArrayBuffer | null = null,\n cal_min = NaN,\n cal_max = NaN,\n trustCalMinMax = true,\n percentileFrac = 0.02,\n ignoreZeroVoxels = false,\n // TODO this was marked as true by default in the docs!\n useQFormNotSForm = false,\n colormapNegative = '',\n frame4D = 0,\n imageType = NVIMAGE_TYPE.UNKNOWN,\n cal_minNeg = NaN,\n cal_maxNeg = NaN,\n colorbarVisible = true,\n colormapLabel: LUT | null = null,\n colormapType = 0\n ) {\n this.init(\n dataBuffer,\n name,\n colormap,\n opacity,\n pairedImgData,\n cal_min,\n cal_max,\n trustCalMinMax,\n percentileFrac,\n ignoreZeroVoxels,\n useQFormNotSForm,\n colormapNegative,\n frame4D,\n imageType,\n cal_minNeg,\n cal_maxNeg,\n colorbarVisible,\n colormapLabel,\n colormapType\n )\n }\n\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n init(\n // can be an array of Typed arrays or just a typed array. If an array of Typed arrays then it is assumed you are loading DICOM (perhaps the only real use case?)\n dataBuffer: ArrayBuffer | ArrayBuffer[] | ArrayBufferLike | null = null,\n name = '',\n colormap = '',\n opacity = 1.0,\n _pairedImgData: ArrayBuffer | null = null,\n cal_min = NaN,\n cal_max = NaN,\n trustCalMinMax = true,\n percentileFrac = 0.02,\n ignoreZeroVoxels = false,\n useQFormNotSForm = false,\n colormapNegative = '',\n frame4D = 0,\n imageType = NVIMAGE_TYPE.UNKNOWN,\n cal_minNeg = NaN,\n cal_maxNeg = NaN,\n colorbarVisible = true,\n colormapLabel: LUT | null = null,\n colormapType = 0,\n imgRaw: ArrayBuffer | ArrayBufferLike | null = null\n ): void {\n const isNoColormap = colormap === ''\n if (isNoColormap) {\n colormap = 'gray'\n }\n this.name = name\n this.imageType = imageType\n this.id = uuidv4()\n this._colormap = colormap\n this._opacity = opacity > 1.0 ? 1.0 : opacity // make sure opacity can't be initialized greater than 1 see: #107 and #117 on github\n this.percentileFrac = percentileFrac\n this.ignoreZeroVoxels = ignoreZeroVoxels\n this.trustCalMinMax = trustCalMinMax\n this.colormapNegative = colormapNegative\n this.colormapLabel = colormapLabel\n this.frame4D = frame4D // indexed from 0!\n this.cal_minNeg = cal_minNeg\n this.cal_maxNeg = cal_maxNeg\n this.colorbarVisible = colorbarVisible\n this.colormapType = colormapType // COLORMAP_TYPE MIN_TO_MAX\n // TODO this was missing\n this.useQFormNotSForm = useQFormNotSForm\n // Added to support zerosLike\n // TODO this line causes an absurd amount of handling undefined fields - it would probably be better to isolate this as a separate class.\n if (!dataBuffer) {\n return\n }\n if (isNoColormap && this.hdr && this.hdr.intent_code === 1002) {\n colormap = 'random'\n this._colormap = colormap\n }\n if (this.hdr && typeof this.hdr.magic === 'number') {\n this.hdr.magic = 'n+1'\n } // fix for issue 481, where magic is set to the number 1 rather than a string\n this.nFrame4D = 1\n if (this.hdr) {\n for (let i = 4; i < 7; i++) {\n if (this.hdr.dims[i] > 1) {\n this.nFrame4D *= this.hdr.dims[i]\n }\n }\n }\n this.frame4D = Math.min(this.frame4D, this.nFrame4D - 1)\n this.nTotalFrame4D = this.nFrame4D\n\n if (!this.hdr || !imgRaw) {\n return\n }\n if (this.hdr.dims[1] === 0 && this.hdr.dims[2] === 0 && this.hdr.dims[3] === 0) {\n log.warn('Invalid volume: First three dimensions are all zero')\n }\n // e.g. 2D image has 1 slice, so dim[3] should be at least 1\n this.hdr.dims[1] = Math.max(this.hdr.dims[1], 1)\n this.hdr.dims[2] = Math.max(this.hdr.dims[2], 1)\n this.hdr.dims[3] = Math.max(this.hdr.dims[3], 1)\n\n this.nVox3D = this.hdr.dims[1] * this.hdr.dims[2] * this.hdr.dims[3]\n const bytesPerVol = this.nVox3D * (this.hdr.numBitsPerVoxel / 8)\n const nVol4D = imgRaw.byteLength / bytesPerVol\n if (nVol4D !== this.nFrame4D) {\n if (nVol4D > 0 && nVol4D * bytesPerVol === imgRaw.byteLength) {\n log.debug('Loading the first ' + nVol4D + ' of ' + this.nFrame4D + ' volumes')\n } else {\n log.warn('This header does not match voxel data', this.hdr, imgRaw.byteLength)\n }\n this.nFrame4D = nVol4D\n }\n\n // n.b. NIfTI standard says \"NIFTI_INTENT_RGB_VECTOR\" should be RGBA, but FSL only stores RGB\n if (\n (this.hdr.intent_code === NiiIntentCode.NIFTI_INTENT_VECTOR || this.hdr.intent_code === NiiIntentCode.NIFTI_INTENT_RGB_VECTOR) &&\n this.nFrame4D === 3 &&\n this.hdr.datatypeCode === NiiDataType.DT_FLOAT32\n ) {\n // change data from float32 to rgba32\n imgRaw = this.float32V1asRGBA(new Float32Array(imgRaw)).buffer as ArrayBuffer\n } // NIFTI_INTENT_VECTOR: this is a RGB tensor\n // Process affine matrix: validate, calculate from QForm if needed, repair if defective\n AffineProcessor.processAffine(this.hdr, useQFormNotSForm)\n // Swap bytes if foreign endian, then convert to appropriate typed array\n ImageDataProcessor.swapBytesIfNeeded(imgRaw, this.hdr)\n const conversionResult = ImageDataProcessor.convertDataType(imgRaw, this.hdr)\n this.img = conversionResult.img\n if (conversionResult.imaginary) {\n this.imaginary = conversionResult.imaginary\n }\n if (conversionResult.updatedDatatypeCode !== undefined) {\n this.hdr.datatypeCode = conversionResult.updatedDatatypeCode\n }\n if (conversionResult.updatedNumBitsPerVoxel !== undefined) {\n this.hdr.numBitsPerVoxel = conversionResult.updatedNumBitsPerVoxel\n }\n this.calculateRAS()\n // Store original affine for reset functionality\n this.originalAffine = copyAffine(this.hdr.affine)\n if (!isNaN(cal_min)) {\n this.hdr.cal_min = cal_min\n }\n if (!isNaN(cal_max)) {\n this.hdr.cal_max = cal_max\n }\n this.calMinMax()\n }\n\n static async new(\n // can be an array of Typed arrays or just a typed array. If an array of Typed arrays then it is assumed you are loading DICOM (perhaps the only real use case?)\n dataBuffer: ArrayBuffer | ArrayBuffer[] | ArrayBufferLike | null = null,\n name = '',\n colormap = '',\n opacity = 1.0,\n pairedImgData: ArrayBuffer | null = null,\n cal_min = NaN,\n cal_max = NaN,\n trustCalMinMax = true,\n percentileFrac = 0.02,\n ignoreZeroVoxels = false,\n useQFormNotSForm = false,\n colormapNegative = '',\n frame4D = 0,\n imageType = NVIMAGE_TYPE.UNKNOWN,\n cal_minNeg = NaN,\n cal_maxNeg = NaN,\n colorbarVisible = true,\n colormapLabel: LUT | null = null,\n colormapType = 0,\n zarrData: null | unknown\n ): Promise<NVImage> {\n const newImg = new NVImage()\n const re = /(?:\\.([^.]+))?$/\n let ext = re.exec(name)![1] || '' // TODO ! guaranteed?\n ext = ext.toUpperCase()\n if (ext === 'GZ') {\n ext = re.exec(name.slice(0, -3))![1] // img.trk.gz -> img.trk\n ext = ext.toUpperCase()\n }\n let imgRaw: ArrayBufferLike | Uint8Array | null = null\n if (imageType === NVIMAGE_TYPE.UNKNOWN) {\n imageType = NVIMAGE_TYPE.parse(ext)\n }\n if (dataBuffer instanceof ArrayBuffer && dataBuffer.byteLength >= 2 && imageType === NVIMAGE_TYPE.DCM) {\n // unknown extension defaults to DICOM, which starts `dcm`\n // since NIfTI1 is popular, lets make sure the filename has not been mangled\n const u8s = new Uint8Array(dataBuffer) // Create a view of the buffer\n const isNifti1 = (u8s[0] === 92 && u8s[1] === 1) || (u8s[1] === 92 && u8s[0] === 1)\n if (isNifti1) {\n imageType = NVIMAGE_TYPE.NII\n }\n }\n newImg.imageType = imageType\n switch (imageType) {\n case NVIMAGE_TYPE.DCM_FOLDER:\n case NVIMAGE_TYPE.DCM_MANIFEST:\n case NVIMAGE_TYPE.DCM:\n return\n case NVIMAGE_TYPE.FIB:\n ;[imgRaw, newImg.v1] = await ImageReaders.DsiStudio.readFIB(newImg, dataBuffer as ArrayBuffer)\n break\n case NVIMAGE_TYPE.MIH:\n case NVIMAGE_TYPE.MIF:\n imgRaw = await ImageReaders.Mrtrix.readMIF(newImg, dataBuffer as ArrayBuffer, pairedImgData) // detached\n break\n case NVIMAGE_TYPE.NHDR:\n case NVIMAGE_TYPE.NRRD:\n imgRaw = await ImageReaders.Nrrd.readNrrd(newImg, dataBuffer as ArrayBuffer)\n if (imgRaw === null) {\n throw new Error(`Failed to parse NHDR/NRRD file ${name}`)\n }\n break\n case NVIMAGE_TYPE.MHD:\n case NVIMAGE_TYPE.MHA:\n imgRaw = await ImageReaders.Itk.readMHA(newImg, dataBuffer as ArrayBuffer, pairedImgData)\n break\n case NVIMAGE_TYPE.MGH:\n case NVIMAGE_TYPE.MGZ:\n imgRaw = await ImageReaders.Mgh.readMgh(newImg, dataBuffer as ArrayBuffer, name)\n if (imgRaw === null) {\n throw new Error(`Failed to parse MGH/MGZ file ${name}`)\n }\n break\n case NVIMAGE_TYPE.SRC:\n imgRaw = await ImageReaders.DsiStudio.readSRC(newImg, dataBuffer as ArrayBuffer)\n break\n case NVIMAGE_TYPE.V:\n imgRaw = ImageReaders.Ecat.readECAT(newImg, dataBuffer as ArrayBuffer)\n break\n case NVIMAGE_TYPE.V16:\n imgRaw = ImageReaders.BrainVoyager.readV16(newImg, dataBuffer as ArrayBuffer)\n break\n case NVIMAGE_TYPE.VMR:\n imgRaw = ImageReaders.BrainVoyager.readVMR(newImg, dataBuffer as ArrayBuffer)\n break\n case NVIMAGE_TYPE.HEAD:\n imgRaw = await ImageReaders.Afni.readHEAD(newImg, dataBuffer as ArrayBuffer, pairedImgData) // paired = .BRIK\n break\n case NVIMAGE_TYPE.BMP:\n imgRaw = await ImageReaders.Image.readBMP(newImg, dataBuffer as ArrayBuffer)\n break\n case NVIMAGE_TYPE.NPY:\n imgRaw = await ImageReaders.Numpy.readNPY(newImg, dataBuffer as ArrayBuffer)\n break\n case NVIMAGE_TYPE.NPZ:\n imgRaw = await ImageReaders.Numpy.readNPZ(newImg, dataBuffer as ArrayBuffer)\n break\n case NVIMAGE_TYPE.ZARR:\n imgRaw = await ImageReaders.Zarr.readZARR(newImg, dataBuffer as ArrayBuffer, zarrData)\n break\n case NVIMAGE_TYPE.NII:\n imgRaw = await ImageReaders.Nii.readNifti(newImg, dataBuffer as ArrayBuffer, pairedImgData)\n if (imgRaw === null) {\n throw new Error(`Failed to parse NIfTI file ${name}.`)\n }\n break\n default:\n throw new Error('Image type not supported')\n }\n newImg.init(\n dataBuffer,\n name,\n colormap,\n opacity,\n pairedImgData,\n cal_min,\n cal_max,\n trustCalMinMax,\n percentileFrac,\n ignoreZeroVoxels,\n useQFormNotSForm,\n colormapNegative,\n frame4D,\n imageType,\n cal_minNeg,\n cal_maxNeg,\n colorbarVisible,\n colormapLabel,\n colormapType,\n imgRaw\n )\n return newImg\n }\n\n // not included in public docs\n // detect difference between voxel grid and world space\n // https://github.com/afni/afni/blob/25e77d564f2c67ff480fa99a7b8e48ec2d9a89fc/src/thd_coords.c#L717\n computeObliqueAngle(mtx44: mat4): number {\n return ImageOrientation.computeObliqueAngle(mtx44)\n }\n\n float32V1asRGBA(inImg: Float32Array): Uint8Array {\n return TensorProcessing.float32V1asRGBA(this, inImg)\n }\n\n loadImgV1(isFlipX: boolean = false, isFlipY: boolean = false, isFlipZ: boolean = false): boolean {\n return TensorProcessing.loadImgV1(this, isFlipX, isFlipY, isFlipZ)\n }\n\n // not included in public docs\n // detect difference between voxel grid and world space\n calculateOblique(): void {\n ImageOrientation.calculateOblique(this)\n }\n\n // not included in public docs\n // read DICOM format image and treat it like a NIfTI\n // -----------------\n // readDICOM(buf: ArrayBuffer | ArrayBuffer[]): ArrayBuffer {\n // this.series = new daikon.Series()\n // // parse DICOM file\n // if (Array.isArray(buf)) {\n // for (let i = 0; i < buf.length; i++) {\n // const dataview = new DataView(buf[i])\n // const image = daikon.Series.parseImage(dataview)\n // if (image === null) {\n // log.error(daikon.Series.parserError)\n // } else if (image.hasPixelData()) {\n // // if it's part of the same series, add it\n // if (this.series.images.length === 0 || image.getSeriesId() === this.series.images[0].getSeriesId()) {\n // this.series.addImage(image)\n // }\n // } // if hasPixelData\n // } // for i\n // } else {\n // // not a dicom folder drop\n // const image = daikon.Series.parseImage(new DataView(buf))\n // if (image === null) {\n // log.error(daikon.Series.parserError)\n // } else if (image.hasPixelData()) {\n // // if it's part of the same series, add it\n // if (this.series.images.length === 0 || image.getSeriesId() === this.series.images[0].getSeriesId()) {\n // this.series.addImage(image)\n // }\n // }\n // }\n // // order the image files, determines number of frames, etc.\n // this.series.buildSeries()\n // // output some header info\n // this.hdr = new nifti.NIFTI1()\n // const hdr = this.hdr\n // hdr.scl_inter = 0\n // hdr.scl_slope = 1\n // if (this.series.images[0].getDataScaleIntercept()) {\n // hdr.scl_inter = this.series.images[0].getDataScaleIntercept()\n // }\n // if (this.series.images[0].getDataScaleSlope()) {\n // hdr.scl_slope = this.series.images[0].getDataScaleSlope()\n // }\n // hdr.dims = [3, 1, 1, 1, 0, 0, 0, 0]\n // hdr.pixDims = [1, 1, 1, 1, 1, 0, 0, 0]\n // hdr.dims[1] = this.series.images[0].getCols()\n // hdr.dims[2] = this.series.images[0].getRows()\n // hdr.dims[3] = this.series.images[0].getNumberOfFrames()\n // if (this.series.images.length > 1) {\n // if (hdr.dims[3] > 1) {\n // log.debug('To Do: multiple slices per file and multiple files (XA30 DWI)')\n // }\n // hdr.dims[3] = this.series.images.length\n // }\n // const rc = this.series.images[0].getPixelSpacing() // TODO: order?\n // hdr.pixDims[1] = rc[0]\n // hdr.pixDims[2] = rc[1]\n // if (this.series.images.length > 1) {\n // // Multiple slices. The depth of a pixel is the physical distance between offsets. This is not the same as slice\n // // spacing for tilted slices (skew).\n // const p0 = vec3.fromValues(...(this.series.images[0].getImagePosition() as [number, number, number]))\n // const p1 = vec3.fromValues(...(this.series.images[1].getImagePosition() as [number, number, number]))\n // const n = vec3.fromValues(0, 0, 0)\n // vec3.subtract(n, p0, p1)\n // hdr.pixDims[3] = vec3.length(n)\n // } else {\n // // Single slice. Use the slice thickness as pixel depth.\n // hdr.pixDims[3] = this.series.images[0].getSliceThickness()\n // }\n // hdr.pixDims[4] = this.series.images[0].getTR() / 1000.0 // msec -> sec\n // const dt = this.series.images[0].getDataType() // 2=int,3=uint,4=float,\n // const bpv = this.series.images[0].getBitsAllocated()\n // hdr.numBitsPerVoxel = bpv\n // this.hdr.littleEndian = this.series.images[0].littleEndian\n // if (bpv === 8 && dt === 2) {\n // hdr.datatypeCode = NiiDataType.DT_INT8\n // } else if (bpv === 8 && dt === 3) {\n // hdr.datatypeCode = NiiDataType.DT_UINT8\n // } else if (bpv === 16 && dt === 2) {\n // hdr.datatypeCode = NiiDataType.DT_INT16\n // } else if (bpv === 16 && dt === 3) {\n // hdr.datatypeCode = NiiDataType.DT_UINT16\n // } else if (bpv === 32 && dt === 2) {\n // hdr.datatypeCode = NiiDataType.DT_INT32\n // } else if (bpv === 32 && dt === 3) {\n // hdr.datatypeCode = NiiDataType.DT_UINT32\n // } else if (bpv === 32 && dt === 4) {\n // hdr.datatypeCode = NiiDataType.DT_FLOAT32\n // } else if (bpv === 64 && dt === 4) {\n // hdr.datatypeCode = NiiDataType.DT_FLOAT64\n // } else if (bpv === 1) {\n // hdr.datatypeCode = NiiDataType.DT_BINARY\n // } else {\n // log.warn('Unsupported DICOM format: ' + dt + ' ' + bpv)\n // }\n // const voxelDimensions = hdr.pixDims.slice(1, 4)\n // const m = getBestTransform(\n // this.series.images[0].getImageDirections(),\n // voxelDimensions,\n // this.series.images[0].getImagePosition()\n // )\n // if (m) {\n // hdr.sform_code = 1\n // hdr.affine = [\n // [m[0][0], m[0][1], m[0][2], m[0][3]],\n // [m[1][0], m[1][1], m[1][2], m[1][3]],\n // [m[2][0], m[2][1], m[2][2], m[2][3]],\n // [0, 0, 0, 1]\n // ]\n // }\n // let data\n // let length = this.series.validatePixelDataLength(this.series.images[0])\n // const buffer = new Uint8Array(new ArrayBuffer(length * this.series.images.length))\n // // implementation copied from:\n // // https://github.com/rii-mango/Daikon/blob/bbe08bad9758dfbdf31ca22fb79048c7bad85706/src/series.js#L496\n // for (let i = 0; i < this.series.images.length; i++) {\n // if (this.series.isMosaic) {\n // data = this.series.getMosaicData(this.series.images[i], this.series.images[i].getPixelDataBytes())\n // } else {\n // data = this.series.images[i].getPixelDataBytes()\n // }\n // length = this.series.validatePixelDataLength(this.series.images[i])\n // this.series.images[i].clearPixelData()\n // buffer.set(new Uint8Array(data, 0, length), length * i)\n // } // for images.length\n // return buffer.buffer\n // } // readDICOM()\n // -----------------------\n\n // not included in public docs\n // read ECAT7 format image\n // https://github.com/openneuropet/PET2BIDS/tree/28aae3fab22309047d36d867c624cd629c921ca6/ecat_validation/ecat_info\n readECAT(buffer: ArrayBuffer): ArrayBuffer {\n return ImageReaders.Ecat.readECAT(this, buffer)\n }\n\n readV16(buffer: ArrayBuffer): ArrayBuffer {\n return ImageReaders.BrainVoyager.readV16(this, buffer)\n }\n\n async readNPY(buffer: ArrayBuffer): Promise<ArrayBuffer> {\n return ImageReaders.Numpy.readNPY(this, buffer)\n }\n\n async readNPZ(buffer: ArrayBuffer): Promise<ArrayBuffer | undefined> {\n return ImageReaders.Numpy.readNPZ(this, buffer)\n }\n\n async imageDataFromArrayBuffer(buffer: ArrayBuffer): Promise<ImageData> {\n return ImageReaders.Image.imageDataFromArrayBuffer(buffer)\n }\n\n async readBMP(buffer: ArrayBuffer): Promise<ArrayBuffer> {\n return ImageReaders.Image.readBMP(this, buffer)\n }\n\n async readZARR(buffer: ArrayBuffer, zarrData: unknown): Promise<ArrayBufferLike> {\n return ImageReaders.Zarr.readZARR(this, buffer, zarrData)\n }\n\n // not included in public docs\n // read brainvoyager format VMR image\n // https://support.brainvoyager.com/brainvoyager/automation-development/84-file-formats/343-developer-guide-2-6-the-format-of-vmr-files\n readVMR(buffer: ArrayBuffer): ArrayBuffer {\n return ImageReaders.BrainVoyager.readVMR(this, buffer)\n }\n\n // not included in public docs\n // read DSI-Studio FIB format image\n // https://dsi-studio.labsolver.org/doc/cli_data.html\n async readFIB(buffer: ArrayBuffer): Promise<[ArrayBuffer, Float32Array]> {\n return ImageReaders.DsiStudio.readFIB(this, buffer)\n }\n\n // not included in public docs\n // read DSI-Studio SRC format image\n // https://dsi-studio.labsolver.org/doc/cli_data.html\n async readSRC(buffer: ArrayBuffer): Promise<ArrayBuffer> {\n return ImageReaders.DsiStudio.readSRC(this, buffer)\n }\n\n // not included in public docs\n // read AFNI head/brik format image\n async readHEAD(dataBuffer: ArrayBuffer, pairedImgData: ArrayBuffer | null): Promise<ArrayBuffer> {\n return ImageReaders.Afni.readHEAD(this, dataBuffer, pairedImgData)\n }\n\n // not included in public docs\n // read ITK MHA format image\n // https://itk.org/Wiki/ITK/MetaIO/Documentation#Reading_a_Brick-of-Bytes_.28an_N-Dimensional_volume_in_a_single_file.29\n async readMHA(buffer: ArrayBuffer, pairedImgData: ArrayBuffer | null): Promise<ArrayBuffer> {\n return ImageReaders.Itk.readMHA(this, buffer, pairedImgData)\n }\n\n // not included in public docs\n // read mrtrix MIF format image\n // https://mrtrix.readthedocs.io/en/latest/getting_started/image_data.html#mrtrix-image-formats\n async readMIF(buffer: ArrayBuffer, pairedImgData: ArrayBuffer | null): Promise<ArrayBuffer> {\n return ImageReaders.Mrtrix.readMIF(this, buffer, pairedImgData)\n }\n\n // not included in public docs\n // Transform to orient NIfTI image to Left->Right,Posterior->Anterior,Inferior->Superior (48 possible permutations)\n calculateRAS(): void {\n ImageOrientation.calculateRAS(this)\n }\n\n /**\n * Get a deep copy of the current affine matrix.\n * @returns A 4x4 affine matrix as a 2D array (row-major)\n */\n getAffine(): number[][] {\n if (!this.hdr) {\n throw new Error('Image header not loaded')\n }\n return copyAffine(this.hdr.affine)\n }\n\n /**\n * Set a new affine matrix and recalculate all derived RAS matrices.\n * Call updateGLVolume() on the Niivue instance after this to update rendering.\n * @param affine - A 4x4 affine matrix as a 2D array (row-major)\n */\n setAffine(affine: number[][]): void {\n if (!this.hdr) {\n throw new Error('Image header not loaded')\n }\n this.hdr.affine = copyAffine(affine)\n this.calculateRAS()\n }\n\n /**\n * Apply a transform (translation, rotation, scale) to the current affine matrix.\n * The transform is applied in world coordinate space: newAffine = transform * currentAffine\n * Call updateGLVolume() on the Niivue instance after this to update rendering.\n * @param transform - Transform to apply with translation (mm), rotation (degrees), and scale\n */\n applyTransform(transform: AffineTransform): void {\n if (!this.hdr) {\n throw new Error('Image header not loaded')\n }\n const transformMatrix = createTransformMatrix(transform)\n const newAffine = multiplyAffine(this.hdr.affine, transformMatrix)\n this.hdr.affine = newAffine\n this.calculateRAS()\n }\n\n /**\n * Reset the affine matrix to its original state when the image was first loaded.\n * Call updateGLVolume() on the Niivue instance after this to update rendering.\n */\n resetAffine(): void {\n if (!this.hdr) {\n throw new Error('Image header not loaded')\n }\n if (!this.originalAffine) {\n throw new Error('Original affine not stored')\n }\n this.hdr.affine = copyAffine(this.originalAffine)\n this.calculateRAS()\n }\n\n // Reorient raw header data to RAS\n // assume single volume, use nVolumes to specify, set nVolumes = 0 for same as input\n\n async hdr2RAS(nVolumes: number = 1): Promise<NIFTI1 | NIFTI2> {\n return ImageOrientation.hdr2RAS(this, nVolumes)\n }\n\n // Reorient raw image data to RAS\n // note that GPU-based orient shader is much faster\n // returns single 3D volume even for 4D input. Use nVolume to select volume (0 indexed)\n img2RAS(nVolume: number = 0): TypedVoxelArray {\n return ImageOrientation.img2RAS(this, nVolume)\n }\n\n // not included in public docs\n // convert voxel location (row, column slice, indexed from 0) to world space\n vox2mm(XYZ: number[], mtx: mat4): vec3 {\n return CoordinateTransform.vox2mm(this, XYZ, mtx)\n } // vox2mm()\n\n // not included in public docs\n // convert world space to voxel location (row, column slice, indexed from 0)\n mm2vox(mm: number[], frac = false): Float32Array | vec3 {\n return CoordinateTransform.mm2vox(this, mm, frac)\n } // mm2vox()\n\n // not included in public docs\n // returns boolean: are two arrays identical?\n // TODO this won't work for complex objects. Maybe use array-equal from NPM\n arrayEquals(a: unknown[], b: unknown[]): boolean {\n return CoordinateTransform.arrayEquals(a, b)\n }\n\n // not included in public docs\n // base function for niivue.setColormap()\n // colormaps are continuously interpolated between 256 values (0..256)\n setColormap(cm: string): void {\n ColormapManager.setColormap(this, cm)\n }\n\n // not included in public docs\n // base function for niivue.setColormap()\n // label colormaps are discretely sampled from an arbitrary number of colors\n setColormapLabel(cm: ColorMap): void {\n ColormapManager.setColormapLabel(this, cm)\n }\n\n async setColormapLabelFromUrl(url: string): Promise<void> {\n return ColormapManager.setColormapLabelFromUrl(this, url)\n }\n\n get colormap(): string {\n return ColormapManager.getColormap(this)\n }\n\n get colorMap(): string {\n return ColormapManager.getColormap(this)\n }\n\n // TODO duplicate fields, see niivue/loadDocument\n set colormap(cm: string) {\n ColormapManager.setColormap(this, cm)\n }\n\n set colorMap(cm: string) {\n ColormapManager.setColormap(this, cm)\n }\n\n get opacity(): number {\n return ColormapManager.getOpacity(this)\n }\n\n set opacity(opacity) {\n ColormapManager.setOpacity(this, opacity)\n }\n\n /**\n * set contrast/brightness to robust range (2%..98%)\n * @param vol - volume for estimate (use -1 to use estimate on all loaded volumes; use INFINITY for current volume)\n * @param isBorder - if true (default) only center of volume used for estimate\n * @returns volume brightness and returns array [pct2, pct98, mnScale, mxScale]\n * @see {@link https://niivue.com/demos/features/timeseries2.html | live demo usage}\n */\n calMinMax(vol: number = Number.POSITIVE_INFINITY, isBorder: boolean = true): number[] {\n return IntensityCalibration.calMinMax(this, vol, isBorder)\n }\n\n // not included in public docs\n // convert voxel intensity from stored value to scaled intensity\n intensityRaw2Scaled(raw: number): number {\n return IntensityCalibration.intensityRaw2Scaled(this, raw)\n }\n\n // convert voxel intensity from scaled intensity to stored value\n intensityScaled2Raw(scaled: number): number {\n return IntensityCalibration.intensityScaled2Raw(this, scaled)\n }\n\n /**\n * Converts NVImage to NIfTI compliant byte array, potentially compressed.\n * Delegates to ImageWriter.saveToUint8Array.\n */\n async saveToUint8Array(fnm: string, drawing8: Uint8Array | null = null): Promise<Uint8Array> {\n // Delegate to the writer module, passing the instance 'this'\n return ImageWriter.saveToUint8Array(this, fnm, drawing8)\n }\n\n /**\n * save image as NIfTI volume and trigger download.\n * Delegates to ImageWriter.saveToDisk.\n */\n async saveToDisk(fnm: string = '', drawing8: Uint8Array | null = null): Promise<Uint8Array> {\n // Delegate to the writer module, passing the instance 'this'\n return ImageWriter.saveToDisk(this, fnm, drawing8)\n }\n\n static async fetchDicomData(url: string, headers: Record<string, string> = {}): Promise<Array<{ name: string; data: ArrayBuffer }>> {\n return ImageFactory.fetchDicomData(url, headers)\n }\n\n static async readFirstDecompressedBytes(stream: ReadableStream<Uint8Array>, minBytes: number): Promise<Uint8Array> {\n return ImageFactory.readFirstDecompressedBytes(stream, minBytes)\n }\n\n static extractFilenameFromUrl(url: string): string | null {\n return ImageFactory.extractFilenameFromUrl(url)\n }\n\n static async loadInitialVolumesGz(url = '', headers = {}, limitFrames4D = NaN): Promise<ArrayBuffer | null> {\n return ImageFactory.loadInitialVolumesGz(url, headers, limitFrames4D)\n }\n\n static async loadInitialVolumes(url = '', headers = {}, limitFrames4D = NaN): Promise<ArrayBuffer | null> {\n return ImageFactory.loadInitialVolumes(url, headers, limitFrames4D)\n }\n\n /**\n * factory function to load and return a new NVImage instance from a given URL\n */\n static async loadFromUrl({\n url = '',\n urlImgData = '',\n headers = {},\n name = '',\n colormap = '',\n opacity = 1.0,\n cal_min = NaN,\n cal_max = NaN,\n trustCalMinMax = true,\n percentileFrac = 0.02,\n ignoreZeroVoxels = false,\n useQFormNotSForm = false,\n colormapNegative = '',\n frame4D = 0,\n isManifest = false,\n limitFrames4D = NaN,\n imageType = NVIMAGE_TYPE.UNKNOWN,\n colorbarVisible = true,\n buffer = new ArrayBuffer(0),\n zarrLevel,\n zarrMaxVolumeSize,\n zarrChannel,\n zarrConvertUnits,\n zarrCenterMM\n }: Partial<Omit<ImageFromUrlOptions, 'url'>> & { url?: string | Uint8Array | ArrayBuffer } = {}): Promise<NVImage> {\n if (url === '') {\n throw Error('url must not be empty')\n }\n let nvimage = null\n let dataBuffer = null\n let zarrData: null | unknown = null\n\n // Handle input buffer types\n if (url instanceof Uint8Array) {\n url = url.slice().buffer as ArrayBuffer\n }\n if (buffer.byteLength > 0) {\n url = buffer\n }\n if (url instanceof ArrayBuffer) {\n dataBuffer = url\n if (name !== '') {\n url = name\n } else {\n const bytes = new Uint8Array(dataBuffer)\n url = bytes[0] === 31 && bytes[1] === 139 ? 'array.nii.gz' : 'array.nii'\n }\n }\n // Resolve paired image URL if necessary\n let ext = ''\n if (name === '') {\n ext = ImageFactory.getPrimaryExtension(url)\n } else {\n ext = ImageFactory.getPrimaryExtension(name)\n }\n if (imageType === NVIMAGE_TYPE.UNKNOWN) {\n imageType = NVIMAGE_TYPE.parse(ext)\n }\n if (imageType === NVIMAGE_TYPE.UNKNOWN && typeof url === 'string') {\n // perhaps we are not identifying an extension because the url is a redirect\n const response = await fetch(url, {})\n if (response.redirected) {\n const rname = this.extractFilenameFromUrl(response.url)\n if (rname && rname.length > 0) {\n if (name === '') {\n name = rname\n ext = ImageFactory.getPrimaryExtension(name)\n imageType = NVIMAGE_TYPE.parse(ext)\n }\n }\n }\n }\n // try url and name attributes to test for .zarr\n if (imageType === NVIMAGE_TYPE.ZARR) {\n if (zarrLevel !== undefined) {\n // Chunked path: create virtual volume with helper\n return await NVImage.createChunkedZarr(url as string, {\n level: zarrLevel,\n maxVolumeSize: zarrMaxVolumeSize,\n channel: zarrChannel,\n convertUnitsToMm: zarrConvertUnits,\n colormap,\n opacity,\n zarrCenterMM\n })\n }\n // Light path: load entire array (unchanged)\n const zarrResult = await ZarrProcessor.loadZarrData(url)\n dataBuffer = zarrResult.dataBuffer\n zarrData = zarrResult.zarrData\n }\n // DICOM assigned for unknown extensions: therefore test signature to see if mystery file is NIfTI\n const isTestNIfTI = imageType === NVIMAGE_TYPE.DCM || NVIMAGE_TYPE.NII\n if (!dataBuffer && isTestNIfTI) {\n dataBuffer = await this.loadInitialVolumes(url, headers, limitFrames4D)\n }\n // Handle non-limited cases\n if (!dataBuffer) {\n if (isManifest) {\n dataBuffer = await NVImage.fetchDicomData(url, headers)\n imageType = NVIMAGE_TYPE.DCM_MANIFEST\n } else {\n dataBuffer = await StreamingLoader.fetchAndStreamData(url, headers)\n }\n }\n // Handle paired image data for formats with separate header/data files\n const pairedUrl = StreamingLoader.getPairedImageUrl(url, ext.toUpperCase(), urlImgData)\n let pairedImgData = null\n if (pairedUrl) {\n pairedImgData = await StreamingLoader.fetchPairedImageData(pairedUrl, headers)\n }\n\n if (!dataBuffer) {\n throw new Error('Unable to load buffer properly from volume')\n }\n\n // Get filename from URL if not provided\n if (!name) {\n let urlParts: string[]\n try {\n // if a full url like https://domain/path/file.nii.gz?query=filter\n // parse the url and get the pathname component without the query\n urlParts = new URL(url).pathname.split('/')\n } catch (e) {\n // if a relative url then parse the path (assuming no query)\n urlParts = url.split('/')\n }\n name = urlParts.slice(-1)[0] // name will be last part of url (e.g. some/url/image.nii.gz --> image.nii.gz\n if (name.indexOf('?') > -1) {\n name = name.slice(0, name.indexOf('?')) // remove query string if any\n }\n }\n nvimage = await this.new(\n dataBuffer,\n name,\n colormap,\n opacity,\n pairedImgData,\n cal_min,\n cal_max,\n trustCalMinMax,\n percentileFrac,\n ignoreZeroVoxels,\n useQFormNotSForm,\n colormapNegative,\n frame4D,\n imageType,\n NaN,\n NaN,\n true,\n null,\n 0,\n zarrData\n )\n nvimage.url = url\n nvimage.colorbarVisible = colorbarVisible\n return nvimage\n }\n\n /**\n * Factory method: create a chunked zarr NVImage with an attached NVZarrHelper.\n */\n static async createChunkedZarr(\n url: string,\n options: {\n level: number\n maxVolumeSize?: number\n maxTextureSize?: number\n channel?: number\n cacheSize?: number\n convertUnitsToMm?: boolean\n colormap?: string\n opacity?: number\n zarrCenterMM?: [number, number, number]\n }\n ): Promise<NVImage> {\n const nvimage = new NVImage()\n nvimage.zarrHelper = await NVZarrHelper.create(nvimage, url, {\n url,\n level: options.level,\n maxVolumeSize: options.maxVolumeSize,\n maxTextureSize: options.maxTextureSize,\n channel: options.channel,\n cacheSize: options.cacheSize,\n convertUnitsToMm: options.convertUnitsToMm\n })\n if (options.zarrCenterMM) {\n nvimage.zarrHelper.setWorldCenter(options.zarrCenterMM)\n nvimage._hasExplicitZarrCenter = true\n }\n if (options.colormap) {\n nvimage._colormap = options.colormap\n }\n if (options.opacity !== undefined) {\n nvimage._opacity = options.opacity\n }\n nvimage.url = url\n return nvimage\n }\n\n // not included in public docs\n // loading Nifti files\n static async readFileAsync(file: File, bytesToLoad = NaN): Promise<ArrayBuffer> {\n return ImageFactory.readFileAsync(file, bytesToLoad)\n }\n\n /**\n * factory function to load and return a new NVImage instance from a file in the browser\n */\n static async loadFromFile({\n file, // file can be an array of file objects or a single file object\n name = '',\n colormap = '',\n opacity = 1.0,\n urlImgData = null,\n cal_min = NaN,\n cal_max = NaN,\n trustCalMinMax = true,\n percentileFrac = 0.02,\n ignoreZeroVoxels = false,\n useQFormNotSForm = false,\n colormapNegative = '',\n frame4D = 0,\n limitFrames4D = NaN,\n imageType = NVIMAGE_TYPE.UNKNOWN\n }: ImageFromFileOptions): Promise<NVImage> {\n let nvimage: NVImage | null = null\n let dataBuffer: ArrayBuffer | ArrayBuffer[] = []\n try {\n if (Array.isArray(file)) {\n dataBuffer = await Promise.all(file.map((f) => this.readFileAsync(f)))\n } else {\n if (!isNaN(limitFrames4D)) {\n const headerBuffer = await this.readFileAsync(file, 512)\n const headerView = new Uint8Array(headerBuffer)\n\n const isNifti1 = (headerView[0] === 92 && headerView[1] === 1) || (headerView[1] === 92 && headerView[0] === 1)\n\n if (!isNifti1) {\n dataBuffer = await this.readFileAsync(file)\n } else {\n const hdr = await readHeaderAsync(headerBuffer)\n if (!hdr) {\n throw new Error('could not read nifti header')\n }\n\n const nBytesPerVoxel = hdr.numBitsPerVoxel / 8\n const nVox3D = [1, 2, 3].reduce((acc, i) => acc * (hdr.dims[i] > 1 ? hdr.dims[i] : 1), 1)\n const nFrame4D = [4, 5, 6].reduce((acc, i) => acc * (hdr.dims[i] > 1 ? hdr.dims[i] : 1), 1)\n\n const volsToLoad = Math.max(Math.min(limitFrames4D, nFrame4D), 1)\n const bytesToLoad = hdr.vox_offset + volsToLoad * nVox3D * nBytesPerVoxel\n dataBuffer = await this.readFileAsync(file, bytesToLoad)\n }\n } else {\n dataBuffer = await this.readFileAsync(file)\n }\n name = file.name\n }\n let pairedImgData = null\n if (urlImgData) {\n // @ts-expect-error check data type?\n pairedImgData = await this.readFileAsync(urlImgData)\n }\n nvimage = await this.new(\n dataBuffer,\n name,\n colormap,\n opacity,\n pairedImgData,\n cal_min,\n cal_max,\n trustCalMinMax,\n percentileFrac,\n ignoreZeroVoxels,\n useQFormNotSForm,\n colormapNegative,\n frame4D,\n imageType,\n NaN,\n NaN,\n true,\n null,\n 0,\n null\n )\n // add a reference to the file object as a new property of the NVImage instance\n // is this too hacky?\n nvimage.fileObject = file\n } catch (err) {\n log.error(err)\n throw new Error('could not build NVImage')\n }\n if (nvimage === null) {\n throw new Error('could not build NVImage')\n }\n return nvimage\n }\n\n /**\n * Creates a Uint8Array representing a NIFTI file (header + optional image data).\n * Delegates to ImageWriter.createNiftiArray.\n */\n static createNiftiArray(\n dims: number[] = [256, 256, 256],\n pixDims: number[] = [1, 1, 1],\n affine: number[] = [1, 0, 0, -128, 0, 1, 0, -128, 0, 0, 1, -128, 0, 0, 0, 1],\n datatypeCode = NiiDataType.DT_UINT8,\n img: TypedVoxelArray | Uint8Array = new Uint8Array()\n ): Uint8Array {\n return ImageWriter.createNiftiArray(dims, pixDims, affine, datatypeCode, img)\n }\n\n /**\n * Creates a NIFTI1 header object with basic properties.\n * Delegates to ImageWriter.createNiftiHeader.\n */\n static createNiftiHeader(\n dims: number[] = [256, 256, 256],\n pixDims: number[] = [1, 1, 1],\n affine: number[] = [1, 0, 0, -128, 0, 1, 0, -128, 0, 0, 1, -128, 0, 0, 0, 1],\n datatypeCode = NiiDataType.DT_UINT8\n ): NIFTI1 {\n return ImageWriter.createNiftiHeader(dims, pixDims, affine, datatypeCode)\n }\n\n /**\n * read a 3D slab of voxels from a volume\n * @see {@link https://niivue.com/demos/features/slab_selection.html | live demo usage}\n */\n\n /**\n * read a 3D slab of voxels from a volume, specified in RAS coordinates.\n * Delegates to VolumeUtils.getVolumeData.\n */\n getVolumeData(voxStart: number[] = [-1, 0, 0], voxEnd: number[] = [0, 0, 0], dataType = 'same'): [TypedVoxelArray, number[]] {\n return VolumeUtils.getVolumeData(this, voxStart, voxEnd, dataType)\n }\n\n /**\n * write a 3D slab of voxels from a volume\n * @see {@link https://niivue.com/demos/features/slab_selection.html | live demo usage}\n */\n\n /**\n * write a 3D slab of voxels from a volume, specified in RAS coordinates.\n * Delegates to VolumeUtils.setVolumeData.\n * Input slabData is assumed to be in the correct raw data type for the target image.\n */\n setVolumeData(voxStart: number[] = [-1, 0, 0], voxEnd: number[] = [0, 0, 0], img: TypedVoxelArray = new Uint8Array()): void {\n VolumeUtils.setVolumeData(this, voxStart, voxEnd, img)\n }\n\n /**\n * factory function to load and return a new NVImage instance from a base64 encoded string\n * @example\n * myImage = NVImage.loadFromBase64('SomeBase64String')\n */\n static async loadFromBase64({\n base64,\n name = '',\n colormap = '',\n opacity = 1.0,\n cal_min = NaN,\n cal_max = NaN,\n trustCalMinMax = true,\n percentileFrac = 0.02,\n ignoreZeroVoxels = false,\n useQFormNotSForm = false,\n colormapNegative = '',\n frame4D = 0,\n imageType = NVIMAGE_TYPE.UNKNOWN,\n cal_minNeg = NaN,\n cal_maxNeg = NaN,\n colorbarVisible = true,\n colormapLabel = null\n }: ImageFromBase64): Promise<NVImage> {\n let nvimage = null\n try {\n const dataBuffer = ImageFactory.base64ToArrayBuffer(base64)\n const pairedImgData = null\n nvimage = await this.new(\n dataBuffer,\n name,\n colormap,\n opacity,\n pairedImgData,\n cal_min,\n cal_max,\n trustCalMinMax,\n percentileFrac,\n ignoreZeroVoxels,\n useQFormNotSForm,\n colormapNegative,\n frame4D,\n imageType,\n cal_minNeg,\n cal_maxNeg,\n colorbarVisible,\n colormapLabel,\n 0,\n null\n )\n } catch (err) {\n log.debug(err)\n }\n\n if (nvimage === null) {\n throw new Error('could not load NVImage')\n }\n\n return nvimage\n }\n\n /**\n * make a clone of a NVImage instance and return a new NVImage\n * @example\n * myImage = NVImage.loadFromFile(SomeFileObject) // files can be from dialogs or drag and drop\n * clonedImage = myImage.clone()\n */\n clone(): NVImage {\n const clonedImage = new NVImage()\n // important! the clone should have a new ID to avoid conflicts\n // when referencing images by ID. A user could add the cloned\n // image as a viewable volume in any order.\n clonedImage.id = uuidv4()\n clonedImage.hdr = Object.assign({}, this.hdr)\n clonedImage.img = this.img!.slice()\n clonedImage.calculateRAS()\n clonedImage.calMinMax()\n return clonedImage\n }\n\n /**\n * fill a NVImage instance with zeros for the image data\n * @example\n * myImage = NVImage.loadFromFile(SomeFileObject) // files can be from dialogs or drag and drop\n * clonedImageWithZeros = myImage.clone().zeroImage()\n */\n zeroImage(): void {\n this.img!.fill(0)\n }\n\n /**\n * get nifti specific metadata about the image\n */\n getImageMetadata(): ImageMetadata {\n return ImageMetadataModule.getImageMetadata(this)\n }\n\n /**\n * a factory function to make a zero filled image given a NVImage as a reference\n * @example\n * myImage = NVImage.loadFromFile(SomeFileObject) // files can be from dialogs or drag and drop\n * newZeroImage = NVImage.zerosLike(myImage)\n */\n static zerosLike(nvImage: NVImage, dataType = 'same'): NVImage {\n // dataType can be: 'same', 'uint8'\n // 'same' means that the zeroed image data type is the same as the input image\n const zeroClone = nvImage.clone()\n zeroClone.zeroImage()\n if (dataType === 'uint8') {\n zeroClone.img = Uint8Array.from(zeroClone.img!)\n zeroClone.hdr!.datatypeCode = NiiDataType.DT_UINT8\n zeroClone.hdr!.numBitsPerVoxel = 8\n }\n if (dataType === 'float32') {\n zeroClone.img = Float32Array.from(zeroClone.img!)\n zeroClone.hdr!.datatypeCode = NiiDataType.DT_FLOAT32\n zeroClone.hdr!.numBitsPerVoxel = 32\n }\n return zeroClone\n }\n\n /**\n * Returns voxel intensity at specific native coordinates.\n * Delegates to VolumeUtils.getValue.\n */\n getValue(x: number, y: number, z: number, frame4D = 0, isReadImaginary = false): number {\n return VolumeUtils.getValue(this, x, y, z, frame4D, isReadImaginary)\n }\n\n /**\n * Returns voxel intensities at specific native coordinates.\n * Delegates to VolumeUtils.getValue.\n */\n getValues(x: number, y: number, z: number, frame4D = 0, isReadImaginary = false): number[] {\n return VolumeUtils.getValues(this, x, y, z, frame4D, isReadImaginary)\n }\n\n /**\n * Update options for image\n */\n applyOptionsUpdate(options: ImageFromUrlOptions): void {\n ImageMetadataModule.applyOptionsUpdate(this, options)\n }\n\n getImageOptions(): ImageFromUrlOptions {\n return ImageMetadataModule.getImageOptions(this)\n }\n\n /**\n * Converts NVImage to NIfTI compliant byte array.\n * Handles potential re-orientation of drawing data.\n * Delegates to ImageWriter.toUint8Array.\n */\n toUint8Array(drawingBytes: Uint8Array | null = null): Uint8Array {\n // Delegate to the writer module, passing the instance 'this'\n return ImageWriter.toUint8Array(this, drawingBytes)\n }\n\n // not included in public docs\n convertVox2Frac(vox: vec3): vec3 {\n return CoordinateTransform.convertVox2Frac(this, vox)\n }\n\n // not included in public docs\n convertFrac2Vox(frac: vec3): vec3 {\n return CoordinateTransform.convertFrac2Vox(this, frac)\n }\n\n // not included in public docs\n convertFrac2MM(frac: vec3, isForceSliceMM = false): vec4 {\n return CoordinateTransform.convertFrac2MM(this, frac, isForceSliceMM)\n }\n\n // not included in public docs\n convertMM2Frac(mm: vec3 | vec4, isForceSliceMM = false): vec3 {\n return CoordinateTransform.convertMM2Frac(this, mm, isForceSliceMM)\n }\n}\n","import { vec3 } from 'gl-matrix'\nimport { log } from '@/logger'\nimport { NiftiHeader } from '@/types'\nimport { LUT } from '@/colortables'\n\nexport const isPlatformLittleEndian = (): boolean => {\n // inspired by https://github.com/rii-mango/Papaya\n const buffer = new ArrayBuffer(2)\n new DataView(buffer).setInt16(0, 256, true)\n return new Int16Array(buffer)[0] === 256\n}\n\n/**\n * Enum for NIfTI datatype codes\n * // https://nifti.nimh.nih.gov/pub/dist/src/niftilib/nifti1.h\n */\nexport enum NiiDataType {\n DT_NONE = 0,\n DT_BINARY = 1,\n DT_UINT8 = 2,\n DT_INT16 = 4,\n DT_INT32 = 8,\n DT_FLOAT32 = 16,\n DT_COMPLEX64 = 32,\n DT_FLOAT64 = 64,\n DT_RGB24 = 128,\n DT_INT8 = 256,\n DT_UINT16 = 512,\n DT_UINT32 = 768,\n DT_INT64 = 1024,\n DT_UINT64 = 1280,\n DT_FLOAT128 = 1536,\n DT_COMPLEX128 = 1792,\n DT_COMPLEX256 = 2048,\n DT_RGBA32 = 2304\n}\n\n/**\n * Enum for NIfTI intent codes\n * // https://nifti.nimh.nih.gov/pub/dist/src/niftilib/nifti1.h\n */\nexport enum NiiIntentCode {\n NIFTI_INTENT_LABEL = 1002,\n NIFTI_INTENT_VECTOR = 1007,\n NIFTI_INTENT_RGB_VECTOR = 2003\n}\n\n/**\n * Enum for supported image types (e.g. NII, NRRD, DICOM)\n */\nexport enum ImageType {\n UNKNOWN = 0,\n NII = 1,\n DCM = 2,\n DCM_MANIFEST = 3,\n MIH = 4,\n MIF = 5,\n NHDR = 6,\n NRRD = 7,\n MHD = 8,\n MHA = 9,\n MGH = 10,\n MGZ = 11,\n V = 12,\n V16 = 13,\n VMR = 14,\n HEAD = 15,\n DCM_FOLDER = 16,\n SRC = 17,\n FIB = 18,\n BMP = 19,\n ZARR = 20,\n NPY = 21,\n NPZ = 22,\n HDR = 23\n}\n\nexport const NVIMAGE_TYPE = Object.freeze({\n ...ImageType,\n parse: (ext: string) => {\n let imageType: ImageType = ImageType.UNKNOWN\n switch (ext.toUpperCase()) {\n case '':\n case 'DCM':\n imageType = ImageType.DCM\n break\n case 'TXT':\n imageType = ImageType.DCM_MANIFEST\n break\n case 'FZ':\n case 'GQI':\n case 'QSDR':\n case 'FIB':\n imageType = ImageType.FIB\n break\n case 'HDR':\n case 'NII':\n imageType = ImageType.NII\n break\n case 'MIH':\n imageType = ImageType.MIH\n break\n case 'MIF':\n imageType = ImageType.MIF\n break\n case 'NHDR':\n imageType = ImageType.NHDR\n break\n case 'NRRD':\n imageType = ImageType.NRRD\n break\n case 'MHD':\n imageType = ImageType.MHD\n break\n case 'MHA':\n imageType = ImageType.MHA\n break\n case 'MGH':\n imageType = ImageType.MGH\n break\n case 'MGZ':\n imageType = ImageType.MGZ\n break\n case 'NPY':\n imageType = ImageType.NPY\n break\n case 'NPZ':\n imageType = ImageType.NPZ\n break\n case 'SRC':\n imageType = ImageType.SRC\n break\n case 'V':\n imageType = ImageType.V\n break\n case 'V16':\n imageType = ImageType.V16\n break\n case 'VMR':\n imageType = ImageType.VMR\n break\n case 'HEAD':\n imageType = ImageType.HEAD\n break\n case 'PNG':\n case 'BMP':\n case 'GIF':\n case 'JPG':\n case 'JPEG':\n imageType = ImageType.BMP\n break\n case 'ZARR':\n imageType = ImageType.ZARR\n break\n }\n return imageType\n }\n})\n\nexport type ImageFromUrlOptions = {\n // the resolvable URL pointing to a nifti image to load\n url: string\n // Allows loading formats where header and image are separate files (e.g. nifti.hdr, nifti.img)\n urlImageData?: string\n // headers to use in the fetch call\n headers?: Record<string, string>\n // a name for this image (defaults to empty)\n name?: string\n // a color map to use (defaults to gray)\n colorMap?: string\n // TODO see duplicate usage in niivue/loadDocument\n colormap?: string\n // the opacity for this image (defaults to 1)\n opacity?: number\n // minimum intensity for color brightness/contrast\n cal_min?: number\n // maximum intensity for color brightness/contrast\n cal_max?: number\n // whether or not to trust cal_min and cal_max from the nifti header (trusting results in faster loading, defaults to true)\n trustCalMinMax?: boolean\n // the percentile to use for setting the robust range of the display values (smart intensity setting for images with large ranges, defaults to 0.02)\n percentileFrac?: number\n // whether or not to use QForm over SForm constructing the NVImage instance (defaults to false)\n useQFormNotSForm?: boolean\n // if true, values below cal_min are shown as translucent, not transparent (defaults to false)\n alphaThreshold?: boolean\n // a color map to use for negative intensities\n colormapNegative?: string\n // backwards compatible option\n colorMapNegative?: string\n // minimum intensity for colormapNegative brightness/contrast (NaN for symmetrical cal_min)\n cal_minNeg?: number\n // maximum intensity for colormapNegative brightness/contrast (NaN for symmetrical cal_max)\n cal_maxNeg?: number\n // show/hide colormaps (defaults to true)\n colorbarVisible?: boolean\n // TODO the following fields were not documented\n ignoreZeroVoxels?: boolean\n imageType?: ImageType\n frame4D?: number\n colormapLabel?: LUT | null\n pairedImgData?: null\n limitFrames4D?: number\n isManifest?: boolean\n urlImgData?: string\n buffer?: ArrayBuffer\n // Zarr chunked loading options\n zarrLevel?: number\n zarrMaxVolumeSize?: number\n zarrChannel?: number\n /** Convert OME spatial units to millimeters for NIfTI compatibility (default: true) */\n zarrConvertUnits?: boolean\n /** World-space center [x, y, z] in mm where the zarr volume center should be positioned */\n zarrCenterMM?: [number, number, number]\n}\n\n// TODO centralize shared options\nexport type ImageFromFileOptions = {\n // the file object\n file: File | File[]\n // a name for this image. Default is an empty string\n name?: string\n // a color map to use. default is gray\n colormap?: string\n // the opacity for this image. default is 1\n opacity?: number\n // Allows loading formats where header and image are separate files (e.g. nifti.hdr, nifti.img)\n urlImgData?: File | null | FileSystemEntry\n // minimum intensity for color brightness/contrast\n cal_min?: number\n // maximum intensity for color brightness/contrast\n cal_max?: number\n // whether or not to trust cal_min and cal_max from the nifti header (trusting results in faster loading)\n trustCalMinMax?: boolean\n // the percentile to use for setting the robust range of the display values (smart intensity setting for images with large ranges)\n percentileFrac?: number\n // whether or not to ignore zero voxels in setting the robust range of display values\n ignoreZeroVoxels?: boolean\n // whether or not to use QForm instead of SForm during construction\n useQFormNotSForm?: boolean\n // colormap negative for the image\n colormapNegative?: string\n // image type\n imageType?: ImageType\n frame4D?: number\n limitFrames4D?: number\n}\n\nexport type ImageFromBase64 = {\n // base64 string\n base64: string\n // a name for this image. Default is an empty string\n name?: string\n // a color map to use. default is gray\n colormap?: string\n // the opacity for this image. default is 1\n opacity?: number\n // minimum intensity for color brightness/contrast\n cal_min?: number\n // maximum intensity for color brightness/contrast\n cal_max?: number\n // whether or not to trust cal_min and cal_max from the nifti header (trusting results in faster loading)\n trustCalMinMax?: boolean\n // the percentile to use for setting the robust range of the display values (smart intensity setting for images with large ranges)\n percentileFrac?: number\n // whether or not to ignore zero voxels in setting the robust range of display values\n ignoreZeroVoxels?: boolean\n // whether or not use QForm instead of SForm\n useQFormNotSForm?: boolean\n colormapNegative?: string\n frame4D?: number\n imageType?: ImageType\n cal_minNeg?: number\n cal_maxNeg?: number\n colorbarVisible?: boolean\n colormapLabel?: LUT | null\n}\n\nexport type ImageMetadata = {\n // unique if of image\n id: string\n // data type\n datatypeCode: number\n // number of columns\n nx: number\n // number of rows\n ny: number\n // number of slices\n nz: number\n // number of volumes\n nt: number\n // space between columns\n dx: number\n // space between rows\n dy: number\n // space between slices\n dz: number\n // time between volumes\n dt: number\n // bits per voxel\n // TODO was documented as bpx\n bpv: number\n}\n\nexport const NVImageFromUrlOptions = (\n url: string,\n urlImageData = '',\n name = '',\n colormap = 'gray',\n opacity = 1.0,\n cal_min = NaN,\n cal_max = NaN,\n trustCalMinMax = true,\n percentileFrac = 0.02,\n ignoreZeroVoxels = false,\n useQFormNotSForm = false,\n colormapNegative = '',\n frame4D = 0,\n imageType = NVIMAGE_TYPE.UNKNOWN,\n cal_minNeg = NaN,\n cal_maxNeg = NaN,\n colorbarVisible = true,\n alphaThreshold = false,\n colormapLabel = null\n): ImageFromUrlOptions => {\n return {\n url,\n urlImageData,\n name,\n colormap,\n colorMap: colormap,\n opacity,\n cal_min,\n cal_max,\n trustCalMinMax,\n percentileFrac,\n ignoreZeroVoxels,\n useQFormNotSForm,\n colormapNegative,\n imageType,\n cal_minNeg,\n cal_maxNeg,\n colorbarVisible,\n frame4D,\n alphaThreshold,\n colormapLabel\n }\n}\n\n// not included in public docs\n// create NIfTI format SForm from DICOM frame of reference\nexport function getBestTransform(imageDirections: number[], voxelDimensions: number[], imagePosition: number[]): number[][] | null {\n // https://github.com/rii-mango/Papaya/blob/782a19341af77a510d674c777b6da46afb8c65f1/src/js/volume/dicom/header-dicom.js#L605\n /* Copyright (c) 2012-2015, RII-UTHSCSA\nAll rights reserved.\n\nTHIS PRODUCT IS NOT FOR CLINICAL USE.\n\nRedistribution and use in source and binary forms, with or without modification, are permitted provided that the\nfollowing conditions are met:\n\n - Redistributions of source code must retain the above copyright notice, this list of conditions and the following\n disclaimer.\n\n - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following\n disclaimer in the documentation and/or other materials provided with the distribution.\n\n - Neither the name of the RII-UTHSCSA nor the names of its contributors may be used to endorse or promote products\n derived from this software without specific prior written permission.\n\n THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES,\n INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\n DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\n SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,\n WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n*/\n const cosines = imageDirections\n let m = null\n if (cosines) {\n const vs = {\n colSize: voxelDimensions[0],\n rowSize: voxelDimensions[1],\n sliceSize: voxelDimensions[2]\n }\n const coord = imagePosition\n const cosx = [cosines[0], cosines[1], cosines[2]]\n const cosy = [cosines[3], cosines[4], cosines[5]]\n const cosz = [cosx[1] * cosy[2] - cosx[2] * cosy[1], cosx[2] * cosy[0] - cosx[0] * cosy[2], cosx[0] * cosy[1] - cosx[1] * cosy[0]]\n m = [\n [cosx[0] * vs.colSize * -1, cosy[0] * vs.rowSize * -1, cosz[0] * vs.sliceSize * -1, -1 * coord[0]],\n [cosx[1] * vs.colSize * -1, cosy[1] * vs.rowSize * -1, cosz[1] * vs.sliceSize * -1, -1 * coord[1]],\n [cosx[2] * vs.colSize, cosy[2] * vs.rowSize, cosz[2] * vs.sliceSize, coord[2]],\n [0, 0, 0, 1]\n ]\n }\n return m\n}\n\nfunction str2Buffer(str: string, maxLen: number = 80): number[] {\n // emulate node.js Buffer.from\n // remove characters than could be used for shell expansion\n str = str.replace(/[`$]/g, '')\n const bytes = []\n const len = Math.min(maxLen, str.length)\n for (let i = 0; i < len; i++) {\n const char = str.charCodeAt(i)\n bytes.push(char & 0xff)\n }\n return bytes\n}\n\n// save NIfTI header into UINT8 array for saving to disk\nexport function hdrToArrayBuffer(hdr: NiftiHeader, isDrawing8 = false, isInputEndian = false): Uint8Array {\n const SHORT_SIZE = 2\n const FLOAT32_SIZE = 4\n let isLittleEndian = true\n if (isInputEndian) {\n isLittleEndian = hdr.littleEndian\n }\n const byteArray = new Uint8Array(348)\n const view = new DataView(byteArray.buffer)\n // sizeof_hdr\n view.setInt32(0, 348, isLittleEndian)\n\n // data_type, db_name, extents, session_error, regular are not used\n // regular set to 'r' (ASCII 114) for Analyze compatibility\n view.setUint8(38, 114)\n // dim_info\n view.setUint8(39, hdr.dim_info)\n\n // dims\n for (let i = 0; i < 8; i++) {\n view.setUint16(40 + SHORT_SIZE * i, hdr.dims[i], isLittleEndian)\n }\n\n // intent_p1, intent_p2, intent_p3\n view.setFloat32(56, hdr.intent_p1, isLittleEndian)\n view.setFloat32(60, hdr.intent_p2, isLittleEndian)\n view.setFloat32(64, hdr.intent_p3, isLittleEndian)\n // intent_code, datatype, bitpix, slice_start\n view.setInt16(68, hdr.intent_code, isLittleEndian)\n if (isDrawing8) {\n view.setInt16(70, 2, isLittleEndian) // 2 = DT_UINT8\n view.setInt16(72, 8, isLittleEndian)\n } else {\n view.setInt16(70, hdr.datatypeCode, isLittleEndian)\n view.setInt16(72, hdr.numBitsPerVoxel, isLittleEndian)\n }\n view.setInt16(74, hdr.slice_start, isLittleEndian)\n\n // pixdim[8], vox_offset, scl_slope, scl_inter\n for (let i = 0; i < 8; i++) {\n view.setFloat32(76 + FLOAT32_SIZE * i, hdr.pixDims[i], isLittleEndian)\n }\n if (isDrawing8) {\n view.setFloat32(108, 352, isLittleEndian)\n view.setFloat32(112, 1.0, isLittleEndian)\n view.setFloat32(116, 0.0, isLittleEndian)\n } else {\n // view.setFloat32(108, hdr.vox_offset, isLittleEndian)\n view.setFloat32(108, hdr.vox_offset, isLittleEndian)\n view.setFloat32(112, hdr.scl_slope, isLittleEndian)\n view.setFloat32(116, hdr.scl_inter, isLittleEndian)\n }\n // slice_end\n view.setInt16(120, hdr.slice_end, isLittleEndian)\n\n // slice_code, xyzt_units\n view.setUint8(122, hdr.slice_code)\n if (hdr.xyzt_units === 0) {\n view.setUint8(123, 10)\n } else {\n view.setUint8(123, hdr.xyzt_units)\n }\n\n // cal_max, cal_min, slice_duration, toffset\n if (isDrawing8) {\n view.setFloat32(124, 0, isLittleEndian)\n view.setFloat32(128, 0, isLittleEndian)\n } else {\n view.setFloat32(124, hdr.cal_max, isLittleEndian)\n view.setFloat32(128, hdr.cal_min, isLittleEndian)\n }\n view.setFloat32(132, hdr.slice_duration, isLittleEndian)\n view.setFloat32(136, hdr.toffset, isLittleEndian)\n\n // glmax, glmin are unused\n\n // descrip and aux_file\n // node.js byteArray.set(Buffer.from(hdr.description), 148);\n byteArray.set(str2Buffer(hdr.description), 148)\n // node.js: byteArray.set(Buffer.from(hdr.aux_file), 228);\n byteArray.set(str2Buffer(hdr.aux_file), 228)\n // qform_code, sform_code\n view.setInt16(252, hdr.qform_code, isLittleEndian)\n // if sform unknown, assume NIFTI_XFORM_SCANNER_ANAT\n if (hdr.sform_code < 1 || hdr.sform_code < 1) {\n view.setInt16(254, 1, isLittleEndian)\n } else {\n view.setInt16(254, hdr.sform_code, isLittleEndian)\n }\n\n // quatern_b, quatern_c, quatern_d, qoffset_x, qoffset_y, qoffset_z, srow_x[4], srow_y[4], and srow_z[4]\n view.setFloat32(256, hdr.quatern_b, isLittleEndian)\n view.setFloat32(260, hdr.quatern_c, isLittleEndian)\n view.setFloat32(264, hdr.quatern_d, isLittleEndian)\n view.setFloat32(268, hdr.qoffset_x, isLittleEndian)\n view.setFloat32(272, hdr.qoffset_y, isLittleEndian)\n view.setFloat32(276, hdr.qoffset_z, isLittleEndian)\n const flattened = hdr.affine.flat()\n // we only want the first three rows\n for (let i = 0; i < 12; i++) {\n view.setFloat32(280 + FLOAT32_SIZE * i, flattened[i], isLittleEndian)\n }\n // node.js https://www.w3schools.com/nodejs/met_buffer_from.asp\n // intent_name and magic\n // node.js byteArray.set(Buffer.from(hdr.intent_name), 328);\n // byteArray.set(str2Buffer(hdr.intent_name), 328)\n // node.js byteArray.set(Buffer.from(hdr.magic), 344);\n // byteArray.set(str2Buffer(hdr.magic), 344)\n view.setInt32(344, 3222382, true) // \"n+1\\0\"\n\n return byteArray\n // return byteArray.buffer;\n}\n\ntype Extents = {\n // min bounding point\n min: number[]\n // max bounding point\n max: number[]\n // point furthest from origin\n furthestVertexFromOrigin: number\n // origin\n origin: vec3\n}\n\nexport function getExtents(positions: number[], forceOriginInVolume = true): Extents {\n const nV = Math.round(positions.length / 3) // each vertex has 3 components: XYZ\n const origin = vec3.fromValues(0, 0, 0) // default center of rotation\n const mn = vec3.create()\n const mx = vec3.create()\n let mxDx = 0.0\n let nLoops = 1\n if (forceOriginInVolume) {\n nLoops = 2\n } // second pass to reposition origin\n for (let loop = 0; loop < nLoops; loop++) {\n mxDx = 0.0\n for (let i = 0; i < nV; i++) {\n const v = vec3.fromValues(positions[i * 3], positions[i * 3 + 1], positions[i * 3 + 2])\n if (i === 0) {\n vec3.copy(mn, v)\n vec3.copy(mx, v)\n }\n vec3.min(mn, mn, v)\n vec3.max(mx, mx, v)\n vec3.subtract(v, v, origin)\n const dx = vec3.len(v)\n mxDx = Math.max(mxDx, dx)\n }\n if (loop + 1 >= nLoops) {\n break\n }\n let ok = true\n for (let j = 0; j < 3; ++j) {\n if (mn[j] > origin[j]) {\n ok = false\n }\n if (mx[j] < origin[j]) {\n ok = false\n }\n }\n if (ok) {\n break\n }\n vec3.lerp(origin, mn, mx, 0.5)\n log.debug('origin moved inside volume: ', origin)\n }\n const min = [mn[0], mn[1], mn[2]]\n const max = [mx[0], mx[1], mx[2]]\n const furthestVertexFromOrigin = mxDx\n return { min, max, furthestVertexFromOrigin, origin }\n}\n\nexport function isAffineOK(mtx: number[][]): boolean {\n // A good matrix should not have any components that are not a number\n // A good spatial transformation matrix should not have a row or column that is all zeros\n const iOK = [false, false, false, false]\n const jOK = [false, false, false, false]\n for (let i = 0; i < 4; i++) {\n for (let j = 0; j < 4; j++) {\n if (isNaN(mtx[i][j])) {\n return false\n }\n }\n }\n for (let i = 0; i < 3; i++) {\n for (let j = 0; j < 3; j++) {\n if (mtx[i][j] === 0.0) {\n continue\n }\n iOK[i] = true\n jOK[j] = true\n }\n }\n for (let i = 0; i < 3; i++) {\n if (!iOK[i]) {\n return false\n }\n if (!jOK[i]) {\n return false\n }\n }\n return true\n}\n\nexport async function uncompressStream(stream: ReadableStream<Uint8Array>): Promise<ReadableStream<Uint8Array>> {\n const reader = stream.getReader()\n const { done, value } = await reader.read()\n\n // If the first read is done, return an empty stream\n if (done) {\n reader.releaseLock()\n return new ReadableStream({\n start(controller): void {\n controller.close()\n }\n })\n }\n\n // Too short to be compressed\n if (!value || value.length < 2) {\n reader.releaseLock()\n return new ReadableStream({\n start(controller): void {\n if (value) {\n controller.enqueue(value)\n }\n controller.close()\n }\n })\n }\n\n const isGzip = value[0] === 31 && value[1] === 139\n\n // Create new stream starting with the first chunk\n const uncompressedStream = new ReadableStream<Uint8Array>({\n async start(controller): Promise<void> {\n try {\n // Enqueue the first chunk we already read\n controller.enqueue(value)\n\n // Process remaining chunks\n while (true) {\n const { done, value } = await reader.read()\n if (done) {\n controller.close()\n reader.releaseLock()\n break\n }\n controller.enqueue(value)\n }\n } catch (error) {\n controller.error(error)\n reader.releaseLock()\n }\n }\n })\n\n if (isGzip) {\n return uncompressedStream.pipeThrough(new DecompressionStream('gzip'))\n }\n return uncompressedStream\n}\n","import { NIFTI1, NIFTI2 } from 'nifti-reader-js'\nimport { log } from '@/logger'\nimport { NVUtilities } from '@/nvutilities'\nimport { hdrToArrayBuffer, NiiDataType } from '@/nvimage/utils'\nimport type { NVImage, TypedVoxelArray } from '@/nvimage'\n/**\n * Creates a NIFTI1 header object with basic properties.\n */\nexport function createNiftiHeader(\n dims: number[] = [256, 256, 256],\n pixDims: number[] = [1, 1, 1],\n affine: number[] = [1, 0, 0, -128, 0, 1, 0, -128, 0, 0, 1, -128, 0, 0, 0, 1],\n datatypeCode = NiiDataType.DT_UINT8\n): NIFTI1 {\n const hdr = new NIFTI1()\n hdr.littleEndian = true\n hdr.dims = [3, 1, 1, 1, 0, 0, 0, 0]\n hdr.dims[0] = Math.max(3, dims.length)\n for (let i = 0; i < dims.length; i++) {\n hdr.dims[i + 1] = dims[i]\n }\n hdr.pixDims = [1, 1, 1, 1, 1, 0, 0, 0]\n for (let i = 0; i < dims.length; i++) {\n hdr.pixDims[i + 1] = pixDims[i]\n }\n if (affine.length === 16) {\n let k = 0\n for (let i = 0; i < 4; i++) {\n for (let j = 0; j < 4; j++) {\n hdr.affine[i][j] = affine[k]\n k++\n }\n }\n }\n let bpv = 8\n if (datatypeCode === NiiDataType.DT_INT8 || datatypeCode === NiiDataType.DT_UINT8) {\n bpv = 8\n } else if (datatypeCode === NiiDataType.DT_UINT16 || datatypeCode === NiiDataType.DT_INT16) {\n bpv = 16\n } else if (datatypeCode === NiiDataType.DT_FLOAT32 || datatypeCode === NiiDataType.DT_UINT32 || datatypeCode === NiiDataType.DT_INT32 || datatypeCode === NiiDataType.DT_RGBA32) {\n bpv = 32\n } else if (datatypeCode === NiiDataType.DT_FLOAT64) {\n bpv = 64\n } else {\n log.warn('Unsupported NIfTI datatypeCode for header creation: ' + datatypeCode)\n }\n hdr.datatypeCode = datatypeCode\n hdr.numBitsPerVoxel = bpv\n hdr.scl_inter = 0\n hdr.scl_slope = 1 // Default slope should be 1\n hdr.sform_code = 2 // Assume affine is RAS\n hdr.magic = 'n+1'\n hdr.vox_offset = 352 // Standard offset for NIfTI-1 with no extensions\n return hdr\n}\n\n/**\n * Creates a Uint8Array representing a NIFTI file (header + optional image data).\n */\nexport function createNiftiArray(\n dims: number[] = [256, 256, 256],\n pixDims: number[] = [1, 1, 1],\n affine: number[] = [1, 0, 0, -128, 0, 1, 0, -128, 0, 0, 1, -128, 0, 0, 0, 1],\n datatypeCode = NiiDataType.DT_UINT8,\n img: TypedVoxelArray | Uint8Array = new Uint8Array()\n): Uint8Array {\n const hdr = createNiftiHeader(dims, pixDims, affine, datatypeCode)\n // hdrToArrayBuffer should handle creating the byte array correctly based on header info\n const hdrBytes = hdrToArrayBuffer(hdr, false) // Pass header directly\n\n // Ensure the header reports the correct offset, usually 352 for simple NIfTI-1\n hdr.vox_offset = Math.max(352, hdrBytes.length) // Ensure offset is at least header size\n // Re-generate header bytes if vox_offset changed header size itself (unlikely but possible with extensions)\n const finalHdrBytes = hdrToArrayBuffer(hdr, false)\n\n if (img.length < 1) {\n // Return just the header if no image data\n return finalHdrBytes\n }\n\n // Calculate padding needed to reach vox_offset\n const paddingSize = Math.max(0, hdr.vox_offset - finalHdrBytes.length)\n const padding = new Uint8Array(paddingSize)\n\n // Get the image data bytes correctly\n const imgBytes = new Uint8Array(img.buffer, img.byteOffset, img.byteLength)\n\n // Combine header, padding, and image data\n const totalLength = hdr.vox_offset + imgBytes.length\n const outputData = new Uint8Array(totalLength)\n\n outputData.set(finalHdrBytes, 0)\n outputData.set(padding, finalHdrBytes.length)\n outputData.set(imgBytes, hdr.vox_offset) // Place image data at the offset\n\n return outputData\n}\n\n/**\n * Converts NVImage data (header and image) to a NIfTI compliant Uint8Array.\n * Handles potential re-orientation of drawing data if necessary.\n * @param nvImage - The NVImage instance\n * @param drawingBytes - Optional Uint8Array for drawing overlay (assumed to be in RAS order)\n * @returns Uint8Array representing the NIfTI file\n */\nexport function toUint8Array(nvImage: NVImage, drawingBytes: Uint8Array | null = null): Uint8Array {\n if (!nvImage.hdr) {\n throw new Error('NVImage header is not defined for toUint8Array')\n }\n if (!nvImage.img && drawingBytes === null) {\n throw new Error('NVImage image data is not defined for toUint8Array')\n }\n\n const isDrawing = drawingBytes !== null\n // Create a deep copy of the header to modify safely for output\n const hdrCopy = JSON.parse(JSON.stringify(nvImage.hdr)) as NIFTI1 | NIFTI2\n\n // Handle extensions\n const hasExtensions = nvImage.extensions && nvImage.extensions.length > 0\n const extFlag = new Uint8Array(4)\n extFlag[0] = hasExtensions ? 1 : 0\n\n let extensionsData = new Uint8Array(0)\n if (hasExtensions) {\n const blocks: Uint8Array[] = []\n let totalSize = 0\n for (const ext of nvImage.extensions!) {\n const edataBytes = new Uint8Array(ext.edata)\n const block = new Uint8Array(8 + edataBytes.length)\n const dv = new DataView(block.buffer)\n dv.setInt32(0, ext.esize, true)\n dv.setInt32(4, ext.ecode, true)\n block.set(edataBytes, 8)\n blocks.push(block)\n totalSize += block.length\n }\n extensionsData = new Uint8Array(totalSize)\n let offset = 0\n for (const block of blocks) {\n extensionsData.set(block, offset)\n offset += block.length\n }\n }\n\n const headerSize = 348 // nifti-1 standard\n hdrCopy.vox_offset = Math.max(352, headerSize + extFlag.length + extensionsData.length)\n\n // If saving a drawing, ensure output header reflects drawing data type (UINT8) and resets scaling\n if (isDrawing) {\n hdrCopy.datatypeCode = NiiDataType.DT_UINT8\n hdrCopy.numBitsPerVoxel = 8\n hdrCopy.scl_slope = 1.0\n hdrCopy.scl_inter = 0.0\n }\n\n // Generate header bytes using the potentially modified copy\n const hdrBytes = hdrToArrayBuffer(hdrCopy, isDrawing)\n\n let imageBytesToSave: Uint8Array\n\n if (isDrawing) {\n const drawingBytesCurrent = drawingBytes! // Not null asserted by isDrawing check\n const perm = nvImage.permRAS as number[] | undefined\n\n // Check if reorientation from RAS (drawing space) to native space is needed\n if (perm && (perm[0] !== 1 || perm[1] !== 2 || perm[2] !== 3)) {\n log.debug('Reorienting drawing bytes back to native space for saving...')\n const dims = nvImage.hdr!.dims // Use original native dimensions\n const nVox = dims[1] * dims[2] * dims[3] // Total native voxels\n\n // Ensure drawing length matches expected RAS voxel count based on calculated dimsRAS\n const nVoxRAS = nvImage.dimsRAS ? nvImage.dimsRAS[1] * nvImage.dimsRAS[2] * nvImage.dimsRAS[3] : nVox\n if (drawingBytesCurrent.length !== nVoxRAS) {\n console.warn(`Drawing length (${drawingBytesCurrent.length}) does not match expected RAS voxel count (${nVoxRAS}). Cannot reorient drawing reliably.`)\n imageBytesToSave = drawingBytesCurrent // Use original as fallback\n // Ensure necessary transformation arrays exist\n } else if (!nvImage.img2RASstep || !nvImage.img2RASstart || !nvImage.dimsRAS) {\n console.warn(`Missing RAS transformation info (img2RASstep, img2RASstart, dimsRAS). Cannot reorient drawing reliably.`)\n imageBytesToSave = drawingBytesCurrent // Use original as fallback\n } else {\n const step = nvImage.img2RASstep // [stepX, stepY, stepZ] in native index space for RAS increments\n const start = nvImage.img2RASstart // [startX, startY, startZ] starting native index for RAS[0,0,0]\n const dimsRAS = nvImage.dimsRAS // [4, dimRX, dimRY, dimRZ]\n\n const nativeData = new Uint8Array(nVox)\n nativeData.fill(0) // Initialize with background value (e.g., 0)\n const inputDrawingRAS = drawingBytesCurrent // Source data is RAS ordered\n let rasIndex = 0 // Index for the flat inputDrawingRAS array\n\n // Iterate through the source RAS dimensions\n for (let rz = 0; rz < dimsRAS[3]; rz++) {\n const zi = start[2] + rz * step[2] // Native offset component for this RAS Z\n for (let ry = 0; ry < dimsRAS[2]; ry++) {\n const yi = start[1] + ry * step[1] // Native offset component for this RAS Y\n for (let rx = 0; rx < dimsRAS[1]; rx++) {\n const xi = start[0] + rx * step[0] // Native offset component for this RAS X\n const nativeIndex = xi + yi + zi // Calculate the final index in the native orientation buffer\n\n // Check bounds for safety before writing\n if (nativeIndex >= 0 && nativeIndex < nVox) {\n nativeData[nativeIndex] = inputDrawingRAS[rasIndex] // Place RAS voxel into calculated native position\n } else if (rasIndex < inputDrawingRAS.length) {\n // Log if we calculate an invalid native index but still have RAS data\n console.warn(`Calculated native index ${nativeIndex} is out of bounds [0..${nVox - 1}] during drawing reorientation.`)\n }\n rasIndex++ // Increment index into the flat RAS drawing array\n }\n }\n }\n imageBytesToSave = nativeData // Use the reoriented data\n }\n } else {\n // No reorientation needed (image is already native/RAS or drawing is meant to be native)\n imageBytesToSave = drawingBytesCurrent\n }\n } else {\n // Not a drawing, use the main image data directly\n if (!nvImage.img) {\n throw new Error('NVImage image data is null when trying to save non-drawing.')\n }\n imageBytesToSave = new Uint8Array(nvImage.img.buffer, nvImage.img.byteOffset, nvImage.img.byteLength)\n }\n\n // Calculate padding needed to reach the specified vox_offset in the header\n const preImageBytesSize = hdrBytes.length + extFlag.length + extensionsData.length\n const paddingSize = Math.max(0, hdrCopy.vox_offset - preImageBytesSize)\n const padding = new Uint8Array(paddingSize)\n\n const totalLength = hdrCopy.vox_offset + imageBytesToSave.length\n const outputData = new Uint8Array(totalLength)\n\n let offset = 0\n outputData.set(hdrBytes, offset)\n offset += hdrBytes.length\n\n outputData.set(extFlag, offset)\n offset += extFlag.length\n\n outputData.set(extensionsData, offset)\n offset += extensionsData.length\n\n outputData.set(padding, offset)\n offset += padding.length\n\n outputData.set(imageBytesToSave, hdrCopy.vox_offset)\n\n return outputData\n}\n\n/**\n * Generates the NIfTI file as a Uint8Array and optionally compresses it.\n * @param nvImage - The NVImage instance\n * @param fnm - Filename (used to determine if compression is needed, .gz suffix)\n * @param drawing8 - Optional drawing overlay data\n * @returns Uint8Array (potentially compressed)\n */\nexport async function saveToUint8Array(nvImage: NVImage, fnm: string, drawing8: Uint8Array | null = null): Promise<Uint8Array> {\n // Generate the core NIfTI byte array first\n const odata = toUint8Array(nvImage, drawing8)\n // Check filename extension for compression request\n const compress = fnm.toLowerCase().endsWith('.gz')\n\n if (compress) {\n try {\n // Use NVUtilities to compress the data\n const compressedData = await NVUtilities.compress(odata, 'gzip')\n return new Uint8Array(compressedData)\n } catch (error) {\n log.error('Compression failed:', error)\n log.warn('Returning uncompressed data due to compression error.')\n return odata // Return uncompressed data as fallback\n }\n } else {\n // No compression needed\n return odata\n }\n}\n\n/**\n * Generates the NIfTI file data and triggers a browser download.\n * @param nvImage - The NVImage instance\n * @param fnm - Filename for the downloaded file. If empty, returns data only.\n * @param drawing8 - Optional drawing overlay data\n * @returns The generated Uint8Array (potentially compressed)\n */\nexport async function saveToDisk(nvImage: NVImage, fnm: string = '', drawing8: Uint8Array | null = null): Promise<Uint8Array> {\n // Always generate the data first, handling potential compression based on filename\n const saveData = await saveToUint8Array(nvImage, fnm, drawing8)\n\n if (!fnm) {\n log.debug('saveToDisk: empty file name, returning data as Uint8Array rather than triggering download')\n return saveData // Return data if filename is empty\n }\n\n try {\n // Create a Blob from the final data (compressed or not)\n const blob = new Blob([saveData.buffer], {\n type: 'application/octet-stream' // Standard type for binary download\n })\n // Create a temporary URL for the Blob\n const blobUrl = URL.createObjectURL(blob)\n // Create a link element to trigger the download\n const link = document.createElement('a')\n link.setAttribute('href', blobUrl)\n link.setAttribute('download', fnm) // Set the filename for the download\n link.style.visibility = 'hidden' // Hide the link\n document.body.appendChild(link) // Add link to the document\n link.click() // Simulate a click to trigger download\n document.body.removeChild(link) // Remove the link from the document\n // Revoke the temporary URL after a short delay to allow download initiation\n setTimeout(() => URL.revokeObjectURL(blobUrl), 100)\n } catch (e) {\n log.error('Failed to trigger download:', e)\n }\n\n return saveData // Return the data regardless of download success/triggering\n}\n","import { vec4 } from 'gl-matrix'\nimport { log } from '@/logger'\nimport { NiiDataType } from '@/nvimage/utils'\nimport type { NVImage, TypedVoxelArray } from '@/nvimage'\n\n/**\n * Returns all voxel channel values at the specified coordinates.\n * For scalar images, this will return a single-value array [value].\n * For multi-channel images (e.g., RGBA), this will return an array with multiple values.\n *\n * @param nvImage - The NVImage instance.\n * @param x - X coordinate (0-indexed).\n * @param y - Y coordinate (0-indexed).\n * @param z - Z coordinate (0-indexed).\n * @param frame4D - Optional 4D frame index (default is 0).\n * @param isReadImaginary - If true, returns data from the imaginary component (if present).\n * @returns An array of one or more voxel values at the specified location.\n *\n * @see https://niivue.com/demos/features/voxel.html\n */\nexport function getValues(nvImage: NVImage, x: number, y: number, z: number, frame4D = 0, isReadImaginary = false): number[] {\n if (!nvImage.hdr) {\n throw new Error('getValue: NVImage header is not defined.')\n }\n if (!isReadImaginary && !nvImage.img) {\n throw new Error('getValue: NVImage image data is not defined.')\n }\n if (isReadImaginary && !nvImage.imaginary) {\n log.warn('getValue: Attempted to read imaginary data, but none exists.')\n return [0]\n }\n\n const nx = nvImage.hdr.dims[1]\n const ny = nvImage.hdr.dims[2]\n const nz = nvImage.hdr.dims[3]\n\n const perm = nvImage.permRAS!.slice()\n if (perm[0] !== 1 || perm[1] !== 2 || perm[2] !== 3) {\n const pos = vec4.fromValues(x, y, z, 1)\n vec4.transformMat4(pos, pos, nvImage.toRASvox!)\n x = pos[0]\n y = pos[1]\n z = pos[2]\n }\n\n // Clamp coordinates to valid range\n x = Math.max(0, Math.min(Math.round(x), nx - 1))\n y = Math.max(0, Math.min(Math.round(y), ny - 1))\n z = Math.max(0, Math.min(Math.round(z), nz - 1))\n frame4D = Math.max(0, frame4D)\n\n let vx = x + y * nx + z * nx * ny // voxel index within a 3D volume\n\n // Handle RGB(A) data - calculate luminance\n if (nvImage.hdr.datatypeCode === NiiDataType.DT_RGBA32) {\n if (!nvImage.img) {\n return [0]\n }\n vx *= 4 // 4 bytes per voxel\n // Check bounds for RGBA index access\n if (vx + 3 >= nvImage.img.length) {\n log.warn(`getValue: Calculated index ${vx} out of bounds for RGBA data.`)\n return [0] // Or throw? Return 0 for safety.\n }\n return [nvImage.img[vx], nvImage.img[vx + 1], nvImage.img[vx + 2], nvImage.img[vx + 3]]\n }\n if (nvImage.hdr.datatypeCode === NiiDataType.DT_RGB24) {\n if (!nvImage.img) {\n return [0]\n }\n vx *= 3 // 3 bytes per voxel\n if (vx + 2 >= nvImage.img.length) {\n log.warn(`getValue: Calculated index ${vx} out of bounds for RGB data.`)\n return [0]\n }\n return [nvImage.img[vx], nvImage.img[vx + 1], nvImage.img[vx + 2]]\n }\n\n // Calculate offset for 4D frame\n const nVox3D = nx * ny * nz\n const volOffset = frame4D * nVox3D\n const finalVxIndex = vx + volOffset\n\n // Select the correct data array\n const dataArray = isReadImaginary ? nvImage.imaginary! : nvImage.img!\n\n // Check final index bounds\n if (finalVxIndex < 0 || finalVxIndex >= dataArray.length) {\n return [0]\n }\n\n const rawValue = dataArray[finalVxIndex]\n\n // Apply scaling slope and intercept\n // Use default 1.0 slope if hdr value is 0 or NaN, default 0.0 intercept if NaN\n const slope = isNaN(nvImage.hdr.scl_slope) || nvImage.hdr.scl_slope === 0 ? 1.0 : nvImage.hdr.scl_slope\n const inter = isNaN(nvImage.hdr.scl_inter) ? 0.0 : nvImage.hdr.scl_inter\n\n return [slope * rawValue + inter]\n}\n\n/**\n * Returns voxel intensity at specified coordinates.\n * @param nvImage - The NVImage instance\n * @param x - X coordinate (0-indexed)\n * @param y - Y coordinate (0-indexed)\n * @param z - Z coordinate (0-indexed)\n * @param frame4D - 4D frame index (0-indexed)\n * @param isReadImaginary - Flag to read from imaginary data array if complex\n * @returns Scaled voxel intensity\n */\nexport function getValue(nvImage: NVImage, x: number, y: number, z: number, frame4D = 0, isReadImaginary = false): number {\n const vals = getValues(nvImage, x, y, z, frame4D, isReadImaginary)\n if (vals.length < 3) {\n return vals[0]\n }\n // convert RGB to luminance Y = 0.2126 R + 0.7152 G + 0.0722 B (Rec. 709)\n const lum = vals[0] * 0.2126 + vals[1] * 0.7152 + vals[2] * 0.0722\n return lum\n}\n\n/**\n * Reads a 3D slab of voxels from a volume, specified in RAS coordinates.\n * @param nvImage - The NVImage instance\n * @param voxStartRAS - First row, column, slice (RAS order, 0-indexed) for selection\n * @param voxEndRAS - Final row, column, slice (RAS order, 0-indexed) for selection\n * @param dataType - Output array type: 'same', 'uint8', 'float32', 'scaled', 'normalized', 'windowed'\n * @returns Tuple: [TypedVoxelArray, slabDimensions]\n */\nexport function getVolumeData(nvImage: NVImage, voxStartRAS: number[] = [-1, 0, 0], voxEndRAS: number[] = [0, 0, 0], dataType = 'same'): [TypedVoxelArray, number[]] {\n const defaultResult: [TypedVoxelArray, number[]] = [new Uint8Array(), [0, 0, 0]]\n\n if (!nvImage.hdr || !nvImage.img || !nvImage.dimsRAS || !nvImage.img2RASstep || !nvImage.img2RASstart) {\n log.error('getVolumeData: Missing required NVImage properties (hdr, img, dimsRAS, img2RASstep/start).')\n return defaultResult\n }\n // Ensure input arrays have 3 elements\n voxStartRAS = voxStartRAS.slice(0, 3)\n voxEndRAS = voxEndRAS.slice(0, 3)\n\n if (Math.min(...voxStartRAS) < 0 || Math.min(...voxEndRAS) < 0) {\n log.warn('getVolumeData: Invalid start or end coordinates provided.')\n return defaultResult\n }\n\n const dimsRAS = nvImage.dimsRAS.slice(1, 4) // Get RAS dimensions [nx, ny, nz]\n\n // Clamp coordinates to valid RAS range and ensure start <= end\n for (let i = 0; i < 3; i++) {\n voxStartRAS[i] = Math.max(0, Math.min(Math.round(voxStartRAS[i]), dimsRAS[i] - 1))\n voxEndRAS[i] = Math.max(0, Math.min(Math.round(voxEndRAS[i]), dimsRAS[i] - 1))\n if (voxEndRAS[i] < voxStartRAS[i]) {\n const tmp = voxEndRAS[i]\n voxEndRAS[i] = voxStartRAS[i]\n voxStartRAS[i] = tmp\n }\n }\n\n const slabDims = [voxEndRAS[0] - voxStartRAS[0] + 1, voxEndRAS[1] - voxStartRAS[1] + 1, voxEndRAS[2] - voxStartRAS[2] + 1]\n const slabNVox = slabDims[0] * slabDims[1] * slabDims[2]\n\n if (slabNVox <= 0) {\n log.warn('getVolumeData: Calculated slab size is zero or negative.')\n return defaultResult\n }\n\n let OutputArrayConstructor: new (length: number) => TypedVoxelArray = nvImage.img.constructor as new (length: number) => TypedVoxelArray // Default to same as input\n\n if (dataType === 'uint8') {\n OutputArrayConstructor = Uint8Array\n } else if (dataType === 'int16') {\n OutputArrayConstructor = Int16Array\n } else if (dataType === 'uint16') {\n OutputArrayConstructor = Uint16Array\n } else if (dataType === 'float32' || dataType === 'scaled' || dataType === 'normalized' || dataType === 'windowed') {\n OutputArrayConstructor = Float32Array\n } else if (dataType === 'float64') {\n OutputArrayConstructor = Float64Array\n } else if (dataType !== 'same') {\n log.warn(`getVolumeData: Unsupported dataType '${dataType}'. Using 'same'.`)\n }\n\n // Create the output array\n let outputImg: TypedVoxelArray\n try {\n outputImg = new OutputArrayConstructor(slabNVox)\n } catch (e) {\n log.error(`getVolumeData: Failed to create output array for dataType '${dataType}'.`, e)\n return defaultResult\n }\n\n // Get transformation parameters\n const step = nvImage.img2RASstep\n const start = nvImage.img2RASstart\n const sourceImg = nvImage.img // Source data in native orientation\n\n let outputIndex = 0\n // Iterate through the requested RAS slab dimensions\n for (let rz = voxStartRAS[2]; rz <= voxEndRAS[2]; rz++) {\n const zi = start[2] + rz * step[2] // Native offset component for RAS Z\n for (let ry = voxStartRAS[1]; ry <= voxEndRAS[1]; ry++) {\n const yi = start[1] + ry * step[1] // Native offset component for RAS Y\n for (let rx = voxStartRAS[0]; rx <= voxEndRAS[0]; rx++) {\n const xi = start[0] + rx * step[0] // Native offset component for RAS X\n const nativeIndex = xi + yi + zi // Final index in the native source buffer\n\n let value = 0\n // Safely read from source image\n if (nativeIndex >= 0 && nativeIndex < sourceImg.length) {\n value = sourceImg[nativeIndex]\n }\n\n // Store the raw value in the output array\n outputImg[outputIndex++] = value\n }\n }\n }\n\n // Apply post-processing based on dataType AFTER extracting raw values\n const slope = isNaN(nvImage.hdr.scl_slope) || nvImage.hdr.scl_slope === 0 ? 1.0 : nvImage.hdr.scl_slope\n const inter = isNaN(nvImage.hdr.scl_inter) ? 0.0 : nvImage.hdr.scl_inter\n\n if (dataType === 'scaled' || dataType === 'normalized' || dataType === 'windowed') {\n // Ensure output is Float32 if scaling is requested but wasn't the original type\n if (!(outputImg instanceof Float32Array)) {\n log.warn(`getVolumeData: Converting output to Float32 for scaling type '${dataType}'.`)\n outputImg = Float32Array.from(outputImg)\n }\n for (let i = 0; i < outputImg.length; i++) {\n outputImg[i] = outputImg[i] * slope + inter\n }\n }\n\n if (dataType === 'normalized' || dataType === 'windowed') {\n let minVal = nvImage.cal_min\n let maxVal = nvImage.cal_max\n\n if (dataType === 'normalized') {\n minVal = nvImage.global_min\n maxVal = nvImage.global_max\n }\n\n const range = maxVal - minVal\n const scale = range === 0 ? 0 : 1 / range\n\n for (let i = 0; i < outputImg.length; i++) {\n outputImg[i] = (outputImg[i] - minVal) * scale\n outputImg[i] = Math.max(0, Math.min(outputImg[i], 1))\n }\n }\n\n return [outputImg, slabDims]\n}\n\n/**\n * Writes a 3D slab of voxels into a volume, specified in RAS coordinates.\n * @param nvImage - The NVImage instance to modify\n * @param voxStartRAS - First row, column, slice (RAS order, 0-indexed) for selection\n * @param voxEndRAS - Final row, column, slice (RAS order, 0-indexed) for selection\n * @param slabData - Array of voxel values (TypedVoxelArray) matching slab dimensions\n */\nexport function setVolumeData(nvImage: NVImage, voxStartRAS: number[] = [-1, 0, 0], voxEndRAS: number[] = [0, 0, 0], slabData: TypedVoxelArray = new Uint8Array()): void {\n if (!nvImage.hdr || !nvImage.img || !nvImage.dimsRAS || !nvImage.img2RASstep || !nvImage.img2RASstart) {\n log.error('setVolumeData: Missing required NVImage properties (hdr, img, dimsRAS, img2RASstep/start).')\n return\n }\n\n if (slabData.length < 1) {\n log.warn('setVolumeData: Input slabData is empty.')\n return\n }\n // Ensure input arrays have 3 elements\n voxStartRAS = voxStartRAS.slice(0, 3)\n voxEndRAS = voxEndRAS.slice(0, 3)\n\n if (Math.min(...voxStartRAS) < 0 || Math.min(...voxEndRAS) < 0) {\n log.warn('setVolumeData: Invalid start or end coordinates provided.')\n return\n }\n\n const dimsRAS = nvImage.dimsRAS.slice(1, 4) // Get RAS dimensions [nx, ny, nz]\n\n // Clamp coordinates to valid RAS range and ensure start <= end\n for (let i = 0; i < 3; i++) {\n voxStartRAS[i] = Math.max(0, Math.min(Math.round(voxStartRAS[i]), dimsRAS[i] - 1))\n voxEndRAS[i] = Math.max(0, Math.min(Math.round(voxEndRAS[i]), dimsRAS[i] - 1))\n if (voxEndRAS[i] < voxStartRAS[i]) {\n const tmp = voxEndRAS[i]\n voxEndRAS[i] = voxStartRAS[i]\n voxStartRAS[i] = tmp\n }\n }\n\n const slabDims = [voxEndRAS[0] - voxStartRAS[0] + 1, voxEndRAS[1] - voxStartRAS[1] + 1, voxEndRAS[2] - voxStartRAS[2] + 1]\n const slabNVox = slabDims[0] * slabDims[1] * slabDims[2]\n\n if (slabNVox <= 0) {\n log.warn('setVolumeData: Calculated slab size is zero or negative.')\n return\n }\n\n if (slabData.length < slabNVox) {\n log.error(`setVolumeData: Input slabData length (${slabData.length}) is less than the calculated slab size (${slabNVox}).`)\n return\n }\n\n // Get transformation parameters\n const step = nvImage.img2RASstep\n const start = nvImage.img2RASstart\n const targetImg = nvImage.img // Target data in native orientation\n\n let sourceIndex = 0\n // Iterate through the requested RAS slab dimensions\n for (let rz = voxStartRAS[2]; rz <= voxEndRAS[2]; rz++) {\n const zi = start[2] + rz * step[2] // Native offset component for RAS Z\n for (let ry = voxStartRAS[1]; ry <= voxEndRAS[1]; ry++) {\n const yi = start[1] + ry * step[1] // Native offset component for RAS Y\n for (let rx = voxStartRAS[0]; rx <= voxEndRAS[0]; rx++) {\n const xi = start[0] + rx * step[0] // Native offset component for RAS X\n const nativeIndex = xi + yi + zi // Final index in the native target buffer\n\n if (nativeIndex >= 0 && nativeIndex < targetImg.length) {\n targetImg[nativeIndex] = slabData[sourceIndex]\n }\n sourceIndex++\n }\n }\n }\n // Note: This function does NOT handle inverse scaling (converting scaled slabData back to raw).\n // The input slabData is assumed to be in the correct raw data type for the target nvImage.img.\n}\n","import { NIFTI1, NIFTIEXTENSION } from 'nifti-reader-js'\nimport { mat4, vec3, vec4 } from 'gl-matrix'\nimport { log } from '@/logger'\nimport type { NVImage } from '@/nvimage'\nimport { NiiDataType } from '@/nvimage/utils'\nimport { NVUtilities } from '@/nvutilities'\n\n/**\n * Helper function to convert voxel location (row, column slice, indexed from 0) to world space.\n * @param XYZ - Voxel coordinates [x, y, z]\n * @param mtx - 4x4 transformation matrix\n * @returns World space coordinates as vec3\n */\nfunction vox2mm(XYZ: number[], mtx: mat4): vec3 {\n const sform = mat4.clone(mtx)\n mat4.transpose(sform, sform)\n const pos = vec4.fromValues(XYZ[0], XYZ[1], XYZ[2], 1)\n vec4.transformMat4(pos, pos, sform)\n const pos3 = vec3.fromValues(pos[0], pos[1], pos[2])\n return pos3\n}\n\n/**\n * Helper function to determine spacing voxel centers (rows, columns, slices).\n * @param nvImage - The NVImage instance whose header will be modified\n */\nfunction setPixDimFromSForm(nvImage: NVImage): void {\n if (!nvImage.hdr) {\n throw new Error('hdr not defined')\n }\n const m = nvImage.hdr.affine\n const mat = mat4.fromValues(m[0][0], m[0][1], m[0][2], m[0][3], m[1][0], m[1][1], m[1][2], m[1][3], m[2][0], m[2][1], m[2][2], m[2][3], m[3][0], m[3][1], m[3][2], m[3][3])\n const mm000 = vox2mm([0, 0, 0], mat)\n const mm100 = vox2mm([1, 0, 0], mat)\n vec3.subtract(mm100, mm100, mm000)\n const mm010 = vox2mm([0, 1, 0], mat)\n vec3.subtract(mm010, mm010, mm000)\n const mm001 = vox2mm([0, 0, 1], mat)\n vec3.subtract(mm001, mm001, mm000)\n nvImage.hdr.pixDims[1] = vec3.length(mm100)\n nvImage.hdr.pixDims[2] = vec3.length(mm010)\n nvImage.hdr.pixDims[3] = vec3.length(mm001)\n}\n\n/**\n * Helper function to convert AFNI head/brik space to NIfTI format.\n *\n * Based on AFNI source code:\n * https://github.com/afni/afni/blob/d6997e71f2b625ac1199460576d48f3136dac62c/src/thd_niftiwrite.c#L315\n *\n * @param nvImage - The NVImage instance whose header will be modified\n * @param xyzDelta - Voxel spacing in each dimension\n * @param xyzOrigin - Origin coordinates\n * @param orientSpecific - Orientation codes for each axis\n */\nfunction THD_daxes_to_NIFTI(nvImage: NVImage, xyzDelta: number[], xyzOrigin: number[], orientSpecific: number[]): void {\n const hdr = nvImage.hdr\n\n if (hdr === null) {\n throw new Error('HDR is not set')\n }\n\n hdr.sform_code = 2\n const ORIENT_xyz = 'xxyyzzg' // note strings indexed from 0!\n let nif_x_axnum = -1\n let nif_y_axnum = -1\n let nif_z_axnum = -1\n const axcode = ['x', 'y', 'z']\n axcode[0] = ORIENT_xyz[orientSpecific[0]]\n axcode[1] = ORIENT_xyz[orientSpecific[1]]\n axcode[2] = ORIENT_xyz[orientSpecific[2]]\n const axstep = xyzDelta.slice(0, 3)\n const axstart = xyzOrigin.slice(0, 3)\n for (let ii = 0; ii < 3; ii++) {\n if (axcode[ii] === 'x') {\n nif_x_axnum = ii\n } else if (axcode[ii] === 'y') {\n nif_y_axnum = ii\n } else {\n nif_z_axnum = ii\n }\n }\n if (nif_x_axnum < 0 || nif_y_axnum < 0 || nif_z_axnum < 0) {\n return\n } // not assigned\n if (nif_x_axnum === nif_y_axnum || nif_x_axnum === nif_z_axnum || nif_y_axnum === nif_z_axnum) {\n return\n } // not assigned\n hdr.pixDims[1] = Math.abs(axstep[0])\n hdr.pixDims[2] = Math.abs(axstep[1])\n hdr.pixDims[3] = Math.abs(axstep[2])\n hdr.affine = [\n [1, 0, 0, 0],\n [0, 1, 0, 0],\n [0, 0, 1, 0],\n [0, 0, 0, 1]\n ]\n hdr.affine[0][nif_x_axnum] = -axstep[nif_x_axnum]\n hdr.affine[1][nif_y_axnum] = -axstep[nif_y_axnum]\n hdr.affine[2][nif_z_axnum] = axstep[nif_z_axnum]\n hdr.affine[0][3] = -axstart[nif_x_axnum]\n hdr.affine[1][3] = -axstart[nif_y_axnum]\n hdr.affine[2][3] = axstart[nif_z_axnum]\n}\n\n/**\n * Reads AFNI HEAD/BRIK format image, modifying the provided NVImage header\n * and returning the raw image data buffer.\n *\n * HEAD files contain the header information as text attributes.\n * BRIK files contain the binary image data.\n *\n * Format specification: https://afni.nimh.nih.gov/pub/dist/doc/program_help/README.attributes.html\n *\n * @param nvImage - The NVImage instance whose header will be modified.\n * @param dataBuffer - ArrayBuffer containing the HEAD file data.\n * @param pairedImgData - ArrayBuffer containing the BRIK file data (required).\n * @returns Promise resolving to ArrayBuffer containing the image data.\n * @throws Error if pairedImgData is not provided.\n */\nexport async function readHEAD(nvImage: NVImage, dataBuffer: ArrayBuffer, pairedImgData: ArrayBuffer | null): Promise<ArrayBuffer> {\n nvImage.hdr = new NIFTI1()\n const hdr = nvImage.hdr\n hdr.dims[0] = 3\n hdr.pixDims = [1, 1, 1, 1, 1, 0, 0, 0]\n let orientSpecific = [0, 0, 0]\n let xyzOrigin = [0, 0, 0]\n let xyzDelta = [1, 1, 1]\n const txt = new TextDecoder().decode(dataBuffer)\n const lines = txt.split(/\\r?\\n/)\n // embed entire AFNI HEAD text as NIfTI extension\n const mod = (dataBuffer.byteLength + 8) % 16\n const len = dataBuffer.byteLength + (16 - mod)\n log.debug(dataBuffer.byteLength, 'len', len)\n const extBuffer = new ArrayBuffer(len)\n new Uint8Array(extBuffer).set(new Uint8Array(dataBuffer))\n const newExtension = new NIFTIEXTENSION(len + 8, 42, extBuffer, true)\n hdr.addExtension(newExtension)\n hdr.extensionCode = 42\n hdr.extensionFlag[0] = 1\n hdr.extensionSize = len + 8\n // Done creating an extension\n const nlines = lines.length\n let i = 0\n let hasIJK_TO_DICOM_REAL = false\n while (i < nlines) {\n let line = lines[i] // e.g. 'type = string-attribute'\n i++\n if (!line.startsWith('type')) {\n continue\n } // n.b. white space varies, \"type =\" vs \"type =\"\n const isInt = line.includes('integer-attribute')\n const isFloat = line.includes('float-attribute')\n line = lines[i] // e.g. 'name = IDCODE_DATE'\n i++\n if (!line.startsWith('name')) {\n continue\n }\n let items: Array<string | number> = line.split('= ')\n const key = items[1] // e.g. 'IDCODE_DATE'\n line = lines[i] // e.g. 'count = 5'\n i++\n items = line.split('= ')\n let count = parseInt(items[1] as string) // e.g. '5'\n if (count < 1) {\n continue\n }\n line = lines[i] // e.g. ''LSB_FIRST~'\n i++\n items = line.trim().split(/\\s+/)\n if (isFloat || isInt) {\n // read arrays written on multiple lines\n while (items.length < count) {\n line = lines[i] // e.g. ''LSB_FIRST~'\n i++\n const items2 = line.trim().split(/\\s+/)\n items.push(...items2)\n }\n for (let j = 0; j < count; j++) {\n items[j] = parseFloat(items[j] as string)\n }\n }\n switch (key) {\n case 'BYTEORDER_STRING':\n if ((items[0] as string).includes('LSB_FIRST')) {\n hdr.littleEndian = true\n } else if ((items[0] as string).includes('MSB_FIRST')) {\n hdr.littleEndian = false\n }\n break\n case 'BRICK_TYPES':\n {\n hdr.dims[4] = count\n const datatype = parseInt(items[0] as string)\n if (datatype === 0) {\n hdr.numBitsPerVoxel = 8\n hdr.datatypeCode = NiiDataType.DT_UINT8\n } else if (datatype === 1) {\n hdr.numBitsPerVoxel = 16\n hdr.datatypeCode = NiiDataType.DT_INT16\n } else if (datatype === 3) {\n hdr.numBitsPerVoxel = 32\n hdr.datatypeCode = NiiDataType.DT_FLOAT32\n } else {\n log.warn('Unknown BRICK_TYPES ', datatype)\n }\n }\n break\n case 'IJK_TO_DICOM_REAL':\n if (count < 12) {\n break\n }\n hasIJK_TO_DICOM_REAL = true\n hdr.sform_code = 2\n // note DICOM space is LPS while NIfTI is RAS\n hdr.affine = [\n [-items[0], -items[1], -items[2], -items[3]],\n [-items[4], -items[5], -items[6], -items[7]],\n // TODO don't reuse items for numeric values\n [items[8] as number, items[9] as number, items[10] as number, items[11] as number],\n [0, 0, 0, 1]\n ]\n break\n case 'DATASET_DIMENSIONS':\n count = Math.max(count, 3)\n for (let j = 0; j < count; j++) {\n hdr.dims[j + 1] = items[j] as number\n }\n break\n case 'ORIENT_SPECIFIC':\n orientSpecific = items as number[]\n break\n case 'ORIGIN':\n xyzOrigin = items as number[]\n break\n case 'DELTA':\n xyzDelta = items as number[]\n break\n case 'TAXIS_FLOATS':\n hdr.pixDims[4] = items[0] as number\n break\n default:\n log.warn('Unknown:', key)\n } // read item\n } // read all lines\n if (!hasIJK_TO_DICOM_REAL) {\n THD_daxes_to_NIFTI(nvImage, xyzDelta, xyzOrigin, orientSpecific)\n } else {\n setPixDimFromSForm(nvImage)\n }\n const nBytes = (hdr.numBitsPerVoxel / 8) * hdr.dims[1] * hdr.dims[2] * hdr.dims[3] * hdr.dims[4]\n if (!pairedImgData) {\n throw new Error('pairedImgData not set')\n }\n if (pairedImgData.byteLength < nBytes) {\n // n.b. npm run dev implicitly extracts gz, npm run demo does not!\n return await NVUtilities.decompressToBuffer(new Uint8Array(pairedImgData))\n }\n return pairedImgData.slice(0)\n}\n","import { NIFTI1 } from 'nifti-reader-js'\nimport { log } from '@/logger'\nimport type { NVImage } from '@/nvimage'\nimport { NiiDataType } from '@/nvimage/utils'\n\n/**\n * Reads BrainVoyager V16 format image, modifying the provided NVImage header\n * and returning the raw image data buffer.\n * @param nvImage - The NVImage instance whose header will be modified.\n * @param buffer - ArrayBuffer containing the V16 file data.\n * @returns ArrayBuffer containing the image data.\n */\nexport function readV16(nvImage: NVImage, buffer: ArrayBuffer): ArrayBuffer {\n nvImage.hdr = new NIFTI1()\n const hdr = nvImage.hdr\n hdr.dims = [3, 1, 1, 1, 0, 0, 0, 0]\n hdr.pixDims = [1, 1, 1, 1, 1, 0, 0, 0]\n const reader = new DataView(buffer)\n hdr.dims[1] = reader.getUint16(0, true)\n hdr.dims[2] = reader.getUint16(2, true)\n hdr.dims[3] = reader.getUint16(4, true)\n const nBytes = 2 * hdr.dims[1] * hdr.dims[2] * hdr.dims[3]\n if (nBytes + 6 !== buffer.byteLength) {\n log.warn('This does not look like a valid BrainVoyager V16 file')\n }\n hdr.numBitsPerVoxel = 16\n hdr.datatypeCode = NiiDataType.DT_UINT16\n log.warn('Warning: V16 files have no spatial transforms')\n hdr.affine = [\n [0, 0, -hdr.pixDims[1], (hdr.dims[1] - 2) * 0.5 * hdr.pixDims[1]],\n [-hdr.pixDims[2], 0, 0, (hdr.dims[2] - 2) * 0.5 * hdr.pixDims[2]],\n [0, -hdr.pixDims[3], 0, (hdr.dims[3] - 2) * 0.5 * hdr.pixDims[3]],\n [0, 0, 0, 1]\n ]\n hdr.littleEndian = true\n return buffer.slice(6)\n}\n\n/**\n * Reads BrainVoyager VMR format image, modifying the provided NVImage header\n * and returning the raw image data buffer.\n *\n * Format specification:\n * https://support.brainvoyager.com/brainvoyager/automation-development/84-file-formats/343-developer-guide-2-6-the-format-of-vmr-files\n *\n * @param nvImage - The NVImage instance whose header will be modified.\n * @param buffer - ArrayBuffer containing the VMR file data.\n * @returns ArrayBuffer containing the image data.\n */\nexport function readVMR(nvImage: NVImage, buffer: ArrayBuffer): ArrayBuffer {\n nvImage.hdr = new NIFTI1()\n const hdr = nvImage.hdr\n hdr.dims = [3, 1, 1, 1, 0, 0, 0, 0]\n hdr.pixDims = [1, 1, 1, 1, 1, 0, 0, 0]\n const reader = new DataView(buffer)\n const version = reader.getUint16(0, true)\n if (version !== 4) {\n log.warn('Not a valid version 4 VMR image')\n }\n hdr.dims[1] = reader.getUint16(2, true)\n hdr.dims[2] = reader.getUint16(4, true)\n hdr.dims[3] = reader.getUint16(6, true)\n const nBytes = hdr.dims[1] * hdr.dims[2] * hdr.dims[3]\n if (version >= 4) {\n let pos = 8 + nBytes // offset to post header\n // let xoff = reader.getUint16(pos, true);\n // let yoff = reader.getUint16(pos + 2, true);\n // let zoff = reader.getUint16(pos + 4, true);\n // let framingCube = reader.getUint16(pos + 6, true);\n // let posInfo = reader.getUint32(pos + 8, true);\n // let coordSys = reader.getUint32(pos + 12, true);\n // let XmmStart = reader.getFloat32(pos + 16, true);\n // let YmmStart = reader.getFloat32(pos + 20, true);\n // let ZmmStart = reader.getFloat32(pos + 24, true);\n // let XmmEnd = reader.getFloat32(pos + 28, true);\n // let YmmEnd = reader.getFloat32(pos + 32, true);\n // let ZmmEnd = reader.getFloat32(pos + 36, true);\n // let Xsl = reader.getFloat32(pos + 40, true);\n // let Ysl = reader.getFloat32(pos + 44, true);\n // let Zsl = reader.getFloat32(pos + 48, true);\n // let colDirX = reader.getFloat32(pos + 52, true);\n // let colDirY = reader.getFloat32(pos + 56, true);\n // let colDirZ = reader.getFloat32(pos + 60, true);\n // let nRow = reader.getUint32(pos + 64, true);\n // let nCol = reader.getUint32(pos + 68, true);\n // let FOVrow = reader.getFloat32(pos + 72, true);\n // let FOVcol = reader.getFloat32(pos + 76, true);\n // let sliceThickness = reader.getFloat32(pos + 80, true);\n // let gapThickness = reader.getFloat32(pos + 84, true);\n const nSpatialTransforms = reader.getUint32(pos + 88, true)\n pos = pos + 92\n if (nSpatialTransforms > 0) {\n const len = buffer.byteLength\n for (let i = 0; i < nSpatialTransforms; i++) {\n // read variable length name name...\n while (pos < len && reader.getUint8(pos) !== 0) {\n pos++\n }\n pos++\n // let typ = reader.getUint32(pos, true);\n pos += 4\n // read variable length name name...\n while (pos < len && reader.getUint8(pos) !== 0) {\n pos++\n }\n pos++\n const nValues = reader.getUint32(pos, true)\n pos += 4\n for (let j = 0; j < nValues; j++) {\n pos += 4\n }\n }\n }\n // let LRconv = reader.getUint8(pos);\n // let ref = reader.getUint8(pos + 1);\n hdr.pixDims[1] = reader.getFloat32(pos + 2, true)\n hdr.pixDims[2] = reader.getFloat32(pos + 6, true)\n hdr.pixDims[3] = reader.getFloat32(pos + 10, true)\n // let isVer = reader.getUint8(pos + 14);\n // let isTal = reader.getUint8(pos + 15);\n // let minInten = reader.getInt32(pos + 16, true);\n // let meanInten = reader.getInt32(pos + 20, true);\n // let maxInten = reader.getInt32(pos + 24, true);\n }\n log.warn('Warning: VMR spatial transform not implemented')\n // if (XmmStart === XmmEnd) { // https://brainvoyager.com/bv/sampledata/index.html??\n hdr.affine = [\n [0, 0, -hdr.pixDims[1], (hdr.dims[1] - 2) * 0.5 * hdr.pixDims[1]],\n [-hdr.pixDims[2], 0, 0, (hdr.dims[2] - 2) * 0.5 * hdr.pixDims[2]],\n [0, -hdr.pixDims[3], 0, (hdr.dims[3] - 2) * 0.5 * hdr.pixDims[3]],\n [0, 0, 0, 1]\n ]\n // }\n log.debug(hdr)\n hdr.numBitsPerVoxel = 8\n hdr.datatypeCode = NiiDataType.DT_UINT8\n return buffer.slice(8, 8 + nBytes)\n}\n","import { NIFTI1 } from 'nifti-reader-js'\nimport type { NVImage } from '@/nvimage'\nimport { NiiDataType } from '@/nvimage/utils'\nimport { NVUtilities } from '@/nvutilities'\n\n/**\n * Reads DSI Studio FIB format (fiber orientation data), modifying the provided\n * NVImage header and returning the raw image data buffer and V1 vector data.\n *\n * Format specification:\n * https://dsi-studio.labsolver.org/doc/cli_data.html\n *\n * @param nvImage - The NVImage instance whose header will be modified.\n * @param buffer - ArrayBuffer containing the FIB file data.\n * @returns Promise resolving to [ArrayBuffer, Float32Array] - image data and V1 vectors.\n * @throws Error if the file is not a valid DSI Studio FIB file.\n */\n// eslint-disable-next-line @typescript-eslint/no-unused-vars\nexport async function readFIB(nvImage: NVImage, buffer: ArrayBuffer): Promise<[ArrayBuffer, Float32Array]> {\n nvImage.hdr = new NIFTI1()\n const hdr = nvImage.hdr\n hdr.littleEndian = false // MGH always big endian\n hdr.dims = [3, 1, 1, 1, 0, 0, 0, 0]\n hdr.pixDims = [1, 1, 1, 1, 1, 0, 0, 0]\n const mat = await NVUtilities.readMatV4(buffer, true)\n if (!('dimension' in mat) || !('dti_fa' in mat)) {\n throw new Error('Not a valid DSIstudio FIB file')\n }\n const hasV1 = 'index0' in mat && 'index1' in mat && 'index2' in mat && 'odf_vertices' in mat\n // const hasV1 = false\n hdr.numBitsPerVoxel = 32\n hdr.datatypeCode = NiiDataType.DT_FLOAT32\n hdr.dims[1] = mat.dimension[0]\n hdr.dims[2] = mat.dimension[1]\n hdr.dims[3] = mat.dimension[2]\n hdr.dims[4] = 1\n hdr.pixDims[1] = mat.voxel_size[0]\n hdr.pixDims[2] = mat.voxel_size[1]\n hdr.pixDims[3] = mat.voxel_size[2]\n hdr.sform_code = 1\n const xmm = (hdr.dims[1] - 1) * 0.5 * hdr.pixDims[1]\n const ymm = (hdr.dims[2] - 1) * 0.5 * hdr.pixDims[2]\n const zmm = (hdr.dims[3] - 1) * 0.5 * hdr.pixDims[3]\n hdr.affine = [\n [hdr.pixDims[1], 0, 0, -xmm],\n [0, -hdr.pixDims[2], 0, ymm],\n [0, 0, hdr.pixDims[2], -zmm],\n [0, 0, 0, 1]\n ]\n hdr.littleEndian = true\n const nVox3D = hdr.dims[1] * hdr.dims[2] * hdr.dims[3]\n const nBytes3D = nVox3D * Math.ceil(hdr.numBitsPerVoxel / 8)\n const nBytes = nBytes3D * hdr.dims[4]\n const buff8v1 = new Uint8Array(new ArrayBuffer(nVox3D * 4 * 3)) // 4=Float32, 3=x,y,z\n if (hasV1) {\n // read directions, stored as index\n const nvox = hdr.dims[1] * hdr.dims[2] * hdr.dims[3]\n const dir0 = new Float32Array(nvox)\n const dir1 = new Float32Array(nvox)\n const dir2 = new Float32Array(nvox)\n const idxs = mat.index0\n const dirs = mat.odf_vertices\n for (let i = 0; i < nvox; i++) {\n const idx = idxs[i] * 3\n dir0[i] = dirs[idx + 0]\n dir1[i] = dirs[idx + 1]\n dir2[i] = -dirs[idx + 2]\n }\n buff8v1.set(new Uint8Array(dir0.buffer, dir0.byteOffset, dir0.byteLength), 0 * nBytes3D)\n buff8v1.set(new Uint8Array(dir1.buffer, dir1.byteOffset, dir1.byteLength), 1 * nBytes3D)\n buff8v1.set(new Uint8Array(dir2.buffer, dir2.byteOffset, dir2.byteLength), 2 * nBytes3D)\n }\n if ('report' in mat) {\n hdr.description = new TextDecoder().decode(mat.report.subarray(0, Math.min(79, mat.report.byteLength)))\n }\n const buff8 = new Uint8Array(new ArrayBuffer(nBytes))\n const arrFA = Float32Array.from(mat.dti_fa)\n if ('mask' in mat) {\n let slope = 1\n if ('dti_fa_slope' in mat) {\n slope = mat.dti_fa_slope[0]\n }\n let inter = 1\n if ('dti_fa_inter' in mat) {\n inter = mat.dti_fa_inter[0]\n }\n const nvox = hdr.dims[1] * hdr.dims[2] * hdr.dims[3]\n const mask = mat.mask\n const f32 = new Float32Array(nvox)\n let j = 0\n for (let i = 0; i < nvox; i++) {\n if (mask[i] !== 0) {\n f32[i] = arrFA[j] * slope + inter\n j++\n }\n }\n return [f32.buffer, new Float32Array(buff8v1.buffer)]\n }\n // read FA\n const imgFA = new Uint8Array(arrFA.buffer, arrFA.byteOffset, arrFA.byteLength)\n buff8.set(imgFA, 0)\n return [buff8.buffer, new Float32Array(buff8v1.buffer)]\n}\n\n/**\n * Reads DSI Studio SRC format (DWI source data), modifying the provided\n * NVImage header and returning the raw image data buffer.\n *\n * Format specification:\n * https://dsi-studio.labsolver.org/doc/cli_data.html\n *\n * @param nvImage - The NVImage instance whose header will be modified.\n * @param buffer - ArrayBuffer containing the SRC file data.\n * @returns Promise resolving to ArrayBuffer containing the image data.\n * @throws Error if the file is not a valid DSI Studio SRC file.\n */\n// eslint-disable-next-line @typescript-eslint/no-unused-vars\nexport async function readSRC(nvImage: NVImage, buffer: ArrayBuffer): Promise<ArrayBuffer> {\n nvImage.hdr = new NIFTI1()\n const hdr = nvImage.hdr\n hdr.littleEndian = false // MGH always big ending\n hdr.dims = [3, 1, 1, 1, 0, 0, 0, 0]\n hdr.pixDims = [1, 1, 1, 1, 1, 0, 0, 0]\n const mat = await NVUtilities.readMatV4(buffer)\n if (!('dimension' in mat) || !('image0' in mat)) {\n throw new Error('Not a valid DSIstudio SRC file')\n }\n let n = 0\n let len = 0\n for (const [key, value] of Object.entries(mat)) {\n if (!key.startsWith('image')) {\n continue\n }\n if (n === 0) {\n len = value.length\n } else if (len !== value.length) {\n len = -1\n }\n if (value.constructor !== Uint16Array) {\n throw new Error('DSIstudio SRC files always use Uint16 datatype')\n }\n n++\n }\n if (len < 1 || n < 1) {\n throw new Error('SRC file not valid DSI Studio data. The image(s) should have the same length')\n }\n hdr.numBitsPerVoxel = 16\n hdr.datatypeCode = NiiDataType.DT_UINT16\n hdr.dims[1] = mat.dimension[0]\n hdr.dims[2] = mat.dimension[1]\n hdr.dims[3] = mat.dimension[2]\n hdr.dims[4] = n\n if (hdr.dims[4] > 1) {\n hdr.dims[0] = 4\n }\n hdr.pixDims[1] = mat.voxel_size[0]\n hdr.pixDims[2] = mat.voxel_size[1]\n hdr.pixDims[3] = mat.voxel_size[2]\n hdr.sform_code = 1\n const xmm = (hdr.dims[1] - 1) * 0.5 * hdr.pixDims[1]\n const ymm = (hdr.dims[2] - 1) * 0.5 * hdr.pixDims[2]\n const zmm = (hdr.dims[3] - 1) * 0.5 * hdr.pixDims[3]\n hdr.affine = [\n [hdr.pixDims[1], 0, 0, -xmm],\n [0, -hdr.pixDims[2], 0, ymm],\n [0, 0, hdr.pixDims[2], -zmm],\n [0, 0, 0, 1]\n ]\n hdr.littleEndian = true\n const nBytes3D = hdr.dims[1] * hdr.dims[2] * hdr.dims[3] * (hdr.numBitsPerVoxel / 8)\n const nBytes = nBytes3D * hdr.dims[4]\n const buff8 = new Uint8Array(new ArrayBuffer(nBytes))\n let offset = 0\n for (let i = 0; i < n; i++) {\n const arr = mat[`image${i}`]\n const img8 = new Uint8Array(arr.buffer, arr.byteOffset, arr.byteLength)\n buff8.set(img8, offset)\n offset += nBytes3D\n }\n if ('report' in mat) {\n hdr.description = new TextDecoder().decode(mat.report.subarray(0, Math.min(79, mat.report.byteLength)))\n }\n return buff8.buffer\n}\n","import { NIFTI1 } from 'nifti-reader-js'\nimport { log } from '@/logger'\nimport type { NVImage } from '@/nvimage'\nimport { NiiDataType } from '@/nvimage/utils'\n\n/**\n * Reads ECAT7 PET format image, modifying the provided NVImage header\n * and returning the raw image data buffer.\n * @param nvImage - The NVImage instance whose header will be modified.\n * @param buffer - ArrayBuffer containing the ECAT file data.\n * @returns ArrayBuffer containing the image data.\n * @throws Error if the file is not a valid ECAT file.\n */\nexport function readECAT(nvImage: NVImage, buffer: ArrayBuffer): ArrayBuffer {\n nvImage.hdr = new NIFTI1()\n const hdr = nvImage.hdr\n hdr.dims = [3, 1, 1, 1, 0, 0, 0, 0]\n hdr.pixDims = [1, 1, 1, 1, 1, 0, 0, 0]\n const reader = new DataView(buffer)\n\n const signature = reader.getInt32(0, false) // \"MATR\"\n const filetype = reader.getInt16(50, false)\n if (signature !== 1296127058 || filetype < 1 || filetype > 14) {\n throw new Error('Not a valid ECAT file')\n }\n // list header, starts at 512 bytes: int32_t hdr[4], r[31][4];\n let pos = 512 // 512=main header, 4*32-bit hdr\n let vols = 0\n const frame_duration = []\n let rawImg = new Float32Array()\n while (true) {\n // read 512 block lists\n const hdr0 = reader.getInt32(pos, false)\n const hdr3 = reader.getInt32(pos + 12, false)\n if (hdr0 + hdr3 !== 31) {\n break\n }\n let lpos = pos + 20 // skip hdr and read slice offset (r[0][1])\n let r = 0\n let voloffset = 0\n while (r < 31) {\n // r[0][1]...r[30][1]\n voloffset = reader.getInt32(lpos, false)\n lpos += 16 // e.g. r[0][1] to r[1][1]\n if (voloffset === 0) {\n break\n }\n r++\n let ipos = voloffset * 512 // image start position\n const spos = ipos - 512 // subheader for matrix image, immediately before image\n const data_type = reader.getUint16(spos, false)\n hdr.dims[1] = reader.getUint16(spos + 4, false)\n hdr.dims[2] = reader.getUint16(spos + 6, false)\n hdr.dims[3] = reader.getUint16(spos + 8, false)\n const scale_factor = reader.getFloat32(spos + 26, false)\n hdr.pixDims[1] = reader.getFloat32(spos + 34, false) * 10.0 // cm -> mm\n hdr.pixDims[2] = reader.getFloat32(spos + 38, false) * 10.0 // cm -> mm\n hdr.pixDims[3] = reader.getFloat32(spos + 42, false) * 10.0 // cm -> mm\n hdr.pixDims[4] = reader.getUint32(spos + 46, false) / 1000.0 // ms -> sec\n frame_duration.push(hdr.pixDims[4])\n const nvox3D = hdr.dims[1] * hdr.dims[2] * hdr.dims[3]\n const newImg = new Float32Array(nvox3D) // convert to float32 as scale varies\n if (data_type === 1) {\n // uint8\n for (let i = 0; i < nvox3D; i++) {\n newImg[i] = reader.getUint8(ipos) * scale_factor\n ipos++\n }\n } else if (data_type === 6) {\n // uint16\n for (let i = 0; i < nvox3D; i++) {\n newImg[i] = reader.getUint16(ipos, false) * scale_factor\n ipos += 2\n }\n } else if (data_type === 7) {\n // uint32\n for (let i = 0; i < nvox3D; i++) {\n newImg[i] = reader.getUint32(ipos, false) * scale_factor\n ipos += 4\n }\n } else {\n log.warn('Unknown ECAT data type ' + data_type)\n }\n const prevImg = rawImg.slice(0)\n rawImg = new Float32Array(prevImg.length + newImg.length)\n rawImg.set(prevImg)\n rawImg.set(newImg, prevImg.length)\n vols++\n }\n if (voloffset === 0) {\n break\n }\n pos += 512 // possible to have multiple 512-byte lists of images\n }\n hdr.dims[4] = vols\n hdr.pixDims[4] = frame_duration[0]\n if (vols > 1) {\n hdr.dims[0] = 4\n let isFDvaries = false\n for (let i = 0; i < vols; i++) {\n if (frame_duration[i] !== frame_duration[0]) {\n isFDvaries = true\n }\n }\n if (isFDvaries) {\n log.warn('Frame durations vary')\n }\n }\n hdr.sform_code = 1\n hdr.affine = [\n [-hdr.pixDims[1], 0, 0, (hdr.dims[1] - 2) * 0.5 * hdr.pixDims[1]],\n [0, -hdr.pixDims[2], 0, (hdr.dims[2] - 2) * 0.5 * hdr.pixDims[2]],\n [0, 0, -hdr.pixDims[3], (hdr.dims[3] - 2) * 0.5 * hdr.pixDims[3]],\n [0, 0, 0, 1]\n ]\n hdr.numBitsPerVoxel = 32\n hdr.datatypeCode = NiiDataType.DT_FLOAT32\n return rawImg.buffer as ArrayBuffer\n}\n","import { NIFTI1 } from 'nifti-reader-js'\nimport type { NVImage } from '@/nvimage'\nimport { NiiDataType } from '@/nvimage/utils'\n\n/**\n * Helper function to convert an ArrayBuffer to ImageData using the browser's Image API.\n * @param buffer - ArrayBuffer containing image data (BMP, PNG, JPG, etc.)\n * @returns Promise resolving to ImageData\n */\nexport async function imageDataFromArrayBuffer(buffer: ArrayBuffer): Promise<ImageData> {\n return new Promise<ImageData>((resolve, reject): void => {\n const blob = new Blob([buffer]) // Convert ArrayBuffer to Blob\n const url = URL.createObjectURL(blob) // Create a Blob URL\n const img = new Image()\n img.crossOrigin = 'Anonymous' // Allow CORS if needed\n img.src = url\n img.onload = (): void => {\n URL.revokeObjectURL(url) // Clean up the object URL\n const canvas = document.createElement('canvas')\n canvas.width = img.width\n canvas.height = img.height\n const ctx = canvas.getContext('2d')\n if (!ctx) {\n reject(new Error('Failed to get 2D context'))\n return\n }\n ctx.drawImage(img, 0, 0)\n resolve(ctx.getImageData(0, 0, img.width, img.height))\n }\n img.onerror = (err): void => {\n URL.revokeObjectURL(url) // Ensure cleanup on error\n reject(err)\n }\n })\n}\n\n/**\n * Reads standard image formats (BMP, PNG, JPG) using the browser's Image API,\n * modifying the provided NVImage header and returning the raw image data buffer.\n *\n * Automatically detects grayscale images and converts RGB data accordingly.\n *\n * @param nvImage - The NVImage instance whose header will be modified.\n * @param buffer - ArrayBuffer containing the image file data.\n * @returns Promise resolving to ArrayBuffer containing the image data.\n */\nexport async function readBMP(nvImage: NVImage, buffer: ArrayBuffer): Promise<ArrayBuffer> {\n const imageData = await imageDataFromArrayBuffer(buffer)\n const { width, height, data } = imageData\n nvImage.hdr = new NIFTI1()\n const hdr = nvImage.hdr\n hdr.dims = [3, width, height, 1, 0, 0, 0, 0]\n hdr.pixDims = [1, 1, 1, 1, 1, 0, 0, 0]\n hdr.affine = [\n [hdr.pixDims[1], 0, 0, -(hdr.dims[1] - 2) * 0.5 * hdr.pixDims[1]],\n [0, -hdr.pixDims[2], 0, (hdr.dims[2] - 2) * 0.5 * hdr.pixDims[2]],\n [0, 0, -hdr.pixDims[3], (hdr.dims[3] - 2) * 0.5 * hdr.pixDims[3]],\n [0, 0, 0, 1]\n ]\n hdr.numBitsPerVoxel = 8\n hdr.datatypeCode = NiiDataType.DT_RGBA32\n let isGrayscale = true\n for (let i = 0; i < data.length; i += 4) {\n if (data[i] !== data[i + 1] || data[i] !== data[i + 2]) {\n isGrayscale = false\n break\n }\n }\n if (isGrayscale) {\n hdr.datatypeCode = NiiDataType.DT_UINT8\n const grayscaleData = new Uint8Array(width * height)\n for (let i = 0, j = 0; i < data.length; i += 4, j++) {\n grayscaleData[j] = data[i]\n }\n return grayscaleData.buffer\n }\n return data.buffer as ArrayBuffer\n}\n","import { NIFTI1 } from 'nifti-reader-js'\nimport { mat3, vec3 } from 'gl-matrix'\nimport { log } from '@/logger'\nimport type { NVImage } from '@/nvimage'\nimport { NiiDataType } from '@/nvimage/utils'\nimport { NVUtilities } from '@/nvutilities'\n\n/**\n * Reads ITK MetaImage format (MHA/MHD), modifying the provided NVImage header\n * and returning the raw image data buffer.\n *\n * MHA files contain both header and image data in one file.\n * MHD files contain only the header, with image data in a separate file.\n *\n * Format specification: https://itk.org/Wiki/ITK/MetaIO/Documentation\n *\n * @param nvImage - The NVImage instance whose header will be modified.\n * @param buffer - ArrayBuffer containing the MHA/MHD file data.\n * @param pairedImgData - Optional paired image data for detached MHD headers.\n * @returns Promise resolving to ArrayBuffer containing the image data.\n * @throws Error if the file is too small or has unsupported data types.\n */\nexport async function readMHA(nvImage: NVImage, buffer: ArrayBuffer, pairedImgData: ArrayBuffer | null): Promise<ArrayBuffer> {\n const len = buffer.byteLength\n if (len < 20) {\n throw new Error('File too small to be VTK: bytes = ' + buffer.byteLength)\n }\n const bytes = new Uint8Array(buffer)\n let pos = 0\n function eol(c: number): boolean {\n return c === 10 || c === 13 // c is either a line feed character (10) or carriage return character (13)\n }\n function readStr(): string {\n while (pos < len && eol(bytes[pos])) {\n pos++\n } // Skip blank lines\n const startPos = pos\n while (pos < len && !eol(bytes[pos])) {\n pos++\n } // Forward until end of line\n if (pos - startPos < 2) {\n return ''\n }\n return new TextDecoder().decode(buffer.slice(startPos, pos))\n }\n let line = readStr() // 1st line: signature\n nvImage.hdr = new NIFTI1()\n const hdr = nvImage.hdr\n hdr.pixDims = [1, 1, 1, 1, 1, 0, 0, 0]\n hdr.dims = [1, 1, 1, 1, 1, 1, 1, 1]\n hdr.littleEndian = true\n let isGz = false\n let isDetached = false\n const mat33 = mat3.fromValues(NaN, 0, 0, 0, 1, 0, 0, 0, 1)\n const offset = vec3.fromValues(0, 0, 0)\n while (line !== '') {\n let items = line.split(' ')\n if (items.length > 2) {\n items = items.slice(2)\n }\n if (line.startsWith('BinaryDataByteOrderMSB') && items[0].includes('False')) {\n hdr.littleEndian = true\n }\n if (line.startsWith('BinaryDataByteOrderMSB') && items[0].includes('True')) {\n hdr.littleEndian = false\n }\n if (line.startsWith('CompressedData') && items[0].includes('True')) {\n isGz = true\n }\n if (line.startsWith('TransformMatrix')) {\n for (let d = 0; d < 9; d++) {\n mat33[d] = parseFloat(items[d])\n }\n }\n if (line.startsWith('Offset')) {\n for (let d = 0; d < Math.min(items.length, 3); d++) {\n offset[d] = parseFloat(items[d])\n }\n }\n // if (line.startsWith(\"AnatomicalOrientation\")) //we can ignore, tested with Slicer3D converting NIfTIspace images\n if (line.startsWith('ElementSpacing')) {\n for (let d = 0; d < items.length; d++) {\n hdr.pixDims[d + 1] = parseFloat(items[d])\n }\n }\n if (line.startsWith('DimSize')) {\n hdr.dims[0] = items.length\n for (let d = 0; d < items.length; d++) {\n hdr.dims[d + 1] = parseInt(items[d])\n }\n }\n if (line.startsWith('ElementType')) {\n switch (items[0]) {\n case 'MET_UCHAR':\n hdr.numBitsPerVoxel = 8\n hdr.datatypeCode = NiiDataType.DT_UINT8\n break\n case 'MET_CHAR':\n hdr.numBitsPerVoxel = 8\n hdr.datatypeCode = NiiDataType.DT_INT8\n break\n case 'MET_SHORT':\n hdr.numBitsPerVoxel = 16\n hdr.datatypeCode = NiiDataType.DT_INT16\n break\n case 'MET_USHORT':\n hdr.numBitsPerVoxel = 16\n hdr.datatypeCode = NiiDataType.DT_UINT16\n break\n case 'MET_INT':\n hdr.numBitsPerVoxel = 32\n hdr.datatypeCode = NiiDataType.DT_INT32\n break\n case 'MET_UINT':\n hdr.numBitsPerVoxel = 32\n hdr.datatypeCode = NiiDataType.DT_UINT32\n break\n case 'MET_FLOAT':\n hdr.numBitsPerVoxel = 32\n hdr.datatypeCode = NiiDataType.DT_FLOAT32\n break\n case 'MET_DOUBLE':\n hdr.numBitsPerVoxel = 64\n hdr.datatypeCode = NiiDataType.DT_FLOAT64\n break\n default:\n throw new Error('Unsupported MHA data type: ' + items[0])\n }\n }\n if (line.startsWith('ObjectType') && !items[0].includes('Image')) {\n log.warn('Only able to read ObjectType = Image, not ' + line)\n }\n if (line.startsWith('ElementDataFile')) {\n if (items[0] !== 'LOCAL') {\n isDetached = true\n }\n break\n }\n line = readStr()\n }\n const mmMat = mat3.fromValues(hdr.pixDims[1], 0, 0, 0, hdr.pixDims[2], 0, 0, 0, hdr.pixDims[3])\n mat3.multiply(mat33, mat33, mmMat)\n hdr.affine = [\n [-mat33[0], -mat33[3], -mat33[6], -offset[0]],\n [-mat33[1], -mat33[4], -mat33[7], -offset[1]],\n [mat33[2], mat33[5], mat33[8], offset[2]],\n [0, 0, 0, 1]\n ]\n while (bytes[pos] === 10) {\n pos++\n }\n hdr.vox_offset = pos\n if (isDetached && pairedImgData) {\n if (isGz) {\n return await NVUtilities.decompressToBuffer(new Uint8Array(pairedImgData.slice(0)))\n }\n return pairedImgData.slice(0)\n }\n if (isGz) {\n return await NVUtilities.decompressToBuffer(new Uint8Array(buffer.slice(hdr.vox_offset)))\n }\n return buffer.slice(hdr.vox_offset)\n}\n","import { NIFTI1, NIFTI2 } from 'nifti-reader-js'\nimport { mat4 } from 'gl-matrix'\nimport { log } from '@/logger'\nimport { NVUtilities } from '@/nvutilities'\nimport type { NVImage } from '@/nvimage'\nimport { isPlatformLittleEndian, NiiDataType } from '@/nvimage/utils'\n\n/**\n * @internal\n * Read all occurrences of a metadata tag from the MGH image footer.\n *\n * MGH images can contain strings in an optional footer.\n * These items are discriminated by their \"tag\" number.\n * Tags can be used to detect indexed atlases.\n * Note: Some tags (like tag 1) include a combination of ASCII and binary data.\n *\n * @param view - DataView representing the footer bytes\n * @param offset - Byte offset to the start of the footer\n * @param footerLength - Length of the footer\n * @param tagToRead - Tag identifier to extract (default = 1)\n * @returns Concatenated string of all matched tag contents, separated by double newlines\n */\nfunction readTag(view: DataView, offset: number, footerLength: number, tagToRead: number = 1): string {\n const end = offset + footerLength\n let pos = offset\n const results: string[] = []\n while (pos + 12 <= end) {\n const tag = view.getInt32(pos, false) // tag (little-endian)\n // skip 4 bytes (padding), read 4-byte length\n const length = view.getInt32(pos + 8, false) // length of data\n pos += 12\n if (length <= 0 || pos + length > end) {\n break // corrupt or truncated footer\n }\n if (tag !== tagToRead) {\n pos += length\n continue\n }\n let strLen = length\n let contentPos = pos\n if (tagToRead === 1) {\n if (pos + 4 > end) {\n break\n }\n strLen = view.getInt32(pos, false)\n contentPos += 4\n }\n if (strLen > 1 && contentPos + strLen <= end) {\n const raw = new Uint8Array(view.buffer, contentPos, strLen)\n const str = new TextDecoder('utf-8').decode(raw.slice(0, -1)) // remove null terminator\n results.push(str)\n }\n pos += length\n }\n return results.join('\\n\\n')\n}\n\n/**\n * @internal\n * Optimize FreeSurfer label image data by converting from float/int32 to the smallest suitable integer type.\n * Returns the raw image buffer if the input type is unsupported or values are not valid label indices.\n *\n * This function:\n * - Handles byte-swapping for little-endian systems.\n * - Ensures all values are finite integers within valid label ranges.\n * - Converts data to INT32, INT16, or UINT8 to reduce memory usage when possible.\n *\n * @param hdr - The NIfTI header object, which will be updated in-place.\n * @param imgRaw - The raw image data as an ArrayBuffer.\n * @returns A possibly transformed ArrayBuffer, or the original buffer if optimization is not possible.\n */\nexport function optimizeFreeSurferLabels(hdr: NIFTI1 | NIFTI2, imgRaw: ArrayBuffer): ArrayBuffer {\n hdr.intent_code = 1002\n if (hdr.datatypeCode !== NiiDataType.DT_FLOAT32 && hdr.datatypeCode !== NiiDataType.DT_INT32) {\n return imgRaw\n }\n // Parse input to float or int array\n let img: Float32Array | Int32Array = new Float32Array(imgRaw)\n if (hdr.datatypeCode === NiiDataType.DT_INT32) {\n img = new Int32Array(imgRaw)\n }\n // Byte-swap if needed\n if (isPlatformLittleEndian()) {\n const u32 = new Uint32Array(imgRaw)\n for (let i = 0; i < u32.length; i++) {\n const val = u32[i]\n u32[i] = ((val & 0x000000ff) << 24) | ((val & 0x0000ff00) << 8) | ((val & 0x00ff0000) >>> 8) | ((val & 0xff000000) >>> 24)\n }\n }\n hdr.littleEndian = isPlatformLittleEndian()\n // Validate values\n let isInteger = true\n let mn = Infinity\n let mx = -Infinity\n for (let i = 0; i < img.length; i++) {\n const v = img[i]\n if (!Number.isFinite(v)) {\n continue\n }\n if (!Number.isInteger(v)) {\n isInteger = false\n }\n if (v < mn) {\n mn = v\n }\n if (v > mx) {\n mx = v\n }\n }\n if (!isInteger || mn < 0 || mx > 2147483647) {\n log.warn(`FreeSurfer Labels must be integers in INT32 range. range ${mn}..${mx}`)\n return imgRaw\n }\n // Optimize datatype\n if (mx > 32767) {\n hdr.datatypeCode = NiiDataType.DT_INT32\n const out = new Int32Array(img.length)\n for (let i = 0; i < img.length; i++) {\n out[i] = Math.trunc(img[i])\n }\n return out.buffer\n } else if (mx > 255) {\n hdr.datatypeCode = NiiDataType.DT_INT16\n hdr.numBitsPerVoxel = 16\n const out = new Int16Array(img.length)\n for (let i = 0; i < img.length; i++) {\n out[i] = Math.trunc(img[i])\n }\n return out.buffer\n } else {\n hdr.datatypeCode = NiiDataType.DT_UINT8\n hdr.numBitsPerVoxel = 8\n const out = new Uint8Array(img.length)\n for (let i = 0; i < img.length; i++) {\n out[i] = Math.trunc(img[i])\n }\n return out.buffer\n }\n}\n\n/**\n * @internal\n * Determine if an MGH file is a FreeSurfer label image by inspecting the footer.\n * MGH label images often include additional metadata after the image data.\n * This function checks for specific patterns in the footer to infer if the image\n * represents labeled data (e.g., label2vol-generated volumes or files referencing LUTs).\n *\n * @param raw - The complete ArrayBuffer of the MGH file.\n * @param hdr - The parsed NIfTI header, including vox_offset and datatypeCode.\n * @param expectedBytes - The expected size of the image data in bytes.\n * @returns A boolean indicating whether the file is likely a labeled atlas.\n * @see https://niivue.com/demos/features/labels.html\n */\nexport function isFreeSurferLabelImage(raw: ArrayBuffer, hdr: NIFTI1 | NIFTI2, expectedBytes: number): boolean {\n const remainingBytes = raw.byteLength - hdr.vox_offset\n if (remainingBytes < expectedBytes) {\n log.error(`MGH image data size mismatch: expected ${expectedBytes}, found ${remainingBytes}`)\n return false\n }\n\n if (remainingBytes === expectedBytes) {\n return false\n }\n // Skip the first 20 bytes (5 * float32) of the MGH footer\n const footerStart = hdr.vox_offset + expectedBytes + 20\n const footerLength = raw.byteLength - footerStart\n if (footerLength <= 12) {\n return false\n }\n const tag1 = readTag(new DataView(raw), footerStart, footerLength)\n if (tag1.toLowerCase().endsWith('lut.txt')) {\n return true\n }\n const tag3 = readTag(new DataView(raw), footerStart, footerLength, 3)\n return tag3.includes('mri_label2vol')\n}\n\n/**\n * Reads FreeSurfer MGH/MGZ format image, modifying the provided NVImage header\n * and returning the raw image data buffer.\n * @param nvImage - The NVImage instance whose header will be modified.\n * @param buffer - ArrayBuffer containing the MGH/MGZ file data.\n * @param name - optional name of image, allowing label detection.\n * @returns Promise resolving to the imgRaw ArrayBuffer or null on critical error.\n */\nexport async function readMgh(nvImage: NVImage, buffer: ArrayBuffer, name: string = ''): Promise<ArrayBuffer | null> {\n if (!nvImage.hdr) {\n log.debug('readMgh called before nvImage.hdr was initialized. Creating default.')\n nvImage.hdr = new NIFTI1() // Ensure header object exists\n }\n const hdr = nvImage.hdr\n hdr.littleEndian = false\n\n let raw = buffer\n let reader = new DataView(raw)\n\n // Decompression logic\n if (raw.byteLength >= 2 && reader.getUint8(0) === 31 && reader.getUint8(1) === 139) {\n try {\n raw = await NVUtilities.decompressToBuffer(new Uint8Array(buffer))\n reader = new DataView(raw)\n } catch (err) {\n log.error('Failed to decompress MGZ file.', err)\n return null\n }\n }\n\n if (raw.byteLength < 284) {\n log.error('File too small to be a valid MGH/MGZ header.')\n return null\n }\n\n // --- Read MGH Header Fields ---\n const version = reader.getInt32(0, false)\n const width = reader.getInt32(4, false)\n const height = reader.getInt32(8, false)\n const depth = reader.getInt32(12, false)\n const nframes = reader.getInt32(16, false)\n const mtype = reader.getInt32(20, false)\n const spacingX = reader.getFloat32(30, false)\n const spacingY = reader.getFloat32(34, false)\n const spacingZ = reader.getFloat32(38, false)\n const xr = reader.getFloat32(42, false)\n const xa = reader.getFloat32(46, false)\n const xs = reader.getFloat32(50, false)\n const yr = reader.getFloat32(54, false)\n const ya = reader.getFloat32(58, false)\n const ys = reader.getFloat32(62, false)\n const zr = reader.getFloat32(66, false)\n const za = reader.getFloat32(70, false)\n const zs = reader.getFloat32(74, false)\n const cr = reader.getFloat32(78, false)\n const ca = reader.getFloat32(82, false)\n const cs = reader.getFloat32(86, false)\n\n if (version !== 1 && version !== 257) {\n log.warn(`Unexpected MGH version: ${version}.`)\n }\n if (width <= 0 || height <= 0 || depth <= 0) {\n log.error(`Invalid MGH dimensions: ${width}x${height}x${depth}`)\n return null\n }\n\n // Map MGH data type directly onto nvImage.hdr\n switch (mtype) {\n case 0:\n hdr.numBitsPerVoxel = 8\n hdr.datatypeCode = NiiDataType.DT_UINT8\n break\n case 4:\n hdr.numBitsPerVoxel = 16\n hdr.datatypeCode = NiiDataType.DT_INT16\n break\n case 1:\n hdr.numBitsPerVoxel = 32\n hdr.datatypeCode = NiiDataType.DT_INT32\n break\n case 3:\n hdr.numBitsPerVoxel = 32\n hdr.datatypeCode = NiiDataType.DT_FLOAT32\n break\n default:\n log.error(`Unsupported MGH data type: ${mtype}`)\n return null\n }\n\n // Set dimensions directly onto nvImage.hdr\n hdr.dims[1] = width\n hdr.dims[2] = height\n hdr.dims[3] = depth\n hdr.dims[4] = Math.max(1, nframes)\n hdr.dims[0] = hdr.dims[4] > 1 ? 4 : 3\n\n // Set pixel dimensions directly onto nvImage.hdr (using abs)\n hdr.pixDims[0] = 1\n hdr.pixDims[1] = Math.abs(spacingX)\n hdr.pixDims[2] = Math.abs(spacingY)\n hdr.pixDims[3] = Math.abs(spacingZ)\n hdr.pixDims[4] = 0\n\n hdr.sform_code = 1\n hdr.qform_code = 0\n hdr.sform_code = 1\n const rot44 = mat4.fromValues(\n xr * hdr.pixDims[1],\n yr * hdr.pixDims[2],\n zr * hdr.pixDims[3],\n 0,\n xa * hdr.pixDims[1],\n ya * hdr.pixDims[2],\n za * hdr.pixDims[3],\n 0,\n xs * hdr.pixDims[1],\n ys * hdr.pixDims[2],\n zs * hdr.pixDims[3],\n 0,\n 0,\n 0,\n 0,\n 1\n )\n const Pcrs = [hdr.dims[1] / 2.0, hdr.dims[2] / 2.0, hdr.dims[3] / 2.0, 1]\n\n const PxyzOffset = [0, 0, 0, 0]\n for (let i = 0; i < 3; i++) {\n PxyzOffset[i] = 0\n for (let j = 0; j < 3; j++) {\n PxyzOffset[i] = PxyzOffset[i] + rot44[j + i * 4] * Pcrs[j]\n }\n }\n hdr.affine = [\n [rot44[0], rot44[1], rot44[2], cr - PxyzOffset[0]],\n [rot44[4], rot44[5], rot44[6], ca - PxyzOffset[1]],\n [rot44[8], rot44[9], rot44[10], cs - PxyzOffset[2]],\n [0, 0, 0, 1]\n ]\n\n hdr.vox_offset = 284\n hdr.magic = 'n+1'\n\n // Check data size\n const nBytesPerVoxel = hdr.numBitsPerVoxel / 8\n const nVoxels = width * height * depth * hdr.dims[4]\n const expectedBytes = nVoxels * nBytesPerVoxel\n // Return only the raw image data buffer\n const imgRaw = raw.slice(hdr.vox_offset, hdr.vox_offset + expectedBytes)\n // label detection based on:\n // https://github.com/pwighton/mgz-optimize/blob/main/mgz_optimize.py\n // option 1: detect label by version number\n let isLabel = version === 257\n // option 2: detect label by filename\n if (!isLabel) {\n const mgLabelFiles = [\n 'aparc.DKTatlas+aseg.deep.mg',\n 'aparc+aseg.mg',\n 'aparc.DKTatlas+aseg.mg',\n 'aparc.a2005s+aseg.mg',\n 'aparc.a2009s+aseg.mg',\n 'apas+head.mg',\n 'apas+head.samseg.mg',\n 'aseg.auto.mg',\n 'aseg.auto_noCCseg.mg',\n 'aseg.mg',\n 'aseg.presurf.hypos.mg',\n 'aseg.presurf.mg',\n 'brainstemSsLabels.v13.FSvoxelSpace.mg',\n 'brainstemSsLabels.v13.mg',\n 'ctrl_pts.mg',\n 'filled.auto.mg',\n 'filled.mg',\n 'gtmseg.mg',\n 'hypothalamic_subunits_seg.v1.mg',\n 'lh.hippoAmygLabels-T1.v22.CA.FSvoxelSpace.mg',\n 'lh.hippoAmygLabels-T1.v22.CA.mg',\n 'lh.hippoAmygLabels-T1.v22.FS60.FSvoxelSpace.mg',\n 'lh.hippoAmygLabels-T1.v22.FS60.mg',\n 'lh.hippoAmygLabels-T1.v22.FSvoxelSpace.mg',\n 'lh.hippoAmygLabels-T1.v22.HBT.FSvoxelSpace.mg',\n 'lh.hippoAmygLabels-T1.v22.HBT.mg',\n 'lh.hippoAmygLabels-T1.v22.mg',\n 'lh.ribbon.mg',\n 'mca-dura.mg',\n 'rh.hippoAmygLabels-T1.v22.CA.FSvoxelSpace.mg',\n 'rh.hippoAmygLabels-T1.v22.CA.mg',\n 'rh.hippoAmygLabels-T1.v22.FS60.FSvoxelSpace.mg',\n 'rh.hippoAmygLabels-T1.v22.FS60.mg',\n 'rh.hippoAmygLabels-T1.v22.FSvoxelSpace.mg',\n 'rh.hippoAmygLabels-T1.v22.HBT.FSvoxelSpace.mg',\n 'rh.hippoAmygLabels-T1.v22.HBT.mg',\n 'rh.hippoAmygLabels-T1.v22.mg',\n 'rh.ribbon.mg',\n 'ribbon.mg',\n 'synthseg.mg',\n 'synthseg.rca.mg',\n 'vsinus.mg',\n 'subcort.mask.1mm.mg',\n 'subcort.mask.mg',\n 'surface.defects.mg',\n 'ThalamicNuclei.v13.T1.FSvoxelSpace.mg',\n 'ThalamicNuclei.v13.T1.mg',\n 'wm.asegedit.mg',\n 'wmparc.mg'\n ]\n isLabel = mgLabelFiles.some((label) => name.includes(label))\n }\n // option 3: detect label using MGH footer\n if (!isLabel) {\n isLabel = isFreeSurferLabelImage(raw, hdr, expectedBytes)\n }\n if (isLabel) {\n return optimizeFreeSurferLabels(hdr, imgRaw)\n }\n return imgRaw\n}\n","import { NIFTI1 } from 'nifti-reader-js'\nimport { log } from '@/logger'\nimport type { NVImage } from '@/nvimage'\nimport { NiiDataType } from '@/nvimage/utils'\nimport { NVUtilities } from '@/nvutilities'\n\n/**\n * Reads MRtrix MIF format image, modifying the provided NVImage header\n * and returning the raw image data buffer.\n *\n * MIF files typically contain 3D (anatomical) or 4D (fMRI, DWI) data.\n * 5D data is rarely seen. This reader supports up to 5D.\n *\n * Format specification: https://mrtrix.readthedocs.io/en/latest/\n *\n * @param nvImage - The NVImage instance whose header will be modified.\n * @param buffer - ArrayBuffer containing the MIF file data.\n * @param pairedImgData - Optional paired image data for detached MIH headers.\n * @returns Promise resolving to ArrayBuffer containing the image data.\n * @throws Error if the file is not a valid MIF file.\n */\nexport async function readMIF(nvImage: NVImage, buffer: ArrayBuffer, pairedImgData: ArrayBuffer | null): Promise<ArrayBuffer> {\n // MIF files typically 3D (e.g. anatomical), 4D (fMRI, DWI). 5D rarely seen\n // This read currently supports up to 5D. To create test: \"mrcat -axis 4 a4d.mif b4d.mif out5d.mif\"\n nvImage.hdr = new NIFTI1()\n const hdr = nvImage.hdr\n hdr.pixDims = [1, 1, 1, 1, 1, 0, 0, 0]\n hdr.dims = [1, 1, 1, 1, 1, 1, 1, 1]\n let len = buffer.byteLength\n if (len < 20) {\n throw new Error('File too small to be MIF: bytes = ' + len)\n }\n let bytes = new Uint8Array(buffer)\n if (bytes[0] === 31 && bytes[1] === 139) {\n log.debug('MIF with GZ decompression')\n buffer = await NVUtilities.decompressToBuffer(new Uint8Array(buffer))\n len = buffer.byteLength\n bytes = new Uint8Array(buffer)\n }\n let pos = 0\n function readStr(): string {\n while (pos < len && bytes[pos] === 10) {\n pos++\n } // skip blank lines\n const startPos = pos\n while (pos < len && bytes[pos] !== 10) {\n pos++\n }\n pos++ // skip EOLN\n if (pos - startPos < 1) {\n return ''\n }\n return new TextDecoder().decode(buffer.slice(startPos, pos - 1))\n }\n let line = readStr() // 1st line: signature 'mrtrix tracks'\n if (!line.startsWith('mrtrix image')) {\n throw new Error('Not a valid MIF file')\n }\n const layout = []\n let isBit = false\n let nTransform = 0\n let TR = 0\n let isDetached = false\n // let isTensor = false\n line = readStr()\n while (pos < len && !line.startsWith('END')) {\n let items = line.split(':') // \"vox: 1,1,1\" -> \"vox\", \" 1,1,1\"\n line = readStr()\n if (items.length < 2) {\n break\n } //\n const tag = items[0] // \"datatype\", \"dim\"\n items = items[1].split(',') // \" 1,1,1\" -> \" 1\", \"1\", \"1\"\n for (let i = 0; i < items.length; i++) {\n items[i] = items[i].trim()\n } // \" 1\", \"1\", \"1\" -> \"1\", \"1\", \"1\"\n switch (tag) {\n case 'dim':\n hdr.dims[0] = items.length\n for (let i = 0; i < items.length; i++) {\n hdr.dims[i + 1] = parseInt(items[i])\n }\n break\n case 'vox':\n for (let i = 0; i < items.length; i++) {\n hdr.pixDims[i + 1] = parseFloat(items[i])\n if (isNaN(hdr.pixDims[i + 1])) {\n hdr.pixDims[i + 1] = 0.0\n }\n }\n break\n case 'layout':\n for (let i = 0; i < items.length; i++) {\n layout.push(parseInt(items[i]))\n } // n.b. JavaScript preserves sign for -0\n break\n case 'datatype':\n {\n const dt = items[0]\n if (dt.startsWith('Bit')) {\n isBit = true\n hdr.datatypeCode = NiiDataType.DT_UINT8\n } else if (dt.startsWith('Int8')) {\n hdr.datatypeCode = NiiDataType.DT_INT8\n } else if (dt.startsWith('UInt8')) {\n hdr.datatypeCode = NiiDataType.DT_UINT8\n } else if (dt.startsWith('Int16')) {\n hdr.datatypeCode = NiiDataType.DT_INT16\n } else if (dt.startsWith('UInt16')) {\n hdr.datatypeCode = NiiDataType.DT_UINT16\n } else if (dt.startsWith('Int32')) {\n hdr.datatypeCode = NiiDataType.DT_INT32\n } else if (dt.startsWith('UInt32')) {\n hdr.datatypeCode = NiiDataType.DT_UINT32\n } else if (dt.startsWith('Float32')) {\n hdr.datatypeCode = NiiDataType.DT_FLOAT32\n } else if (dt.startsWith('Float64')) {\n hdr.datatypeCode = NiiDataType.DT_FLOAT64\n } else {\n log.warn('Unsupported datatype ' + dt)\n }\n if (dt.includes('8')) {\n hdr.numBitsPerVoxel = 8\n } else if (dt.includes('16')) {\n hdr.numBitsPerVoxel = 16\n } else if (dt.includes('32')) {\n hdr.numBitsPerVoxel = 32\n } else if (dt.includes('64')) {\n hdr.numBitsPerVoxel = 64\n }\n hdr.littleEndian = true // native, to do support big endian readers\n if (dt.endsWith('LE')) {\n hdr.littleEndian = true\n }\n if (dt.endsWith('BE')) {\n hdr.littleEndian = false\n }\n }\n break\n case 'transform':\n if (nTransform > 2 || items.length !== 4) {\n break\n }\n hdr.affine[nTransform][0] = parseFloat(items[0])\n hdr.affine[nTransform][1] = parseFloat(items[1])\n hdr.affine[nTransform][2] = parseFloat(items[2])\n hdr.affine[nTransform][3] = parseFloat(items[3])\n nTransform++\n break\n case 'comments':\n hdr.description = items[0].substring(0, Math.min(79, items[0].length))\n break\n /* case 'command_history':\n if (items[0].startsWith('dwi2tensor')) {\n isTensor = true\n }\n break */\n case 'RepetitionTime':\n TR = parseFloat(items[0])\n break\n case 'file':\n isDetached = !items[0].startsWith('. ')\n if (!isDetached) {\n items = items[0].split(' ') // \". 2336\" -> \". \", \"2336\"\n hdr.vox_offset = parseInt(items[1])\n }\n break\n }\n }\n const ndim = hdr.dims[0]\n if (ndim > 5) {\n log.warn('reader only designed for a maximum of 5 dimensions (XYZTD)')\n }\n let nvox = 1\n for (let i = 0; i < ndim; i++) {\n nvox *= Math.max(hdr.dims[i + 1], 1)\n }\n // let nvox = hdr.dims[1] * hdr.dims[2] * hdr.dims[3] * hdr.dims[4];\n for (let i = 0; i < 3; i++) {\n for (let j = 0; j < 3; j++) {\n // hdr.affine[i][j] *= hdr.pixDims[i + 1];\n hdr.affine[i][j] *= hdr.pixDims[j + 1]\n }\n }\n log.debug('mif affine:' + hdr.affine[0])\n if (TR > 0) {\n hdr.pixDims[4] = TR\n }\n if (isDetached && !pairedImgData) {\n log.warn('MIH header provided without paired image data')\n }\n let rawImg: ArrayBuffer\n if (pairedImgData && isDetached) {\n rawImg = pairedImgData.slice(0)\n } else if (isBit) {\n hdr.numBitsPerVoxel = 8\n const img8 = new Uint8Array(nvox)\n const buffer1 = buffer.slice(hdr.vox_offset, hdr.vox_offset + Math.ceil(nvox / 8))\n const img1 = new Uint8Array(buffer1)\n let j = 0\n for (let i = 0; i < nvox; i++) {\n const bit = i % 8\n img8[i] = (img1[j] >> (7 - bit)) & 1\n if (bit === 7) {\n j++\n }\n }\n rawImg = img8.buffer\n } else {\n // n.b. mrconvert can pad files? See dtitest_Siemens_SC 4_dti_nopf_x2_pitch\n rawImg = buffer.slice(hdr.vox_offset, hdr.vox_offset + nvox * (hdr.numBitsPerVoxel / 8))\n }\n if (layout.length !== hdr.dims[0]) {\n log.warn('dims does not match layout')\n }\n // estimate strides:\n let stride = 1\n const instride = [1, 1, 1, 1, 1]\n const inflip = [false, false, false, false, false]\n for (let i = 0; i < layout.length; i++) {\n for (let j = 0; j < layout.length; j++) {\n const a = Math.abs(layout[j])\n if (a !== i) {\n continue\n }\n instride[j] = stride\n // detect -0: https://medium.com/coding-at-dawn/is-negative-zero-0-a-number-in-javascript-c62739f80114\n if (layout[j] < 0 || Object.is(layout[j], -0)) {\n inflip[j] = true\n }\n stride *= hdr.dims[j + 1]\n }\n }\n // lookup table for flips and stride offsets:\n let xlut = NVUtilities.range(0, hdr.dims[1] - 1, 1)\n if (inflip[0]) {\n xlut = NVUtilities.range(hdr.dims[1] - 1, 0, -1)\n }\n for (let i = 0; i < hdr.dims[1]; i++) {\n xlut[i] *= instride[0]\n }\n let ylut = NVUtilities.range(0, hdr.dims[2] - 1, 1)\n if (inflip[1]) {\n ylut = NVUtilities.range(hdr.dims[2] - 1, 0, -1)\n }\n for (let i = 0; i < hdr.dims[2]; i++) {\n ylut[i] *= instride[1]\n }\n let zlut = NVUtilities.range(0, hdr.dims[3] - 1, 1)\n if (inflip[2]) {\n zlut = NVUtilities.range(hdr.dims[3] - 1, 0, -1)\n }\n for (let i = 0; i < hdr.dims[3]; i++) {\n zlut[i] *= instride[2]\n }\n let tlut = NVUtilities.range(0, hdr.dims[4] - 1, 1)\n if (inflip[3]) {\n tlut = NVUtilities.range(hdr.dims[4] - 1, 0, -1)\n }\n for (let i = 0; i < hdr.dims[4]; i++) {\n tlut[i] *= instride[3]\n }\n let dlut = NVUtilities.range(0, hdr.dims[5] - 1, 1)\n if (inflip[4]) {\n dlut = NVUtilities.range(hdr.dims[5] - 1, 0, -1)\n }\n for (let i = 0; i < hdr.dims[5]; i++) {\n dlut[i] *= instride[4]\n }\n // input and output arrays\n let j = 0\n let inVs: Int8Array | Uint8Array | Int16Array | Uint16Array | Int32Array | Uint32Array | Float32Array | Float64Array\n let outVs: Int8Array | Uint8Array | Int16Array | Uint16Array | Int32Array | Uint32Array | Float32Array | Float64Array\n switch (hdr.datatypeCode) {\n case NiiDataType.DT_INT8:\n inVs = new Int8Array(rawImg)\n outVs = new Int8Array(nvox)\n break\n case NiiDataType.DT_UINT8:\n inVs = new Uint8Array(rawImg)\n outVs = new Uint8Array(nvox)\n break\n case NiiDataType.DT_INT16:\n inVs = new Int16Array(rawImg)\n outVs = new Int16Array(nvox)\n break\n case NiiDataType.DT_UINT16:\n inVs = new Uint16Array(rawImg)\n outVs = new Uint16Array(nvox)\n break\n case NiiDataType.DT_INT32:\n inVs = new Int32Array(rawImg)\n outVs = new Int32Array(nvox)\n break\n case NiiDataType.DT_UINT32:\n inVs = new Uint32Array(rawImg)\n outVs = new Uint32Array(nvox)\n break\n case NiiDataType.DT_FLOAT32:\n inVs = new Float32Array(rawImg)\n outVs = new Float32Array(nvox)\n break\n case NiiDataType.DT_FLOAT64:\n inVs = new Float64Array(rawImg)\n outVs = new Float64Array(nvox)\n break\n default:\n throw new Error('unknown datatypeCode')\n }\n for (let d = 0; d < hdr.dims[5]; d++) {\n for (let t = 0; t < hdr.dims[4]; t++) {\n for (let z = 0; z < hdr.dims[3]; z++) {\n for (let y = 0; y < hdr.dims[2]; y++) {\n for (let x = 0; x < hdr.dims[1]; x++) {\n outVs[j] = inVs[xlut[x] + ylut[y] + zlut[z] + tlut[t] + dlut[d]]\n j++\n } // for x\n } // for y\n } // for z\n } // for t (time)\n } // for d (direction, phase/real, etc)\n return outVs.buffer as ArrayBuffer\n}\n","import { isCompressed, decompressAsync, readHeaderAsync, readImage, hasExtension } from 'nifti-reader-js'\nimport { log } from '@/logger'\nimport type { NVImage } from '@/nvimage'\nimport { NiiDataType } from '@/nvimage/utils'\n\n/**\n * Reads NIfTI format image (.nii, .nii.gz), modifying the provided NVImage header\n * and returning the raw image data buffer.\n * @param nvImage - The NVImage instance whose header will be modified.\n * @param buffer - ArrayBuffer containing the NIfTI file data.\n * @returns Promise resolving to the imgRaw ArrayBufferLike or null on critical error.\n */\nexport async function readNifti(nvImage: NVImage, buffer: ArrayBuffer, pairedImageData?: ArrayBuffer): Promise<ArrayBufferLike | null> {\n let dataBuffer = buffer // Work with a local variable\n let imgRaw: ArrayBufferLike | null = null\n\n try {\n if (isCompressed(dataBuffer as ArrayBuffer)) {\n log.debug(`Decompressing NIfTI file: ${nvImage.name}`) // Use name from nvImage instance\n dataBuffer = await decompressAsync(dataBuffer as ArrayBuffer)\n log.debug(`Decompression complete for: ${nvImage.name}`)\n }\n\n // Ensure buffer is still valid after potential decompression\n if (!dataBuffer || dataBuffer.byteLength === 0) {\n throw new Error('Buffer became invalid after decompression attempt.')\n }\n\n nvImage.hdr = await readHeaderAsync(dataBuffer as ArrayBuffer, pairedImageData != null)\n if (hasExtension(nvImage.hdr)) {\n nvImage.extensions = nvImage.hdr.extensions\n }\n\n if (nvImage.hdr === null) {\n throw new Error(`Failed to read NIfTI header: ${nvImage.name}`)\n }\n\n if (nvImage.hdr.cal_min === 0 && nvImage.hdr.cal_max === 255 && nvImage.hdr.datatypeCode !== NiiDataType.DT_UINT8) {\n log.debug(`Resetting suspicious cal_min/max (0/255) for non-uint8 NIfTI: ${nvImage.name}`)\n nvImage.hdr.cal_min = 0.0 // Use 0.0 to signal unset/recalculate later\n nvImage.hdr.cal_max = 0.0\n }\n\n imgRaw = pairedImageData ? readImage(nvImage.hdr, pairedImageData) : readImage(nvImage.hdr, dataBuffer as ArrayBuffer)\n\n if (imgRaw === null) {\n throw new Error(`nifti-reader-js readImage returned null for ${nvImage.name}`)\n }\n\n return imgRaw\n } catch (err) {\n log.error(`Error processing NIfTI file ${nvImage.name}:`, err)\n nvImage.hdr = null\n return null\n }\n}\n","import { NIFTI1 } from 'nifti-reader-js'\nimport { mat3, mat4, vec3 } from 'gl-matrix'\nimport { log } from '@/logger'\nimport { NVUtilities } from '@/nvutilities'\nimport type { NVImage } from '@/nvimage'\nimport { NiiDataType } from '@/nvimage/utils'\n\n/**\n * Reads NRRD/NHDR format image, modifying the provided NVImage header\n * and returning the raw image data buffer.\n * @param nvImage - The NVImage instance whose header will be modified.\n * @param dataBuffer - ArrayBuffer containing the NRRD header or full file.\n * @param pairedImgData - Optional ArrayBuffer for detached data file (used by NHDR).\n * @returns Promise resolving to the imgRaw ArrayBuffer or null on critical error.\n */\nexport async function readNrrd(nvImage: NVImage, dataBuffer: ArrayBuffer, pairedImgData: ArrayBuffer | null = null): Promise<ArrayBuffer | null> {\n if (!nvImage.hdr) {\n log.warn('readNrrd called before nvImage.hdr was initialized. Creating default.')\n nvImage.hdr = new NIFTI1()\n }\n const hdr = nvImage.hdr // Use nvImage.hdr directly\n hdr.pixDims = [1, 1, 1, 1, 1, 0, 0, 0]\n hdr.dims = [0, 0, 0, 0, 0, 0, 0, 0]\n const len = dataBuffer.byteLength\n let txt: string | null = null\n const bytes = new Uint8Array(dataBuffer)\n\n for (let i = 1; i < len; i++) {\n if (bytes[i - 1] === 10 && bytes[i] === 10) {\n const v = dataBuffer.slice(0, i - 1)\n txt = new TextDecoder().decode(v)\n hdr.vox_offset = i + 1 // Set based on header end position\n break\n }\n }\n\n if (txt === null) {\n log.error('readNrrd: could not extract txt')\n return null\n }\n\n const lines = txt.split('\\n')\n if (!lines[0].startsWith('NRRD')) {\n log.error('Invalid NRRD image (magic signature missing)')\n return null\n }\n\n const n = lines.length\n let isGz = false\n let isMicron = false\n let isDetached = false\n const mat33 = mat3.fromValues(NaN, 0, 0, 0, 1, 0, 0, 0, 1)\n const offset = vec3.fromValues(0, 0, 0)\n let rot33 = mat3.create() // Initialize space correction matrix\n\n for (let i = 1; i < n; i++) {\n let str = lines[i]\n if (str.length === 0 || str[0] === '#') {\n if (str.startsWith('#')) {\n continue\n }\n if (str.trim().length === 0) {\n continue\n }\n }\n str = str.toLowerCase()\n const items = str.split(':')\n if (items.length < 2) {\n continue\n }\n const key = items[0].trim()\n let value = items[1].trim()\n\n value = value.replaceAll(')', ' ')\n value = value.replaceAll('(', ' ')\n value = value.trim()\n\n switch (key) {\n case 'data file':\n isDetached = true\n break\n case 'encoding':\n if (value.includes('raw')) {\n isGz = false\n } else if (value.includes('gz')) {\n isGz = true\n } else {\n log.error(`Unsupported NRRD encoding : ${value}`)\n return null\n }\n break\n case 'type':\n switch (value) {\n case 'uchar':\n case 'unsigned char':\n case 'uint8':\n case 'uint8_t':\n hdr.numBitsPerVoxel = 8\n hdr.datatypeCode = NiiDataType.DT_UINT8\n break\n case 'signed char':\n case 'int8':\n case 'int8_t':\n hdr.numBitsPerVoxel = 8\n hdr.datatypeCode = NiiDataType.DT_INT8\n break\n case 'short':\n case 'short int':\n case 'signed short':\n case 'signed short int':\n case 'int16':\n case 'int16_t':\n hdr.numBitsPerVoxel = 16\n hdr.datatypeCode = NiiDataType.DT_INT16\n break\n case 'ushort':\n case 'unsigned short':\n case 'unsigned short int':\n case 'uint16':\n case 'uint16_t':\n hdr.numBitsPerVoxel = 16\n hdr.datatypeCode = NiiDataType.DT_UINT16\n break\n case 'int':\n case 'signed int':\n case 'int32':\n case 'int32_t':\n hdr.numBitsPerVoxel = 32\n hdr.datatypeCode = NiiDataType.DT_INT32\n break\n case 'uint':\n case 'unsigned int':\n case 'uint32':\n case 'uint32_t':\n hdr.numBitsPerVoxel = 32\n hdr.datatypeCode = NiiDataType.DT_UINT32\n break\n case 'float':\n hdr.numBitsPerVoxel = 32\n hdr.datatypeCode = NiiDataType.DT_FLOAT32\n break\n case 'double':\n hdr.numBitsPerVoxel = 64\n hdr.datatypeCode = NiiDataType.DT_FLOAT64\n break\n default:\n log.error('Unsupported NRRD data type: ' + value)\n return null\n }\n break\n case 'spacings':\n {\n const values = value.split(/[ ,]+/)\n for (let d = 0; d < values.length; d++) {\n hdr.pixDims[d + 1] = parseFloat(values[d])\n }\n }\n break\n case 'sizes':\n {\n const dims = value.split(/[ ,]+/)\n hdr.dims[0] = dims.length\n for (let d = 0; d < dims.length; d++) {\n hdr.dims[d + 1] = parseInt(dims[d])\n }\n }\n break\n case 'endian':\n if (value.includes('little')) {\n hdr.littleEndian = true\n } else if (value.includes('big')) {\n hdr.littleEndian = false\n }\n break\n case 'space directions':\n {\n const vs = value.split(/[ ,]+/)\n if (vs.length === 9) {\n for (let d = 0; d < 9; d++) {\n mat33[d] = parseFloat(vs[d])\n }\n }\n }\n break\n case 'space origin':\n {\n const ts = value.split(/[ ,]+/)\n if (ts.length === 3) {\n offset[0] = parseFloat(ts[0])\n offset[1] = parseFloat(ts[1])\n offset[2] = parseFloat(ts[2])\n }\n }\n break\n case 'space units':\n if (value.includes('microns')) {\n isMicron = true\n }\n break\n case 'space':\n if (value.includes('right-anterior-superior') || value.includes('ras')) {\n rot33 = mat3.fromValues(1, 0, 0, 0, 1, 0, 0, 0, 1)\n } else if (value.includes('left-anterior-superior') || value.includes('las')) {\n rot33 = mat3.fromValues(-1, 0, 0, 0, 1, 0, 0, 0, 1)\n } else if (value.includes('left-posterior-superior') || value.includes('lps')) {\n rot33 = mat3.fromValues(-1, 0, 0, 0, -1, 0, 0, 0, 1)\n } else {\n log.warn('Unsupported NRRD space value:', value)\n }\n break\n default:\n log.warn('Unknown:', key)\n break\n }\n }\n\n if (!isNaN(mat33[0])) {\n hdr.sform_code = 2\n if (isMicron) {\n // @ts-expect-error FIXME: converting mat3 to mat4\n mat4.multiplyScalar(mat33, mat33, 0.001)\n offset[0] *= 0.001\n offset[1] *= 0.001\n offset[2] *= 0.001\n }\n if (rot33[0] < 0) {\n offset[0] = -offset[0]\n }\n if (rot33[4] < 0) {\n offset[1] = -offset[1]\n }\n if (rot33[8] < 0) {\n offset[2] = -offset[2]\n }\n mat3.multiply(mat33, rot33, mat33)\n\n const mat = mat4.fromValues(mat33[0], mat33[3], mat33[6], offset[0], mat33[1], mat33[4], mat33[7], offset[1], mat33[2], mat33[5], mat33[8], offset[2], 0, 0, 0, 1)\n\n // Ensure vox2mm function is accessible via nvImage\n if (!nvImage.vox2mm) {\n return null\n }\n const mm000 = nvImage.vox2mm([0, 0, 0], mat)\n const mm100 = nvImage.vox2mm([1, 0, 0], mat)\n vec3.subtract(mm100, mm100, mm000)\n const mm010 = nvImage.vox2mm([0, 1, 0], mat)\n vec3.subtract(mm010, mm010, mm000)\n const mm001 = nvImage.vox2mm([0, 0, 1], mat)\n vec3.subtract(mm001, mm001, mm000)\n hdr.pixDims[0] = 1\n hdr.pixDims[1] = vec3.length(mm100)\n hdr.pixDims[2] = vec3.length(mm010)\n hdr.pixDims[3] = vec3.length(mm001)\n\n hdr.affine = [\n [mat[0], mat[1], mat[2], mat[3]],\n [mat[4], mat[5], mat[6], mat[7]],\n [mat[8], mat[9], mat[10], mat[11]],\n [0, 0, 0, 1]\n ]\n }\n let imgRaw: ArrayBuffer | null = null // Use null for error case\n\n // Data source depends on whether header was detached\n const sourceBuffer = isDetached ? pairedImgData : dataBuffer\n // Offset where data starts within the sourceBuffer\n const sourceOffset = isDetached ? 0 : hdr.vox_offset // Use hdr.vox_offset set during header parsing\n\n if (isDetached && !sourceBuffer) {\n log.warn('Missing data: NRRD header describes detached data file but only one URL provided')\n return null\n }\n\n if (!sourceBuffer || sourceOffset >= sourceBuffer.byteLength) {\n log.error(`NRRD data offset (${sourceOffset}) invalid for buffer length (${sourceBuffer?.byteLength ?? 0})`)\n return null\n }\n\n // Slice the data section\n let dataSection = sourceBuffer.slice(sourceOffset)\n\n // Decompress if necessary\n if (isGz) {\n try {\n log.debug('Decompressing NRRD data...')\n dataSection = await NVUtilities.decompressToBuffer(new Uint8Array(dataSection))\n log.debug('Decompression complete.')\n } catch (err) {\n log.error('Failed to decompress NRRD data.', err)\n return null\n }\n }\n\n const nBytesPerVoxel = hdr.numBitsPerVoxel / 8\n const nVoxels = hdr.dims.slice(1, hdr.dims[0] + 1).reduce((acc, dim) => acc * Math.max(1, dim), 1)\n const expectedBytes = nVoxels * nBytesPerVoxel\n if (dataSection.byteLength < expectedBytes) {\n log.error(`NRRD image data size mismatch: expected ${expectedBytes}, found ${dataSection.byteLength}`)\n return null\n } else if (dataSection.byteLength > expectedBytes) {\n log.warn(`NRRD has extra ${dataSection.byteLength - expectedBytes} bytes after expected image data. Truncating.`)\n dataSection = dataSection.slice(0, expectedBytes)\n }\n\n imgRaw = dataSection // Assign the final buffer\n\n // Ensure header has essential NIFTI fields if missing defaults\n if (!hdr.datatypeCode) {\n log.error('NRRD parsing failed to set datatypeCode.')\n return null\n }\n if (!hdr.numBitsPerVoxel) {\n log.error('NRRD parsing failed to set numBitsPerVoxel.')\n return null\n }\n\n return imgRaw // Return the image data buffer\n}\n","import { NIFTI1 } from 'nifti-reader-js'\nimport type { NVImage } from '@/nvimage'\nimport { Zip } from '@/nvutilities'\n\n/**\n * Helper function to determine byte size per element from numpy dtype string.\n * @param dtype - NumPy dtype string (e.g., '<f4', '<i2', '|b1')\n * @returns Byte size of the data type\n */\nfunction getTypeSize(dtype: string): number {\n const typeMap: Record<string, number> = {\n '|b1': 1, // Boolean\n '<i1': 1, // Int8\n '<u1': 1, // UInt8\n '<i2': 2, // Int16\n '<u2': 2, // UInt16\n '<i4': 4, // Int32\n '<u4': 4, // UInt32\n '<f4': 4, // Float32\n '<f8': 8 // Float64\n }\n return typeMap[dtype] ?? 1\n}\n\n/**\n * Helper function to determine NIfTI datatype code from numpy dtype string.\n * @param dtype - NumPy dtype string (e.g., '<f4', '<i2', '|b1')\n * @returns NIfTI datatype code\n */\nfunction getDataTypeCode(dtype: string): number {\n const typeMap: Record<string, number> = {\n '|b1': 2, // DT_BINARY\n '<i1': 256, // DT_INT8\n '<u1': 2, // DT_UINT8\n '<i2': 4, // DT_INT16\n '<u2': 512, // DT_UINT16\n '<i4': 8, // DT_INT32\n '<u4': 768, // DT_UINT32\n '<f4': 16, // DT_FLOAT32\n '<f8': 64 // DT_FLOAT64\n }\n return typeMap[dtype] ?? 16 // Default to FLOAT32\n}\n\n/**\n * Reads NumPy NPY format file, modifying the provided NVImage header\n * and returning the raw image data buffer.\n * @param nvImage - The NVImage instance whose header will be modified.\n * @param buffer - ArrayBuffer containing the NPY file data.\n * @returns Promise resolving to ArrayBuffer containing the image data.\n * @throws Error if the file is not a valid NPY file.\n */\nexport async function readNPY(nvImage: NVImage, buffer: ArrayBuffer): Promise<ArrayBuffer> {\n const dv = new DataView(buffer)\n // Verify magic number\n const magicBytes = [dv.getUint8(0), dv.getUint8(1), dv.getUint8(2), dv.getUint8(3), dv.getUint8(4), dv.getUint8(5)]\n\n // Expected magic number: [0x93, 0x4E, 0x55, 0x4D, 0x50, 0x59] ('\\x93NUMPY')\n const expectedMagic = [0x93, 0x4e, 0x55, 0x4d, 0x50, 0x59]\n\n if (!magicBytes.every((byte, i) => byte === expectedMagic[i])) {\n throw new Error('Not a valid NPY file: Magic number mismatch')\n }\n\n // Extract version and header length\n // const _version = dv.getUint8(6)\n // const _minorVersion = dv.getUint8(7)\n const headerLen = dv.getUint16(8, true) // Little-endian\n // Decode header as ASCII string\n const headerText = new TextDecoder('utf-8').decode(buffer.slice(10, 10 + headerLen))\n\n // Extract shape from header\n const shapeMatch = headerText.match(/'shape': \\((.*?)\\)/)\n if (!shapeMatch) {\n throw new Error('Invalid NPY header: Shape not found')\n }\n const shape = shapeMatch[1]\n .split(',')\n .map((s) => s.trim())\n .filter((s) => s !== '')\n .map(Number)\n\n // Determine data type (assumes '|b1' (bool), '<f4' (float32), etc.)\n const dtypeMatch = headerText.match(/'descr': '([^']+)'/)\n if (!dtypeMatch) {\n throw new Error('Invalid NPY header: Data type not found')\n }\n const dtype = dtypeMatch[1]\n // Compute number of elements\n const numElements = shape.reduce((a, b) => a * b, 1)\n // Extract data start position\n const dataStart = 10 + headerLen\n // Read data as an ArrayBuffer\n const dataBuffer = buffer.slice(dataStart, dataStart + numElements * getTypeSize(dtype))\n // Interpret as 2D/3D data\n const width = shape.length > 0 ? shape[shape.length - 1] : 1\n const height = shape.length > 1 ? shape[shape.length - 2] : 1\n const slices = shape.length > 2 ? shape[shape.length - 3] : 1\n // Create NIFTI header\n nvImage.hdr = new NIFTI1()\n const hdr = nvImage.hdr\n hdr.dims = [3, width, height, slices, 0, 0, 0, 0]\n hdr.pixDims = [1, 1, 1, 1, 1, 0, 0, 0]\n hdr.affine = [\n [hdr.pixDims[1], 0, 0, -(hdr.dims[1] - 2) * 0.5 * hdr.pixDims[1]],\n [0, -hdr.pixDims[2], 0, (hdr.dims[2] - 2) * 0.5 * hdr.pixDims[2]],\n [0, 0, -hdr.pixDims[3], (hdr.dims[3] - 2) * 0.5 * hdr.pixDims[3]],\n [0, 0, 0, 1]\n ]\n hdr.numBitsPerVoxel = getTypeSize(dtype) * 8\n hdr.datatypeCode = getDataTypeCode(dtype)\n return dataBuffer\n}\n\n/**\n * Reads NumPy NPZ format file (zipped NPY arrays), modifying the provided\n * NVImage header and returning the raw image data buffer.\n *\n * Note: Currently only reads the first NPY file found in the archive.\n * TODO: Support reading multiple NPY images from a single NPZ file.\n *\n * @param nvImage - The NVImage instance whose header will be modified.\n * @param buffer - ArrayBuffer containing the NPZ file data.\n * @returns Promise resolving to ArrayBuffer containing the image data.\n */\nexport async function readNPZ(nvImage: NVImage, buffer: ArrayBuffer): Promise<ArrayBuffer | undefined> {\n // todo: a single NPZ file can contain multiple NPY images\n const zip = new Zip(buffer)\n for (let i = 0; i < zip.entries.length; i++) {\n const entry = zip.entries[i]\n if (entry.fileName.toLowerCase().endsWith('.npy')) {\n const data = await entry.extract()\n return await readNPY(nvImage, data.buffer as ArrayBuffer)\n }\n }\n}\n","import { NIFTI1 } from 'nifti-reader-js'\nimport type { NVImage } from '@/nvimage'\nimport { NiiDataType } from '@/nvimage/utils'\n\n/**\n * Helper function to convert ZXY RGB data to XYZ format for Zarr arrays.\n * @param data - Input data in ZXY order\n * @param X - Width dimension\n * @param Y - Height dimension\n * @param Z - Depth dimension\n * @returns Uint8Array in XYZ order\n */\nfunction zxy2xyz(data: any, X: number, Y: number, Z: number): Uint8Array {\n const voxelCount = X * Y\n const rgb = new Uint8Array(voxelCount * Z * 3)\n const offsets = new Array(Z)\n for (let s = 0; s < Z; s++) {\n offsets[s] = voxelCount * 3 * s\n }\n let srcIndex = 0\n let dstIndex = 0\n for (let v = 0; v < voxelCount; v++) {\n for (let s = 0; s < Z; s++) {\n rgb[offsets[s] + dstIndex] = data[srcIndex++] // R\n rgb[offsets[s] + dstIndex + 1] = data[srcIndex++] // G\n rgb[offsets[s] + dstIndex + 2] = data[srcIndex++] // B\n }\n dstIndex += 3\n }\n return rgb\n}\n\n/**\n * Reads Zarr multi-dimensional array format, modifying the provided\n * NVImage header and returning the raw image data buffer.\n *\n * Format specification: https://zarrita.dev/get-started.html\n *\n * Note: RGB channels may be returned as depth dimension.\n *\n * @param nvImage - The NVImage instance whose header will be modified.\n * @param buffer - ArrayBuffer containing the Zarr file data (unused, kept for consistency).\n * @param zarrData - Parsed Zarr data object containing width, height, depth, and data.\n * @returns Promise resolving to ArrayBuffer containing the image data.\n * @throws Error if data dimensions don't match expected size.\n */\nexport async function readZARR(nvImage: NVImage, buffer: ArrayBuffer, zarrData: unknown): Promise<ArrayBufferLike> {\n let { width, height, depth = 1, data } = (zarrData ?? {}) as any\n let expectedLength = width * height * depth * 3\n let isRGB = expectedLength === data.length\n if (!isRGB) {\n expectedLength = width * height * depth\n if (depth === 3) {\n // see https://zarrita.dev/get-started.html R,G,B channels returns as depth!\n isRGB = true\n depth = 1\n }\n }\n if (expectedLength !== data.length) {\n throw new Error(`Expected RGB ${width}×${height}×${depth}×3 = ${expectedLength}, but ZARR length ${data.length}`)\n }\n nvImage.hdr = new NIFTI1()\n const hdr = nvImage.hdr\n hdr.dims = [3, width, height, depth, 1, 1, 1, 1]\n hdr.pixDims = [1, 1, 1, 1, 0, 0, 0, 0]\n\n hdr.affine = [\n [hdr.pixDims[1], 0, 0, -(hdr.dims[1] - 2) * 0.5 * hdr.pixDims[1]],\n [0, -hdr.pixDims[2], 0, (hdr.dims[2] - 2) * 0.5 * hdr.pixDims[2]],\n [0, 0, -hdr.pixDims[3], (hdr.dims[3] - 2) * 0.5 * hdr.pixDims[3]],\n [0, 0, 0, 1]\n ]\n if (!isRGB) {\n hdr.numBitsPerVoxel = 8\n hdr.datatypeCode = NiiDataType.DT_UINT8\n // if data is a Uint8Array, convert to ArrayBuffer\n if (data instanceof Uint8Array) {\n const retBuffer = new ArrayBuffer(data.length)\n const retView = new Uint8Array(retBuffer)\n retView.set(data)\n return retBuffer\n }\n return data\n }\n hdr.numBitsPerVoxel = 24\n hdr.datatypeCode = NiiDataType.DT_RGB24\n const retData = zxy2xyz(data, hdr.dims[1], hdr.dims[2], hdr.dims[3])\n // convert retData Uint8Array to ArrayBuffer\n const retBuffer = new ArrayBuffer(retData.length)\n const retView = new Uint8Array(retBuffer)\n retView.set(retData)\n return retBuffer\n}\n","/**\n * CoordinateTransform Module\n *\n * Handles all coordinate system transformations for NVImage:\n * - Voxel ↔ Millimeter conversions\n * - Voxel ↔ Fractional texture coordinate conversions\n * - Fractional ↔ Millimeter conversions\n *\n * This module provides pure functions for transforming between different\n * coordinate spaces used in medical imaging visualization.\n */\n\nimport { mat4, vec3, vec4 } from 'gl-matrix'\nimport type { NVImage } from './index'\n\n/**\n * Convert voxel location to millimeter coordinates.\n *\n * Transforms from 0-indexed voxel space (row, column, slice) to world space\n * using the provided transformation matrix.\n *\n * @param _nvImage - The NVImage instance (unused, for API consistency)\n * @param XYZ - Voxel coordinates [x, y, z]\n * @param mtx - Transformation matrix (typically matRAS)\n * @returns Millimeter coordinates in world space\n */\nexport function vox2mm(_nvImage: NVImage, XYZ: number[], mtx: mat4): vec3 {\n const sform = mat4.clone(mtx)\n mat4.transpose(sform, sform)\n const pos = vec4.fromValues(XYZ[0], XYZ[1], XYZ[2], 1)\n vec4.transformMat4(pos, pos, sform)\n const pos3 = vec3.fromValues(pos[0], pos[1], pos[2])\n return pos3\n}\n\n/**\n * Convert millimeter coordinates to voxel location.\n *\n * Transforms from world space (millimeters) to 0-indexed voxel space\n * using the image's matRAS transformation matrix.\n *\n * @param nvImage - The NVImage instance\n * @param mm - Millimeter coordinates [x, y, z]\n * @param frac - If true, return fractional voxel coordinates; if false, return rounded integers\n * @returns Voxel coordinates (Float32Array if rounded, vec3 if fractional)\n * @throws Error if matRAS is undefined\n */\nexport function mm2vox(nvImage: NVImage, mm: number[], frac = false): Float32Array | vec3 {\n if (!nvImage.matRAS) {\n throw new Error('matRAS undefined')\n }\n\n const sform = mat4.clone(nvImage.matRAS)\n const out = mat4.clone(sform)\n mat4.transpose(out, sform)\n mat4.invert(out, out)\n const pos = vec4.fromValues(mm[0], mm[1], mm[2], 1)\n vec4.transformMat4(pos, pos, out)\n const pos3 = vec3.fromValues(pos[0], pos[1], pos[2])\n if (frac) {\n return pos3\n }\n return new Float32Array([Math.round(pos3[0]), Math.round(pos3[1]), Math.round(pos3[2])])\n}\n\n/**\n * Convert voxel coordinates to fractional texture coordinates.\n *\n * Transforms from 0-indexed voxel space [0..dim[i]-1] to normalized\n * texture space [0..1]. Voxel centers are positioned at fractional\n * coordinates (e.g., for 3 voxels: centers at 0.25, 0.5, 0.75).\n *\n * @param nvImage - The NVImage instance\n * @param vox - Voxel coordinates [x, y, z]\n * @returns Fractional texture coordinates [0..1, 0..1, 0..1]\n */\nexport function convertVox2Frac(nvImage: NVImage, vox: vec3): vec3 {\n // convert from 0-index voxel space [0..dim[1]-1, 0..dim[2]-1, 0..dim[3]-1] to normalized texture space XYZ= [0..1, 0..1 ,0..1]\n // consider dimension with 3 voxels, the voxel centers are at 0.25, 0.5, 0.75 corresponding to 0,1,2\n const frac = vec3.fromValues((vox[0] + 0.5) / nvImage.dimsRAS![1], (vox[1] + 0.5) / nvImage.dimsRAS![2], (vox[2] + 0.5) / nvImage.dimsRAS![3])\n return frac\n}\n\n/**\n * Convert fractional texture coordinates to voxel coordinates.\n *\n * Transforms from normalized texture space [0..1] to 0-indexed voxel\n * space, rounding to the nearest voxel center.\n *\n * @param nvImage - The NVImage instance\n * @param frac - Fractional texture coordinates [0..1, 0..1, 0..1]\n * @returns Voxel coordinates (rounded to nearest integer)\n */\nexport function convertFrac2Vox(nvImage: NVImage, frac: vec3): vec3 {\n const vox = vec3.fromValues(\n Math.round(frac[0] * nvImage.dims![1] - 0.5), // dims === RAS\n Math.round(frac[1] * nvImage.dims![2] - 0.5), // dims === RAS\n Math.round(frac[2] * nvImage.dims![3] - 0.5) // dims === RAS\n )\n return vox\n}\n\n/**\n * Convert fractional texture coordinates to millimeter coordinates.\n *\n * Transforms from normalized texture space [0..1] to world space (millimeters)\n * using either the oblique-corrected or original transformation matrix.\n *\n * @param nvImage - The NVImage instance\n * @param frac - Fractional texture coordinates [0..1, 0..1, 0..1]\n * @param isForceSliceMM - If true, use frac2mm; if false, use frac2mmOrtho\n * @returns Millimeter coordinates in world space (vec4 with homogeneous coordinate)\n */\nexport function convertFrac2MM(nvImage: NVImage, frac: vec3, isForceSliceMM = false): vec4 {\n const pos = vec4.fromValues(frac[0], frac[1], frac[2], 1)\n if (isForceSliceMM) {\n vec4.transformMat4(pos, pos, nvImage.frac2mm!)\n } else {\n vec4.transformMat4(pos, pos, nvImage.frac2mmOrtho!)\n }\n return pos\n}\n\n/**\n * Convert millimeter coordinates to fractional texture coordinates.\n *\n * Transforms from world space (millimeters) to normalized texture space [0..1].\n * Uses either the oblique-corrected or original inverse transformation.\n *\n * @param nvImage - The NVImage instance\n * @param mm - Millimeter coordinates [x, y, z] or [x, y, z, w]\n * @param isForceSliceMM - If true, use matRAS inverse; if false, use frac2mmOrtho inverse\n * @returns Fractional texture coordinates [0..1, 0..1, 0..1]\n */\nexport function convertMM2Frac(nvImage: NVImage, mm: vec3 | vec4, isForceSliceMM = false): vec3 {\n // given mm, return volume fraction\n // convert from object space in millimeters to normalized texture space XYZ= [0..1, 0..1 ,0..1]\n const mm4 = vec4.fromValues(mm[0], mm[1], mm[2], 1)\n const d = nvImage.dimsRAS\n const frac = vec3.fromValues(0, 0, 0)\n if (typeof d === 'undefined') {\n return frac\n }\n if (!isForceSliceMM) {\n const xform = mat4.clone(nvImage.frac2mmOrtho!)\n mat4.invert(xform, xform)\n vec4.transformMat4(mm4, mm4, xform)\n frac[0] = mm4[0]\n frac[1] = mm4[1]\n frac[2] = mm4[2]\n return frac\n }\n if (d[1] < 1 || d[2] < 1 || d[3] < 1) {\n return frac\n }\n const sform = mat4.clone(nvImage.matRAS!)\n mat4.invert(sform, sform)\n mat4.transpose(sform, sform)\n vec4.transformMat4(mm4, mm4, sform)\n frac[0] = (mm4[0] + 0.5) / d[1]\n frac[1] = (mm4[1] + 0.5) / d[2]\n frac[2] = (mm4[2] + 0.5) / d[3]\n return frac\n}\n\n/**\n * Check if two arrays are equal.\n *\n * Compares two arrays element-by-element for equality.\n * Note: This is a shallow comparison and won't work correctly for complex objects.\n *\n * @param a - First array\n * @param b - Second array\n * @returns true if arrays have the same length and all elements are equal\n */\nexport function arrayEquals(a: unknown[], b: unknown[]): boolean {\n return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((val, index) => val === b[index])\n}\n","/**\n * ImageOrientation module\n *\n * Handles image orientation, RAS calculation, and reorientation.\n * This module contains functions for:\n * - Calculating RAS (Right-Anterior-Superior) orientation from NIfTI headers\n * - Computing oblique transformation matrices\n * - Detecting misalignment between voxel and world space\n * - Reorienting image data and headers to RAS\n */\n\nimport { mat3, mat4, vec3, vec4 } from 'gl-matrix'\nimport { NIFTI1, NIFTI2, readHeaderAsync } from 'nifti-reader-js'\nimport type { NVImage, TypedVoxelArray } from './index'\nimport { log } from '@/logger'\nimport { hdrToArrayBuffer } from '@/nvimage/utils'\nimport * as CoordinateTransform from '@/nvimage/CoordinateTransform'\n\n/**\n * Detect difference between voxel grid and world space.\n * https://github.com/afni/afni/blob/25e77d564f2c67ff480fa99a7b8e48ec2d9a89fc/src/thd_coords.c#L717\n *\n * @param mtx44 - Transformation matrix\n * @returns Oblique angle in degrees\n */\nexport function computeObliqueAngle(mtx44: mat4): number {\n const mtx = mat4.clone(mtx44)\n mat4.transpose(mtx, mtx44)\n const dxtmp = Math.sqrt(mtx[0] * mtx[0] + mtx[1] * mtx[1] + mtx[2] * mtx[2])\n const xmax = Math.max(Math.max(Math.abs(mtx[0]), Math.abs(mtx[1])), Math.abs(mtx[2])) / dxtmp\n const dytmp = Math.sqrt(mtx[4] * mtx[4] + mtx[5] * mtx[5] + mtx[6] * mtx[6])\n const ymax = Math.max(Math.max(Math.abs(mtx[4]), Math.abs(mtx[5])), Math.abs(mtx[6])) / dytmp\n const dztmp = Math.sqrt(mtx[8] * mtx[8] + mtx[9] * mtx[9] + mtx[10] * mtx[10])\n const zmax = Math.max(Math.max(Math.abs(mtx[8]), Math.abs(mtx[9])), Math.abs(mtx[10])) / dztmp\n const fig_merit = Math.min(Math.min(xmax, ymax), zmax)\n let oblique_angle = Math.abs((Math.acos(fig_merit) * 180.0) / 3.141592653)\n if (oblique_angle > 0.01) {\n log.warn('Warning voxels not aligned with world space: ' + oblique_angle + ' degrees from plumb.\\n')\n } else {\n oblique_angle = 0.0\n }\n return oblique_angle\n}\n\n/**\n * Calculate oblique transformation matrices.\n * Detects difference between voxel grid and world space.\n *\n * @param nvImage - The NVImage instance\n */\nexport function calculateOblique(nvImage: NVImage): void {\n if (!nvImage.matRAS) {\n throw new Error('matRAS not defined')\n }\n if (nvImage.pixDimsRAS === undefined) {\n throw new Error('pixDimsRAS not defined')\n }\n if (!nvImage.dimsRAS) {\n throw new Error('dimsRAS not defined')\n }\n\n nvImage.oblique_angle = computeObliqueAngle(nvImage.matRAS)\n const LPI = CoordinateTransform.vox2mm(nvImage, [0.0, 0.0, 0.0], nvImage.matRAS)\n const X1mm = CoordinateTransform.vox2mm(nvImage, [1.0 / nvImage.pixDimsRAS[1], 0.0, 0.0], nvImage.matRAS)\n const Y1mm = CoordinateTransform.vox2mm(nvImage, [0.0, 1.0 / nvImage.pixDimsRAS[2], 0.0], nvImage.matRAS)\n const Z1mm = CoordinateTransform.vox2mm(nvImage, [0.0, 0.0, 1.0 / nvImage.pixDimsRAS[3]], nvImage.matRAS)\n vec3.subtract(X1mm, X1mm, LPI)\n vec3.subtract(Y1mm, Y1mm, LPI)\n vec3.subtract(Z1mm, Z1mm, LPI)\n const oblique = mat4.fromValues(X1mm[0], X1mm[1], X1mm[2], 0, Y1mm[0], Y1mm[1], Y1mm[2], 0, Z1mm[0], Z1mm[1], Z1mm[2], 0, 0, 0, 0, 1)\n nvImage.obliqueRAS = mat4.clone(oblique)\n const XY = Math.abs(90 - vec3.angle(X1mm, Y1mm) * (180 / Math.PI))\n const XZ = Math.abs(90 - vec3.angle(X1mm, Z1mm) * (180 / Math.PI))\n const YZ = Math.abs(90 - vec3.angle(Y1mm, Z1mm) * (180 / Math.PI))\n nvImage.maxShearDeg = Math.max(Math.max(XY, XZ), YZ)\n if (nvImage.maxShearDeg > 0.1) {\n log.warn('Warning: voxels are rhomboidal, maximum shear is %f degrees.', nvImage.maxShearDeg)\n }\n // compute a matrix to transform vectors from factional space to mm:\n const dim = vec4.fromValues(nvImage.dimsRAS[1], nvImage.dimsRAS[2], nvImage.dimsRAS[3], 1)\n const sform = mat4.clone(nvImage.matRAS)\n mat4.transpose(sform, sform)\n const shim = vec4.fromValues(-0.5, -0.5, -0.5, 0) // bitmap with 5 voxels scaled 0..1, voxel centers are 0.1,0.3,0.5,0.7,0.9\n mat4.translate(sform, sform, vec3.fromValues(shim[0], shim[1], shim[2]))\n // mat.mat4.scale(sform, sform, dim);\n sform[0] *= dim[0]\n sform[1] *= dim[0]\n sform[2] *= dim[0]\n sform[4] *= dim[1]\n sform[5] *= dim[1]\n sform[6] *= dim[1]\n sform[8] *= dim[2]\n sform[9] *= dim[2]\n sform[10] *= dim[2]\n nvImage.frac2mm = mat4.clone(sform)\n const pixdimX = nvImage.pixDimsRAS[1] // vec3.length(X1mm);\n const pixdimY = nvImage.pixDimsRAS[2] // vec3.length(Y1mm);\n const pixdimZ = nvImage.pixDimsRAS[3] // vec3.length(Z1mm);\n // orthographic view\n const oform = mat4.clone(sform)\n oform[0] = pixdimX * dim[0]\n oform[1] = 0\n oform[2] = 0\n oform[4] = 0\n oform[5] = pixdimY * dim[1]\n oform[6] = 0\n oform[8] = 0\n oform[9] = 0\n oform[10] = pixdimZ * dim[2]\n const originVoxel = CoordinateTransform.mm2vox(nvImage, [0, 0, 0], true)\n // set matrix translation for distance from origin\n oform[12] = (-originVoxel[0] - 0.5) * pixdimX\n oform[13] = (-originVoxel[1] - 0.5) * pixdimY\n oform[14] = (-originVoxel[2] - 0.5) * pixdimZ\n nvImage.frac2mmOrtho = mat4.clone(oform)\n nvImage.extentsMinOrtho = [oform[12], oform[13], oform[14]]\n nvImage.extentsMaxOrtho = [oform[0] + oform[12], oform[5] + oform[13], oform[10] + oform[14]]\n nvImage.mm2ortho = mat4.create()\n mat4.invert(nvImage.mm2ortho, oblique)\n}\n\n/**\n * Transform to orient NIfTI image to Left->Right, Posterior->Anterior, Inferior->Superior (48 possible permutations).\n * Port of Matlab reorient() https://github.com/xiangruili/dicm2nii/blob/master/nii_viewer.m\n *\n * @param nvImage - The NVImage instance\n */\nexport function calculateRAS(nvImage: NVImage): void {\n if (!nvImage.hdr) {\n throw new Error('hdr not set')\n }\n // not elegant, as JavaScript arrays are always 1D\n const a = nvImage.hdr.affine\n const header = nvImage.hdr\n const absR = mat3.fromValues(\n Math.abs(a[0][0]),\n Math.abs(a[0][1]),\n Math.abs(a[0][2]),\n Math.abs(a[1][0]),\n Math.abs(a[1][1]),\n Math.abs(a[1][2]),\n Math.abs(a[2][0]),\n Math.abs(a[2][1]),\n Math.abs(a[2][2])\n )\n // 1st column = x\n const ixyz = [1, 1, 1]\n if (absR[3] > absR[0]) {\n ixyz[0] = 2 // (absR[1][0] > absR[0][0]) ixyz[0] = 2;\n }\n if (absR[6] > absR[0] && absR[6] > absR[3]) {\n ixyz[0] = 3 // ((absR[2][0] > absR[0][0]) && (absR[2][0]> absR[1][0])) ixyz[0] = 3;\n } // 2nd column = y\n ixyz[1] = 1\n if (ixyz[0] === 1) {\n if (absR[4] > absR[7]) {\n // (absR[1][1] > absR[2][1])\n ixyz[1] = 2\n } else {\n ixyz[1] = 3\n }\n } else if (ixyz[0] === 2) {\n if (absR[1] > absR[7]) {\n // (absR[0][1] > absR[2][1])\n ixyz[1] = 1\n } else {\n ixyz[1] = 3\n }\n } else {\n if (absR[1] > absR[4]) {\n // (absR[0][1] > absR[1][1])\n ixyz[1] = 1\n } else {\n ixyz[1] = 2\n }\n }\n // 3rd column = z: constrained as x+y+z = 1+2+3 = 6\n ixyz[2] = 6 - ixyz[1] - ixyz[0]\n let perm = [1, 2, 3]\n perm[ixyz[0] - 1] = 1\n perm[ixyz[1] - 1] = 2\n perm[ixyz[2] - 1] = 3\n let rotM = mat4.fromValues(a[0][0], a[0][1], a[0][2], a[0][3], a[1][0], a[1][1], a[1][2], a[1][3], a[2][0], a[2][1], a[2][2], a[2][3], 0, 0, 0, 1)\n // n.b. 0.5 in these values to account for voxel centers, e.g. a 3-pixel wide bitmap in unit space has voxel centers at 0.25, 0.5 and 0.75\n nvImage.mm000 = CoordinateTransform.vox2mm(nvImage, [-0.5, -0.5, -0.5], rotM)\n nvImage.mm100 = CoordinateTransform.vox2mm(nvImage, [header.dims[1] - 0.5, -0.5, -0.5], rotM)\n nvImage.mm010 = CoordinateTransform.vox2mm(nvImage, [-0.5, header.dims[2] - 0.5, -0.5], rotM)\n nvImage.mm001 = CoordinateTransform.vox2mm(nvImage, [-0.5, -0.5, header.dims[3] - 0.5], rotM)\n const R = mat4.create()\n mat4.copy(R, rotM)\n for (let i = 0; i < 3; i++) {\n for (let j = 0; j < 3; j++) {\n R[i * 4 + j] = rotM[i * 4 + perm[j] - 1] // rotM[i+(4*(perm[j]-1))];//rotM[i],[perm[j]-1];\n }\n }\n const flip = [0, 0, 0]\n if (R[0] < 0) {\n flip[0] = 1\n } // R[0][0]\n if (R[5] < 0) {\n flip[1] = 1\n } // R[1][1]\n if (R[10] < 0) {\n flip[2] = 1\n } // R[2][2]\n nvImage.dimsRAS = [header.dims[0], header.dims[perm[0]], header.dims[perm[1]], header.dims[perm[2]]]\n nvImage.pixDimsRAS = [header.pixDims[0], header.pixDims[perm[0]], header.pixDims[perm[1]], header.pixDims[perm[2]]]\n nvImage.permRAS = perm.slice()\n for (let i = 0; i < 3; i++) {\n if (flip[i] === 1) {\n nvImage.permRAS[i] = -nvImage.permRAS[i]\n }\n }\n if (CoordinateTransform.arrayEquals(perm, [1, 2, 3]) && CoordinateTransform.arrayEquals(flip, [0, 0, 0])) {\n nvImage.toRAS = mat4.create() // aka fromValues(1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1);\n nvImage.matRAS = mat4.clone(rotM)\n calculateOblique(nvImage)\n nvImage.img2RASstep = [1, nvImage.dimsRAS[1], nvImage.dimsRAS[1] * nvImage.dimsRAS[2]]\n nvImage.img2RASstart = [0, 0, 0]\n return // no rotation required!\n }\n mat4.identity(rotM)\n rotM[0 + 0 * 4] = 1 - flip[0] * 2\n rotM[1 + 1 * 4] = 1 - flip[1] * 2\n rotM[2 + 2 * 4] = 1 - flip[2] * 2\n rotM[3 + 0 * 4] = (header.dims[perm[0]] - 1) * flip[0]\n rotM[3 + 1 * 4] = (header.dims[perm[1]] - 1) * flip[1]\n rotM[3 + 2 * 4] = (header.dims[perm[2]] - 1) * flip[2]\n const residualR = mat4.create()\n mat4.invert(residualR, rotM)\n mat4.multiply(residualR, residualR, R)\n nvImage.matRAS = mat4.clone(residualR)\n rotM = mat4.fromValues(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1)\n rotM[perm[0] - 1 + 0 * 4] = -flip[0] * 2 + 1\n rotM[perm[1] - 1 + 1 * 4] = -flip[1] * 2 + 1\n rotM[perm[2] - 1 + 2 * 4] = -flip[2] * 2 + 1\n rotM[3 + 0 * 4] = flip[0]\n rotM[3 + 1 * 4] = flip[1]\n rotM[3 + 2 * 4] = flip[2]\n nvImage.toRAS = mat4.clone(rotM) // webGL unit textures\n // voxel based column-major,\n rotM[3] = 0\n rotM[7] = 0\n rotM[11] = 0\n rotM[12] = 0\n if (nvImage.permRAS[0] === -1 || nvImage.permRAS[1] === -1 || nvImage.permRAS[2] === -1) {\n rotM[12] = header.dims[1] - 1\n }\n rotM[13] = 0\n if (nvImage.permRAS[0] === -2 || nvImage.permRAS[1] === -2 || nvImage.permRAS[2] === -2) {\n rotM[13] = header.dims[2] - 1\n }\n rotM[14] = 0\n if (nvImage.permRAS[0] === -3 || nvImage.permRAS[1] === -3 || nvImage.permRAS[2] === -3) {\n rotM[14] = header.dims[3] - 1\n }\n nvImage.toRASvox = mat4.clone(rotM)\n log.debug(nvImage.hdr.dims)\n log.debug(nvImage.dimsRAS)\n\n // compute img2RASstep[] and img2RASstart[] for rapid native<->RAS conversion\n // TODO: replace all other outStep/outStart calculations with img2RASstep/img2RASstart\n const hdr = nvImage.hdr\n perm = nvImage.permRAS\n const aperm = [Math.abs(perm[0]), Math.abs(perm[1]), Math.abs(perm[2])]\n const outdim = [hdr.dims[aperm[0]], hdr.dims[aperm[1]], hdr.dims[aperm[2]]]\n const inStep = [1, hdr.dims[1], hdr.dims[1] * hdr.dims[2]] // increment i,j,k\n const outStep = [inStep[aperm[0] - 1], inStep[aperm[1] - 1], inStep[aperm[2] - 1]]\n const outStart = [0, 0, 0]\n for (let p = 0; p < 3; p++) {\n // flip dimensions\n if (perm[p] < 0) {\n outStart[p] = outStep[p] * (outdim[p] - 1)\n outStep[p] = -outStep[p]\n }\n }\n nvImage.img2RASstep = outStep\n nvImage.img2RASstart = outStart\n\n calculateOblique(nvImage)\n}\n\n/**\n * Reorient raw header data to RAS.\n * Assume single volume, use nVolumes to specify, set nVolumes = 0 for same as input.\n *\n * @param nvImage - The NVImage instance\n * @param nVolumes - Number of volumes (default: 1)\n * @returns Promise<NIFTI1 | NIFTI2> - Reoriented header\n */\nexport async function hdr2RAS(nvImage: NVImage, nVolumes: number = 1): Promise<NIFTI1 | NIFTI2> {\n if (!nvImage.permRAS) {\n throw new Error('permRAS undefined')\n }\n if (!nvImage.hdr) {\n throw new Error('hdr undefined')\n }\n // make a deep clone\n const hdrBytes = hdrToArrayBuffer({ ...nvImage.hdr!, vox_offset: 352 }, false)\n const hdr = await readHeaderAsync(hdrBytes.buffer as ArrayBuffer, true)\n // n.b. if nVolumes < 1, input volumes = output volumess\n if (nVolumes === 1) {\n // 3D\n hdr.dims[0] = 3\n hdr.dims[4] = 1\n } else if (nVolumes > 1) {\n // 4D\n hdr.dims[0] = 4\n hdr.dims[4] = nVolumes\n }\n const perm = nvImage.permRAS.slice()\n if (perm[0] === 1 && perm[1] === 2 && perm[2] === 3) {\n return hdr\n } // header is already in RAS\n hdr.qform_code = 0\n for (let i = 1; i < 4; i++) {\n hdr.dims[i] = nvImage.dimsRAS[i]\n }\n\n for (let i = 0; i < nvImage.pixDimsRAS.length; i++) {\n hdr.pixDims[i] = nvImage.pixDimsRAS[i]\n }\n let k = 0\n for (let i = 0; i < 4; i++) {\n for (let j = 0; j < 4; j++) {\n hdr.affine[i][j] = nvImage.matRAS[k]\n k++\n }\n }\n return hdr\n}\n\n/**\n * Reorient raw image data to RAS.\n * Note that GPU-based orient shader is much faster.\n * Returns single 3D volume even for 4D input. Use nVolume to select volume (0 indexed).\n *\n * @param nvImage - The NVImage instance\n * @param nVolume - Volume index (default: 0)\n * @returns TypedVoxelArray - Reoriented image data\n */\nexport function img2RAS(nvImage: NVImage, nVolume: number = 0): TypedVoxelArray {\n if (!nvImage.permRAS) {\n throw new Error('permRAS undefined')\n }\n if (!nvImage.img) {\n throw new Error('img undefined')\n }\n if (!nvImage.hdr) {\n throw new Error('hdr undefined')\n }\n\n const perm = nvImage.permRAS.slice()\n if (perm[0] === 1 && perm[1] === 2 && perm[2] === 3) {\n return nvImage.img\n } // image is already in RAS\n const hdr = nvImage.hdr\n const nVox = hdr.dims[1] * hdr.dims[2] * hdr.dims[3]\n let volSkip = nVolume * nVox\n if (volSkip + nVox > nvImage.img.length || volSkip < 0) {\n volSkip = 0\n log.warn(`img2RAS nVolume (${nVolume}) out of bounds (${nVolume}+1)×${nVox} > ${nvImage.img.length}`)\n }\n // preallocate/clone image (only 3D for 4D datasets!)\n const imgRAS = nvImage.img.slice(0, nVox)\n const aperm = [Math.abs(perm[0]), Math.abs(perm[1]), Math.abs(perm[2])]\n const outdim = [hdr.dims[aperm[0]], hdr.dims[aperm[1]], hdr.dims[aperm[2]]]\n const inStep = [1, hdr.dims[1], hdr.dims[1] * hdr.dims[2]] // increment i,j,k\n const outStep = [inStep[aperm[0] - 1], inStep[aperm[1] - 1], inStep[aperm[2] - 1]]\n const outStart = [0, 0, 0]\n for (let p = 0; p < 3; p++) {\n // flip dimensions\n if (perm[p] < 0) {\n outStart[p] = outStep[p] * (outdim[p] - 1)\n outStep[p] = -outStep[p]\n }\n }\n let j = 0\n for (let z = 0; z < outdim[2]; z++) {\n const zi = outStart[2] + z * outStep[2]\n for (let y = 0; y < outdim[1]; y++) {\n const yi = outStart[1] + y * outStep[1]\n for (let x = 0; x < outdim[0]; x++) {\n const xi = outStart[0] + x * outStep[0]\n imgRAS[j] = nvImage.img[xi + yi + zi + volSkip]\n j++\n } // for x\n } // for y\n } // for z\n return imgRAS\n}\n","export { default as $itksnap } from './_itksnap.json'\nexport { default as $slicer3d } from './_slicer3d.json'\nexport { default as actc } from './actc.json'\nexport { default as batlow } from './batlow.json'\nexport { default as bcgwhw_dark } from './bcgwhw_dark.json'\nexport { default as bcgwhw } from './bcgwhw.json'\nexport { default as blue2red } from './blue2red.json'\nexport { default as blue2magenta } from './blue2magenta.json'\nexport { default as blue2cyan } from './blue2cyan.json'\nexport { default as bluegrn } from './bluegrn.json'\nexport { default as blue } from './blue.json'\nexport { default as bone } from './bone.json'\nexport { default as bronze } from './bronze.json'\nexport { default as cet_l17 } from './cet_l17.json'\nexport { default as cividis } from './cividis.json'\nexport { default as cool } from './cool.json'\nexport { default as copper2 } from './copper2.json'\nexport { default as copper } from './copper.json'\nexport { default as ct_airways } from './ct_airways.json'\nexport { default as ct_artery } from './ct_artery.json'\nexport { default as ct_bones } from './ct_bones.json'\nexport { default as ct_brain_gray } from './ct_brain_gray.json'\nexport { default as ct_brain } from './ct_brain.json'\nexport { default as ct_cardiac } from './ct_cardiac.json'\nexport { default as ct_head } from './ct_head.json'\nexport { default as ct_kidneys } from './ct_kidneys.json'\nexport { default as ct_liver } from './ct_liver.json'\nexport { default as ct_muscles } from './ct_muscles.json'\nexport { default as ct_scalp } from './ct_scalp.json'\nexport { default as ct_skull } from './ct_skull.json'\nexport { default as ct_soft } from './ct_soft.json'\nexport { default as ct_soft_tissue } from './ct_soft_tissue.json'\nexport { default as ct_surface } from './ct_surface.json'\nexport { default as ct_vessels } from './ct_vessels.json'\nexport { default as ct_w_contrast } from './ct_w_contrast.json'\nexport { default as cubehelix } from './cubehelix.json'\nexport { default as electric_blue } from './electric_blue.json'\nexport { default as freesurfer } from './freesurfer.json'\nexport { default as ge_color } from './ge_color.json'\nexport { default as gold } from './gold.json'\nexport { default as gray } from './gray.json'\nexport { default as green } from './green.json'\nexport { default as green2cyan } from './green2cyan.json'\nexport { default as green2orange } from './green2orange.json'\nexport { default as hotiron } from './hotiron.json'\nexport { default as hot } from './hot.json'\nexport { default as hsv } from './hsv.json'\nexport { default as inferno } from './inferno.json'\nexport { default as jet } from './jet.json'\nexport { default as kry } from './kry.json'\nexport { default as linspecer } from './linspecer.json'\nexport { default as lipari } from './lipari.json'\nexport { default as magma } from './magma.json'\nexport { default as mako } from './mako.json'\nexport { default as navia } from './navia.json'\nexport { default as nih } from './nih.json'\nexport { default as plasma } from './plasma.json'\nexport { default as random } from './random.json'\nexport { default as red } from './red.json'\nexport { default as redyell } from './redyell.json'\nexport { default as rocket } from './rocket.json'\nexport { default as surface } from './surface.json'\nexport { default as thermal } from './thermal.json'\nexport { default as turbo } from './turbo.json'\nexport { default as violet } from './violet.json'\nexport { default as viridis } from './viridis.json'\nexport { default as warm } from './warm.json'\nexport { default as winter } from './winter.json'\nexport { default as x_rain } from './x_rain.json'\nexport { default as afni_blues_inv } from './afni_blues_inv.json'\nexport { default as afni_reds_inv } from './afni_reds_inv.json'\nexport { default as roi_i256 } from './roi_i256.json'\n","{\n \"R\": [\n 0, 255, 0, 0, 255, 0, 255, 255, 0, 205, 210, 102, 0, 0, 46, 255, 106, 221,\n 233, 165, 255, 147, 218, 75, 255, 60, 255, 255, 218, 0, 188, 255, 255, 222,\n 127, 139, 124, 255, 70, 0, 238, 238, 240, 245, 184, 32, 255, 25, 112, 34,\n 248, 245, 255, 144, 173, 65, 255, 250, 128, 50, 244, 255, 123, 255, 173,\n 255, 127, 255, 143, 220, 253, 255, 0, 0, 128, 255, 250, 148, 178, 255, 135,\n 100, 240, 250, 255, 107, 135, 0, 139, 245, 186, 255, 255, 0, 210, 255, 47,\n 72, 175, 128, 176, 255, 139, 240, 255, 216, 119, 219, 72, 255, 199, 154,\n 189, 240, 230, 0, 85, 64, 153, 205, 250, 95, 0, 255, 224, 176, 138, 30, 240,\n 152, 160\n ],\n \"G\": [\n 0, 0, 255, 0, 255, 255, 0, 239, 0, 133, 180, 205, 0, 139, 139, 228, 90, 160,\n 150, 42, 250, 112, 112, 0, 182, 179, 235, 228, 165, 128, 143, 105, 218, 184,\n 255, 69, 252, 255, 130, 100, 130, 232, 255, 222, 134, 178, 20, 25, 128, 139,\n 248, 255, 160, 238, 255, 105, 99, 240, 0, 205, 164, 255, 104, 165, 216, 192,\n 255, 140, 188, 20, 245, 250, 206, 255, 0, 250, 128, 0, 34, 127, 206, 149,\n 230, 235, 245, 142, 206, 0, 0, 245, 85, 228, 222, 191, 105, 248, 79, 61,\n 238, 128, 224, 240, 0, 255, 215, 191, 136, 112, 209, 0, 21, 205, 183, 248,\n 230, 250, 107, 224, 50, 92, 250, 158, 128, 69, 255, 196, 43, 144, 128, 251,\n 82\n ],\n \"B\": [\n 0, 0, 0, 255, 0, 255, 255, 213, 205, 63, 140, 170, 128, 139, 87, 225, 205,\n 221, 122, 42, 250, 219, 214, 130, 193, 113, 205, 196, 32, 128, 143, 180,\n 185, 135, 0, 19, 0, 224, 180, 0, 238, 170, 240, 179, 11, 170, 147, 112, 144,\n 34, 255, 250, 122, 144, 47, 225, 71, 230, 0, 50, 96, 240, 238, 0, 230, 203,\n 212, 0, 143, 60, 230, 240, 209, 127, 128, 205, 114, 211, 34, 80, 235, 237,\n 140, 215, 238, 35, 250, 139, 139, 220, 211, 181, 173, 255, 30, 220, 79, 139,\n 238, 0, 230, 245, 0, 255, 0, 216, 153, 147, 204, 255, 133, 50, 107, 255,\n 250, 154, 47, 208, 204, 92, 210, 160, 0, 0, 255, 222, 226, 255, 128, 152, 45\n ],\n \"A\": [\n 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,\n 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,\n 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,\n 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,\n 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,\n 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,\n 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,\n 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,\n 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255\n ],\n \"I\": [\n 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,\n 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39,\n 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58,\n 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77,\n 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96,\n 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112,\n 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127,\n 128, 129, 130\n ]\n}\n","{\n \"labels\": [\n \"background\",\n \"tissue\",\n \"bone\",\n \"skin\",\n \"connective tissue\",\n \"blood\",\n \"organ\",\n \"mass\",\n \"muscle\",\n \"foreign object\",\n \"waste\",\n \"teeth\",\n \"fat\",\n \"gray matter\",\n \"white matter\",\n \"nerve\",\n \"vein\",\n \"artery\",\n \"capillary\",\n \"ligament\",\n \"tendon\",\n \"cartilage\",\n \"meniscus\",\n \"lymph node\",\n \"lymphatic vessel\",\n \"cerebro-spinal fluid\",\n \"bile\",\n \"urine\",\n \"feces\",\n \"gas\",\n \"fluid\",\n \"edema\",\n \"bleeding\",\n \"necrosis\",\n \"clot\",\n \"embolism\",\n \"head\",\n \"central nervous system\",\n \"brain\",\n \"gray matter of brain\",\n \"telencephalon\",\n \"cerebral cortex\",\n \"right frontal lobe\",\n \"left frontal lobe\",\n \"right temporal lobe\",\n \"left temporal lobe\",\n \"right parietal lobe\",\n \"left parietal lobe\",\n \"right occipital lobe\",\n \"left occipital lobe\",\n \"right insular lobe\",\n \"left insular lobe\",\n \"right limbic lobe\",\n \"left limbic lobe\",\n \"right striatum\",\n \"left striatum\",\n \"right caudate nucleus\",\n \"left caudate nucleus\",\n \"right putamen\",\n \"left putamen\",\n \"right pallidum\",\n \"left pallidum\",\n \"right amygdaloid complex\",\n \"left amygdaloid complex\",\n \"diencephalon\",\n \"thalamus\",\n \"right thalamus\",\n \"left thalamus\",\n \"pineal gland\",\n \"midbrain\",\n \"substantia nigra\",\n \"right substantia nigra\",\n \"left substantia nigra\",\n \"cerebral white matter\",\n \"right superior longitudinal fasciculus\",\n \"left superior longitudinal fasciculus\",\n \"right inferior longitudinal fasciculus\",\n \"left inferior longitudinal fasciculus\",\n \"right arcuate fasciculus\",\n \"left arcuate fasciculus\",\n \"right uncinate fasciculus\",\n \"left uncinate fasciculus\",\n \"right cingulum bundle\",\n \"left cingulum bundle\",\n \"projection fibers\",\n \"right corticospinal tract\",\n \"left corticospinal tract\",\n \"right optic radiation\",\n \"left optic radiation\",\n \"right medial lemniscus\",\n \"left medial lemniscus\",\n \"right superior cerebellar peduncle\",\n \"left superior cerebellar peduncle\",\n \"right middle cerebellar peduncle\",\n \"left middle cerebellar peduncle\",\n \"right inferior cerebellar peduncle\",\n \"left inferior cerebellar peduncle\",\n \"optic chiasm\",\n \"right optic tract\",\n \"left optic tract\",\n \"right fornix\",\n \"left fornix\",\n \"commissural fibers\",\n \"corpus callosum\",\n \"posterior commissure\",\n \"cerebellar white matter\",\n \"CSF space\",\n \"ventricles of brain\",\n \"right lateral ventricle\",\n \"left lateral ventricle\",\n \"right third ventricle\",\n \"left third ventricle\",\n \"cerebral aqueduct\",\n \"fourth ventricle\",\n \"subarachnoid space\",\n \"spinal cord\",\n \"gray matter of spinal cord\",\n \"white matter of spinal cord\",\n \"endocrine system of brain\",\n \"pituitary gland\",\n \"adenohypophysis\",\n \"neurohypophysis\",\n \"meninges\",\n \"dura mater\",\n \"arachnoid\",\n \"pia mater\",\n \"muscles of head\",\n \"salivary glands\",\n \"lips\",\n \"nose\",\n \"tongue\",\n \"soft palate\",\n \"right inner ear\",\n \"left inner ear\",\n \"right external ear\",\n \"left external ear\",\n \"right middle ear\",\n \"left middle ear\",\n \"right eyeball\",\n \"left eyeball\",\n \"skull\",\n \"right frontal bone\",\n \"left frontal bone\",\n \"right parietal bone\",\n \"left parietal bone\",\n \"right temporal bone\",\n \"left temporal bone\",\n \"right sphenoid bone\",\n \"left sphenoid bone\",\n \"right ethmoid bone\",\n \"left ethmoid bone\",\n \"occipital bone\",\n \"maxilla\",\n \"right zygomatic bone\",\n \"right lacrimal bone\",\n \"vomer bone\",\n \"right palatine bone\",\n \"left palatine bone\",\n \"mandible\",\n \"neck\",\n \"muscles of neck\",\n \"pharynx\",\n \"larynx\",\n \"thyroid gland\",\n \"right parathyroid glands\",\n \"left parathyroid glands\",\n \"skeleton of neck\",\n \"hyoid bone\",\n \"cervical vertebral column\",\n \"thorax\",\n \"trachea\",\n \"bronchi\",\n \"right lung\",\n \"left lung\",\n \"superior lobe of right lung\",\n \"superior lobe of left lung\",\n \"middle lobe of right lung\",\n \"inferior lobe of right lung\",\n \"inferior lobe of left lung\",\n \"pleura\",\n \"heart\",\n \"right atrium\",\n \"left atrium\",\n \"atrial septum\",\n \"ventricular septum\",\n \"right ventricle of heart\",\n \"left ventricle of heart\",\n \"mitral valve\",\n \"tricuspid valve\",\n \"aortic valve\",\n \"pulmonary valve\",\n \"aorta\",\n \"pericardium\",\n \"pericardial cavity\",\n \"esophagus\",\n \"thymus\",\n \"mediastinum\",\n \"skin of thoracic wall\",\n \"muscles of thoracic wall\",\n \"skeleton of thorax\",\n \"thoracic vertebral column\",\n \"ribs\",\n \"sternum\",\n \"right clavicle\",\n \"left clavicle\",\n \"abdominal cavity\",\n \"abdomen\",\n \"peritoneum\",\n \"omentum\",\n \"peritoneal cavity\",\n \"retroperitoneal space\",\n \"stomach\",\n \"duodenum\",\n \"small bowel\",\n \"colon\",\n \"anus\",\n \"liver\",\n \"biliary tree\",\n \"gallbladder\",\n \"pancreas\",\n \"spleen\",\n \"urinary system\",\n \"right kidney\",\n \"left kidney\",\n \"right ureter\",\n \"left ureter\",\n \"urinary bladder\",\n \"urethra\",\n \"right adrenal gland\",\n \"left adrenal gland\",\n \"female internal genitalia\",\n \"uterus\",\n \"right fallopian tube\",\n \"left fallopian tube\",\n \"right ovary\",\n \"left ovary\",\n \"vagina\",\n \"male internal genitalia\",\n \"prostate\",\n \"right seminal vesicle\",\n \"left seminal vesicle\",\n \"right deferent duct\",\n \"left deferent duct\",\n \"skin of abdominal wall\",\n \"muscles of abdominal wall\",\n \"skeleton of abdomen\",\n \"lumbar vertebral column\",\n \"female external genitalia\",\n \"male external genitalia\",\n \"skeleton of upper limb\",\n \"muscles of upper limb\",\n \"right upper limb\",\n \"left upper limb\",\n \"right shoulder\",\n \"left shoulder\",\n \"right arm\"\n ],\n \"R\": [\n 0, 128, 241, 177, 111, 216, 221, 144, 192, 220, 78, 255, 230, 200, 250, 244,\n 0, 216, 183, 183, 152, 111, 178, 68, 111, 85, 0, 214, 78, 218, 170, 140,\n 188, 216, 145, 150, 177, 244, 250, 200, 68, 128, 83, 83, 162, 162, 141, 141,\n 182, 182, 188, 188, 154, 154, 177, 177, 30, 30, 210, 210, 48, 48, 98, 98,\n 69, 166, 122, 122, 253, 145, 46, 0, 0, 250, 127, 127, 159, 159, 125, 125,\n 106, 106, 154, 154, 126, 201, 201, 78, 78, 174, 174, 139, 139, 148, 148,\n 186, 186, 99, 156, 156, 64, 64, 138, 97, 126, 194, 85, 88, 88, 88, 88, 88,\n 88, 88, 88, 244, 200, 250, 82, 57, 60, 92, 255, 255, 255, 255, 201, 70, 188,\n 177, 166, 182, 229, 229, 174, 174, 201, 201, 194, 194, 241, 203, 203, 229,\n 229, 255, 255, 209, 209, 248, 248, 255, 196, 255, 255, 255, 242, 242, 222,\n 177, 213, 184, 150, 62, 62, 62, 242, 250, 255, 177, 182, 175, 197, 197, 172,\n 172, 202, 224, 224, 255, 206, 210, 203, 233, 195, 181, 152, 159, 166, 218,\n 225, 224, 255, 184, 211, 47, 255, 173, 188, 255, 226, 253, 244, 205, 205,\n 186, 177, 255, 234, 204, 180, 216, 255, 205, 204, 255, 221, 0, 139, 249,\n 157, 203, 185, 185, 247, 247, 222, 124, 249, 249, 244, 255, 255, 227, 213,\n 213, 193, 216, 230, 245, 245, 241, 241, 177, 171, 217, 212, 185, 185, 198,\n 194, 177, 177, 177, 177, 177\n ],\n \"G\": [\n 0, 174, 214, 122, 184, 101, 130, 238, 104, 245, 63, 250, 220, 200, 250, 214,\n 151, 101, 156, 214, 189, 184, 212, 172, 197, 188, 145, 230, 63, 255, 250,\n 224, 65, 191, 60, 98, 122, 214, 250, 200, 131, 174, 146, 146, 115, 115, 93,\n 93, 166, 166, 135, 135, 150, 150, 140, 140, 111, 111, 157, 157, 129, 129,\n 153, 153, 110, 113, 101, 101, 135, 92, 101, 108, 108, 250, 150, 150, 116,\n 116, 102, 102, 174, 174, 146, 146, 126, 160, 160, 152, 152, 140, 140, 126,\n 126, 120, 120, 135, 135, 106, 171, 171, 123, 123, 95, 113, 161, 195, 188,\n 106, 106, 106, 106, 106, 106, 106, 106, 214, 200, 250, 174, 157, 143, 162,\n 244, 244, 244, 244, 121, 163, 91, 122, 84, 105, 147, 147, 122, 122, 112,\n 112, 142, 142, 213, 179, 179, 204, 204, 243, 243, 185, 185, 223, 223, 230,\n 172, 255, 250, 237, 217, 217, 198, 122, 124, 105, 208, 162, 162, 162, 206,\n 210, 255, 122, 228, 216, 165, 165, 138, 138, 164, 186, 186, 245, 110, 115,\n 108, 138, 100, 85, 55, 63, 70, 123, 130, 97, 244, 122, 171, 150, 244, 121,\n 95, 239, 202, 232, 217, 179, 179, 124, 122, 255, 234, 142, 119, 132, 253,\n 167, 168, 224, 130, 145, 150, 180, 108, 136, 102, 102, 182, 182, 154, 186,\n 186, 186, 170, 181, 190, 153, 141, 141, 123, 146, 158, 172, 172, 172, 172,\n 124, 85, 198, 188, 135, 135, 175, 98, 122, 122, 122, 122, 122\n ],\n \"B\": [\n 0, 128, 145, 101, 210, 79, 101, 144, 88, 20, 0, 220, 70, 235, 210, 49, 206,\n 79, 220, 211, 207, 210, 242, 100, 131, 255, 30, 130, 0, 255, 250, 228, 28,\n 216, 66, 83, 101, 49, 225, 215, 98, 128, 164, 164, 105, 105, 137, 137, 110,\n 110, 166, 166, 201, 201, 190, 190, 85, 85, 166, 166, 126, 126, 112, 112, 53,\n 137, 38, 38, 192, 109, 131, 112, 112, 225, 88, 88, 163, 163, 154, 154, 155,\n 155, 83, 83, 55, 133, 133, 141, 141, 103, 103, 177, 177, 72, 72, 135, 135,\n 24, 108, 108, 147, 147, 74, 158, 197, 164, 255, 215, 215, 215, 215, 215,\n 215, 215, 215, 49, 215, 225, 128, 110, 83, 109, 209, 209, 209, 209, 77, 117,\n 95, 101, 94, 107, 118, 118, 90, 90, 73, 73, 0, 0, 144, 77, 77, 109, 109,\n 152, 152, 85, 85, 131, 131, 138, 68, 167, 160, 145, 123, 123, 101, 101, 109,\n 108, 243, 114, 114, 114, 142, 139, 207, 101, 255, 244, 145, 145, 115, 115,\n 140, 162, 162, 217, 84, 89, 81, 112, 73, 57, 13, 27, 38, 97, 104, 76, 209,\n 154, 143, 103, 209, 88, 76, 172, 134, 158, 154, 108, 108, 161, 101, 220,\n 194, 178, 153, 105, 229, 142, 143, 199, 101, 30, 98, 111, 162, 116, 83, 83,\n 164, 164, 132, 223, 150, 150, 147, 158, 165, 130, 113, 113, 103, 127, 140,\n 147, 147, 151, 151, 92, 68, 131, 102, 134, 134, 125, 79, 101, 101, 101, 101,\n 101\n ],\n \"A\": [\n 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,\n 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,\n 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,\n 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,\n 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,\n 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,\n 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,\n 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,\n 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,\n 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,\n 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,\n 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,\n 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,\n 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,\n 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,\n 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,\n 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,\n 255\n ],\n \"I\": [\n 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,\n 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39,\n 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58,\n 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77,\n 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96,\n 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112,\n 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127,\n 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142,\n 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157,\n 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172,\n 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187,\n 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202,\n 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217,\n 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232,\n 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247,\n 248, 249, 250, 251, 252, 253, 254, 255\n ]\n}\n","{\n \"R\": [0, 0, 24, 248, 255],\n \"G\": [0, 0, 177, 254, 0],\n \"B\": [0, 136, 0, 0, 0],\n \"A\": [0, 32, 64, 78, 128],\n \"I\": [0, 64, 128, 156, 255]\n}\n","{\n \"R\": [1, 10, 15, 17, 21, 27, 36, 49, 65, 82, 99, 118, 140, 161, 183, 203, 222, 238, 248, 253, 253, 253, 252, 250],\n \"G\": [25, 42, 56, 68, 79, 88, 97, 105, 111, 116, 122, 127, 133, 138, 142, 146, 150, 155, 162, 170, 178, 186, 195, 204],\n \"B\": [89, 92, 95, 96, 98, 98, 96, 91, 83, 74, 64, 55, 46, 43, 49, 62, 79, 100, 126, 151, 175, 199, 223, 250],\n \"A\": [0, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64],\n \"I\": [0, 11, 22, 33, 44, 55, 66, 77, 88, 99, 110, 121, 133, 144, 155, 166, 177, 188, 199, 210, 221, 232, 243, 255]\n}\n","{\n \"R\": [\n 0, 248, 242, 235, 229, 222, 212, 202, 193, 183, 173, 145, 117, 78, 39, 0, 0,\n 0, 0, 0, 0, 64, 128, 191, 255, 255, 255, 255, 255, 238, 221, 204, 214, 224,\n 235, 245, 255, 249, 242, 236, 229, 223, 217, 210, 204, 197, 191, 184, 176,\n 169, 161, 154, 146, 139, 132, 124, 117, 109, 102, 0\n ],\n \"G\": [\n 0, 251, 247, 243, 239, 235, 235, 235, 235, 235, 235, 216, 197, 174, 150,\n 127, 153, 178, 204, 229, 255, 255, 255, 255, 255, 220, 185, 150, 115, 77,\n 38, 0, 31, 61, 92, 122, 153, 138, 122, 107, 92, 77, 61, 46, 31, 15, 0, 0, 0,\n 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0\n ],\n \"B\": [\n 0, 254, 253, 252, 251, 250, 251, 252, 253, 254, 255, 226, 198, 132, 66, 0,\n 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 40, 80, 120, 160, 200, 199,\n 198, 197, 196, 196, 195, 194, 193, 192, 191, 184, 176, 169, 161, 154, 146,\n 139, 132, 124, 117, 109, 102, 0\n ],\n \"A\": [\n 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256,\n 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256,\n 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256,\n 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256\n ],\n \"I\": [\n 0, 4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 44, 48, 52, 56, 60, 64, 68, 72, 76,\n 80, 84, 88, 92, 96, 100, 104, 108, 112, 116, 120, 124, 128, 132, 136, 140,\n 144, 148, 152, 156, 160, 164, 168, 172, 176, 180, 184, 188, 192, 196, 200,\n 204, 208, 212, 216, 220, 224, 228, 232, 236, 240, 244, 248, 255\n ]\n}\n","{\n \"R\": [\n 255, 248, 242, 235, 229, 222, 212, 202, 193, 183, 173, 145, 117, 78, 39, 0,\n 0, 0, 0, 0, 0, 64, 128, 191, 255, 255, 255, 255, 255, 238, 221, 204, 214,\n 224, 235, 245, 255, 249, 242, 236, 229, 223, 217, 210, 204, 197, 191, 184,\n 176, 169, 161, 154, 146, 139, 132, 124, 117, 109, 102, 0\n ],\n \"G\": [\n 255, 251, 247, 243, 239, 235, 235, 235, 235, 235, 235, 216, 197, 174, 150,\n 127, 153, 178, 204, 229, 255, 255, 255, 255, 255, 220, 185, 150, 115, 77,\n 38, 0, 31, 61, 92, 122, 153, 138, 122, 107, 92, 77, 61, 46, 31, 15, 0, 0, 0,\n 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0\n ],\n \"B\": [\n 255, 254, 253, 252, 251, 250, 251, 252, 253, 254, 255, 226, 198, 132, 66, 0,\n 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 40, 80, 120, 160, 200, 199,\n 198, 197, 196, 196, 195, 194, 193, 192, 191, 184, 176, 169, 161, 154, 146,\n 139, 132, 124, 117, 109, 102, 0\n ],\n \"A\": [\n 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256,\n 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256,\n 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256,\n 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256\n ],\n \"I\": [\n 0, 4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 44, 48, 52, 56, 60, 64, 68, 72, 76,\n 80, 84, 88, 92, 96, 100, 104, 108, 112, 116, 120, 124, 128, 132, 136, 140,\n 144, 148, 152, 156, 160, 164, 168, 172, 176, 180, 184, 188, 192, 196, 200,\n 204, 208, 212, 216, 220, 224, 228, 232, 236, 240, 244, 248, 255\n ]\n}\n","{\n \"R\": [0, 0, 0, 0, 196, 255],\n \"G\": [0, 32, 128, 128, 128, 32],\n \"B\": [0, 255, 196, 0, 0, 0],\n \"A\": [0, 128, 64, 64, 64, 128],\n \"I\": [0, 1, 64, 128, 192, 255]\n}\n","{\n \"R\": [0, 255],\n \"G\": [0, 0],\n \"B\": [255, 255],\n \"A\": [0, 128],\n \"I\": [0, 255]\n}\n","{\n \"R\": [0, 0],\n \"G\": [0, 255],\n \"B\": [255, 255],\n \"A\": [0, 128],\n \"I\": [0, 255]\n}\n","{\n \"R\": [0, 0, 0, 0],\n \"G\": [0, 1, 128, 255],\n \"B\": [0, 222, 127, 32],\n \"A\": [0, 0, 64, 128],\n \"I\": [0, 1, 128, 255]\n}\n","{\n \"R\": [0, 0, 0],\n \"G\": [0, 0, 0],\n \"B\": [0, 128, 255],\n \"A\": [0, 64, 128],\n \"I\": [0, 128, 255]\n}\n","{\n \"R\": [0, 103, 255],\n \"G\": [0, 126, 255],\n \"B\": [0, 165, 255],\n \"A\": [0, 76, 128],\n \"I\": [0, 153, 255]\n}\n","{\n \"R\": [0, 43, 103, 199, 216, 255],\n \"G\": [0, 0, 37, 155, 213, 255],\n \"B\": [0, 0, 20, 97, 201, 255],\n \"A\": [0, 44, 48, 54, 56, 56],\n \"I\": [0, 64, 128, 196, 240, 255]\n}\n","{\n \"R\": [\n 0, 9, 24, 33, 40, 46, 52, 57, 62, 66, 70, 74, 78, 81, 85, 88, 91, 94, 98,\n 101, 103, 106, 109, 112, 114, 117, 119, 121, 124, 126, 128, 130, 133, 135,\n 137, 139, 141, 143, 145, 147, 149, 151, 153, 155, 157, 159, 161, 163, 164,\n 166, 168, 169, 171, 172, 174, 175, 177, 178, 180, 181, 183, 184, 186, 187,\n 189, 190, 191, 193, 194, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205,\n 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220,\n 221, 222, 222, 223, 224, 224, 225, 226, 226, 227, 228, 228, 229, 229, 230,\n 231, 231, 232, 233, 233, 234, 234, 235, 235, 236, 236, 236, 237, 237, 237,\n 238, 238, 238, 239, 239, 239, 240, 240, 240, 241, 241, 241, 242, 242, 242,\n 243, 243, 243, 243, 243, 243, 243, 243, 244, 244, 244, 244, 244, 244, 244,\n 244, 244, 244, 244, 245, 245, 245, 245, 245, 245, 245, 245, 245, 245, 245,\n 245, 245, 245, 245, 245, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244,\n 244, 244, 244, 244, 244, 243, 243, 243, 243, 243, 243, 243, 243, 243, 243,\n 243, 243, 242, 242, 242, 242, 242, 242, 242, 242, 241, 242, 242, 242, 242,\n 242, 242, 242, 242, 242, 242, 242, 242, 242, 242, 242, 242, 242, 242, 242,\n 242, 242, 242, 242, 243, 243, 244, 244, 245, 246, 246, 247, 247, 248, 249,\n 249, 250, 250, 251, 251, 252, 252, 253, 253, 254, 254, 254\n ],\n \"G\": [\n 42, 41, 41, 41, 40, 40, 40, 39, 39, 39, 38, 38, 37, 37, 36, 36, 35, 35, 34,\n 34, 33, 32, 32, 31, 30, 30, 30, 30, 29, 29, 29, 29, 28, 28, 28, 27, 27, 27,\n 26, 26, 26, 25, 25, 25, 24, 24, 24, 24, 24, 25, 25, 26, 27, 27, 28, 28, 29,\n 30, 30, 31, 31, 32, 33, 33, 34, 34, 35, 36, 36, 37, 38, 39, 40, 42, 43, 44,\n 45, 47, 48, 49, 50, 51, 53, 54, 55, 56, 57, 58, 59, 60, 62, 63, 64, 65, 66,\n 68, 69, 71, 72, 73, 75, 76, 77, 79, 80, 81, 83, 84, 85, 87, 88, 89, 90, 92,\n 93, 94, 95, 97, 98, 100, 101, 102, 104, 105, 107, 108, 109, 111, 112, 113,\n 115, 116, 117, 119, 120, 121, 122, 124, 125, 126, 128, 129, 130, 132, 133,\n 134, 136, 137, 138, 140, 141, 142, 144, 145, 146, 147, 149, 150, 151, 153,\n 154, 155, 156, 158, 159, 160, 161, 163, 164, 165, 167, 168, 169, 170, 172,\n 173, 174, 175, 176, 178, 179, 180, 181, 183, 184, 185, 186, 187, 189, 190,\n 191, 192, 193, 194, 196, 197, 198, 199, 200, 201, 203, 204, 205, 206, 207,\n 208, 209, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223,\n 224, 225, 226, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 238,\n 239, 240, 241, 241, 242, 243, 244, 244, 245, 246, 247, 248, 248, 249, 250,\n 251, 251, 252, 253, 254, 255\n ],\n \"B\": [\n 167, 167, 166, 166, 166, 165, 165, 165, 164, 164, 164, 163, 163, 162, 162,\n 162, 161, 161, 161, 160, 160, 160, 159, 159, 158, 158, 157, 157, 156, 155,\n 155, 154, 154, 153, 153, 152, 151, 151, 150, 150, 149, 149, 148, 147, 147,\n 146, 146, 145, 144, 144, 143, 143, 142, 141, 141, 140, 139, 139, 138, 137,\n 137, 136, 135, 135, 134, 133, 133, 132, 131, 131, 130, 130, 129, 128, 128,\n 127, 127, 126, 125, 125, 124, 124, 123, 123, 122, 121, 121, 120, 119, 119,\n 118, 118, 117, 116, 116, 116, 115, 115, 114, 114, 113, 113, 113, 112, 112,\n 111, 111, 110, 110, 109, 109, 109, 108, 108, 107, 107, 106, 106, 106, 106,\n 106, 105, 105, 105, 105, 105, 105, 105, 104, 104, 104, 104, 104, 103, 103,\n 103, 103, 103, 102, 102, 102, 103, 103, 103, 103, 104, 104, 104, 104, 104,\n 105, 105, 105, 105, 106, 106, 106, 106, 106, 107, 107, 107, 107, 108, 108,\n 109, 110, 111, 111, 112, 113, 113, 114, 115, 115, 116, 117, 117, 118, 119,\n 120, 120, 121, 122, 122, 123, 124, 125, 126, 128, 129, 130, 131, 132, 134,\n 135, 136, 137, 138, 140, 141, 142, 143, 144, 146, 147, 148, 149, 150, 152,\n 153, 155, 157, 158, 160, 162, 163, 165, 167, 168, 170, 172, 173, 175, 177,\n 178, 180, 182, 183, 185, 187, 188, 190, 193, 196, 199, 201, 204, 207, 210,\n 212, 215, 218, 221, 224, 226, 229, 232, 235, 238, 240, 243, 246, 249, 252,\n 255\n ],\n \"A\": [\n 0, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64\n ],\n \"I\": [\n 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,\n 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39,\n 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58,\n 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77,\n 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96,\n 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112,\n 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127,\n 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142,\n 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157,\n 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172,\n 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187,\n 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202,\n 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217,\n 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232,\n 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247,\n 248, 249, 250, 251, 252, 253, 254, 255\n ]\n}\n","{\n \"R\": [0, 86, 166, 255],\n \"G\": [32, 92, 156, 233],\n \"B\": [76, 108, 117, 69],\n \"A\": [0, 56, 80, 88],\n \"I\": [0, 64, 192, 255]\n}\n","{\n \"R\": [0, 0, 0],\n \"G\": [127, 196, 254],\n \"B\": [255, 255, 255],\n \"A\": [0, 64, 128],\n \"I\": [0, 128, 255]\n}\n","{\n \"R\": [0, 61, 122, 183, 244, 255],\n \"G\": [0, 41, 81, 122, 163, 255],\n \"B\": [0, 25, 51, 76, 102, 255],\n \"A\": [0, 25, 51, 71, 102, 128],\n \"I\": [0, 51, 102, 153, 204, 255]\n}\n","{\n \"R\": [0, 61, 122, 183, 244, 255],\n \"G\": [0, 41, 81, 122, 163, 203],\n \"B\": [0, 25, 51, 76, 102, 127],\n \"A\": [0, 25, 51, 71, 102, 128],\n \"I\": [0, 51, 102, 153, 204, 255]\n}\n","{\n \"min\": -643,\n \"max\": -235,\n \"R\": [0, 0, 0],\n \"G\": [154, 154, 154],\n \"B\": [179, 179, 101],\n \"A\": [0, 32, 0],\n \"I\": [0, 163, 255]\n}\n","{\n \"min\": 114,\n \"max\": 800,\n \"R\": [0, 255, 255, 255],\n \"G\": [0, 0, 129, 255],\n \"B\": [0, 0, 0, 255],\n \"A\": [0, 64, 88, 228],\n \"I\": [0, 80, 160, 255]\n}\n","{\n \"min\": 180,\n \"max\": 600,\n \"R\": [0, 0, 113, 255],\n \"G\": [0, 0, 109, 250],\n \"B\": [0, 0, 101, 245],\n \"A\": [0, 0, 100, 160],\n \"I\": [0, 1, 128, 255]\n}\n","{\n \"min\": -10,\n \"max\": 110,\n \"R\": [0, 127, 255],\n \"G\": [0, 127, 255],\n \"B\": [0, 127, 255],\n \"A\": [0, 48, 128],\n \"I\": [0, 124, 255]\n}\n","{\n \"min\": -10,\n \"max\": 110,\n \"R\": [0, 199, 255],\n \"G\": [0, 127, 255],\n \"B\": [0, 127, 255],\n \"A\": [0, 48, 128],\n \"I\": [0, 124, 255]\n}\n","{\n \"min\": -80,\n \"max\": 1000,\n \"R\": [0, 189, 150, 150, 150, 150, 255],\n \"G\": [0, 169, 54, 54, 54, 54, 240],\n \"B\": [0, 153, 52, 52, 52, 52, 242],\n \"A\": [0, 32, 64, 0, 0, 64, 64],\n \"I\": [0, 1, 82, 92, 234, 242, 255]\n}\n","{\n \"min\": -590,\n \"max\": 600,\n \"R\": [0, 241, 241, 248, 248, 178, 178, 232, 255, 255, 255],\n \"G\": [0, 156, 156, 222, 222, 36, 36, 51, 255, 255, 255],\n \"B\": [0, 130, 130, 169, 169, 24, 24, 37, 255, 255, 255],\n \"A\": [0, 8, 0, 0, 0, 64, 64, 0, 0, 222, 222],\n \"I\": [0, 2, 3, 64, 122, 142, 172, 182, 252, 253, 255]\n}\n","{\n \"min\": 114,\n \"max\": 302,\n \"R\": [0, 255, 255],\n \"G\": [0, 129, 255],\n \"B\": [0, 0, 255],\n \"A\": [0, 88, 228],\n \"I\": [0, 103, 255]\n}\n","{\n \"min\": -23,\n \"max\": 246,\n \"R\": [0, 44, 255, 255, 255],\n \"G\": [0, 128, 90, 255, 255],\n \"B\": [0, 0, 70, 0, 255],\n \"A\": [0, 0, 82, 184, 228],\n \"I\": [0, 64, 131, 196, 255]\n}\n","{\n \"min\": -100,\n \"max\": 246,\n \"R\": [0, 128, 159, 255, 255, 255, 255],\n \"G\": [0, 0, 56, 90, 0, 255, 255],\n \"B\": [0, 0, 41, 70, 0, 0, 255],\n \"A\": [0, 63, 105, 135, 167, 184, 228],\n \"I\": [0, 100, 128, 155, 180, 209, 255]\n}\n","{\n \"min\": -590,\n \"max\": 600,\n \"R\": [0, 241, 241, 248, 248, 178, 232, 255, 255],\n \"G\": [0, 156, 156, 222, 222, 36, 51, 255, 255],\n \"B\": [0, 130, 130, 169, 169, 24, 37, 255, 255],\n \"A\": [0, 63, 105, 135, 167, 184, 228, 228, 228],\n \"I\": [0, 1, 52, 127, 137, 162, 172, 252, 255]\n}\n","{\n \"min\": 140,\n \"max\": 1024,\n \"R\": [0, 2, 113, 255],\n \"G\": [0, 1, 109, 250],\n \"B\": [0, 1, 101, 245],\n \"A\": [0, 1, 96, 168],\n \"I\": [0, 1, 128, 255]\n}\n","{\n \"min\": -923,\n \"max\": 679,\n \"R\": [0, 0, 0, 0, 0, 255, 255, 255],\n \"G\": [154, 154, 154, 154, 0, 0, 254, 255],\n \"B\": [179, 179, 179, 179, 0, 0, 0, 255],\n \"A\": [0, 3, 8, 0, 0, 10, 15, 20],\n \"I\": [0, 30, 62, 88, 170, 200, 232, 255]\n}\n","{\n \"min\": -10,\n \"max\": 110,\n \"R\": [0, 199, 255],\n \"G\": [0, 127, 255],\n \"B\": [0, 127, 255],\n \"A\": [0, 48, 128],\n \"I\": [0, 124, 255]\n}\n","{\n \"min\": -600,\n \"max\": 100,\n \"R\": [0, 134, 255],\n \"G\": [0, 109, 250],\n \"B\": [0, 101, 245],\n \"A\": [0, 60, 148],\n \"I\": [0, 128, 255]\n}\n","{\n \"min\": 114,\n \"max\": 246,\n \"R\": [0, 255, 255],\n \"G\": [0, 128, 255],\n \"B\": [0, 128, 255],\n \"A\": [0, 64, 96],\n \"I\": [0, 87, 255]\n}\n","{\n \"min\": 50,\n \"max\": 1000,\n \"R\": [98, 210, 169, 128, 255],\n \"G\": [94, 26, 77, 128, 255],\n \"B\": [45, 21, 74, 128, 255],\n \"A\": [0, 25, 0, 4, 168],\n \"I\": [0, 41, 87, 154, 255]\n}\n","{\n \"R\": [\n 0, 13, 21, 26, 27, 25, 22, 21, 22, 28, 39, 54, 75, 98, 124, 148, 171, 189,\n 202, 210, 213, 211, 206, 200, 195, 193, 195, 201, 211, 225, 240, 255\n ],\n \"G\": [\n 0, 5, 11, 20, 31, 44, 58, 72, 86, 99, 109, 116, 120, 122, 122, 122, 121,\n 121, 124, 129, 137, 147, 161, 175, 190, 205, 218, 229, 238, 245, 251, 255\n ],\n \"B\": [\n 0, 14, 30, 46, 61, 71, 77, 78, 75, 68, 60, 52, 48, 47, 53, 65, 83, 105, 131,\n 157, 183, 205, 222, 235, 241, 243, 242, 240, 239, 240, 245, 255\n ],\n \"A\": [\n 0, 4, 8, 12, 17, 21, 25, 29, 33, 37, 41, 45, 50, 54, 58, 62, 66, 70, 74, 78,\n 83, 87, 91, 95, 99, 103, 107, 111, 116, 120, 124, 128\n ],\n \"I\": [\n 0, 8, 16, 25, 33, 41, 49, 58, 66, 74, 82, 90, 99, 107, 115, 123, 132, 140,\n 148, 156, 165, 173, 181, 189, 197, 206, 214, 222, 230, 239, 247, 255\n ]\n}\n","{\n \"R\": [0, 10, 136, 255],\n \"G\": [0, 39, 220, 255],\n \"B\": [0, 223, 253, 255],\n \"A\": [0, 48, 64, 70],\n \"I\": [0, 92, 192, 255]\n}\n","{\n \"R\": [\n 0, 245, 205, 120, 196, 220, 230, 0, 122, 236, 12, 204, 42, 119, 220, 103,\n 60, 255, 165, 160, 0, 245, 205, 120, 196, 220, 230, 0, 122, 236, 13, 220,\n 103, 255, 165, 160, 0, 120, 200, 255, 255, 164, 164, 164, 234, 0, 0, 0, 0, 0\n ],\n \"G\": [\n 0, 245, 62, 18, 58, 248, 148, 118, 186, 13, 48, 182, 204, 159, 216, 255, 60,\n 165, 42, 32, 200, 245, 62, 18, 58, 248, 148, 118, 186, 13, 48, 216, 255,\n 165, 42, 32, 200, 190, 70, 148, 148, 108, 108, 108, 169, 0, 0, 0, 0, 0\n ],\n \"B\": [\n 0, 245, 78, 134, 250, 164, 34, 14, 220, 176, 255, 142, 164, 176, 20, 255,\n 60, 0, 42, 240, 200, 245, 78, 134, 250, 164, 34, 14, 220, 176, 255, 20, 255,\n 0, 42, 240, 221, 150, 255, 10, 10, 226, 226, 226, 30, 64, 112, 160, 208, 255\n ],\n \"A\": [\n 0, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64\n ],\n \"I\": [\n 0, 2, 3, 4, 5, 7, 8, 10, 11, 12, 13, 14, 15, 16, 17, 18, 24, 26, 28, 30, 31,\n 41, 42, 43, 44, 46, 47, 49, 50, 51, 52, 53, 54, 58, 60, 62, 63, 72, 77, 78,\n 79, 80, 81, 82, 85, 251, 252, 253, 254, 255\n ]\n}\n","{\n \"R\": [0, 0, 128, 255, 255],\n \"G\": [0, 128, 0, 128, 255],\n \"B\": [0, 125, 255, 0, 255],\n \"A\": [0, 32, 64, 96, 128],\n \"I\": [0, 63, 128, 192, 255]\n}\n","{\n \"R\": [0, 142, 227, 255],\n \"G\": [0, 85, 170, 255],\n \"B\": [0, 14, 76, 255],\n \"A\": [0, 42, 84, 128],\n \"I\": [0, 85, 170, 255]\n}\n","{\n \"R\": [0, 255],\n \"G\": [0, 255],\n \"B\": [0, 255],\n \"A\": [0, 128],\n \"I\": [0, 255]\n}\n","{\n \"R\": [0, 0, 0],\n \"G\": [0, 128, 255],\n \"B\": [0, 0, 0],\n \"A\": [0, 64, 128],\n \"I\": [0, 128, 255]\n}\n","{\n \"R\": [0, 0, 0],\n \"G\": [72, 72, 255],\n \"B\": [0, 255, 255],\n \"A\": [0, 64, 128],\n \"I\": [0, 88, 255]\n}\n","{\n \"R\": [0, 255, 255],\n \"G\": [72, 88, 255],\n \"B\": [0, 0, 0],\n \"A\": [0, 64, 128],\n \"I\": [0, 88, 255]\n}\n","{\n \"R\": [0, 255, 255, 255],\n \"G\": [0, 0, 126, 255],\n \"B\": [0, 0, 0, 255],\n \"A\": [0, 64, 96, 128],\n \"I\": [0, 128, 191, 255]\n}\n","{\n \"R\": [3, 255, 255, 255],\n \"G\": [0, 0, 255, 255],\n \"B\": [0, 0, 0, 255],\n \"A\": [0, 48, 96, 128],\n \"I\": [0, 95, 191, 255]\n}\n","{\n \"R\": [255, 255, 0, 0, 0, 255, 255],\n \"G\": [0, 255, 255, 255, 0, 0, 0],\n \"B\": [0, 0, 0, 255, 255, 255, 0],\n \"A\": [0, 14, 28, 43, 57, 71, 85],\n \"I\": [0, 43, 85, 128, 170, 213, 255]\n}\n","{\n \"R\": [0, 120, 237, 240],\n \"G\": [0, 28, 105, 249],\n \"B\": [4, 109, 37, 33],\n \"A\": [0, 56, 80, 88],\n \"I\": [0, 64, 192, 255]\n}\n","{\n \"R\": [0, 0, 127, 255, 127],\n \"G\": [0, 127, 255, 127, 0],\n \"B\": [127, 255, 127, 0, 0],\n \"A\": [0, 32, 64, 96, 128],\n \"I\": [0, 63, 128, 192, 255]\n}\n","{\n \"R\": [0, 255, 255],\n \"G\": [0, 0, 255],\n \"B\": [0, 0, 0],\n \"A\": [0, 64, 64],\n \"I\": [0, 86, 255]\n}\n","{\n \"R\": [94, 50, 90, 152, 215, 238, 249, 254, 252, 241, 209, 158],\n \"G\": [79, 131, 186, 214, 240, 244, 237, 210, 157, 100, 57, 1],\n \"B\": [162, 189, 167, 164, 155, 169, 168, 123, 86, 68, 79, 66],\n \"A\": [0, 12, 23, 35, 47, 58, 70, 81, 93, 105, 116, 128],\n \"I\": [0, 23, 46, 70, 93, 116, 139, 162, 185, 209, 232, 255]\n}\n","{\n \"R\": [3, 7, 13, 25, 45, 67, 84, 98, 110, 124, 138, 154, 173, 191, 210, 225, 233, 233, 231, 229, 230, 235, 243, 253],\n \"G\": [19, 33, 48, 63, 77, 87, 92, 94, 95, 96, 96, 97, 99, 101, 106, 118, 133, 149, 163, 177, 192, 208, 225, 245],\n \"B\": [38, 58, 79, 99, 114, 122, 122, 120, 117, 113, 110, 105, 101, 97, 94, 96, 103, 112, 122, 134, 149, 169, 192, 218],\n \"A\": [0, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64],\n \"I\": [0, 11, 22, 33, 44, 55, 66, 77, 88, 99, 110, 121, 133, 144, 155, 166, 177, 188, 199, 210, 221, 232, 243, 255]\n}\n","{\n \"R\": [0, 148, 183, 223, 247, 252],\n \"G\": [0, 44, 55, 74, 112, 253],\n \"B\": [4, 128, 121, 104, 92, 191],\n \"A\": [0, 44, 53, 64, 75, 107],\n \"I\": [0, 107, 128, 154, 179, 255]\n}\n","{\n \"R\": [11, 59, 55, 222],\n \"G\": [4, 45, 165, 245],\n \"B\": [5, 91, 172, 229],\n \"A\": [0, 23, 70, 107],\n \"I\": [0, 56, 167, 255]\n}\n","{\n \"R\": [3, 5, 6, 8, 12, 19, 28, 36, 42, 48, 54, 61, 68, 76, 86, 96, 110, 129, 154, 181, 205, 224, 239, 252],\n \"G\": [19, 32, 44, 58, 72, 86, 98, 108, 116, 122, 128, 134, 141, 149, 157, 167, 178, 191, 204, 216, 225, 232, 239, 244],\n \"B\": [39, 60, 82, 104, 123, 137, 144, 145, 142, 138, 134, 130, 126, 121, 116, 110, 105, 104, 112, 131, 155, 178, 198, 217],\n \"A\": [0, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64],\n \"I\": [0, 11, 22, 33, 44, 55, 66, 77, 88, 99, 110, 121, 133, 144, 155, 166, 177, 188, 199, 210, 221, 232, 243, 255]\n}\n","{\n \"R\": [0, 85, 0, 0, 0, 0, 0, 0, 85, 255, 255, 255, 172],\n \"G\": [0, 0, 0, 0, 85, 170, 255, 255, 255, 255, 85, 0, 0],\n \"B\": [0, 170, 85, 255, 255, 170, 170, 0, 85, 0, 0, 0, 0],\n \"A\": [0, 5, 10, 21, 26, 32, 37, 42, 48, 53, 64, 72, 85],\n \"I\": [0, 15, 31, 63, 79, 95, 111, 127, 143, 159, 191, 217, 255]\n}\n","{\n \"R\": [13, 156, 237, 240],\n \"G\": [8, 23, 121, 249],\n \"B\": [135, 158, 83, 33],\n \"A\": [0, 56, 80, 88],\n \"I\": [0, 64, 192, 255]\n}\n","{\n \"R\": [\n 208, 71, 33, 192, 32, 195, 208, 173, 233, 202, 25, 210, 145, 89, 87, 245,\n 246, 38, 3, 25, 57, 167, 245, 86, 227, 208, 81, 64, 90, 199, 140, 48, 212,\n 180, 70, 120, 9, 192, 245, 177, 65, 157, 9, 193, 100, 181, 125, 145, 62, 8,\n 108, 36, 140, 237, 242, 248, 161, 189, 41, 114, 65, 121, 97, 50, 238, 149,\n 44, 214, 124, 167, 40, 167, 127, 178, 231, 30, 173, 244, 193, 203, 204, 238,\n 139, 135, 71, 234, 234, 217, 66, 14, 129, 19, 97, 165, 112, 244, 35, 73,\n 192, 12, 149, 71, 33, 192, 32, 195, 208, 173, 233, 202, 25, 210, 145, 89,\n 87, 245, 246, 38, 3, 25, 57, 167, 245, 86, 227, 208, 81, 64, 90, 199, 140,\n 48, 212, 180, 70, 120, 9, 192, 245, 177, 65, 157, 9, 193, 100, 181, 125,\n 145, 62, 8, 108, 36, 140, 237, 242, 248, 161, 189, 41, 114, 65, 121, 97, 50,\n 238, 149, 44, 214, 124, 167, 40, 167, 127, 178, 231, 30, 173, 244, 193, 203,\n 204, 238, 139, 135, 71, 234, 234, 217, 66, 14, 129, 19, 97, 165, 112, 244,\n 35, 73, 192, 12, 149, 71, 33, 192, 32, 195, 208, 173, 233, 202, 25, 210,\n 145, 89, 87, 245, 246, 38, 3, 25, 57, 167, 245, 86, 227, 208, 81, 64, 90,\n 199, 140, 48, 212, 180, 70, 120, 9, 192, 245, 177, 65, 157, 9, 193, 100,\n 181, 125, 145, 62, 8, 108, 36, 140, 237, 242, 248\n ],\n \"G\": [\n 182, 46, 78, 199, 79, 89, 41, 208, 135, 20, 154, 35, 21, 43, 230, 113, 191,\n 147, 208, 37, 28, 27, 86, 203, 25, 209, 148, 187, 139, 111, 48, 102, 76,\n 110, 106, 130, 37, 160, 34, 222, 90, 165, 245, 222, 102, 47, 19, 130, 4,\n 232, 137, 211, 240, 11, 140, 21, 42, 22, 241, 61, 99, 115, 199, 166, 114,\n 190, 204, 60, 233, 66, 115, 230, 125, 103, 203, 125, 13, 176, 94, 131, 39,\n 198, 167, 124, 67, 175, 254, 1, 15, 198, 62, 237, 159, 31, 218, 58, 244, 47,\n 61, 67, 94, 46, 78, 199, 79, 89, 41, 208, 135, 20, 154, 35, 21, 43, 230,\n 113, 191, 147, 208, 37, 28, 27, 86, 203, 25, 209, 148, 187, 139, 111, 48,\n 102, 76, 110, 106, 130, 37, 160, 34, 222, 90, 165, 245, 222, 102, 47, 19,\n 130, 4, 232, 137, 211, 240, 11, 140, 21, 42, 22, 241, 61, 99, 115, 199, 166,\n 114, 190, 204, 60, 233, 66, 115, 230, 125, 103, 203, 125, 13, 176, 94, 131,\n 39, 198, 167, 124, 67, 175, 254, 1, 15, 198, 62, 237, 159, 31, 218, 58, 244,\n 47, 61, 67, 94, 46, 78, 199, 79, 89, 41, 208, 135, 20, 154, 35, 21, 43, 230,\n 113, 191, 147, 208, 37, 28, 27, 86, 203, 25, 209, 148, 187, 139, 111, 48,\n 102, 76, 110, 106, 130, 37, 160, 34, 222, 90, 165, 245, 222, 102, 47, 19,\n 130, 4, 232, 137, 211, 240, 11, 140, 21\n ],\n \"B\": [\n 191, 154, 43, 10, 207, 204, 164, 231, 136, 58, 239, 30, 147, 230, 101, 111,\n 150, 35, 128, 57, 252, 79, 173, 120, 25, 126, 81, 85, 8, 7, 122, 237, 190,\n 152, 246, 182, 130, 219, 67, 76, 167, 178, 235, 250, 28, 61, 186, 250, 199,\n 67, 58, 50, 86, 182, 108, 77, 89, 112, 59, 125, 226, 50, 205, 227, 125, 128,\n 104, 27, 59, 66, 53, 133, 159, 203, 97, 125, 139, 159, 158, 7, 215, 47, 140,\n 226, 223, 231, 44, 110, 184, 61, 233, 47, 67, 148, 22, 120, 173, 156, 117,\n 181, 94, 154, 43, 10, 207, 204, 164, 231, 136, 58, 239, 30, 147, 230, 101,\n 111, 150, 35, 128, 57, 252, 79, 173, 120, 25, 126, 81, 85, 8, 7, 122, 237,\n 190, 152, 246, 182, 130, 219, 67, 76, 167, 178, 235, 250, 28, 61, 186, 250,\n 199, 67, 58, 50, 86, 182, 108, 77, 89, 112, 59, 125, 226, 50, 205, 227, 125,\n 128, 104, 27, 59, 66, 53, 133, 159, 203, 97, 125, 139, 159, 158, 7, 215, 47,\n 140, 226, 223, 231, 44, 110, 184, 61, 233, 47, 67, 148, 22, 120, 173, 156,\n 117, 181, 94, 154, 43, 10, 207, 204, 164, 231, 136, 58, 239, 30, 147, 230,\n 101, 111, 150, 35, 128, 57, 252, 79, 173, 120, 25, 126, 81, 85, 8, 7, 122,\n 237, 190, 152, 246, 182, 130, 219, 67, 76, 167, 178, 235, 250, 28, 61, 186,\n 250, 199, 67, 58, 50, 86, 182, 108, 77\n ],\n \"A\": [\n 0, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64\n ],\n \"I\": [\n 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,\n 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39,\n 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58,\n 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77,\n 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96,\n 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112,\n 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127,\n 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142,\n 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157,\n 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172,\n 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187,\n 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202,\n 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217,\n 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232,\n 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247,\n 248, 249, 250, 251, 252, 253, 254, 255\n ]\n}\n","{\n \"R\": [0, 128, 255],\n \"G\": [0, 0, 0],\n \"B\": [0, 0, 0],\n \"A\": [0, 64, 128],\n \"I\": [0, 128, 255]\n}\n","{\n \"R\": [192, 224, 255],\n \"G\": [1, 128, 255],\n \"B\": [0, 0, 0],\n \"A\": [0, 64, 128],\n \"I\": [0, 128, 255]\n}\n","{\n \"R\": [3, 112, 144, 188, 236, 246, 255],\n \"G\": [5, 31, 29, 22, 76, 158, 250],\n \"B\": [26, 87, 91, 86, 62, 117, 235],\n \"A\": [0, 30, 38, 49, 67, 85, 107],\n \"I\": [0, 73, 92, 118, 160, 205, 255]\n}\n","{\n \"R\": [1, 240, 255],\n \"G\": [1, 128, 255],\n \"B\": [1, 128, 255],\n \"A\": [0, 76, 128],\n \"I\": [0, 153, 255]\n}\n","{\n \"R\": [0, 5, 18, 34, 53, 72, 90, 107, 126, 144, 161, 178, 194, 209, 222, 233, 242, 248, 251, 251, 249, 244, 241, 252],\n \"G\": [0, 4, 10, 11, 9, 11, 17, 23, 30, 36, 43, 50, 59, 70, 83, 98, 117, 136, 157, 177, 199, 220, 240, 254],\n \"B\": [3, 24, 50, 76, 96, 106, 109, 110, 108, 104, 97, 89, 79, 67, 55, 42, 26, 12, 6, 22, 47, 79, 121, 164],\n \"A\": [0, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64],\n \"I\": [0, 11, 22, 33, 44, 55, 66, 77, 88, 99, 110, 121, 133, 144, 155, 166, 177, 188, 199, 210, 221, 232, 243, 255]\n}\n","{\n \"R\": [48, 48, 64, 70, 65, 25, 132, 195, 244, 254, 218, 122],\n \"G\": [18, 18, 64, 107, 150, 226, 255, 241, 199, 158, 57, 4],\n \"B\": [59, 59, 162, 227, 255, 187, 81, 52, 58, 47, 7, 3],\n \"A\": [0, 22, 26, 30, 34, 43, 52, 57, 63, 67, 77, 86],\n \"I\": [0, 1, 16, 32, 49, 83, 118, 140, 164, 181, 219, 255]\n}\n","{\n \"R\": [0, 128, 255],\n \"G\": [0, 0, 0],\n \"B\": [0, 128, 255],\n \"A\": [0, 64, 128],\n \"I\": [0, 128, 255]\n}\n","{\n \"R\": [68, 49, 53, 253],\n \"G\": [1, 104, 183, 231],\n \"B\": [84, 142, 121, 37],\n \"A\": [0, 56, 80, 88],\n \"I\": [0, 64, 192, 255]\n}\n","{\n \"R\": [255, 255, 255],\n \"G\": [127, 196, 254],\n \"B\": [0, 0, 0],\n \"A\": [0, 64, 128],\n \"I\": [0, 128, 255]\n}\n","{\n \"R\": [0, 0, 0],\n \"G\": [0, 128, 255],\n \"B\": [255, 196, 128],\n \"A\": [0, 64, 128],\n \"I\": [0, 128, 255]\n}\n","{\n \"R\": [3, 64, 0, 0, 255, 255, 255],\n \"G\": [0, 0, 0, 255, 255, 192, 3],\n \"B\": [0, 32, 48, 56, 64, 96, 128],\n \"A\": [0, 8, 16, 24, 32, 52, 80],\n \"I\": [0, 32, 64, 96, 160, 192, 255]\n}\n","{\n \"min\": 0,\n \"max\": 0,\n \"R\": [0, 37],\n \"G\": [242, 0],\n \"B\": [255, 255],\n \"A\": [0, 64],\n \"I\": [0, 255]\n}\n","{\n \"R\": [255, 255],\n \"G\": [255, 11],\n \"B\": [0, 0],\n \"A\": [0, 64],\n \"I\": [0, 255]\n}\n","{\n \"min\": 0,\n \"max\": 0,\n \"R\": [\n 65, 10, 223, 120, 216, 207, 251, 93, 252, 217, 21, 253, 131, 165, 173, 143,\n 144, 217, 86, 89, 63, 254, 172, 1, 142, 107, 42, 78, 221, 8, 222, 71, 127,\n 126, 167, 33, 104, 2, 208, 216, 0, 251, 7, 105, 2, 101, 248, 190, 253, 62,\n 255, 224, 166, 37, 110, 138, 45, 34, 6, 37, 69, 201, 43, 122, 199, 37, 173,\n 194, 103, 211, 75, 159, 96, 4, 239, 98, 110, 193, 253, 166, 40, 255, 48,\n 130, 140, 243, 101, 9, 177, 220, 133, 32, 4, 81, 48, 48, 210, 109, 60, 132,\n 1, 119, 1, 159, 247, 33, 212, 187, 253, 144, 196, 254, 168, 79, 64, 196, 39,\n 97, 73, 173, 104, 216, 217, 43, 101, 119, 254, 5, 237, 103, 203, 122, 57,\n 87, 251, 164, 19, 75, 200, 2, 252, 150, 66, 0, 255, 157, 23, 254, 55, 16,\n 240, 161, 69, 253, 207, 195, 5, 60, 255, 251, 200, 217, 134, 123, 253, 180,\n 27, 246, 130, 136, 250, 232, 4, 125, 140, 22, 253, 255, 13, 180, 123, 61,\n 254, 111, 10, 185, 76, 192, 255, 223, 186, 61, 198, 5, 172, 13, 83, 172,\n 171, 6, 23, 73, 134, 133, 109, 61, 213, 55, 57, 132, 36, 209, 2, 144, 1,\n 253, 68, 155, 3, 160, 2, 77, 121, 70, 67, 176, 223, 131, 4, 162, 232, 255,\n 150, 94, 235, 191, 207, 10, 246, 0, 225, 4, 209, 116, 57, 112, 172, 253, 1,\n 6, 92, 227, 73, 62, 135, 223\n ],\n \"G\": [\n 162, 50, 112, 245, 4, 124, 176, 243, 56, 125, 183, 139, 182, 68, 189, 4,\n 225, 10, 20, 223, 7, 46, 31, 193, 225, 250, 219, 41, 2, 100, 83, 181, 34,\n 150, 72, 223, 168, 248, 80, 253, 7, 117, 214, 2, 248, 10, 3, 59, 179, 160,\n 90, 244, 146, 4, 207, 4, 125, 255, 96, 100, 1, 208, 130, 51, 116, 181, 236,\n 53, 244, 5, 165, 28, 181, 86, 96, 42, 254, 1, 134, 194, 214, 92, 41, 204,\n 137, 86, 207, 3, 143, 3, 100, 239, 164, 94, 65, 251, 131, 244, 173, 57, 143,\n 107, 217, 53, 210, 0, 148, 250, 45, 198, 81, 40, 61, 218, 252, 88, 171, 0,\n 248, 24, 248, 4, 41, 100, 187, 46, 208, 145, 43, 168, 121, 46, 175, 250,\n 125, 107, 233, 112, 207, 231, 174, 215, 53, 9, 43, 153, 52, 124, 128, 65,\n 210, 147, 255, 147, 63, 200, 148, 254, 0, 179, 241, 42, 239, 2, 230, 8, 82,\n 135, 87, 16, 3, 87, 86, 151, 254, 8, 255, 86, 53, 19, 7, 192, 171, 201, 253,\n 247, 197, 103, 251, 126, 0, 149, 54, 183, 61, 126, 79, 113, 10, 103, 184,\n 75, 11, 195, 222, 136, 149, 131, 8, 99, 240, 177, 252, 255, 198, 16, 7, 68,\n 178, 66, 191, 150, 73, 26, 211, 109, 78, 209, 240, 254, 1, 166, 247, 131, 2,\n 0, 167, 127, 133, 10, 43, 99, 235, 3, 214, 142, 176, 82, 132, 38, 10, 249,\n 255, 215, 44, 81\n ],\n \"B\": [\n 176, 182, 248, 37, 117, 35, 96, 69, 32, 152, 108, 20, 237, 250, 2, 89, 141,\n 216, 111, 251, 211, 149, 10, 44, 214, 103, 31, 251, 3, 32, 252, 246, 97, 1,\n 219, 167, 197, 4, 36, 116, 206, 118, 106, 43, 205, 204, 114, 69, 127, 205,\n 87, 80, 41, 251, 145, 204, 253, 161, 247, 1, 149, 9, 43, 253, 97, 72, 136,\n 161, 171, 181, 26, 255, 108, 80, 218, 214, 231, 255, 84, 31, 109, 4, 218, 3,\n 217, 36, 68, 85, 241, 39, 221, 2, 240, 2, 173, 42, 206, 5, 110, 46, 103, 27,\n 212, 184, 2, 207, 246, 45, 116, 72, 110, 253, 38, 105, 248, 159, 243, 81,\n 192, 93, 141, 145, 24, 157, 234, 131, 57, 178, 62, 75, 65, 176, 148, 40,\n 253, 66, 76, 240, 51, 154, 17, 251, 139, 253, 207, 9, 114, 49, 200, 254, 96,\n 73, 138, 118, 204, 102, 137, 89, 145, 161, 4, 112, 66, 234, 147, 178, 212,\n 205, 185, 11, 203, 131, 2, 250, 118, 169, 1, 185, 154, 53, 171, 197, 61,\n 175, 249, 96, 15, 254, 95, 5, 222, 75, 246, 194, 2, 61, 180, 25, 133, 165,\n 15, 233, 59, 35, 221, 140, 109, 7, 114, 255, 198, 0, 115, 168, 252, 23, 242,\n 80, 75, 142, 137, 255, 12, 182, 68, 201, 4, 111, 37, 228, 83, 248, 24, 192,\n 249, 5, 54, 223, 160, 122, 160, 114, 145, 119, 252, 31, 253, 250, 10, 214,\n 8, 47, 0, 142, 222, 70\n ],\n \"A\": [\n 0, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,\n 64, 64, 64, 64, 64, 64, 64, 64, 64\n ],\n \"I\": [\n 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,\n 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39,\n 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58,\n 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77,\n 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96,\n 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112,\n 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127,\n 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142,\n 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157,\n 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172,\n 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187,\n 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202,\n 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217,\n 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232,\n 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247,\n 248, 249, 250, 251, 252, 253, 254, 255\n ]\n}\n","import * as cmaps from '@/cmaps'\nimport { log } from '@/logger'\n\nexport enum COLORMAP_TYPE {\n MIN_TO_MAX = 0,\n ZERO_TO_MAX_TRANSPARENT_BELOW_MIN = 1,\n ZERO_TO_MAX_TRANSLUCENT_BELOW_MIN = 2\n}\n\nexport type ColorMap = {\n R: number[]\n G: number[]\n B: number[]\n A: number[]\n I: number[]\n\n min?: number\n max?: number\n labels?: string[]\n}\n\nexport type LUT = {\n lut: Uint8ClampedArray\n min?: number\n max?: number\n labels?: string[]\n}\n\nexport class ColorTables {\n gamma = 1.0\n version = 0.1\n cluts: Record<string, ColorMap> = {}\n\n /**\n * Sets cluts to alphabetically sorted cmaps\n */\n constructor() {\n const cmapKeys = Object.keys(cmaps) as Array<keyof typeof cmaps>\n\n const cmapsSorted = cmapKeys\n .filter((k) => !k.startsWith('$')) // ignore drawing maps\n .sort(new Intl.Collator('en').compare) // case insensitive, e.g. \"ROI_i256\" > \"actc\"\n\n for (const key of cmapsSorted) {\n this.cluts[key] = cmaps[key]\n }\n }\n\n addColormap(key: string, cmap: ColorMap): void {\n this.cluts[key] = cmap\n }\n\n colormaps(): Array<keyof typeof this.cluts> {\n return Object.keys(this.cluts) as Array<keyof typeof this.cluts>\n }\n\n // for backward compatibility: prior to v0.34 \"colormaps\" used to be \"colorMaps\"\n colorMaps(): Array<keyof typeof this.cluts> {\n return this.colormaps()\n }\n\n // returns key name if it exists, otherwise returns default \"gray\"\n colormapFromKey(name: string): ColorMap {\n let cmap = this.cluts[name]\n if (cmap !== undefined) {\n return cmap\n }\n\n cmap = this.cluts[name.toLowerCase()]\n if (cmap !== undefined) {\n return cmap\n }\n\n if (name.length > 0) {\n log.warn('No color map named ' + name)\n }\n return {\n min: 0,\n max: 0,\n R: [0, 255],\n G: [0, 255],\n B: [0, 255],\n A: [0, 255],\n I: [0, 255]\n }\n }\n\n // not included in public docs\n colormap(key = '', isInvert = false): Uint8ClampedArray {\n const cmap = this.colormapFromKey(key)\n return this.makeLut(cmap.R, cmap.G, cmap.B, cmap.A, cmap.I, isInvert)\n }\n\n makeLabelLut(cm: ColorMap, alphaFill = 255, maxIdx = Infinity): LUT {\n if (cm.R === undefined || cm.G === undefined || cm.B === undefined) {\n throw new Error(`Invalid colormap table: ${cm}`)\n }\n const nLabels = cm.R.length\n\n // if indices are not provided, indices default to 0..(nLabels-1)\n const idxs = cm.I ?? [...Array(nLabels).keys()]\n\n // --- Check for out-of-range values: issue 1441\n let hasInvalid = false\n for (let i = 0; i < idxs.length; i++) {\n if (idxs[i] > maxIdx) {\n hasInvalid = true\n idxs[i] = maxIdx // clamp\n }\n }\n if (hasInvalid) {\n log.warn(`Some colormap indices clamped to match label range.`)\n }\n\n if (nLabels !== cm.G.length || nLabels !== cm.B.length || nLabels !== idxs.length) {\n throw new Error(`colormap does not make sense: ${cm} Rs ${cm.R.length} Gs ${cm.G.length} Bs ${cm.B.length} Is ${idxs.length}`)\n }\n\n let As = new Uint8ClampedArray(nLabels).fill(alphaFill)\n As[0] = 0\n if (cm.A !== undefined) {\n As = Uint8ClampedArray.from(cm.A)\n }\n\n const mnIdx = Math.min(...idxs)\n const mxIdx = Math.max(...idxs)\n\n // n.b. number of input labels can be sparse: I:[0,3,4] output is dense [0,1,2,3,4]\n const nLabelsDense = mxIdx - mnIdx + 1\n const lut = new Uint8ClampedArray(nLabelsDense * 4).fill(0)\n for (let i = 0; i < nLabels; i++) {\n let k = (idxs[i] - mnIdx) * 4\n lut[k++] = cm.R[i] // Red\n lut[k++] = cm.G[i] // Green\n lut[k++] = cm.B[i] // Blue\n lut[k++] = As[i] // Alpha\n }\n\n const cmap: LUT = {\n lut,\n min: mnIdx,\n max: mxIdx\n }\n // labels are optional\n if (cm.labels) {\n const nL = cm.labels.length\n if (nL === nLabelsDense) {\n cmap.labels = cm.labels\n } else if (nL === nLabels) {\n cmap.labels = Array(nLabelsDense).fill('?')\n for (let i = 0; i < nLabels; i++) {\n const idx = idxs[i]\n cmap.labels[idx] = cm.labels[i]\n }\n }\n }\n return cmap\n }\n\n async makeLabelLutFromUrl(name: string, alphaFill = 255, maxIdx = Infinity): Promise<LUT> {\n const response = await fetch(name)\n const cm = await response.json()\n return this.makeLabelLut(cm, alphaFill, maxIdx)\n }\n\n // not included in public docs\n // The drawing colormap is a variant of the label colormap with precisely 256 colors\n makeDrawLut(name: string | ColorMap): LUT {\n let cmap: ColorMap = typeof name === 'object' ? name : cmaps[name as keyof typeof cmaps]\n\n if (cmap === undefined) {\n log.warn('colormap undefined ', name)\n cmap = this.colormapFromKey('')\n }\n\n const cm = this.makeLabelLut(cmap, 255)\n\n if (cm.labels === undefined) {\n cm.labels = []\n }\n if (cm.labels.length < 256) {\n const j = cm.labels.length\n for (let i = j; i < 256; i++) {\n // make all unused slots opaque red\n cm.labels.push(i.toString())\n }\n }\n\n const lut = new Uint8ClampedArray(256 * 4)\n let k = 0\n for (let i = 0; i < 256; i++) {\n lut[k++] = 255 // Red\n lut[k++] = 0 // Green\n lut[k++] = 0 // Blue\n lut[k++] = 255 // Alpha\n }\n lut[3] = 0 // make first alpha transparent: not part of drawing\n\n // drawings can have no more than 256 colors\n const explicitLUTbytes = Math.min(cm.lut.length, 256 * 4)\n if (explicitLUTbytes > 0) {\n for (let i = 0; i < explicitLUTbytes; i++) {\n lut[i] = cm.lut[i]\n }\n }\n\n return {\n lut,\n labels: cm.labels\n }\n }\n\n // not included in public docs\n makeLut(Rsi: number[], Gsi: number[], Bsi: number[], Asi: number[], Isi: number[], isInvert: boolean): Uint8ClampedArray {\n // create color lookup table provided arrays of reds, greens, blues, alphas and intensity indices\n // intensity indices should be in increasing order with the first value 0 and the last 255.\n // this.makeLut([0, 255], [0, 0], [0,0], [0,128],[0,255]); //red gradient\n const nIdx = Rsi.length\n const Rs = [...Rsi]\n const Gs = [...Gsi]\n const Bs = [...Bsi]\n if (!Isi) {\n Isi = new Array(nIdx)\n for (let i = 0; i < nIdx; i++) {\n Isi[i] = (i / (nIdx - 1)) * 255\n }\n }\n if (!Asi) {\n Asi = new Array(nIdx).fill(64)\n Asi[0] = 0\n }\n let As = Uint8ClampedArray.from(Asi)\n let Is = Uint8ClampedArray.from(Isi)\n if (isInvert) {\n for (let i = 0; i < nIdx; i++) {\n Rs[i] = Rsi[nIdx - 1 - i]\n Gs[i] = Gsi[nIdx - 1 - i]\n Bs[i] = Bsi[nIdx - 1 - i]\n As[i] = 255 - Asi[nIdx - 1 - i]\n Is[i] = 255 - Isi[nIdx - 1 - i]\n }\n }\n const lut = new Uint8ClampedArray(256 * 4)\n if (typeof Is === 'undefined') {\n Is = new Uint8ClampedArray(nIdx).fill(0)\n for (let i = 0; i < nIdx; i++) {\n Is[i] = Math.round((i * 255.0) / (nIdx - 1))\n }\n }\n if (typeof As === 'undefined') {\n As = new Uint8ClampedArray(nIdx).fill(64)\n As[0] = 0\n }\n for (let i = 0; i < nIdx - 1; i++) {\n const idxLo = Is[i]\n let idxHi = Is[i + 1]\n if (i === 0 && idxLo !== 0) {\n log.warn('colormap issue: indices expected to start with 0 not ', idxLo)\n }\n if (i === Is.length - 2 && idxHi !== 255) {\n log.warn('padding colormap: indices expected end with 255 not ', idxHi)\n idxHi = 255\n }\n const idxRng = idxHi - idxLo\n let k = idxLo * 4\n for (let j = idxLo; j <= idxHi; j++) {\n const f = (j - idxLo) / idxRng\n lut[k++] = Rs[i] + f * (Rs[i + 1] - Rs[i]) // Red\n lut[k++] = Gs[i] + f * (Gs[i + 1] - Gs[i]) // Green\n lut[k++] = Bs[i] + f * (Bs[i + 1] - Bs[i]) // Blue\n lut[k++] = As[i] + f * (As[i + 1] - As[i]) // Alpha\n }\n }\n if (this.gamma === 1.0) {\n return lut\n }\n for (let i = 0; i < 255 * 4; i++) {\n if (i % 4 === 3) {\n continue\n } // gamma changes RGB, not Alpha\n lut[i] = Math.pow(lut[i] / 255, 1 / this.gamma) * 255\n }\n return lut\n }\n}\n\nexport const cmapper = new ColorTables()\n","/**\n * ImageFactory Module\n *\n * Pure utility functions for loading and creating NVImage instances.\n * This module contains helper functions for:\n * - Fetching data from URLs and files\n * - Stream decompression and partial loading\n * - Format detection and filename parsing\n * - Data conversion utilities\n *\n * These functions are intentionally pure and do not depend on the NVImage class,\n * avoiding circular dependencies.\n */\n\nimport { Gunzip } from 'fflate'\nimport { readHeaderAsync } from 'nifti-reader-js'\nimport { uncompressStream } from '@/nvimage/utils'\n\n/**\n * Fetch DICOM manifest and return array of file data\n * @param url - URL to DICOM manifest or folder\n * @param headers - HTTP headers for requests\n * @returns Array of {name, data} objects for each DICOM file\n */\nexport async function fetchDicomData(url: string, headers: Record<string, string> = {}): Promise<Array<{ name: string; data: ArrayBuffer }>> {\n if (url === '') {\n throw Error('url must not be empty')\n }\n\n // https://stackoverflow.com/questions/10687099/how-to-test-if-a-url-string-is-absolute-or-relative\n const absoluteUrlRE = /^(?:[a-z+]+:)?\\/\\//i\n\n let manifestUrl = absoluteUrlRE.test(url) ? url : new URL(url, window.location.href)\n const extensionRE = /(?:.([^.]+))?$/\n const extension = extensionRE.exec((manifestUrl as URL).pathname)\n if (!extension) {\n manifestUrl = new URL('niivue-manifest.txt', url)\n }\n\n let response = await fetch(manifestUrl, { headers })\n if (!response.ok) {\n throw Error(response.statusText)\n }\n const text = await response.text()\n const lines = text.split('\\n')\n\n const baseUrlRE = /(.*\\/).*/\n const folderUrl = baseUrlRE.exec(manifestUrl as string)![0]\n const dataBuffer = []\n for (const line of lines) {\n const fileUrl = new URL(line, folderUrl)\n response = await fetch(fileUrl, { headers })\n if (!response.ok) {\n throw Error(response.statusText)\n }\n const contents = await response.arrayBuffer()\n dataBuffer.push({ name: line, data: contents })\n }\n return dataBuffer\n}\n\n/**\n * Read and decompress first N bytes from a gzipped stream\n * @param stream - ReadableStream of compressed data\n * @param minBytes - Minimum bytes to read (decompressed)\n * @returns Decompressed bytes\n */\nexport async function readFirstDecompressedBytes(stream: ReadableStream<Uint8Array>, minBytes: number): Promise<Uint8Array> {\n const reader: ReadableStreamDefaultReader<Uint8Array> = stream.getReader()\n const gunzip = new Gunzip()\n\n const decompressedChunks: Uint8Array[] = []\n let totalDecompressed = 0\n let doneReading = false\n\n let resolveFn: (value: Uint8Array) => void\n let rejectFn: (reason?: any) => void\n\n const promise = new Promise<Uint8Array>((resolve, reject): undefined => {\n resolveFn = resolve\n rejectFn = reject\n return undefined\n })\n\n function finalize(): void {\n // Combine chunks into a single Uint8Array\n const result = new Uint8Array(totalDecompressed)\n let offset = 0\n for (const chunk of decompressedChunks) {\n result.set(chunk, offset)\n offset += chunk.length\n }\n resolveFn(result)\n }\n\n gunzip.ondata = (chunk: Uint8Array): void => {\n decompressedChunks.push(chunk)\n totalDecompressed += chunk.length\n if (totalDecompressed >= minBytes) {\n doneReading = true\n reader.cancel().catch(() => {})\n finalize()\n }\n }\n ;(async (): Promise<void> => {\n try {\n while (!doneReading) {\n const { done, value } = await reader.read()\n if (done) {\n doneReading = true\n gunzip.push(new Uint8Array(), true) // Signal end-of-stream\n return\n }\n gunzip.push(value, false) // Push data into fflate\n }\n } catch (err) {\n rejectFn(err)\n }\n })().catch(() => {})\n\n return promise\n}\n\n/**\n * Extract filename from URL query parameters or pathname\n * @param url - URL string to parse\n * @returns Extracted filename or null\n */\nexport function extractFilenameFromUrl(url: string): string | null {\n const params = new URL(url).searchParams\n const contentDisposition = params.get('response-content-disposition')\n if (contentDisposition) {\n const match = contentDisposition.match(/filename\\*?=(?:UTF-8'')?\"?([^\";]+)\"?/)\n if (match) {\n return decodeURIComponent(match[1])\n }\n }\n // Fallback: extract from pathname if possible\n return url.split('/').pop()!.split('?')[0]\n}\n\n/**\n * Load partial 4D volumes from gzipped NIfTI file\n * @param url - URL to gzipped NIfTI file\n * @param headers - HTTP headers\n * @param limitFrames4D - Maximum number of frames to load\n * @returns ArrayBuffer with partial data or null if full load needed\n */\nexport async function loadInitialVolumesGz(url = '', headers = {}, limitFrames4D = NaN): Promise<ArrayBuffer | null> {\n if (isNaN(limitFrames4D)) {\n return null\n }\n const response = await fetch(url, { headers, cache: 'force-cache' })\n let hdrBytes = 352\n let hdrU8s = await readFirstDecompressedBytes(response.body!, hdrBytes)\n const hdrView = new DataView(hdrU8s.buffer, hdrU8s.byteOffset, hdrU8s.byteLength)\n const u16 = hdrView.getUint16(0, true)\n const isNIfTI1 = u16 === 348\n const isNIfTI1be = u16 === 23553\n if (!isNIfTI1 && !isNIfTI1be) {\n return null\n }\n // start of edge cases: huge header extensions with small gz block size\n if (hdrU8s.length > 111) {\n hdrBytes = hdrView.getFloat32(108, isNIfTI1)\n }\n if (hdrBytes > hdrU8s.length) {\n hdrU8s = await readFirstDecompressedBytes(response.body!, hdrBytes)\n }\n // end of edge case\n const isNifti1 = (hdrU8s[0] === 92 && hdrU8s[1] === 1) || (hdrU8s[1] === 92 && hdrU8s[0] === 1)\n if (!isNifti1) {\n return null\n }\n const hdr = await readHeaderAsync(hdrU8s.buffer as ArrayBuffer)\n if (!hdr) {\n throw new Error('Could not read NIfTI header')\n }\n // Calculate required data size\n const nBytesPerVoxel = hdr.numBitsPerVoxel / 8\n const nVox3D = [1, 2, 3].reduce((acc, i) => acc * (hdr.dims[i] > 1 ? hdr.dims[i] : 1), 1)\n const nFrame4D = [4, 5, 6].reduce((acc, i) => acc * (hdr.dims[i] > 1 ? hdr.dims[i] : 1), 1)\n const volsToLoad = Math.max(Math.min(limitFrames4D, nFrame4D), 1)\n const bytesToLoad = hdr.vox_offset + volsToLoad * nVox3D * nBytesPerVoxel\n if (volsToLoad === nFrame4D) {\n // read entire file: compression streams is faster than fflate\n return null\n }\n const responseImg = await fetch(url, { headers, cache: 'force-cache' })\n const dataBytes = await readFirstDecompressedBytes(responseImg.body!, bytesToLoad)\n return dataBytes.buffer.slice(0, bytesToLoad) as ArrayBuffer\n}\n\n/**\n * Load partial 4D volumes from uncompressed NIfTI file\n * @param url - URL to NIfTI file\n * @param headers - HTTP headers\n * @param limitFrames4D - Maximum number of frames to load\n * @returns ArrayBuffer with partial data or null if full load needed\n */\nexport async function loadInitialVolumes(url = '', headers = {}, limitFrames4D = NaN): Promise<ArrayBuffer | null> {\n if (isNaN(limitFrames4D)) {\n return null\n }\n const response = await fetch(url, { headers, cache: 'force-cache' })\n const reader = response.body!.getReader()\n const { value, done } = await reader.read()\n let hdrU8s = value!\n if (done || !hdrU8s || hdrU8s.length < 2) {\n throw new Error('Not enough data to determine compression')\n }\n const hdrView = new DataView(hdrU8s.buffer, hdrU8s.byteOffset, hdrU8s.byteLength)\n const u16 = hdrView.getUint16(0, true)\n const isGz = u16 === 35615\n if (isGz) {\n await reader.cancel() // Stop streaming and release the lock\n return loadInitialVolumesGz(url, headers, limitFrames4D)\n }\n const isNIfTI1 = u16 === 348\n const isNIfTI1be = u16 === 23553\n if (!isNIfTI1 && !isNIfTI1be) {\n await reader.cancel()\n return null\n }\n // start of edge cases: huge header extensions with degraded packet size\n let hdrBytes = 352\n if (hdrU8s.length > 111) {\n hdrBytes = hdrView.getFloat32(108, isNIfTI1)\n }\n while (hdrU8s.length < hdrBytes) {\n const { value, done } = await reader.read()\n if (done || !value) {\n break\n }\n function concatU8s(arr1: Uint8Array, arr2: Uint8Array): Uint8Array {\n const result = new Uint8Array(arr1.length + arr2.length)\n result.set(arr1, 0)\n result.set(arr2, arr1.length)\n return result\n }\n hdrU8s = concatU8s(hdrU8s, value)\n }\n // end of edge cases\n const hdr = await readHeaderAsync(hdrU8s.buffer as ArrayBuffer)\n if (!hdr) {\n throw new Error('Could not read NIfTI header')\n }\n // Calculate required data size\n const nBytesPerVoxel = hdr.numBitsPerVoxel / 8\n const nVox3D = [1, 2, 3].reduce((acc, i) => acc * (hdr.dims[i] > 1 ? hdr.dims[i] : 1), 1)\n const nFrame4D = [4, 5, 6].reduce((acc, i) => acc * (hdr.dims[i] > 1 ? hdr.dims[i] : 1), 1)\n const volsToLoad = Math.max(Math.min(limitFrames4D, nFrame4D), 1)\n const bytesToLoad = hdr.vox_offset + volsToLoad * nVox3D * nBytesPerVoxel\n const imgU8s = new Uint8Array(bytesToLoad)\n // Ensure we don't copy more than needed from hdrU8s\n const hdrCopyLength = Math.min(hdrU8s.length, bytesToLoad)\n imgU8s.set(hdrU8s.subarray(0, hdrCopyLength), 0)\n let bytesRead = hdrCopyLength\n while (bytesRead < bytesToLoad) {\n const { value, done } = await reader.read()\n if (done || !value) {\n await reader.cancel()\n return null\n }\n // Ensure we only copy up to bytesToLoad\n const remaining = Math.min(value.length, bytesToLoad - bytesRead)\n imgU8s.set(value.subarray(0, remaining), bytesRead)\n bytesRead += remaining\n }\n await reader.cancel()\n return imgU8s.buffer\n}\n\n/**\n * Read File object with streaming decompression\n * @param file - Browser File object\n * @param bytesToLoad - Optional limit on bytes to read\n * @returns ArrayBuffer with file data\n */\nexport async function readFileAsync(file: File, bytesToLoad = NaN): Promise<ArrayBuffer> {\n let stream = file.stream()\n\n if (!isNaN(bytesToLoad)) {\n let bytesRead = 0\n const limiter = new TransformStream({\n transform(chunk: Uint8Array, controller: TransformStreamDefaultController): void {\n if (bytesRead >= bytesToLoad) {\n controller.terminate()\n return\n }\n const remainingBytes = bytesToLoad - bytesRead\n if (chunk.length > remainingBytes) {\n controller.enqueue(chunk.slice(0, remainingBytes))\n controller.terminate()\n } else {\n controller.enqueue(chunk)\n }\n bytesRead += chunk.length\n }\n })\n stream = stream.pipeThrough(limiter)\n }\n\n const uncompressedStream = await uncompressStream(stream)\n\n const chunks: Uint8Array[] = []\n const reader = uncompressedStream.getReader()\n\n while (true) {\n const { done, value } = await reader.read()\n if (done) {\n break\n }\n chunks.push(value)\n }\n\n const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0)\n const result = new ArrayBuffer(totalLength)\n const resultView = new Uint8Array(result)\n\n let offset = 0\n for (const chunk of chunks) {\n resultView.set(chunk, offset)\n offset += chunk.length\n }\n\n return result\n}\n\n/**\n * Convert base64 string to ArrayBuffer\n * @param base64 - Base64 encoded string\n * @returns Decoded ArrayBuffer\n */\nexport function base64ToArrayBuffer(base64: string): ArrayBuffer {\n const binary_string = window.atob(base64)\n const len = binary_string.length\n const bytes = new Uint8Array(len)\n for (let i = 0; i < len; i++) {\n bytes[i] = binary_string.charCodeAt(i)\n }\n return bytes.buffer\n}\n\n/**\n * Get primary file extension (without compression suffix)\n * @param filename - Filename to parse\n * @returns Primary extension (e.g., \"nii\" from \"file.nii.gz\")\n */\nexport function getPrimaryExtension(filename: string): string {\n // .nii.gz -> .nii\n const match = filename.match(/\\.([^.]+)(?:\\.gz|\\.bz2|\\.xz)?$/)\n return match ? match[1] : ''\n}\n","/**\n * ZarrProcessor module\n *\n * Handles Zarr format-specific loading logic:\n * - Parses Zarr URL parameters for slice selection\n * - Fetches Zarr array data from remote stores\n * - Handles 3D and 4D array slicing\n * - Validates slice indices against array dimensions\n *\n * Zarr is a cloud-optimized format for chunked, compressed N-dimensional arrays.\n */\n\nimport * as zarr from 'zarrita'\n\n/**\n * Result of Zarr data loading, containing the raw data buffer\n * and metadata about the array structure.\n */\nexport interface ZarrLoadResult {\n dataBuffer: ArrayBuffer\n zarrData: {\n data: ArrayBuffer\n width: number\n height: number\n depth: number\n channels?: number\n }\n}\n\n/**\n * Load Zarr array data from a URL with optional slice selection.\n * Supports query parameters ?x=N, ?y=N, ?z=N for slice selection.\n *\n * @param url - Zarr URL (may include query parameters for slicing)\n * @returns Load result with data buffer and metadata\n *\n * @example\n * ```typescript\n * // Load full array\n * const result = await loadZarrData('https://example.com/data.zarr')\n *\n * // Load specific slice\n * const result = await loadZarrData('https://example.com/data.zarr?z=50&y=100&x=100')\n * ```\n */\nexport async function loadZarrData(url: string): Promise<ZarrLoadResult> {\n // Get the z, x, y slice indices from the query string\n const urlParams = new URL(url).searchParams\n const zIndex = urlParams.get('z')\n const yIndex = urlParams.get('y')\n const xIndex = urlParams.get('x')\n\n const zRange = zIndex ? zarr.slice(parseInt(zIndex), parseInt(zIndex) + 1) : null\n const yRange = yIndex ? zarr.slice(parseInt(yIndex), parseInt(yIndex) + 1) : null\n const xRange = xIndex ? zarr.slice(parseInt(xIndex), parseInt(xIndex) + 1) : null\n\n // Remove the query string from the original URL\n const zarrUrl = url.split('?')[0]\n\n // If multiscale, must provide the full path to the zarr array data\n const store = new zarr.FetchStore(zarrUrl)\n const root = zarr.root(store)\n\n let arr\n try {\n // Try to resolve the full URL path first\n arr = await zarr.open(root.resolve(url), { kind: 'array' })\n } catch (e) {\n // Fall back to opening the root directly\n arr = await zarr.open(root, { kind: 'array' })\n }\n\n let view\n if (arr.shape.length === 4) {\n // 4D array (e.g., RGB/RGBA volume data)\n const cRange = null // Load all channels\n\n // Make sure we are not exceeding the array shape. If so, clamp to max\n const zDim = arr.shape[2]\n const yDim = arr.shape[1]\n const xDim = arr.shape[0]\n\n if (zRange && zRange[0] >= zDim) {\n zRange[0] = zDim - 1\n }\n if (yRange && yRange[0] >= yDim) {\n yRange[0] = yDim - 1\n }\n if (xRange && xRange[0] >= xDim) {\n xRange[0] = xDim - 1\n }\n\n view = await zarr.get(arr, [xRange, yRange, zRange, cRange])\n } else {\n // 3D array\n view = await zarr.get(arr, [xRange, yRange, zRange])\n }\n\n const dataBuffer = view.data as ArrayBuffer\n const [height, width, zDim, cDim] = view.shape\n\n const zarrData = {\n data: dataBuffer,\n width,\n height,\n depth: zDim,\n channels: cDim\n }\n\n return {\n dataBuffer,\n zarrData\n }\n}\n","/**\n * NVZarrHelper - Simplified zarr chunk management for NVImage.\n *\n * Attaches to a host NVImage and manages chunked loading of OME-Zarr data.\n * No zoom, no prefetching - just pan and level switching.\n * All coordinates are in current-level pixel space.\n *\n * Spatial dimensions are kept in OME metadata order throughout.\n * The mapping to NIfTI layout is:\n * - OME dim[0] (slowest in C-order) → NIfTI dim 3 (depth, slowest in Fortran-order)\n * - OME dim[1] → NIfTI dim 2 (height)\n * - OME dim[2] (fastest in C-order) → NIfTI dim 1 (width, fastest in Fortran-order)\n * This means chunk data can be copied directly without stride remapping.\n * The affine matrix maps NIfTI (i, j, k) indices to physical (x, y, z) space\n * using the OME axis names.\n */\n\nimport { NIFTI1 } from 'nifti-reader-js'\n\nimport { v4 as uuidv4 } from '@lukeed/uuid'\nimport { copyAffine } from '../affineUtils'\nimport { ZarrChunkClient, type ZarrPyramidInfo, type ChunkCoord } from './ZarrChunkClient.js'\nimport { ZarrChunkCache, type TypedArray } from './ZarrChunkCache.js'\nimport { NiiDataType } from '@/nvimage/utils.js'\nimport type { NVImage, TypedVoxelArray } from '@/nvimage/index.js'\n\nexport interface NVZarrHelperOptions {\n url: string\n level: number\n maxVolumeSize?: number\n maxTextureSize?: number\n channel?: number\n cacheSize?: number\n /** Convert OME spatial units to millimeters for NIfTI compatibility (default: true) */\n convertUnitsToMm?: boolean\n}\n\n/**\n * Map zarr dtype string to NIfTI datatype code\n */\nfunction zarrDtypeToNifti(dtype: string): number {\n const normalized = dtype.toLowerCase().replace(/[<>|]/g, '')\n if (normalized === 'u1' || normalized === 'uint8') {\n return NiiDataType.DT_UINT8\n }\n if (normalized === 'i1' || normalized === 'int8') {\n return NiiDataType.DT_INT8\n }\n if (normalized === 'u2' || normalized === 'uint16') {\n return NiiDataType.DT_UINT16\n }\n if (normalized === 'i2' || normalized === 'int16') {\n return NiiDataType.DT_INT16\n }\n if (normalized === 'u4' || normalized === 'uint32') {\n return NiiDataType.DT_UINT32\n }\n if (normalized === 'i4' || normalized === 'int32') {\n return NiiDataType.DT_INT32\n }\n if (normalized === 'f4' || normalized === 'float32') {\n return NiiDataType.DT_FLOAT32\n }\n if (normalized === 'f8' || normalized === 'float64') {\n return NiiDataType.DT_FLOAT64\n }\n console.warn(`Unknown zarr dtype: ${dtype}, defaulting to uint8`)\n return NiiDataType.DT_UINT8\n}\n\nfunction getBytesPerVoxel(datatypeCode: number): number {\n switch (datatypeCode) {\n case NiiDataType.DT_UINT8:\n case NiiDataType.DT_INT8:\n return 1\n case NiiDataType.DT_UINT16:\n case NiiDataType.DT_INT16:\n return 2\n case NiiDataType.DT_UINT32:\n case NiiDataType.DT_INT32:\n case NiiDataType.DT_FLOAT32:\n return 4\n case NiiDataType.DT_FLOAT64:\n return 8\n default:\n return 1\n }\n}\n\nfunction createTypedVoxelArray(datatypeCode: number, size: number): TypedVoxelArray {\n switch (datatypeCode) {\n case NiiDataType.DT_UINT8:\n case NiiDataType.DT_INT8:\n return new Uint8Array(size)\n case NiiDataType.DT_UINT16:\n return new Uint16Array(size)\n case NiiDataType.DT_INT16:\n return new Int16Array(size)\n case NiiDataType.DT_INT32:\n return new Int32Array(size)\n case NiiDataType.DT_UINT32:\n return new Uint32Array(size)\n case NiiDataType.DT_FLOAT32:\n return new Float32Array(size)\n case NiiDataType.DT_FLOAT64:\n return new Float64Array(size)\n default:\n return new Uint8Array(size)\n }\n}\n\n/**\n * Convert a value from an OME spatial unit to millimeters.\n * Returns the value unchanged if the unit is unrecognized or absent.\n */\nfunction omeUnitToMm(value: number, unit?: string): number {\n if (!unit) {\n return value\n }\n switch (unit.toLowerCase()) {\n case 'micrometer':\n case 'um':\n case 'µm':\n return value / 1000\n case 'nanometer':\n case 'nm':\n return value / 1_000_000\n case 'millimeter':\n case 'mm':\n return value\n case 'centimeter':\n case 'cm':\n return value * 10\n case 'meter':\n case 'm':\n return value * 1000\n default:\n return value\n }\n}\n\nexport class NVZarrHelper {\n private hostImage: NVImage\n private chunkClient: ZarrChunkClient\n private chunkCache: ZarrChunkCache\n private pyramidInfo: ZarrPyramidInfo\n private datatypeCode: number\n\n private pyramidLevel: number\n /** Level dimensions in OME metadata order: depth=dim[0], height=dim[1], width=dim[2] */\n private levelDims: { width: number; height: number; depth: number }\n private volumeDims: { width: number; height: number; depth: number }\n private chunkSize: { width: number; height: number; depth: number }\n\n /** Voxel scales in OME metadata order: depth=dim[0], height=dim[1], width=dim[2] */\n private voxelScales: { width: number; height: number; depth: number }\n /** Voxel translations in OME metadata order */\n private voxelTranslations: { width: number; height: number; depth: number }\n private hasTranslations: boolean\n private convertUnitsToMm: boolean\n private worldOffsetMM: [number, number, number] = [0, 0, 0]\n\n private centerX: number\n private centerY: number\n private centerZ: number\n\n private channel: number\n private nonSpatialCoords: Record<string, number> = {}\n\n private isUpdating = false\n private needsUpdate = false\n private currentAbortController: AbortController | null = null\n private runningMin = Infinity\n private runningMax = -Infinity\n private calibrationDone = false\n\n // Debounce state for batching chunk updates\n private updateDebounceTimer: ReturnType<typeof setTimeout> | null = null\n private readonly UPDATE_DEBOUNCE_MS = 50 // Batch chunks arriving within 50ms\n private pendingChunkCount = 0\n private lastRenderedChunkCount = 0\n\n centerAtDragStart: { x: number; y: number; z: number } | null = null\n\n onChunksUpdated?: () => void\n onAllChunksLoaded?: () => void\n\n private constructor(hostImage: NVImage) {\n this.hostImage = hostImage\n this.chunkClient = null as unknown as ZarrChunkClient\n this.chunkCache = null as unknown as ZarrChunkCache\n this.pyramidInfo = null as unknown as ZarrPyramidInfo\n this.datatypeCode = NiiDataType.DT_UINT8\n this.pyramidLevel = 0\n this.levelDims = { width: 0, height: 0, depth: 0 }\n this.volumeDims = { width: 0, height: 0, depth: 0 }\n this.chunkSize = { width: 0, height: 0, depth: 0 }\n this.voxelScales = { width: 1, height: 1, depth: 1 }\n this.voxelTranslations = { width: 0, height: 0, depth: 0 }\n this.hasTranslations = false\n this.convertUnitsToMm = true\n this.centerX = 0\n this.centerY = 0\n this.centerZ = 0\n this.channel = 0\n }\n\n static async create(hostImage: NVImage, url: string, options: NVZarrHelperOptions): Promise<NVZarrHelper> {\n const helper = new NVZarrHelper(hostImage)\n\n helper.chunkClient = new ZarrChunkClient({ baseUrl: url })\n helper.pyramidInfo = await helper.chunkClient.fetchInfo()\n helper.chunkCache = new ZarrChunkCache(options.cacheSize ?? 500)\n\n const maxTexSize = options.maxTextureSize ?? 2048\n const maxDim = options.maxVolumeSize ?? 256\n\n // Check if level 0 (highest resolution) fits within the max volume size.\n // If so, use the actual dimensions directly instead of a virtual volume.\n const level0 = helper.pyramidInfo.levels[0]\n const level0Shape = level0.shape\n const level0FitsInVolume = level0Shape.every((dim) => dim <= maxDim && dim <= maxTexSize)\n\n if (level0FitsInVolume) {\n // Full dataset fits — use exact dimensions (reversed: depth=dim[0], height=dim[1], width=dim[2])\n if (helper.pyramidInfo.is3D && level0Shape.length >= 3) {\n helper.volumeDims = { depth: level0Shape[0], height: level0Shape[1], width: level0Shape[2] }\n } else {\n helper.volumeDims = { height: level0Shape[0], width: level0Shape[1], depth: 1 }\n }\n } else {\n helper.volumeDims = {\n width: Math.min(maxDim, maxTexSize),\n height: Math.min(maxDim, maxTexSize),\n depth: Math.min(maxDim, maxTexSize)\n }\n }\n\n helper.datatypeCode = zarrDtypeToNifti(helper.pyramidInfo.levels[0].dtype)\n helper.convertUnitsToMm = options.convertUnitsToMm ?? true\n\n helper.channel = options.channel ?? 0\n const axisMapping = helper.pyramidInfo.axisMapping\n for (const nsa of axisMapping.nonSpatialAxes) {\n if (nsa.name === 'c') {\n helper.nonSpatialCoords[nsa.name] = helper.channel\n }\n }\n\n // Set pyramid level\n const level = Math.max(0, Math.min(options.level, helper.pyramidInfo.levels.length - 1))\n helper.pyramidLevel = level\n helper.updateLevelInfo()\n\n // Center on the level (use levelDims so initial view shows the center of the zarr)\n helper.centerX = helper.levelDims.width / 2\n helper.centerY = helper.levelDims.height / 2\n helper.centerZ = helper.levelDims.depth / 2\n\n // Configure the host NVImage\n helper.configureHostImage()\n\n // Don't load chunks here — loadInitialChunks() must be called after\n // the onChunksUpdated callback is registered (see addVolume in Niivue).\n\n return helper\n }\n\n async loadInitialChunks(): Promise<void> {\n await this.updateVolume()\n }\n\n private updateLevelInfo(): void {\n const levelInfo = this.pyramidInfo.levels[this.pyramidLevel]\n const shape = levelInfo.shape\n // Map OME spatial dims to NIfTI: dim[0]→depth (slowest), dim[1]→height, dim[2]→width (fastest)\n if (this.pyramidInfo.is3D && shape.length >= 3) {\n this.levelDims = { depth: shape[0], height: shape[1], width: shape[2] }\n } else {\n this.levelDims = { height: shape[0], width: shape[1], depth: 1 }\n }\n\n const chunks = levelInfo.chunks\n if (this.pyramidInfo.is3D && chunks.length >= 3) {\n this.chunkSize = { depth: chunks[0], height: chunks[1], width: chunks[2] }\n } else {\n this.chunkSize = { height: chunks[0], width: chunks[1], depth: 1 }\n }\n\n // Extract physical voxel scales from OME coordinateTransformations (in metadata order)\n if (levelInfo.scales) {\n if (this.pyramidInfo.is3D && levelInfo.scales.length >= 3) {\n this.voxelScales = { depth: levelInfo.scales[0], height: levelInfo.scales[1], width: levelInfo.scales[2] }\n } else if (levelInfo.scales.length >= 2) {\n this.voxelScales = { height: levelInfo.scales[0], width: levelInfo.scales[1], depth: 1 }\n }\n } else {\n this.voxelScales = { width: 1, height: 1, depth: 1 }\n }\n\n // Extract physical voxel translations from OME coordinateTransformations (in metadata order)\n if (levelInfo.translations) {\n if (this.pyramidInfo.is3D && levelInfo.translations.length >= 3) {\n this.voxelTranslations = { depth: levelInfo.translations[0], height: levelInfo.translations[1], width: levelInfo.translations[2] }\n } else if (levelInfo.translations.length >= 2) {\n this.voxelTranslations = { height: levelInfo.translations[0], width: levelInfo.translations[1], depth: 0 }\n }\n this.hasTranslations = true\n } else {\n this.voxelTranslations = { width: 0, height: 0, depth: 0 }\n this.hasTranslations = false\n }\n }\n\n private configureHostImage(): void {\n const { width, height, depth } = this.volumeDims\n const bytesPerVoxel = getBytesPerVoxel(this.datatypeCode)\n\n // Get unit-converted scales for pixDims\n const { scaleW, scaleH, scaleD } = this.getConvertedScales()\n\n const hdr = new NIFTI1()\n hdr.littleEndian = true\n hdr.dims = [3, width, height, depth, 1, 0, 0, 0]\n hdr.pixDims = [1, scaleW, scaleH, scaleD, 1, 0, 0, 0]\n hdr.datatypeCode = this.datatypeCode\n hdr.numBitsPerVoxel = bytesPerVoxel * 8\n hdr.scl_inter = 0\n hdr.scl_slope = 1\n hdr.sform_code = 2\n hdr.magic = 'n+1'\n hdr.vox_offset = 352\n\n // Placeholder affine — updateAffine() will set the real one\n hdr.affine = [\n [1, 0, 0, 0],\n [0, 1, 0, 0],\n [0, 0, 1, 0],\n [0, 0, 0, 1]\n ]\n\n const img = this.hostImage\n img.name = `zarr:${this.pyramidInfo.name}`\n img.id = uuidv4()\n img._colormap = 'gray'\n img._opacity = 1.0\n img.hdr = hdr\n img.nFrame4D = 1\n img.frame4D = 0\n img.nTotalFrame4D = 1\n img.nVox3D = width * height * depth\n img.dims = [width, height, depth]\n img.pixDims = [scaleW, scaleH, scaleD]\n\n img.img = createTypedVoxelArray(this.datatypeCode, width * height * depth)\n\n // Set the affine based on current center position and call calculateRAS()\n this.updateAffine()\n\n // Store original affine so resetVolumeAffine() works on zarr volumes\n img.originalAffine = copyAffine(hdr.affine)\n\n img.cal_min = 0\n img.cal_max = 0\n img.robust_min = 0\n img.robust_max = 0\n img.global_min = 0\n img.global_max = 0\n }\n\n /** Get unit-converted voxel scales */\n private getConvertedScales(): { scaleW: number; scaleH: number; scaleD: number } {\n const units = this.pyramidInfo.spatialUnits\n let scaleD = this.voxelScales.depth\n let scaleH = this.voxelScales.height\n let scaleW = this.voxelScales.width\n if (this.convertUnitsToMm && units) {\n if (units.length >= 3) {\n scaleD = omeUnitToMm(scaleD, units[0])\n scaleH = omeUnitToMm(scaleH, units[1])\n scaleW = omeUnitToMm(scaleW, units[2])\n } else if (units.length >= 2) {\n scaleH = omeUnitToMm(scaleH, units[0])\n scaleW = omeUnitToMm(scaleW, units[1])\n }\n }\n return { scaleW, scaleH, scaleD }\n }\n\n /** Get unit-converted voxel translations */\n private getConvertedTranslations(): { transW: number; transH: number; transD: number } {\n const units = this.pyramidInfo.spatialUnits\n let transD = this.voxelTranslations.depth\n let transH = this.voxelTranslations.height\n let transW = this.voxelTranslations.width\n if (this.convertUnitsToMm && units) {\n if (units.length >= 3) {\n transD = omeUnitToMm(transD, units[0])\n transH = omeUnitToMm(transH, units[1])\n transW = omeUnitToMm(transW, units[2])\n } else if (units.length >= 2) {\n transH = omeUnitToMm(transH, units[0])\n transW = omeUnitToMm(transW, units[1])\n }\n }\n return { transW, transH, transD }\n }\n\n /**\n * Build the NIfTI affine from OME axis names, scales, and translations.\n *\n * NIfTI dimensions map to OME spatial dimensions as:\n * i (dim 1, width) = OME spatial[-1] (last, fastest in C-order)\n * j (dim 2, height) = OME spatial[-2]\n * k (dim 3, depth) = OME spatial[-3] (first, slowest in C-order)\n *\n * The affine maps (i, j, k) → physical (x, y, z):\n * physical_axis = scale * nifti_dim + translation\n * where nifti_dim is the column index (0=i, 1=j, 2=k) and\n * physical_axis row is determined by the OME axis name.\n */\n private updateAffine(): void {\n const hdr = this.hostImage.hdr\n if (!hdr) {\n return\n }\n\n const { width, height, depth } = this.volumeDims\n const axisNames = this.pyramidInfo.axisMapping.spatialAxisNames\n\n if (this.hasTranslations && axisNames.length >= 3) {\n const { scaleW, scaleH, scaleD } = this.getConvertedScales()\n const { transW, transH, transD } = this.getConvertedTranslations()\n\n // Volume window offsets in level coords\n const volStartW = this.centerX - width / 2 // width = last OME dim\n const volStartH = this.centerY - height / 2 // height = middle OME dim\n const volStartD = this.centerZ - depth / 2 // depth = first OME dim\n\n // Scale + offset for each OME spatial dimension, indexed by NIfTI column:\n // column 0 (i=width) → OME dim[-1]: scaleW, transW + volStartW * scaleW\n // column 1 (j=height) → OME dim[-2]: scaleH, transH + volStartH * scaleH\n // column 2 (k=depth) → OME dim[-3]: scaleD, transD + volStartD * scaleD\n const niftiCols = [\n { name: axisNames[axisNames.length - 1], scale: scaleW, trans: transW + volStartW * scaleW },\n { name: axisNames[axisNames.length - 2], scale: scaleH, trans: transH + volStartH * scaleH },\n { name: axisNames[axisNames.length - 3], scale: scaleD, trans: transD + volStartD * scaleD }\n ]\n\n // Map physical axis name → affine row: x→0, y→1, z→2\n const affine: number[][] = [\n [0, 0, 0, 0],\n [0, 0, 0, 0],\n [0, 0, 0, 0],\n [0, 0, 0, 1]\n ]\n for (let col = 0; col < 3; col++) {\n const row = niftiCols[col].name === 'x' ? 0 : niftiCols[col].name === 'y' ? 1 : 2\n affine[row][col] = niftiCols[col].scale\n affine[row][3] = niftiCols[col].trans\n }\n affine[0][3] += this.worldOffsetMM[0]\n affine[1][3] += this.worldOffsetMM[1]\n affine[2][3] += this.worldOffsetMM[2]\n hdr.affine = affine\n } else {\n // No OME translations — calculate offset based on pan position\n const { scaleW, scaleH, scaleD } = this.getConvertedScales()\n\n // Volume window offsets in level coords (same calculation as 3D path)\n const volStartW = this.centerX - width / 2\n const volStartH = this.centerY - height / 2\n const volStartD = this.centerZ - depth / 2\n\n if (axisNames.length >= 2) {\n // Build affine based on axis names for 2D data\n // NIfTI cols: 0=width (last spatial), 1=height (2nd-to-last), 2=depth\n const niftiCols = [\n { name: axisNames[axisNames.length - 1], scale: scaleW, trans: volStartW * scaleW },\n { name: axisNames[axisNames.length - 2], scale: scaleH, trans: volStartH * scaleH },\n { name: 'z', scale: scaleD, trans: volStartD * scaleD }\n ]\n\n const affine: number[][] = [\n [0, 0, 0, 0],\n [0, 0, 0, 0],\n [0, 0, 0, 0],\n [0, 0, 0, 1]\n ]\n for (let col = 0; col < 3; col++) {\n const row = niftiCols[col].name === 'x' ? 0 : niftiCols[col].name === 'y' ? 1 : 2\n affine[row][col] = niftiCols[col].scale\n affine[row][3] = niftiCols[col].trans\n }\n affine[0][3] += this.worldOffsetMM[0]\n affine[1][3] += this.worldOffsetMM[1]\n affine[2][3] += this.worldOffsetMM[2]\n hdr.affine = affine\n } else {\n // Fallback: simple diagonal affine with pan position\n hdr.affine = [\n [scaleW, 0, 0, volStartW * scaleW + this.worldOffsetMM[0]],\n [0, -scaleH, 0, volStartH * scaleH + this.worldOffsetMM[1]],\n [0, 0, -scaleD, volStartD * scaleD + this.worldOffsetMM[2]],\n [0, 0, 0, 1]\n ]\n }\n }\n\n this.hostImage.calculateRAS()\n\n // Keep originalAffine in sync so resetVolumeAffine() restores the\n // current zarr-computed affine (correct for this level/pan), not the\n // one from initial load.\n this.hostImage.originalAffine = copyAffine(this.hostImage.hdr.affine)\n }\n\n beginDrag(): void {\n this.centerAtDragStart = { x: this.centerX, y: this.centerY, z: this.centerZ }\n }\n\n endDrag(): void {\n this.centerAtDragStart = null\n }\n\n async panBy(dx: number, dy: number, dz: number = 0): Promise<void> {\n this.centerX -= dx\n this.centerY -= dy\n this.centerZ -= dz\n this.clampCenter()\n this.updateAffine()\n await this.updateVolume()\n }\n\n async panTo(newCenterX: number, newCenterY: number, newCenterZ?: number): Promise<void> {\n const prevX = this.centerX\n const prevY = this.centerY\n const prevZ = this.centerZ\n\n this.centerX = newCenterX\n this.centerY = newCenterY\n if (newCenterZ !== undefined) {\n this.centerZ = newCenterZ\n }\n this.clampCenter()\n\n // Skip if clamped position is same as before (prevents infinite loop at boundaries)\n if (Math.abs(this.centerX - prevX) < 0.001 && Math.abs(this.centerY - prevY) < 0.001 && Math.abs(this.centerZ - prevZ) < 0.001) {\n return\n }\n\n this.updateAffine()\n await this.updateVolume()\n }\n\n async setPyramidLevel(level: number): Promise<void> {\n const newLevel = Math.max(0, Math.min(this.pyramidInfo.levels.length - 1, level))\n if (newLevel === this.pyramidLevel) {\n return\n }\n\n // Convert center from old level coords to new level coords\n const oldDims = this.levelDims\n this.pyramidLevel = newLevel\n this.updateLevelInfo()\n const newDims = this.levelDims\n\n this.centerX = (this.centerX / oldDims.width) * newDims.width\n this.centerY = (this.centerY / oldDims.height) * newDims.height\n this.centerZ = (this.centerZ / oldDims.depth) * newDims.depth\n this.clampCenter()\n this.updateAffine()\n\n await this.updateVolume()\n }\n\n getViewportState(): { centerX: number; centerY: number; centerZ: number; level: number } {\n return {\n centerX: this.centerX,\n centerY: this.centerY,\n centerZ: this.centerZ,\n level: this.pyramidLevel\n }\n }\n\n getPyramidInfo(): ZarrPyramidInfo {\n return this.pyramidInfo\n }\n\n getPyramidLevel(): number {\n return this.pyramidLevel\n }\n\n getLevelDims(): { width: number; height: number; depth: number } {\n return { ...this.levelDims }\n }\n\n getVolumeDims(): { width: number; height: number; depth: number } {\n return { ...this.volumeDims }\n }\n\n getWorldOffset(): [number, number, number] {\n return [...this.worldOffsetMM]\n }\n\n /**\n * Set the world-space offset so the full level's center maps to targetMM in world space.\n * Computes the native physical center of the zarr level, then sets worldOffsetMM\n * so that center aligns with targetMM. Also centers the viewport on the level center.\n */\n setWorldCenter(targetMM: [number, number, number]): void {\n const { scaleW, scaleH, scaleD } = this.getConvertedScales()\n const { transW, transH, transD } = this.getConvertedTranslations()\n const axisNames = this.pyramidInfo.axisMapping.spatialAxisNames\n\n // Native physical center per OME dim (width=last, height=2nd-to-last, depth=3rd-to-last)\n const physW = (this.levelDims.width / 2) * scaleW + transW\n const physH = (this.levelDims.height / 2) * scaleH + transH\n const physD = (this.levelDims.depth / 2) * scaleD + transD\n\n // Map OME dims to x/y/z rows using axis names\n const nativeCenter: [number, number, number] = [0, 0, 0]\n const dims = [\n { name: axisNames[axisNames.length - 1], phys: physW },\n { name: axisNames[axisNames.length - 2], phys: physH },\n { name: axisNames.length >= 3 ? axisNames[axisNames.length - 3] : 'z', phys: physD }\n ]\n for (const d of dims) {\n const row = d.name === 'x' ? 0 : d.name === 'y' ? 1 : 2\n nativeCenter[row] = d.phys\n }\n\n this.worldOffsetMM = [targetMM[0] - nativeCenter[0], targetMM[1] - nativeCenter[1], targetMM[2] - nativeCenter[2]]\n\n // Center viewport on level center so visible region is at the target\n this.centerX = this.levelDims.width / 2\n this.centerY = this.levelDims.height / 2\n this.centerZ = this.levelDims.depth / 2\n this.clampCenter()\n\n this.updateAffine()\n }\n\n /**\n * Convert physical (mm) coordinates back to real zarr level pixel coordinates.\n * Inverts the affine: levelPixel = (mm - OME_translation) / scale\n */\n mmToLevelCoords(\n mmX: number,\n mmY: number,\n mmZ: number\n ): {\n width: number\n height: number\n depth: number\n level: number\n levelDims: { width: number; height: number; depth: number }\n } {\n // Subtract world offset to convert from offset world-space back to native zarr space\n mmX -= this.worldOffsetMM[0]\n mmY -= this.worldOffsetMM[1]\n mmZ -= this.worldOffsetMM[2]\n\n const { scaleW, scaleH, scaleD } = this.getConvertedScales()\n const axisNames = this.pyramidInfo.axisMapping.spatialAxisNames\n\n // Map physical mm (x/y/z) to OME spatial dimensions (width/height/depth)\n // The affine maps NIfTI cols to physical rows by axis name:\n // col 0 (width) → axis[-1], col 1 (height) → axis[-2], col 2 (depth) → axis[-3]\n // We need the reverse: given physical x/y/z, find which OME dim it maps to\n const mmByAxis: Record<string, number> = { x: mmX, y: mmY, z: mmZ }\n\n let levelW = 0\n let levelH = 0\n let levelD = 0\n\n if (axisNames.length >= 2) {\n // Width maps to last spatial axis name\n const wAxisName = axisNames[axisNames.length - 1]\n const hAxisName = axisNames[axisNames.length - 2]\n const dAxisName = axisNames.length >= 3 ? axisNames[axisNames.length - 3] : 'z'\n\n const mmW = mmByAxis[wAxisName] ?? mmX\n const mmH = mmByAxis[hAxisName] ?? mmY\n const mmD = mmByAxis[dAxisName] ?? mmZ\n\n if (this.hasTranslations) {\n const { transW, transH, transD } = this.getConvertedTranslations()\n levelW = scaleW !== 0 ? (mmW - transW) / scaleW : 0\n levelH = scaleH !== 0 ? (mmH - transH) / scaleH : 0\n levelD = scaleD !== 0 ? (mmD - transD) / scaleD : 0\n } else {\n levelW = scaleW !== 0 ? mmW / scaleW : 0\n levelH = scaleH !== 0 ? mmH / scaleH : 0\n levelD = scaleD !== 0 ? mmD / scaleD : 0\n }\n } else {\n // Fallback: simple diagonal\n if (this.hasTranslations) {\n const { transW, transH, transD } = this.getConvertedTranslations()\n levelW = scaleW !== 0 ? (mmX - transW) / scaleW : 0\n levelH = scaleH !== 0 ? (mmY - transH) / scaleH : 0\n levelD = scaleD !== 0 ? (mmZ - transD) / scaleD : 0\n } else {\n levelW = scaleW !== 0 ? mmX / scaleW : 0\n levelH = scaleH !== 0 ? mmY / scaleH : 0\n levelD = scaleD !== 0 ? mmZ / scaleD : 0\n }\n }\n\n return {\n width: Math.round(levelW),\n height: Math.round(levelH),\n depth: Math.round(levelD),\n level: this.pyramidLevel,\n levelDims: { ...this.levelDims }\n }\n }\n\n private clampCenter(): void {\n const halfW = this.volumeDims.width / 2\n const halfH = this.volumeDims.height / 2\n const halfD = this.volumeDims.depth / 2\n\n this.centerX = Math.max(-halfW, Math.min(this.levelDims.width + halfW, this.centerX))\n this.centerY = Math.max(-halfH, Math.min(this.levelDims.height + halfH, this.centerY))\n this.centerZ = Math.max(-halfD, Math.min(this.levelDims.depth + halfD, this.centerZ))\n }\n\n private getVisibleChunks(): ChunkCoord[] {\n const MAX_CHUNKS = 1000\n const { width, height, depth } = this.volumeDims\n const level = this.pyramidLevel\n\n // Visible region in level coords\n const minX = Math.max(0, Math.floor(this.centerX - width / 2))\n const maxX = Math.min(this.levelDims.width, Math.ceil(this.centerX + width / 2))\n const minY = Math.max(0, Math.floor(this.centerY - height / 2))\n const maxY = Math.min(this.levelDims.height, Math.ceil(this.centerY + height / 2))\n const minZ = Math.max(0, Math.floor(this.centerZ - depth / 2))\n const maxZ = Math.min(this.levelDims.depth, Math.ceil(this.centerZ + depth / 2))\n\n const startCX = Math.max(0, Math.floor(minX / this.chunkSize.width))\n const startCY = Math.max(0, Math.floor(minY / this.chunkSize.height))\n const startCZ = Math.max(0, Math.floor(minZ / this.chunkSize.depth))\n const endCX = Math.min(Math.ceil(this.levelDims.width / this.chunkSize.width), Math.ceil(maxX / this.chunkSize.width))\n const endCY = Math.min(Math.ceil(this.levelDims.height / this.chunkSize.height), Math.ceil(maxY / this.chunkSize.height))\n const endCZ = Math.min(Math.ceil(this.levelDims.depth / this.chunkSize.depth), Math.ceil(maxZ / this.chunkSize.depth))\n\n const chunks: ChunkCoord[] = []\n for (let z = startCZ; z < endCZ; z++) {\n for (let y = startCY; y < endCY; y++) {\n for (let x = startCX; x < endCX; x++) {\n if (this.pyramidInfo.is3D) {\n chunks.push({ level, x, y, z })\n } else {\n chunks.push({ level, x, y })\n }\n if (chunks.length >= MAX_CHUNKS) {\n break\n }\n }\n if (chunks.length >= MAX_CHUNKS) {\n break\n }\n }\n if (chunks.length >= MAX_CHUNKS) {\n break\n }\n }\n\n // Sort by distance from center chunk (closest first)\n const centerCX = (startCX + endCX) / 2\n const centerCY = (startCY + endCY) / 2\n const centerCZ = (startCZ + endCZ) / 2\n chunks.sort((a, b) => {\n const distA = (a.x - centerCX) ** 2 + (a.y - centerCY) ** 2 + ((a.z ?? 0) - centerCZ) ** 2\n const distB = (b.x - centerCX) ** 2 + (b.y - centerCY) ** 2 + ((b.z ?? 0) - centerCZ) ** 2\n return distA - distB\n })\n\n return chunks\n }\n\n private async updateVolume(): Promise<void> {\n if (this.isUpdating) {\n this.needsUpdate = true\n this.currentAbortController?.abort()\n return\n }\n\n this.isUpdating = true\n try {\n do {\n this.needsUpdate = false\n this.currentAbortController?.abort()\n const abortController = new AbortController()\n this.currentAbortController = abortController\n\n await new Promise<void>((resolve) => {\n requestAnimationFrame(() => resolve())\n })\n\n if (this.needsUpdate) {\n continue\n }\n\n this.clearVolumeData()\n await this.assembleVisibleChunks(abortController.signal)\n } while (this.needsUpdate)\n } finally {\n this.isUpdating = false\n }\n }\n\n private clearVolumeData(): void {\n const img = this.hostImage.img as TypedArray\n if (img.fill) {\n img.fill(0)\n } else {\n for (let i = 0; i < img.length; i++) {\n img[i] = 0\n }\n }\n\n // Reset chunk counters for the new update cycle\n this.pendingChunkCount = 0\n this.lastRenderedChunkCount = 0\n\n // Clear any pending debounce timer\n if (this.updateDebounceTimer !== null) {\n clearTimeout(this.updateDebounceTimer)\n this.updateDebounceTimer = null\n }\n }\n\n private async assembleVisibleChunks(signal?: AbortSignal): Promise<void> {\n const chunks = this.getVisibleChunks()\n const name = this.pyramidInfo.name\n const level = this.pyramidLevel\n\n const cachedChunks: ChunkCoord[] = []\n const uncachedChunks: ChunkCoord[] = []\n\n for (const chunk of chunks) {\n const key = ZarrChunkCache.getKey(name, chunk.level, chunk.x, chunk.y, chunk.z)\n if (this.chunkCache.has(key)) {\n cachedChunks.push(chunk)\n } else if (!this.chunkCache.isLoading(key)) {\n uncachedChunks.push(chunk)\n }\n }\n\n // Phase 1: assemble cached chunks synchronously\n for (const chunk of cachedChunks) {\n const key = ZarrChunkCache.getKey(name, chunk.level, chunk.x, chunk.y, chunk.z)\n const data = this.chunkCache.get(key)\n if (data) {\n this.assembleChunkIntoVolume(chunk, data)\n this.pendingChunkCount++\n }\n }\n\n if (cachedChunks.length > 0) {\n this.updateCalibration()\n this.onChunksUpdated?.()\n }\n\n // Phase 2: fetch uncached chunks with progressive rendering\n if (uncachedChunks.length > 0) {\n const fetchPromises = uncachedChunks.map(async (chunk) => {\n const key = ZarrChunkCache.getKey(name, chunk.level, chunk.x, chunk.y, chunk.z)\n this.chunkCache.startLoading(key)\n\n try {\n const data = await this.chunkClient.fetchChunk(chunk.level, chunk.x, chunk.y, chunk.z, this.nonSpatialCoords, signal)\n this.chunkCache.doneLoading(key)\n\n if (signal?.aborted) {\n return\n }\n\n if (data) {\n this.chunkCache.set(key, data)\n if (this.pyramidLevel === level) {\n this.assembleChunkIntoVolume(chunk, data)\n this.pendingChunkCount++\n // Use debounced callback to batch multiple chunk updates\n this.scheduleChunksUpdated()\n }\n }\n } catch (err: unknown) {\n this.chunkCache.doneLoading(key)\n if (err instanceof DOMException && err.name === 'AbortError') {\n return\n }\n console.warn(`Failed to fetch chunk ${key}:`, err)\n }\n })\n\n await Promise.all(fetchPromises)\n\n // After all chunks are fetched, clear any pending debounce and trigger final update\n if (this.updateDebounceTimer !== null) {\n clearTimeout(this.updateDebounceTimer)\n this.updateDebounceTimer = null\n }\n // Ensure final calibration and update if any chunks were loaded\n if (this.pendingChunkCount > this.lastRenderedChunkCount) {\n this.lastRenderedChunkCount = this.pendingChunkCount\n this.updateCalibration()\n this.onChunksUpdated?.()\n }\n this.onAllChunksLoaded?.()\n }\n }\n\n private assembleChunkIntoVolume(chunk: ChunkCoord, data: TypedArray): void {\n const { width, height, depth } = this.volumeDims\n const img = this.hostImage.img as TypedArray\n\n // Chunk pixel position in level coords\n const chunkStartX = chunk.x * this.chunkSize.width\n const chunkStartY = chunk.y * this.chunkSize.height\n const chunkStartZ = (chunk.z ?? 0) * this.chunkSize.depth\n\n // Volume starts at center - volumeDims/2\n const volStartX = this.centerX - width / 2\n const volStartY = this.centerY - height / 2\n const volStartZ = this.centerZ - depth / 2\n\n // Destination in volume texture\n const destX = Math.round(chunkStartX - volStartX)\n const destY = Math.round(chunkStartY - volStartY)\n const destZ = Math.round(chunkStartZ - volStartZ)\n\n // Actual chunk size (may be smaller at edges)\n const actualChunkW = Math.min(this.chunkSize.width, this.levelDims.width - chunkStartX)\n const actualChunkH = Math.min(this.chunkSize.height, this.levelDims.height - chunkStartY)\n const actualChunkD = Math.min(this.chunkSize.depth, this.levelDims.depth - chunkStartZ)\n\n if (actualChunkW <= 0 || actualChunkH <= 0 || actualChunkD <= 0) {\n return\n }\n\n // Track min/max for initial calibration (skipped once calibration is done)\n if (!this.calibrationDone) {\n const step = Math.max(1, Math.floor(data.length / 2000))\n for (let i = 0; i < data.length; i += step) {\n const val = data[i]\n if (val < this.runningMin) {\n this.runningMin = val\n }\n if (val > this.runningMax) {\n this.runningMax = val\n }\n }\n }\n\n // Copy chunk data into volume (no scaling - 1:1 pixel mapping).\n // Since spatial indices are kept in OME metadata order (not reordered),\n // the zarr C-order layout naturally aligns with NIfTI Fortran-order:\n // - dz iterates depth = OME dim[0] (slowest in both C-order and NIfTI)\n // - dy iterates height = OME dim[1]\n // - dx iterates width = OME dim[2] (fastest in both C-order and NIfTI)\n // srcIdx uses C-order strides based on the actual chunk data dimensions.\n // Edge chunks may be padded (full chunk size) or truncated.\n const fullChunkVol = this.chunkSize.width * this.chunkSize.height * this.chunkSize.depth\n const srcChunkW = data.length >= fullChunkVol ? this.chunkSize.width : actualChunkW\n const srcChunkH = data.length >= fullChunkVol ? this.chunkSize.height : actualChunkH\n for (let dz = 0; dz < actualChunkD; dz++) {\n const vZ = destZ + dz\n if (vZ < 0 || vZ >= depth) {\n continue\n }\n for (let dy = 0; dy < actualChunkH; dy++) {\n const vY = destY + dy\n if (vY < 0 || vY >= height) {\n continue\n }\n for (let dx = 0; dx < actualChunkW; dx++) {\n const vX = destX + dx\n if (vX < 0 || vX >= width) {\n continue\n }\n const srcIdx = dz * srcChunkH * srcChunkW + dy * srcChunkW + dx\n const dstIdx = vX + vY * width + vZ * width * height\n if (srcIdx >= 0 && srcIdx < data.length && dstIdx >= 0 && dstIdx < img.length) {\n img[dstIdx] = data[srcIdx]\n }\n }\n }\n }\n }\n\n private updateCalibration(): void {\n if (this.calibrationDone) {\n return\n }\n if (this.runningMin < Infinity && this.runningMax > -Infinity) {\n this.hostImage.cal_min = this.runningMin\n this.hostImage.cal_max = this.runningMax\n this.hostImage.robust_min = this.runningMin\n this.hostImage.robust_max = this.runningMax\n this.hostImage.global_min = this.runningMin\n this.hostImage.global_max = this.runningMax\n this.calibrationDone = true\n }\n }\n\n /**\n * Schedule a debounced chunks update callback.\n * Batches multiple chunk arrivals within UPDATE_DEBOUNCE_MS into a single GPU update.\n */\n private scheduleChunksUpdated(): void {\n // Skip if no new chunks since last render\n if (this.pendingChunkCount === this.lastRenderedChunkCount) {\n return\n }\n\n // Clear existing timer and schedule new one\n if (this.updateDebounceTimer !== null) {\n clearTimeout(this.updateDebounceTimer)\n }\n\n this.updateDebounceTimer = setTimeout(() => {\n this.updateDebounceTimer = null\n this.lastRenderedChunkCount = this.pendingChunkCount\n this.updateCalibration()\n this.onChunksUpdated?.()\n }, this.UPDATE_DEBOUNCE_MS)\n }\n\n clearCache(): void {\n this.chunkCache.clear()\n }\n\n async refresh(): Promise<void> {\n await this.updateVolume()\n }\n}\n","import { mat4 } from 'gl-matrix'\n\n/**\n * Represents an affine transformation in decomposed form.\n */\nexport interface AffineTransform {\n translation: [number, number, number] // X, Y, Z in mm\n rotation: [number, number, number] // Euler angles in degrees (X, Y, Z order)\n scale: [number, number, number] // Scale factors\n}\n\n/**\n * Identity transform with no translation, rotation, or scale change.\n */\nexport const identityTransform: AffineTransform = {\n translation: [0, 0, 0],\n rotation: [0, 0, 0],\n scale: [1, 1, 1]\n}\n\n/**\n * Convert degrees to radians.\n */\nexport function degToRad(degrees: number): number {\n return (degrees * Math.PI) / 180\n}\n\n/**\n * Create a rotation matrix from Euler angles (XYZ order).\n * Angles are in degrees.\n */\nexport function eulerToRotationMatrix(rx: number, ry: number, rz: number): mat4 {\n const out = mat4.create()\n\n const radX = degToRad(rx)\n const radY = degToRad(ry)\n const radZ = degToRad(rz)\n\n // Apply rotations in XYZ order\n mat4.rotateX(out, out, radX)\n mat4.rotateY(out, out, radY)\n mat4.rotateZ(out, out, radZ)\n\n return out\n}\n\n/**\n * Create a 4x4 transformation matrix from decomposed transform components.\n * Order: Scale -> Rotate -> Translate\n */\nexport function createTransformMatrix(transform: AffineTransform): mat4 {\n const out = mat4.create()\n\n // Translation\n mat4.translate(out, out, transform.translation)\n\n // Rotation (XYZ Euler angles)\n const rotation = eulerToRotationMatrix(transform.rotation[0], transform.rotation[1], transform.rotation[2])\n mat4.multiply(out, out, rotation)\n\n // Scale\n mat4.scale(out, out, transform.scale)\n\n return out\n}\n\n/**\n * Convert a 2D array (row-major, as used by NIfTI) to gl-matrix mat4 (column-major).\n */\nexport function arrayToMat4(arr: number[][]): mat4 {\n // NIfTI stores as row-major, gl-matrix uses column-major\n // arr[row][col] -> mat4 is column-major (index = col * 4 + row)\n return mat4.fromValues(\n arr[0][0],\n arr[1][0],\n arr[2][0],\n arr[3][0], // column 0\n arr[0][1],\n arr[1][1],\n arr[2][1],\n arr[3][1], // column 1\n arr[0][2],\n arr[1][2],\n arr[2][2],\n arr[3][2], // column 2\n arr[0][3],\n arr[1][3],\n arr[2][3],\n arr[3][3] // column 3\n )\n}\n\n/**\n * Convert gl-matrix mat4 (column-major) to 2D array (row-major, as used by NIfTI).\n */\nexport function mat4ToArray(m: mat4): number[][] {\n // mat4 is column-major: index = col * 4 + row\n // We need row-major: arr[row][col]\n return [\n [m[0], m[4], m[8], m[12]], // row 0\n [m[1], m[5], m[9], m[13]], // row 1\n [m[2], m[6], m[10], m[14]], // row 2\n [m[3], m[7], m[11], m[15]] // row 3\n ]\n}\n\n/**\n * Multiply a transformation matrix by an affine matrix (as 2D array).\n * Returns the result as a 2D array.\n *\n * The transform is applied to the left: result = transform * original\n * This means the transform happens in world coordinate space.\n */\nexport function multiplyAffine(original: number[][], transform: mat4): number[][] {\n const originalMat = arrayToMat4(original)\n const result = mat4.create()\n mat4.multiply(result, transform, originalMat)\n return mat4ToArray(result)\n}\n\n/**\n * Deep copy a 2D affine matrix array.\n */\nexport function copyAffine(affine: number[][]): number[][] {\n return affine.map((row) => [...row])\n}\n\n/**\n * Check if two transforms are approximately equal.\n */\nexport function transformsEqual(a: AffineTransform, b: AffineTransform, epsilon = 0.0001): boolean {\n for (let i = 0; i < 3; i++) {\n if (Math.abs(a.translation[i] - b.translation[i]) > epsilon) {\n return false\n }\n if (Math.abs(a.rotation[i] - b.rotation[i]) > epsilon) {\n return false\n }\n if (Math.abs(a.scale[i] - b.scale[i]) > epsilon) {\n return false\n }\n }\n return true\n}\n","/**\n * ZarrChunkClient - HTTP client for fetching zarr array data using zarrita.js.\n *\n * Handles pyramid discovery and chunk fetching for OME-ZARR and regular zarr stores.\n */\n\nimport * as zarr from 'zarrita'\nimport type { TypedArray } from './ZarrChunkCache.js'\n\nexport interface ZarrChunkClientConfig {\n /** Base URL for zarr store (e.g., \"http://localhost:8090/lightsheet.zarr\") */\n baseUrl: string\n}\n\nexport interface ZarrPyramidLevel {\n /** Level index (0 = highest resolution) */\n index: number\n /** Path to this level in the zarr hierarchy (e.g., \"/0\", \"/1\") */\n path: string\n /** Spatial-only shape in OME metadata order (non-spatial dims stripped) */\n shape: number[]\n /** Spatial-only chunk dimensions matching shape order */\n chunks: number[]\n /** Data type (e.g., \"uint8\", \"uint16\", \"float32\") */\n dtype: string\n /** Physical scale factors per spatial axis in OME metadata order from coordinateTransformations */\n scales?: number[]\n /** Physical translation offsets per spatial axis in OME metadata order from coordinateTransformations */\n translations?: number[]\n}\n\n/**\n * Mapping from spatial chunk coordinates to full zarr array chunk coordinates.\n * Handles non-spatial dimensions like channel (c) and time (t).\n */\nexport interface AxisMapping {\n /** Total number of dimensions in the original zarr array */\n originalNdim: number\n /** Indices of spatial axes in the original array, in OME metadata order */\n spatialIndices: number[]\n /** Names of spatial axes in OME metadata order (e.g., ['x', 'y', 'z'] or ['z', 'y', 'x']) */\n spatialAxisNames: string[]\n /** Non-spatial axes: their index in the original array, chunk size, and default chunk coord */\n nonSpatialAxes: Array<{ index: number; name: string; chunkSize: number; defaultChunkCoord: number }>\n}\n\nexport interface ZarrPyramidInfo {\n /** Name/URL of the zarr store */\n name: string\n /** Pyramid levels (index 0 = highest resolution) */\n levels: ZarrPyramidLevel[]\n /** Whether this is a 3D dataset (based on spatial dimensions) */\n is3D: boolean\n /** Number of spatial dimensions (2 or 3) */\n ndim: number\n /** Mapping from spatial to full array coordinates */\n axisMapping: AxisMapping\n /** Units for spatial axes in OME metadata order (e.g., \"micrometer\", \"millimeter\") */\n spatialUnits?: string[]\n}\n\nexport interface ChunkCoord {\n /** Pyramid level */\n level: number\n /** Chunk X index */\n x: number\n /** Chunk Y index */\n y: number\n /** Chunk Z index (for 3D) */\n z?: number\n}\n\n// Type for zarr Array (simplified for our use)\ntype ZarrArray = zarr.Array<zarr.DataType, zarr.Readable>\n\n// Interface for OME multiscales metadata\ninterface OmeMultiscalesDataset {\n path: string\n coordinateTransformations?: Array<{\n type: string\n scale?: number[]\n translation?: number[]\n }>\n}\n\ninterface OmeMultiscales {\n axes?: Array<{ name: string; type: string; unit?: string }>\n datasets: OmeMultiscalesDataset[]\n name?: string\n}\n\nexport class ZarrChunkClient {\n private store: zarr.FetchStore\n private baseUrl: string\n private arrays: Map<number, ZarrArray> = new Map()\n /** Maps level index to actual path in the zarr store */\n private levelPaths: Map<number, string> = new Map()\n /** Axis mapping for coordinate translation */\n private axisMapping: AxisMapping | null = null\n\n constructor(config: ZarrChunkClientConfig) {\n this.baseUrl = config.baseUrl\n this.store = new zarr.FetchStore(config.baseUrl)\n }\n\n /**\n * Discover pyramid structure by reading OME-ZARR multiscales metadata,\n * or falling back to probing for arrays at /0, /1, /2, etc.\n */\n async fetchInfo(): Promise<ZarrPyramidInfo> {\n const root = zarr.root(this.store)\n\n // Raw levels before axis stripping (with full original shapes)\n const rawLevels: Array<{ index: number; path: string; shape: number[]; chunks: number[]; dtype: string }> = []\n let omeAxes: Array<{ name: string; type: string; unit?: string }> | null = null\n let omeMultiscales: OmeMultiscales | null = null\n\n // Try to open as a single array first (non-pyramidal case)\n try {\n const arr = await zarr.open(root, { kind: 'array' })\n rawLevels.push({\n index: 0,\n path: '/',\n shape: [...arr.shape],\n chunks: [...arr.chunks],\n dtype: arr.dtype\n })\n this.arrays.set(0, arr)\n this.levelPaths.set(0, '/')\n } catch {\n // Not a single array, try to open as a group and read OME metadata\n\n try {\n const group = await zarr.open(root, { kind: 'group' })\n // Look for OME multiscales metadata in attributes\n // Can be at .ome.multiscales (v0.5) or .multiscales (older)\n const attrs = group.attrs as Record<string, unknown>\n if (attrs.ome && typeof attrs.ome === 'object') {\n const ome = attrs.ome as { multiscales?: OmeMultiscales[] }\n if (ome.multiscales && Array.isArray(ome.multiscales) && ome.multiscales.length > 0) {\n omeMultiscales = ome.multiscales[0]\n }\n } else if (attrs.multiscales && Array.isArray(attrs.multiscales) && attrs.multiscales.length > 0) {\n omeMultiscales = attrs.multiscales[0] as OmeMultiscales\n }\n } catch {\n // Could not open as group\n }\n\n // Extract OME axes metadata if available\n if (omeMultiscales?.axes && Array.isArray(omeMultiscales.axes)) {\n omeAxes = omeMultiscales.axes\n }\n\n if (omeMultiscales && omeMultiscales.datasets && omeMultiscales.datasets.length > 0) {\n // Use OME multiscales paths\n for (let i = 0; i < omeMultiscales.datasets.length; i++) {\n const dataset = omeMultiscales.datasets[i]\n const path = dataset.path.startsWith('/') ? dataset.path : `/${dataset.path}`\n try {\n const loc = root.resolve(path)\n const arr = await zarr.open(loc, { kind: 'array' })\n rawLevels.push({\n index: i,\n path,\n shape: [...arr.shape],\n chunks: [...arr.chunks],\n dtype: arr.dtype\n })\n this.arrays.set(i, arr)\n this.levelPaths.set(i, path)\n } catch (err) {\n console.warn(`Failed to open array at path ${path}:`, err)\n }\n }\n } else {\n // No OME metadata, probe for arrays at /0, /1, /2, ... until we get an error\n for (let i = 0; i < 20; i++) {\n const path = `/${i}`\n try {\n const loc = root.resolve(path)\n const arr = await zarr.open(loc, { kind: 'array' })\n rawLevels.push({\n index: i,\n path,\n shape: [...arr.shape],\n chunks: [...arr.chunks],\n dtype: arr.dtype\n })\n this.arrays.set(i, arr)\n this.levelPaths.set(i, path)\n } catch {\n // No more levels\n break\n }\n }\n }\n }\n\n if (rawLevels.length === 0) {\n throw new Error(`No zarr arrays found at ${this.baseUrl}`)\n }\n\n // Build axis mapping: identify spatial vs non-spatial dimensions\n const originalNdim = rawLevels[0].shape.length\n const axisMapping = this.buildAxisMapping(originalNdim, rawLevels[0].chunks, omeAxes)\n this.axisMapping = axisMapping\n\n // Extract spatial units from OME axes metadata (in spatial index order: [Z, Y, X] or [Y, X])\n let spatialUnits: string[] | undefined\n if (omeAxes) {\n spatialUnits = axisMapping.spatialIndices.map((i) => omeAxes[i]?.unit ?? '')\n }\n\n // Strip non-spatial dimensions from shape and chunks, and extract per-level scales\n const levels: ZarrPyramidLevel[] = rawLevels.map((raw) => {\n const level: ZarrPyramidLevel = {\n index: raw.index,\n path: raw.path,\n shape: axisMapping.spatialIndices.map((i) => raw.shape[i]),\n chunks: axisMapping.spatialIndices.map((i) => raw.chunks[i]),\n dtype: raw.dtype\n }\n\n // Extract spatial scale factors and translations from OME coordinateTransformations\n if (omeMultiscales?.datasets?.[raw.index]?.coordinateTransformations) {\n const transforms = omeMultiscales.datasets[raw.index].coordinateTransformations!\n const scaleTransform = transforms.find((t) => t.type === 'scale' && t.scale)\n if (scaleTransform?.scale) {\n // Extract scales for spatial axes only, in the reordered [Z, Y, X] order\n level.scales = axisMapping.spatialIndices.map((i) => scaleTransform.scale![i])\n }\n const translationTransform = transforms.find((t) => t.type === 'translation' && t.translation)\n if (translationTransform?.translation) {\n // Extract translations for spatial axes only, in the reordered [Z, Y, X] order\n level.translations = axisMapping.spatialIndices.map((i) => translationTransform.translation![i])\n }\n }\n\n return level\n })\n\n // Determine dimensionality from spatial axes\n const spatialNdim = axisMapping.spatialIndices.length\n const is3D = spatialNdim >= 3\n\n console.log(\n `Zarr axis mapping: original ndim=${originalNdim}, spatial ndim=${spatialNdim}, ` +\n `spatial indices=${JSON.stringify(axisMapping.spatialIndices)}, ` +\n `non-spatial=${JSON.stringify(axisMapping.nonSpatialAxes.map((a) => a.name))}`\n )\n\n return {\n name: this.baseUrl,\n levels,\n is3D,\n ndim: spatialNdim,\n axisMapping,\n spatialUnits\n }\n }\n\n /**\n * Build axis mapping from OME axes metadata or infer from array dimensions.\n * Identifies spatial (x, y, z) vs non-spatial (c, t) dimensions and returns\n * indices for extracting spatial-only shape/chunks.\n * Spatial indices are kept in the original OME metadata order (NOT reordered).\n */\n private buildAxisMapping(originalNdim: number, originalChunks: number[], omeAxes: Array<{ name: string; type: string; unit?: string }> | null): AxisMapping {\n const spatialIndices: number[] = []\n const spatialAxisNames: string[] = []\n const nonSpatialAxes: AxisMapping['nonSpatialAxes'] = []\n\n if (omeAxes && omeAxes.length === originalNdim) {\n // Use OME axes metadata to identify spatial vs non-spatial\n // Keep spatial axes in their original metadata order\n for (let i = 0; i < omeAxes.length; i++) {\n const axis = omeAxes[i]\n if (axis.type === 'space') {\n spatialIndices.push(i)\n spatialAxisNames.push(axis.name.toLowerCase())\n } else {\n nonSpatialAxes.push({\n index: i,\n name: axis.name,\n chunkSize: originalChunks[i],\n defaultChunkCoord: 0\n })\n }\n }\n } else {\n // No OME axes metadata — infer from ndim.\n // OME convention: leading dims are non-spatial (t, c), trailing are spatial (z, y, x)\n if (omeAxes === null) {\n console.warn('No OME axes metadata found — inferring axis layout from array dimensions')\n }\n if (originalNdim <= 3) {\n // 2D or 3D — all spatial, assume z/y/x names for trailing dims\n const defaultNames3D = ['z', 'y', 'x']\n const defaultNames2D = ['y', 'x']\n const names = originalNdim === 2 ? defaultNames2D : defaultNames3D.slice(3 - originalNdim)\n for (let i = 0; i < originalNdim; i++) {\n spatialIndices.push(i)\n spatialAxisNames.push(names[i])\n }\n } else if (originalNdim === 4) {\n // Assume [C, Z, Y, X]\n nonSpatialAxes.push({ index: 0, name: 'c', chunkSize: originalChunks[0], defaultChunkCoord: 0 })\n spatialIndices.push(1, 2, 3)\n spatialAxisNames.push('z', 'y', 'x')\n } else if (originalNdim === 5) {\n // Assume [T, C, Z, Y, X]\n nonSpatialAxes.push({ index: 0, name: 't', chunkSize: originalChunks[0], defaultChunkCoord: 0 })\n nonSpatialAxes.push({ index: 1, name: 'c', chunkSize: originalChunks[1], defaultChunkCoord: 0 })\n spatialIndices.push(2, 3, 4)\n spatialAxisNames.push('z', 'y', 'x')\n } else {\n // Unknown layout — treat last 3 as spatial, rest as non-spatial\n for (let i = 0; i < originalNdim - 3; i++) {\n nonSpatialAxes.push({ index: i, name: `dim${i}`, chunkSize: originalChunks[i], defaultChunkCoord: 0 })\n }\n for (let i = originalNdim - 3; i < originalNdim; i++) {\n spatialIndices.push(i)\n spatialAxisNames.push(['z', 'y', 'x'][i - (originalNdim - 3)])\n }\n }\n }\n\n return { originalNdim, spatialIndices, spatialAxisNames, nonSpatialAxes }\n }\n\n /**\n * Open a zarr array at a specific pyramid level.\n * Uses cached arrays when available.\n */\n private async openLevel(level: number): Promise<ZarrArray> {\n if (this.arrays.has(level)) {\n return this.arrays.get(level)!\n }\n\n const root = zarr.root(this.store)\n // Use stored path from fetchInfo, or fall back to numbered path\n const path = this.levelPaths.get(level) ?? (level === 0 ? '/' : `/${level}`)\n\n try {\n const loc = root.resolve(path)\n const arr = await zarr.open(loc, { kind: 'array' })\n this.arrays.set(level, arr)\n this.levelPaths.set(level, path)\n return arr\n } catch {\n // Fallback: try root for level 0\n if (level === 0) {\n const arr = await zarr.open(root, { kind: 'array' })\n this.arrays.set(level, arr)\n this.levelPaths.set(level, '/')\n return arr\n }\n throw new Error(`Cannot open zarr array at level ${level}`)\n }\n }\n\n /**\n * Fetch a single chunk by spatial coordinates.\n * Uses the axis mapping to build full chunk coordinates including non-spatial dims.\n * Returns the spatial-only decoded TypedArray data.\n *\n * @param level - Pyramid level\n * @param x - Spatial X chunk index\n * @param y - Spatial Y chunk index\n * @param z - Spatial Z chunk index (for 3D)\n * @param nonSpatialCoords - Optional overrides for non-spatial dimensions (e.g., channel index)\n */\n async fetchChunk(level: number, x: number, y: number, z?: number, nonSpatialCoords?: Record<string, number>, signal?: AbortSignal): Promise<TypedArray | null> {\n try {\n const arr = await this.openLevel(level)\n const mapping = this.axisMapping\n\n let chunkCoords: number[]\n\n if (mapping && mapping.originalNdim > mapping.spatialIndices.length) {\n // Build full chunk coordinates using axis mapping\n chunkCoords = new Array(mapping.originalNdim).fill(0)\n\n // Set non-spatial axes to default values (or overrides)\n for (const nsa of mapping.nonSpatialAxes) {\n chunkCoords[nsa.index] = nonSpatialCoords?.[nsa.name] ?? nsa.defaultChunkCoord\n }\n\n // Set spatial axes — spatialIndices are in [Z, Y, X] or [Y, X] order\n const spatialCoords = z !== undefined && mapping.spatialIndices.length >= 3 ? [z, y, x] : [y, x]\n\n for (let i = 0; i < spatialCoords.length && i < mapping.spatialIndices.length; i++) {\n chunkCoords[mapping.spatialIndices[i]] = spatialCoords[i]\n }\n } else {\n // No non-spatial dims — use spatial coords directly\n if (z !== undefined && arr.shape.length >= 3) {\n chunkCoords = [z, y, x]\n } else {\n chunkCoords = [y, x]\n }\n }\n\n const chunk = await arr.getChunk(chunkCoords, { signal })\n let data = chunk.data as TypedArray\n\n // If there are non-spatial dimensions, extract the spatial-only slice.\n // For non-spatial chunk sizes of 1 (common case), data starts at offset 0\n // and has exactly spatialSize elements — no slicing needed.\n if (mapping && mapping.nonSpatialAxes.length > 0) {\n const spatialSize = mapping.spatialIndices.reduce((acc, idx) => acc * arr.chunks[idx], 1)\n if (data.length > spatialSize) {\n // Compute offset for the requested non-spatial chunk coords\n // Data is in row-major order: leading dims have largest strides\n let offset = 0\n let stride = data.length\n for (let d = 0; d < mapping.originalNdim; d++) {\n stride = Math.floor(stride / arr.chunks[d])\n const isSpatial = mapping.spatialIndices.includes(d)\n if (!isSpatial) {\n // Use 0 within the chunk (we fetched the right chunk coord already)\n // This handles the case where non-spatial chunk size > 1\n offset += 0 * stride\n }\n }\n data = data.subarray(offset, offset + spatialSize) as TypedArray\n }\n }\n\n return data\n } catch (err) {\n console.warn(`Failed to fetch chunk at level ${level}, x=${x}, y=${y}, z=${z}:`, err)\n return null\n }\n }\n\n /**\n * Fetch multiple chunks in parallel.\n * Returns a Map from chunk key to TypedArray.\n */\n async fetchChunks(name: string, level: number, coords: ChunkCoord[]): Promise<Map<string, TypedArray>> {\n const results = new Map<string, TypedArray>()\n\n const promises = coords.map(async (coord) => {\n const data = await this.fetchChunk(level, coord.x, coord.y, coord.z)\n if (data) {\n const key = coord.z !== undefined ? `${name}:${level}/${coord.x}/${coord.y}/${coord.z}` : `${name}:${level}/${coord.x}/${coord.y}`\n results.set(key, data)\n }\n })\n\n await Promise.all(promises)\n return results\n }\n\n /**\n * Fetch a rectangular region using zarr.get with slices.\n * Useful for fetching exact viewport regions rather than whole chunks.\n * Uses axis mapping to handle non-spatial dimensions.\n */\n async fetchRegion(\n level: number,\n region: {\n xStart: number\n xEnd: number\n yStart: number\n yEnd: number\n zStart?: number\n zEnd?: number\n }\n ): Promise<{ data: TypedArray; shape: number[] } | null> {\n try {\n const arr = await this.openLevel(level)\n const mapping = this.axisMapping\n\n // Build slice selections for all dimensions\n const selections: Array<ReturnType<typeof zarr.slice> | number> = []\n\n if (mapping && mapping.originalNdim > mapping.spatialIndices.length) {\n // Build full selections with non-spatial dims fixed to 0\n for (let d = 0; d < mapping.originalNdim; d++) {\n const spatialIdx = mapping.spatialIndices.indexOf(d)\n if (spatialIdx === -1) {\n // Non-spatial dim — select index 0\n selections.push(0)\n } else if (mapping.spatialIndices.length >= 3) {\n // Spatial dim in [Z, Y, X] order\n if (spatialIdx === 0 && region.zStart !== undefined && region.zEnd !== undefined) {\n selections.push(zarr.slice(region.zStart, region.zEnd))\n } else if (spatialIdx === 1) {\n selections.push(zarr.slice(region.yStart, region.yEnd))\n } else if (spatialIdx === 2) {\n selections.push(zarr.slice(region.xStart, region.xEnd))\n } else {\n selections.push(zarr.slice(0, arr.shape[d]))\n }\n } else {\n // 2D spatial: [Y, X]\n if (spatialIdx === 0) {\n selections.push(zarr.slice(region.yStart, region.yEnd))\n } else {\n selections.push(zarr.slice(region.xStart, region.xEnd))\n }\n }\n }\n } else {\n // No non-spatial dims — use spatial coords directly\n if (region.zStart !== undefined && region.zEnd !== undefined && arr.shape.length >= 3) {\n selections.push(zarr.slice(region.zStart, region.zEnd))\n selections.push(zarr.slice(region.yStart, region.yEnd))\n selections.push(zarr.slice(region.xStart, region.xEnd))\n } else {\n selections.push(zarr.slice(region.yStart, region.yEnd))\n selections.push(zarr.slice(region.xStart, region.xEnd))\n }\n }\n\n const result = await zarr.get(arr, selections)\n return {\n data: result.data as TypedArray,\n shape: result.shape\n }\n } catch (err) {\n console.warn(`Failed to fetch region at level ${level}:`, err)\n return null\n }\n }\n\n /**\n * Get the zarr store URL\n */\n getUrl(): string {\n return this.baseUrl\n }\n\n /**\n * Clear cached array references\n */\n clearArrayCache(): void {\n this.arrays.clear()\n this.levelPaths.clear()\n }\n}\n","import { vec3, vec4 } from 'gl-matrix'\nimport { v4 as uuidv4 } from '@lukeed/uuid'\nimport { log } from '@/logger'\nimport { NiivueObject3D } from '@/niivue-object3D'\nimport { ColorMap, LUT, cmapper, COLORMAP_TYPE } from '@/colortables'\nimport { NVMeshUtilities } from '@/nvmesh-utilities'\nimport { NVMeshLoaders } from '@/nvmesh-loaders'\nimport { NVLabel3D, LabelTextAlignment, LabelLineTerminator } from '@/nvlabel'\n\nimport { LegacyConnectome, LegacyNodes, NVConnectomeEdge, NVConnectomeNode, Point } from '@/types'\nimport { DefaultMeshType, GII, MZ3, TCK, TRACT, TRK, TT, TRX, VTK, ValuesArray, X3D, AnyNumberArray } from '@/nvmesh-types'\n\n/** Enum for text alignment\n */\nexport enum MeshType {\n MESH = 'mesh',\n CONNECTOME = 'connectome',\n FIBER = 'fiber'\n}\n\nexport type NVMeshLayer = {\n name?: string\n key?: string\n url?: string\n headers?: Record<string, string>\n opacity: number\n colormap: string\n colormapNegative?: string\n colormapInvert?: boolean\n colormapLabel?: ColorMap | LUT\n useNegativeCmap?: boolean\n global_min?: number\n global_max?: number\n cal_min: number\n cal_max: number\n cal_minNeg: number\n cal_maxNeg: number\n isAdditiveBlend?: boolean\n frame4D: number\n nFrame4D: number\n values: AnyNumberArray // number[] | Float32Array | Uint32Array\n outlineBorder?: number\n isTransparentBelowCalMin?: boolean\n colormapType?: number\n base64?: string\n // TODO referenced in niivue/refreshColormaps\n colorbarVisible?: boolean\n showLegend?: boolean\n labels?: NVLabel3D[]\n atlasValues?: AnyNumberArray\n}\n\nexport const NVMeshLayerDefaults = {\n colormap: 'gray',\n opacity: 0.0,\n nFrame4D: 0,\n frame4D: 0,\n outlineBorder: 0,\n cal_min: 0,\n cal_max: 0,\n cal_minNeg: 0,\n cal_maxNeg: 0,\n colormapType: COLORMAP_TYPE.MIN_TO_MAX,\n values: new Array<number>(),\n useNegativeCmap: false,\n showLegend: true\n}\n\nexport class NVMeshFromUrlOptions {\n url: string\n gl: WebGL2RenderingContext | null\n name: string\n opacity: number\n rgba255: Uint8Array\n visible: boolean\n layers: NVMeshLayer[]\n colorbarVisible: boolean\n meshShaderIndex: number\n\n constructor(url = '', gl = null, name = '', opacity = 1.0, rgba255 = new Uint8Array([255, 255, 255, 255]), visible = true, layers = [], colorbarVisible = true, meshShaderIndex = 0) {\n this.url = url\n this.gl = gl\n this.name = name\n this.opacity = opacity\n this.rgba255 = rgba255\n this.visible = visible\n this.layers = layers\n this.colorbarVisible = colorbarVisible\n this.meshShaderIndex = meshShaderIndex\n }\n}\n\n/**\n * Parameters for loading a base mesh or volume.\n */\ntype BaseLoadParams = {\n /** WebGL rendering context. */\n gl: WebGL2RenderingContext\n /** Name for this image. Default is an empty string. */\n name: string\n /** Opacity for this image. Default is 1. */\n opacity: number\n /** Base color of the mesh in RGBA [0-255]. Default is white. */\n rgba255: number[] | Uint8Array\n /** Whether this image is visible. */\n visible: boolean\n /** Layers of the mesh to load. */\n layers: NVMeshLayer[]\n /** Shader index for mesh rendering. Default is 0 (Phong). */\n meshShaderIndex: number\n}\n\nexport type LoadFromUrlParams = Partial<BaseLoadParams> & {\n // the resolvable URL pointing to a mesh to load\n url: string\n headers?: Record<string, string>\n buffer?: ArrayBuffer\n}\n\ntype LoadFromFileParams = BaseLoadParams & {\n // the file object\n file: Blob\n}\n\ntype LoadFromBase64Params = BaseLoadParams & {\n // the base64 encoded string\n base64: string\n}\n\n/**\n * a NVMesh encapsulates some mesh data and provides methods to query and operate on meshes\n */\nexport class NVMesh {\n id: string\n name: string\n anatomicalStructurePrimary: string\n colorbarVisible: boolean\n furthestVertexFromOrigin: number\n extentsMin: number | number[]\n extentsMax: number | number[]\n opacity: number\n visible: boolean\n meshShaderIndex = 0\n offsetPt0: Uint32Array | null = null\n\n colormapInvert = false\n fiberGroupColormap: ColorMap | null = null\n\n indexBuffer: WebGLBuffer\n vertexBuffer: WebGLBuffer\n vao: WebGLVertexArrayObject\n vaoFiber: WebGLVertexArrayObject\n\n pts: Float32Array\n tris?: Uint32Array\n layers: NVMeshLayer[]\n type = MeshType.MESH\n\n data_type?: string\n rgba255: Uint8Array\n fiberLength?: number\n fiberLengths?: Uint32Array\n fiberDensity?: Float32Array\n fiberDither = 0.1\n fiberColor = 'Global'\n fiberDecimationStride = 1 // e.g. if 2 the 50% of streamlines visible, if 3 then 1/3rd\n fiberSides = 5 // 1=streamline, 2=imposter, >2=mesh(cylinder with fiberSides sides)\n fiberRadius = 0 // in mm, e.g. 3 means 6mm diameter fibers, ignored if fiberSides < 3\n fiberOcclusion = 0 // value 0..1 to simulate ambient occlusion\n f32PerVertex = 5 // MUST be 5 or 7: number of float32s per vertex DEPRECATED, future releases will ALWAYS be 5\n dpsThreshold = NaN\n fiberMask?: unknown[]\n colormap?: ColorMap | LegacyConnectome | string | null\n dpg?: ValuesArray | null\n dps?: ValuesArray | null\n dpv?: ValuesArray | null\n groups?: ValuesArray | null\n hasConnectome = false\n connectome?: LegacyConnectome | string\n\n // TODO this should somehow get aligned with connectome\n indexCount?: number\n vertexCount = 1\n nodeScale = 4\n edgeScale = 1\n legendLineThickness = 0\n showLegend = true\n nodeColormap = 'warm'\n edgeColormap = 'warm'\n nodeColormapNegative?: string\n edgeColormapNegative?: string\n nodeMinColor?: number\n nodeMaxColor?: number\n edgeMin?: number\n edgeMax?: number\n\n nodes?: LegacyNodes | NVConnectomeNode[]\n\n edges?: number[] | NVConnectomeEdge[]\n\n points?: Point[]\n\n /**\n * @param pts - a 3xN array of vertex positions (X,Y,Z coordinates).\n * @param tris - a 3xN array of triangle indices (I,J,K; indexed from zero). Each triangle generated from three vertices.\n * @param name - a name for this image. Default is an empty string\n * @param rgba255 - the base color of the mesh. RGBA values from 0 to 255. Default is white\n * @param opacity - the opacity for this mesh. default is 1\n * @param visible - whether or not this image is to be visible\n * @param gl - WebGL rendering context\n * @param connectome - specify connectome edges and nodes. Default is null (not a connectome).\n * @param dpg - Data per group for tractography, see TRK format. Default is null (not tractograpgy)\n * @param dps - Data per streamline for tractography, see TRK format. Default is null (not tractograpgy)\n * @param dpv - Data per vertex for tractography, see TRK format. Default is null (not tractograpgy)\n * @param groups - Groups for tractography, see TRK format. Default is null (not tractograpgy)\n * @param colorbarVisible - does this mesh display a colorbar\n * @param anatomicalStructurePrimary - region for mesh. Default is an empty string\n */\n constructor(\n pts: Float32Array,\n tris: Uint32Array,\n name = '',\n rgba255 = new Uint8Array([255, 255, 255, 255]),\n opacity = 1.0,\n visible = true,\n gl: WebGL2RenderingContext,\n connectome: LegacyConnectome | string | null = null,\n dpg: ValuesArray | null = null,\n dps: ValuesArray | null = null,\n dpv: ValuesArray | null = null,\n groups: ValuesArray | null = null,\n colorbarVisible = true,\n anatomicalStructurePrimary = ''\n ) {\n this.anatomicalStructurePrimary = anatomicalStructurePrimary\n this.name = name\n this.colorbarVisible = colorbarVisible\n this.id = uuidv4()\n const obj = NVMeshUtilities.getExtents(pts)\n this.furthestVertexFromOrigin = obj.mxDx\n this.extentsMin = obj.extentsMin\n this.extentsMax = obj.extentsMax\n this.opacity = opacity > 1.0 ? 1.0 : opacity // make sure opacity can't be initialized greater than 1 see: #107 and #117 on github\n this.visible = visible\n this.meshShaderIndex = 0\n this.indexBuffer = gl.createBuffer()!\n this.vertexBuffer = gl.createBuffer()!\n this.vao = gl.createVertexArray()!\n // the VAO binds the vertices and indices as well as describing the vertex layout\n gl.bindVertexArray(this.vao)\n gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer)\n gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer)\n // vertex position: 3 floats X,Y,Z\n gl.enableVertexAttribArray(0)\n\n gl.enableVertexAttribArray(1)\n const f32PerVertex = this.f32PerVertex\n if (f32PerVertex !== 7) {\n // n32\n gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 20, 0)\n // vertex surface normal vector: (also three floats)\n gl.vertexAttribPointer(1, 4, gl.BYTE, true, 20, 12)\n // vertex color\n gl.enableVertexAttribArray(2)\n gl.vertexAttribPointer(2, 4, gl.UNSIGNED_BYTE, true, 20, 16)\n } else {\n gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 28, 0)\n // vertex surface normal vector: (also three floats)\n gl.vertexAttribPointer(1, 3, gl.FLOAT, false, 28, 12)\n // vertex color\n gl.enableVertexAttribArray(2)\n gl.vertexAttribPointer(2, 4, gl.UNSIGNED_BYTE, true, 28, 24)\n }\n gl.bindVertexArray(null) // https://stackoverflow.com/questions/43904396/are-we-not-allowed-to-bind-gl-array-buffer-and-vertex-attrib-array-to-0-in-webgl\n\n this.vaoFiber = gl.createVertexArray()!\n this.offsetPt0 = null\n this.hasConnectome = false\n this.colormapInvert = false\n this.fiberGroupColormap = null\n this.pts = pts\n this.layers = []\n this.type = MeshType.MESH\n this.tris = tris\n if (rgba255[3] < 1) {\n this.rgba255 = rgba255\n this.fiberLength = 2\n this.fiberDither = 0.1\n this.fiberColor = 'Global'\n this.fiberDecimationStride = 1 // e.g. if 2 the 50% of streamlines visible, if 3 then 1/3rd\n this.fiberMask = [] // provide method to show/hide specific fibers\n this.colormap = connectome\n this.dpg = dpg\n this.dps = dps\n this.dpv = dpv\n this.groups = groups\n if (dpg) {\n this.initValuesArray(dpg)\n }\n if (dps) {\n this.initValuesArray(dps)\n }\n if (dpv) {\n this.initValuesArray(dpv)\n }\n if (groups) {\n this.initValuesArray(groups)\n }\n this.offsetPt0 = new Uint32Array(tris)\n this.tris = new Uint32Array(0)\n this.updateFibers(gl)\n // define VAO\n gl.bindVertexArray(this.vaoFiber)\n gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer)\n gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer)\n // vertex position: 3 floats X,Y,Z\n gl.enableVertexAttribArray(0)\n gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 16, 0)\n // vertex color\n gl.enableVertexAttribArray(1)\n gl.vertexAttribPointer(1, 4, gl.UNSIGNED_BYTE, true, 16, 12)\n gl.bindVertexArray(null) // https://stackoverflow.com/questions/43904396/are-we-not-allowed-to-bind-gl-array-buffer-and-vertex-attrib-array-to-0-in-webgl\n return\n } // if fiber not mesh\n if (connectome) {\n this.connectome = connectome\n this.hasConnectome = true\n const keysArray = Object.keys(connectome)\n for (let i = 0, len = keysArray.length; i < len; i++) {\n this[keysArray[i]] = connectome[keysArray[i]]\n }\n }\n this.rgba255 = rgba255\n\n this.updateMesh(gl)\n }\n\n initValuesArray(va: ValuesArray): ValuesArray {\n for (let i = 0; i < va.length; i++) {\n const mn = va[i].vals.reduce((acc, current) => Math.min(acc, current))\n const mx = va[i].vals.reduce((acc, current) => Math.max(acc, current))\n va[i].global_min = mn\n va[i].global_max = mx\n va[i].cal_min = mn\n va[i].cal_max = mx\n }\n return va\n }\n\n // given streamlines (which webGL renders as a single pixel), extrude to cylinders\n linesToCylinders(gl: WebGL2RenderingContext, posClrF32: Float32Array, indices: number[]): void {\n // return Float32Array\n // const posClrF32 four 32-bit components X,Y,Z,C where C is Uint32 with RGBA\n function v4ToV3(v4: vec4): vec3 {\n return vec3.fromValues(v4[0], v4[1], v4[2])\n }\n const primitiveRestart = Math.pow(2, 32) - 1 // for gl.UNSIGNED_INT\n const n_count = indices.length\n let n_line_vtx = 0\n let n_streamlines = 0\n // n.b. each streamline terminates with a `primitiveRestart`, even the final one\n for (let i = 0; i < n_count; i++) {\n if (indices[i] === primitiveRestart) {\n n_streamlines++\n continue\n }\n n_line_vtx++\n }\n const cyl_sides = this.fiberSides\n // next: generate extruded cylinders\n // npt is number of points (vertices) for cylinders\n const npt = cyl_sides * n_line_vtx\n const f32PerVertex = this.f32PerVertex // 7 if NormalXYZ is 3 floats, 5 if normalXYZ is packed into rgb32\n if (f32PerVertex !== 5) {\n throw Error('fiberSides > 1 requires f32PerVertex == 5')\n }\n const f32 = new Float32Array(npt * f32PerVertex) // Each vertex has 5 components: PosX, PosY, PosZ, NormalXYZ, RGBA32\n const u8 = new Uint8Array(f32.buffer) // Each vertex has 7 components: PositionXYZ, NormalXYZ, RGBA32\n let vtx = 0\n //\n // previous vector location\n let prevV4 = vec4.create()\n let currV4 = vec4.create()\n let nextV4 = vec4.create()\n const v1 = vec3.create()\n let prevV2 = vec3.create()\n let node = 0\n const radius = this.fiberRadius\n for (let i = 0; i < n_count; i++) {\n const isLineEnd = indices[i] === primitiveRestart\n if (isLineEnd && node < 1) {\n continue\n } // two restarts in a row!\n let idx = indices[i] * 4 // each posClrF32 has 4 elements X,Y,Z,C\n node++\n if (node <= 1) {\n // first vertex in a streamline, no previous vertex\n prevV4 = vec4.fromValues(posClrF32[idx + 0], posClrF32[idx + 1], posClrF32[idx + 2], posClrF32[idx + 3])\n currV4 = vec4.clone(prevV4)\n if (i + 1 < n_count && indices[i + 1] !== primitiveRestart) {\n idx = indices[i + 1] * 4\n nextV4 = vec4.fromValues(posClrF32[idx + 0], posClrF32[idx + 1], posClrF32[idx + 2], posClrF32[idx + 3])\n vec3.subtract(v1, v4ToV3(prevV4), v4ToV3(nextV4))\n vec3.normalize(v1, v1) // principle axis of cylinder\n prevV2 = NiivueObject3D.getFirstPerpVector(v1)\n }\n continue\n }\n if (isLineEnd) {\n // last vertex of streamline, no next vertex\n nextV4 = vec4.clone(currV4)\n } else {\n nextV4 = vec4.fromValues(posClrF32[idx + 0], posClrF32[idx + 1], posClrF32[idx + 2], posClrF32[idx + 3])\n }\n // mean direction at joint\n // n.b. vec4 -> vec3 we ignore 4th dimension (color)\n vec3.subtract(v1, v4ToV3(prevV4), v4ToV3(nextV4))\n vec3.normalize(v1, v1) // principle axis of cylinder\n // avoid twisted cylinders: ensure v2 as closely aligned with previous v2 as possible\n // method simpler than Frenet–Serret apparatus\n // https://math.stackexchange.com/questions/410530/find-closest-vector-to-a-which-is-perpendicular-to-b\n // const v2 = NiivueObject3D.getFirstPerpVector(v1)\n // 𝐷=𝐴×𝐵, and then 𝐶=𝐵×𝐷. 𝐶 is automatically orthogonal to 𝐵\n const D = vec3.create()\n vec3.cross(D, prevV2, v1)\n const v2 = vec3.create()\n vec3.cross(v2, v1, D)\n prevV2 = vec3.clone(prevV2)\n // the next line of code would create arbitrary v2 that might show twisting\n // v2 = NiivueObject3D.getFirstPerpVector(v1)\n // Get the second perp vector by cross product\n const v3 = vec3.create()\n vec3.cross(v3, v1, v2) // a unit length vector orthogonal to v1 and v2\n vec3.normalize(v3, v3)\n const vtxXYZ = vec3.create()\n for (let j = 0; j < cyl_sides; j++) {\n const c = Math.cos((j / cyl_sides) * 2 * Math.PI)\n const s = Math.sin((j / cyl_sides) * 2 * Math.PI)\n vtxXYZ[0] = radius * (c * v2[0] + s * v3[0])\n vtxXYZ[1] = radius * (c * v2[1] + s * v3[1])\n vtxXYZ[2] = radius * (c * v2[2] + s * v3[2])\n vec3.add(vtxXYZ, v4ToV3(currV4), vtxXYZ)\n const fidx = vtx * f32PerVertex\n f32[fidx + 0] = vtxXYZ[0]\n f32[fidx + 1] = vtxXYZ[1]\n f32[fidx + 2] = vtxXYZ[2]\n // compute normal\n const n3 = vec3.create()\n vec3.subtract(n3, vtxXYZ, v4ToV3(currV4))\n vec3.normalize(n3, n3)\n const fidxU8 = (fidx + 3) * 4 // 4 Uint8 per Float32\n u8[fidxU8 + 0] = n3[0] * 127\n u8[fidxU8 + 1] = n3[1] * 127\n u8[fidxU8 + 2] = n3[2] * 127\n // f32[fidx+3] = normal;\n f32[fidx + 4] = currV4[3]\n // u32[fidx+3] = 65555;\n // u32[fidx+4] = 65555;\n vtx++\n }\n prevV4 = vec4.clone(currV4)\n currV4 = vec4.clone(nextV4)\n if (isLineEnd) {\n node = 0\n }\n }\n // ntri = number of triangles\n // each cylinder is composed of 2 * cyl_sides (e.g. triangular cylinder is 6 triangles)\n // each streamline with n nodes has n-1 cylinders (fencepost)\n // each triangle defined by three indices, each referring to a vertex\n const nidx = (n_line_vtx - n_streamlines) * cyl_sides * 2 * 3\n const idxs = new Uint32Array(nidx)\n let idx = 0\n vtx = 0\n for (let i = 1; i < n_count; i++) {\n if (indices[i] === primitiveRestart) {\n vtx += cyl_sides\n continue\n }\n if (indices[i - 1] === primitiveRestart) {\n // fencepost: do not create indices for first node in each streamline\n continue\n }\n let prevStartVtx = vtx // startOfPreviousCylinder\n let startVtx = vtx + cyl_sides // startOfCurrentCylinder\n const prevStartVtxOverflow = startVtx // startOfCurrentCylinder\n const startVtxOverflow = startVtx + cyl_sides // startOfNextCylinder\n for (let j = 0; j < cyl_sides; j++) {\n // emit triangle with one vertex on previous\n idxs[idx++] = prevStartVtx\n idxs[idx++] = startVtx++\n if (startVtx === startVtxOverflow) {\n startVtx = startVtxOverflow - cyl_sides\n }\n idxs[idx++] = startVtx\n // emit triangle with two vertex on previous\n idxs[idx++] = prevStartVtx++\n if (prevStartVtx === prevStartVtxOverflow) {\n prevStartVtx = prevStartVtxOverflow - cyl_sides\n }\n idxs[idx++] = startVtx\n idxs[idx++] = prevStartVtx\n }\n vtx += cyl_sides\n }\n // copy index and vertex buffer to GPU\n // no need to release: https://registry.khronos.org/OpenGL-Refpages/gl4/html/glBufferData.xhtml\n // any pre-existing data store is deleted\n gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer)\n gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, Uint32Array.from(idxs), gl.STATIC_DRAW)\n gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer)\n // issue1129\n // gl.bufferData(gl.ARRAY_BUFFER, Float32Array.from(f32), gl.STATIC_DRAW)\n gl.bufferData(gl.ARRAY_BUFFER, u8, gl.STATIC_DRAW)\n this.indexCount = nidx\n } // linesToCylinders\n\n createFiberDensityMap(): void {\n // generate a fiber density map\n // the array fiberDensity has one element per vertex\n // this provides the normalized (0..1) neighboring vertices\n if (this.fiberDensity) {\n return\n }\n const pts = this.pts\n const npt = pts.length / 3 // each point has three components: X,Y,Z\n let maxExtentsRange = 0\n for (let i = 0; i < 3; i++) {\n const range = this.extentsMax[i] - this.extentsMin[i]\n maxExtentsRange = Math.max(maxExtentsRange, range)\n }\n this.fiberDensity = new Float32Array(npt)\n if (maxExtentsRange === 0) {\n return\n }\n // DSI-Studio counts vertex density per voxel\n // However, some tract formats do not store voxel dimensions\n // therefore, we will create a 3D volume of size bins*bins*bins\n const bins = 64\n const binWidth = maxExtentsRange / (bins - 1)\n const half = binWidth / 2\n const scale = (bins - 1) / maxExtentsRange\n let densityMap = new Float32Array(bins * bins * bins)\n const mn = [this.extentsMin[0] - half, this.extentsMin[1] - half, this.extentsMin[2] - half]\n // sum density map\n const xyz = [0, 0, 0]\n const prevVx = -1\n const binsXbins = bins * bins\n let j = 0\n for (let i = 0; i < npt; i++) {\n xyz[0] = Math.round((pts[j++] - mn[0]) * scale)\n xyz[1] = Math.round((pts[j++] - mn[1]) * scale)\n xyz[2] = Math.round((pts[j++] - mn[2]) * scale)\n const vx = xyz[0] + xyz[1] * bins + xyz[2] * binsXbins\n if (vx === prevVx) {\n // each streamline contributes once per voxel\n continue\n }\n densityMap[vx]++\n }\n function blur3D(vol: Float32Array, dim: number): Float32Array {\n // let raw = vol.slice()\n let raw = vol.slice()\n let v = -1\n const dim1 = dim - 1\n // blur in x\n for (let z = 0; z < dim; z++) {\n for (let y = 0; y < dim; y++) {\n for (let x = 0; x < dim; x++) {\n v++\n if (x < 1 || x >= dim1) {\n continue\n }\n vol[v] = raw[v - 1] + raw[v] + raw[v] + raw[v + 1]\n }\n }\n }\n // blur in y\n v = -1\n raw = vol.slice()\n for (let z = 0; z < dim; z++) {\n for (let y = 0; y < dim; y++) {\n for (let x = 0; x < dim; x++) {\n v++\n if (y < 1 || y >= dim1) {\n continue\n }\n vol[v] = raw[v - dim] + raw[v] + raw[v] + raw[v + dim]\n }\n }\n }\n // blur in z\n const dimXdim = dim * dim\n v = -1\n raw = vol.slice()\n for (let z = 0; z < dim; z++) {\n for (let y = 0; y < dim; y++) {\n for (let x = 0; x < dim; x++) {\n v++\n if (z < 1 || z >= dim1) {\n continue\n }\n vol[v] = raw[v - dimXdim] + raw[v] + raw[v] + raw[dimXdim]\n }\n }\n }\n return vol\n }\n densityMap = blur3D(densityMap, bins)\n densityMap = blur3D(densityMap, bins)\n // let raw = densityMap.slice()\n let mx = 0\n let mn0 = Infinity\n const binsXbinsXbins = bins * bins * bins\n for (let i = 0; i < binsXbinsXbins; i++) {\n if (densityMap[i] <= 0) {\n continue\n }\n mx = Math.max(mx, densityMap[i])\n mn0 = Math.min(mn0, densityMap[i])\n }\n // console.log('Maximum streamlines in a voxel:', mx, mn0)\n if (mx <= 1 || mx <= mn0) {\n // no neighbors: no ambient occlusion\n return\n }\n j = 0\n for (let i = 0; i < binsXbinsXbins; i++) {\n // least occluded vertices should have no occlusion\n densityMap[i] = Math.max(0, densityMap[i] - mn0)\n }\n mx -= mn0\n for (let i = 0; i < npt; i++) {\n xyz[0] = Math.round((pts[j++] - mn[0]) * scale)\n xyz[1] = Math.round((pts[j++] - mn[1]) * scale)\n xyz[2] = Math.round((pts[j++] - mn[2]) * scale)\n const vx = xyz[0] + xyz[1] * bins + xyz[2] * binsXbins\n this.fiberDensity[i] = densityMap[vx] / mx\n }\n }\n\n // not included in public docs\n // internal function filters tractogram to identify which color and visibility of streamlines\n updateFibers(gl: WebGL2RenderingContext): void {\n if (!this.offsetPt0 || !this.fiberLength) {\n return\n }\n const pts = this.pts\n const offsetPt0 = this.offsetPt0\n const n_count = offsetPt0.length - 1\n const npt = pts.length / 3 // each point has three components: X,Y,Z\n // only once: compute length of each streamline\n if (!this.fiberLengths) {\n this.fiberLengths = new Uint32Array(n_count)\n for (let i = 0; i < n_count; i++) {\n // for each streamline\n const vStart3 = offsetPt0[i] * 3 // first vertex in streamline\n const vEnd3 = (offsetPt0[i + 1] - 1) * 3 // last vertex in streamline\n let len = 0\n for (let j = vStart3; j < vEnd3; j += 3) {\n const v = vec3.fromValues(pts[j + 0] - pts[j + 3], pts[j + 1] - pts[j + 4], pts[j + 2] - pts[j + 5])\n len += vec3.len(v)\n }\n this.fiberLengths[i] = len\n }\n } // only once: compute length of each streamline\n // determine fiber colors\n // Each streamline vertex has color and position attributes\n // Interleaved Vertex Data https://developer.apple.com/library/archive/documentation/3DDrawing/Conceptual/OpenGLES_ProgrammingGuide/TechniquesforWorkingwithVertexData/TechniquesforWorkingwithVertexData.html\n const posClrF32 = new Float32Array(npt * 4) // four 32-bit components X,Y,Z,C\n const posClrU32 = new Uint32Array(posClrF32.buffer) // typecast of our X,Y,Z,C array\n // fill XYZ position of XYZC array\n let i3 = 0\n let i4 = 0\n for (let i = 0; i < npt; i++) {\n posClrF32[i4 + 0] = pts[i3 + 0]\n posClrF32[i4 + 1] = pts[i3 + 1]\n posClrF32[i4 + 2] = pts[i3 + 2]\n i3 += 3\n i4 += 4\n }\n // fill fiber Color\n const dither = this.fiberDither\n const ditherHalf = dither * 0.5\n function rgb2int32(r: number, g: number, b: number): number {\n const ditherFrac = dither * Math.random()\n const d = 255.0 * (ditherFrac - ditherHalf)\n r = Math.max(Math.min(r + d, 255.0), 0.0)\n g = Math.max(Math.min(g + d, 255.0), 0.0)\n b = Math.max(Math.min(b + d, 255.0), 0.0)\n return r + (g << 8) + (b << 16)\n }\n function direction2rgb(x1: number, y1: number, z1: number, x2: number, y2: number, z2: number, ditherFrac: number): number {\n // generate color based on direction between two 3D spatial positions\n const v = vec3.fromValues(Math.abs(x1 - x2), Math.abs(y1 - y2), Math.abs(z1 - z2))\n vec3.normalize(v, v)\n const r = ditherFrac - ditherHalf\n for (let j = 0; j < 3; j++) {\n v[j] = 255 * Math.max(Math.min(Math.abs(v[j]) + r, 1.0), 0.0)\n }\n return v[0] + (v[1] << 8) + (v[2] << 16)\n } // direction2rgb()\n // Determine color: local, global, dps0, dpv0, etc.\n const fiberColor = this.fiberColor.toLowerCase()\n let dps: Float32Array | null = null\n let dpv: ValuesArray[0] | null = null\n if (fiberColor.startsWith('dps') && this.dps && this.dps.length > 0) {\n const n = parseInt(fiberColor.substring(3))\n if (n < this.dps.length && this.dps[n].vals.length === n_count) {\n dps = this.dps[n].vals\n }\n }\n if (fiberColor.startsWith('dpv') && this.dpv && this.dpv.length > 0) {\n const n = parseInt(fiberColor.substring(3))\n if (n < this.dpv.length && this.dpv[n].vals.length === npt) {\n dpv = this.dpv[n]\n }\n }\n const streamlineVisible = new Int16Array(n_count)\n if ((this.groups && this.fiberGroupColormap !== null) || (fiberColor.startsWith('dpg') && this.dpg.length > 0)) {\n const lut = new Uint8ClampedArray(this.groups.length * 4) // 4 component RGBA for each group\n const groupVisible = new Array(this.groups.length).fill(false)\n if (this.fiberGroupColormap) {\n const cmap = this.fiberGroupColormap\n if (cmap.A === undefined) {\n cmap.A = Array.from(new Uint8ClampedArray(cmap.I.length).fill(255))\n }\n for (let i = 0; i < cmap.I.length; i++) {\n let idx = cmap.I[i]\n if (idx < 0 || idx >= this.groups.length) {\n continue\n }\n if (cmap.A[i] < 1) {\n continue\n }\n groupVisible[idx] = true\n idx *= 4\n lut[idx] = cmap.R[i]\n lut[idx + 1] = cmap.G[i]\n lut[idx + 2] = cmap.B[i]\n lut[idx + 3] = 255 // opaque\n }\n } else {\n if (fiberColor.startsWith('dpg') && this.dpg.length > 0) {\n const n = parseInt(fiberColor.substring(3))\n const dpg = n < this.dpg.length ? this.dpg[n] : this.dpg[0]\n const lut255 = cmapper.colormap(this.colormap as string, this.colormapInvert)\n const mn = dpg.cal_min\n const mx = dpg.cal_max\n const ngroup = this.groups.length\n for (let i = 0; i < ngroup; i++) {\n const v = dpg.vals[i]\n if (v < mn) {\n continue\n }\n let color255 = Math.round(255 * Math.min(Math.max((v - mn!) / (mx! - mn!), 0), 1))\n groupVisible[i] = true\n const idx = i * 4\n // read RGB values from chosen colormap\n color255 *= 4\n lut[idx] = lut255[color255 + 0]\n lut[idx + 1] = lut255[color255 + 1]\n lut[idx + 2] = lut255[color255 + 2]\n lut[idx + 3] = 255 // opaque\n }\n }\n }\n streamlineVisible.fill(-1) // -1 assume streamline not visible\n for (let i = 0; i < this.groups.length; i++) {\n if (!groupVisible[i]) {\n continue\n } // this group is not visible\n const group = this.groups[i]\n for (let v = 0; v < group.vals.length; v++) {\n streamlineVisible[group.vals[v]] = i\n }\n }\n for (let i = 0; i < n_count; i++) {\n if (streamlineVisible[i] < 0) {\n continue\n } // hidden\n const color = (streamlineVisible[i] % 256) * 4\n // let RGBA = lut[color] + (lut[color + 1] << 8) + (lut[color + 2] << 16);\n const RGBA = rgb2int32(lut[color], lut[color + 1], lut[color + 2])\n const vStart = offsetPt0[i] // first vertex in streamline\n const vEnd = offsetPt0[i + 1] - 1 // last vertex in streamline\n const vStart4 = vStart * 4 + 3 // +3: fill 4th component colors: XYZC = 0123\n const vEnd4 = vEnd * 4 + 3\n for (let j = vStart4; j <= vEnd4; j += 4) {\n posClrU32[j] = RGBA\n }\n }\n } else if (dpv) {\n // color per vertex\n const lut = cmapper.colormap(this.colormap as string, this.colormapInvert)\n const mn = dpv.cal_min\n const mx = dpv.cal_max\n let v4 = 3 // +3: fill 4th component colors: XYZC = 0123\n for (let i = 0; i < npt; i++) {\n let color = Math.min(Math.max((dpv.vals[i] - mn!) / (mx! - mn!), 0), 1)\n color = Math.round(Math.max(Math.min(255, color * 255))) * 4\n const RGBA = lut[color] + (lut[color + 1] << 8) + (lut[color + 2] << 16)\n posClrU32[v4] = RGBA\n v4 += 4\n }\n } else if (dps) {\n const lut = cmapper.colormap(this.colormap as string, this.colormapInvert)\n let mn = dps[0]\n let mx = dps[0]\n for (let i = 0; i < n_count; i++) {\n mn = Math.min(mn, dps[i])\n mx = Math.max(mx, dps[i])\n }\n if (mx === mn) {\n mn -= 1\n } // avoid divide by zero\n for (let i = 0; i < n_count; i++) {\n let color = (dps[i] - mn) / (mx - mn)\n color = Math.round(Math.max(Math.min(255, color * 255))) * 4\n const RGBA = lut[color] + (lut[color + 1] << 8) + (lut[color + 2] << 16)\n const vStart = offsetPt0[i] // first vertex in streamline\n const vEnd = offsetPt0[i + 1] - 1 // last vertex in streamline\n const vStart4 = vStart * 4 + 3 // +3: fill 4th component colors: XYZC = 0123\n const vEnd4 = vEnd * 4 + 3\n for (let j = vStart4; j <= vEnd4; j += 4) {\n posClrU32[j] = RGBA\n }\n }\n } else if (fiberColor.includes('fixed')) {\n if (dither === 0.0) {\n const RGBA = this.rgba255[0] + (this.rgba255[1] << 8) + (this.rgba255[2] << 16)\n let v4 = 3 // +3: fill 4th component colors: XYZC = 0123\n for (let i = 0; i < npt; i++) {\n posClrU32[v4] = RGBA\n v4 += 4\n }\n } else {\n for (let i = 0; i < n_count; i++) {\n const RGBA = rgb2int32(this.rgba255[0], this.rgba255[1], this.rgba255[2])\n const vStart = offsetPt0[i] // first vertex in streamline\n const vEnd = offsetPt0[i + 1] - 1 // last vertex in streamline\n const vStart4 = vStart * 4 + 3 // +3: fill 4th component colors: XYZC = 0123\n const vEnd4 = vEnd * 4 + 3\n for (let j = vStart4; j <= vEnd4; j += 4) {\n posClrU32[j] = RGBA\n }\n }\n } // else fixed with dither\n } else if (fiberColor.includes('local')) {\n for (let i = 0; i < n_count; i++) {\n // for each streamline\n const vStart = offsetPt0[i] // first vertex in streamline\n const vEnd = offsetPt0[i + 1] - 1 // last vertex in streamline\n let v3 = vStart * 3 // pts have 3 components XYZ\n const vEnd3 = vEnd * 3\n const ditherFrac = dither * Math.random() // same dither amount throughout line\n // for first point, we do not have a prior sample\n let RGBA = direction2rgb(pts[v3], pts[v3 + 1], pts[v3 + 2], pts[v3 + 4], pts[v3 + 5], pts[v3 + 6], ditherFrac)\n let v4 = vStart * 4 + 3 // +3: fill 4th component colors: XYZC = 0123\n while (v3 < vEnd3) {\n posClrU32[v4] = RGBA\n v4 += 4 // stride is 4 32-bit values: float32 XYZ and 32-bit rgba\n v3 += 3 // read next vertex\n // direction estimated based on previous and next vertex\n RGBA = direction2rgb(pts[v3 - 3], pts[v3 - 2], pts[v3 - 1], pts[v3 + 3], pts[v3 + 4], pts[v3 + 5], ditherFrac)\n }\n posClrU32[v4] = posClrU32[v4 - 4]\n }\n } else {\n // if color is local direction, else global\n for (let i = 0; i < n_count; i++) {\n // for each streamline\n const vStart = offsetPt0[i] // first vertex in streamline\n const vEnd = offsetPt0[i + 1] - 1 // last vertex in streamline\n const vStart3 = vStart * 3 // pts have 3 components XYZ\n const vEnd3 = vEnd * 3\n const RGBA = direction2rgb(pts[vStart3], pts[vStart3 + 1], pts[vStart3 + 2], pts[vEnd3], pts[vEnd3 + 1], pts[vEnd3 + 2], dither * Math.random())\n const vStart4 = vStart * 4 + 3 // +3: fill 4th component colors: XYZC = 0123\n const vEnd4 = vEnd * 4 + 3\n for (let j = vStart4; j <= vEnd4; j += 4) {\n posClrU32[j] = RGBA\n }\n }\n }\n // SHADING: ambient occlusion\n if (this.fiberOcclusion > 0) {\n this.createFiberDensityMap()\n function shadeRGBA(rgba: number, frac: number): number {\n const r = frac * (rgba & 0xff)\n const g = frac * ((rgba >> 8) & 0xff)\n const b = frac * ((rgba >> 16) & 0xff)\n return r + (g << 8) + (b << 16)\n }\n for (let i = 0; i < n_count; i++) {\n // for each streamline\n const vStart = offsetPt0[i] // first vertex in streamline\n const vEnd = offsetPt0[i + 1] - 1 // last vertex in streamline\n const vStart4 = vStart * 4 + 3 // +3: fill 4th component colors: XYZC = 0123\n const vEnd4 = vEnd * 4 + 3\n let vtx = vStart\n const bias = Math.min(this.fiberOcclusion, 0.99)\n for (let j = vStart4; j <= vEnd4; j += 4) {\n let shade = this.fiberDensity[vtx++]\n if (shade <= 0) {\n continue\n }\n // Schlick's fast bias function\n // https://github.com/ayamflow/schlick-curve\n shade = shade / ((1.0 / bias - 2.0) * (1.0 - shade) + 1.0)\n const frac = 1 - Math.min(shade, 0.9)\n // console.log(shade, frac)\n let RGBA = posClrU32[j]\n RGBA = shadeRGBA(RGBA, frac)\n posClrU32[j] = RGBA\n }\n }\n }\n // issue 1426\n if (Number.isFinite(this.dpsThreshold) && this.dps) {\n if (!dps) {\n // if color is not based on dps, threshold on first dps\n const n = 0\n if (this.dps[n].vals.length === n_count) {\n dps = this.dps[n].vals\n }\n }\n if (dps) {\n for (let i = 0; i < n_count; i++) {\n if (dps[i] < this.dpsThreshold) {\n streamlineVisible[i] = -1\n }\n }\n }\n }\n // INDICES:\n const min_mm = this.fiberLength\n // https://blog.spacepatroldelta.com/a?ID=00950-d878555f-a97a-4e32-9f40-fd9a449cb4fe\n const primitiveRestart = Math.pow(2, 32) - 1 // for gl.UNSIGNED_INT\n const indices: number[] = []\n let stride = -1\n for (let i = 0; i < n_count; i++) {\n // let n_pts = offsetPt0[i + 1] - offsetPt0[i]; //if streamline0 starts at point 0 and streamline1 at point 4, then streamline0 has 4 points: 0,1,2,3\n if (streamlineVisible[i] < 0) {\n continue\n }\n if (this.fiberLengths[i] < min_mm) {\n continue\n }\n stride++\n if (stride % this.fiberDecimationStride !== 0) {\n continue\n } // e.g. if stride is 2 then half culled\n for (let j = offsetPt0[i]; j < offsetPt0[i + 1]; j++) {\n indices.push(j)\n }\n indices.push(primitiveRestart)\n }\n if (this.fiberSides > 2 && this.fiberRadius > 0) {\n this.linesToCylinders(gl, posClrF32, indices)\n } else {\n // copy streamlines to GPU\n this.indexCount = indices.length\n gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer)\n gl.bufferData(gl.ARRAY_BUFFER, Uint32Array.from(posClrU32), gl.STATIC_DRAW)\n gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer)\n gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, Uint32Array.from(indices), gl.STATIC_DRAW)\n }\n } // updateFibers()\n\n // given X,Y,Z coordinates in world space, return index of nearest vertex as well as\n // the distance of this closest vertex to the coordinates\n indexNearestXYZmm(Xmm: number, Ymm: number, Zmm: number): number[] {\n const pts = this.pts\n const nvtx = this.pts.length / 3\n let i = 0\n let mnDx = Infinity\n let mnIdx = 0\n for (let j = 0; j < nvtx; j++) {\n const dx = Math.pow(pts[i] - Xmm, 2) + Math.pow(pts[i + 1] - Ymm, 2) + Math.pow(pts[i + 2] - Zmm, 2)\n if (dx < mnDx) {\n mnDx = dx\n mnIdx = j\n }\n i += 3\n }\n // Pythagorean theorem sqrt(x^2+y^2+z^2)\n // only calculate sqrt once\n mnDx = Math.sqrt(mnDx)\n return [mnIdx, mnDx]\n } // indexNearestXYZmm()\n\n // internal function discards GPU resources\n unloadMesh(gl: WebGL2RenderingContext): void {\n // free WebGL resources\n gl.bindBuffer(gl.ARRAY_BUFFER, null)\n gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null)\n gl.bindVertexArray(null)\n gl.deleteBuffer(this.vertexBuffer)\n gl.deleteBuffer(this.indexBuffer)\n gl.deleteVertexArray(this.vao)\n gl.deleteVertexArray(this.vaoFiber)\n // presumably, if we null the mesh we dereference all the arrays, or do we have to explicitly null arrays\n this.offsetPt0 = null\n this.tris = null\n this.pts = null\n if (this.layers && this.layers.length > 0) {\n for (let i = 0; i < this.layers.length; i++) {\n this.layers[i].values = null\n }\n }\n if (this.dpg && this.dpg.length > 0) {\n for (let i = 0; i < this.dpg.length; i++) {\n this.dpg[i].vals = null\n }\n }\n if (this.dps && this.dps.length > 0) {\n for (let i = 0; i < this.dps.length; i++) {\n this.dps[i].vals = null\n }\n }\n }\n\n // apply color lookup table to convert scalar array to RGBA array\n scalars2RGBA(rgba: Uint8ClampedArray, layer: NVMeshLayer, scalars: AnyNumberArray, isNegativeCmap: boolean = false): Uint8ClampedArray {\n const nValues = scalars.length\n if (4 * nValues < rgba.length) {\n log.error(`colormap2RGBA incorrectly specified ${nValues}*4 != ${rgba.length}`)\n return rgba\n }\n const opa255 = Math.round(layer.opacity * 255)\n let mn = layer.cal_min\n let mx = layer.cal_max\n let lut = cmapper.colormap(layer.colormap as string, this.colormapInvert)\n let flip = 1\n if (isNegativeCmap) {\n if (!layer.useNegativeCmap) {\n return rgba\n }\n flip = -1\n lut = cmapper.colormap(layer.colormapNegative, layer.colormapInvert)\n mn = layer.cal_min\n mx = layer.cal_max\n if (isFinite(layer.cal_minNeg) && isFinite(layer.cal_minNeg)) {\n mn = -layer.cal_minNeg\n mx = -layer.cal_maxNeg\n }\n }\n let mnCal = mn\n if (!layer.isTransparentBelowCalMin) {\n mnCal = Number.NEGATIVE_INFINITY\n }\n const isTranslucentBelowMin = layer.colormapType === COLORMAP_TYPE.ZERO_TO_MAX_TRANSLUCENT_BELOW_MIN\n\n if (layer.colormapType !== COLORMAP_TYPE.MIN_TO_MAX) {\n mn = Math.min(mn, 0.0)\n }\n const scale255 = 255.0 / (mx - mn)\n for (let j = 0; j < nValues; j++) {\n let v = scalars[j] * flip\n if (isNaN(v)) {\n continue\n }\n let opa = opa255\n if (v < mnCal) {\n if (v > 0 && isTranslucentBelowMin) {\n opa = Math.round(layer.opacity * 255 * Math.pow(v / mnCal, 2.0))\n } else {\n continue\n }\n }\n v = (v - mn) * scale255\n if (v < 0 && layer.isTransparentBelowCalMin) {\n continue\n }\n v = Math.min(255, Math.max(0, Math.round(v))) * 4\n const idx = j * 4\n rgba[idx + 0] = lut[v + 0]\n rgba[idx + 1] = lut[v + 1]\n rgba[idx + 2] = lut[v + 2]\n rgba[idx + 3] = opa\n }\n return rgba\n }\n\n blendColormap(u8: Uint8Array, additiveRGBA: Uint8Array, layer: NVMeshLayer, mn: number, mx: number, lut: Uint8ClampedArray, invert: boolean = false): void {\n const nvtx = this.pts.length / 3\n const opacity = Math.min(layer.opacity, 1.0)\n function lerp(x: number, y: number, a: number): number {\n // https://www.khronos.org/registry/OpenGL-Refpages/gl4/html/mix.xhtml\n return x * (1 - a) + y * a\n }\n function additiveBlend(x: number, y: number): number {\n return Math.min(x + y, 255.0)\n }\n const scaleFlip = invert ? -1 : 1\n const frame = Math.min(Math.max(layer.frame4D, 0), layer.nFrame4D - 1)\n const frameOffset = nvtx * frame\n let mnCal = mn\n if (!layer.isTransparentBelowCalMin) {\n mnCal = Number.NEGATIVE_INFINITY\n }\n if (layer.colormapType !== COLORMAP_TYPE.MIN_TO_MAX) {\n mn = Math.min(mn, 0.0)\n }\n const scale255 = 255.0 / (mx - mn)\n // create border map for optional outline\n let borders = new Array(nvtx).fill(false)\n if (layer.outlineBorder !== 0.0) {\n const v255s = new Uint8Array(nvtx).fill(0)\n for (let j = 0; j < nvtx; j++) {\n const v = scaleFlip * layer.values[j + frameOffset]\n if (v >= mnCal) {\n v255s[j] = 1\n }\n }\n borders = NVMeshUtilities.getClusterBoundaryU8(v255s, this.tris)\n for (let j = 0; j < nvtx; j++) {\n const v = scaleFlip * layer.values[j + frameOffset]\n if (v < mnCal) {\n borders[j] = false\n }\n }\n }\n // create lookup table for translucency\n const alphas = new Float32Array(256).fill(opacity)\n if (mnCal > mn && layer.colormapType === COLORMAP_TYPE.ZERO_TO_MAX_TRANSLUCENT_BELOW_MIN) {\n let minOpaque = Math.round((mnCal - mn) * scale255)\n minOpaque = Math.max(minOpaque, 1)\n for (let j = 1; j < minOpaque; j++) {\n alphas[j] = opacity * Math.pow(j / minOpaque, 2.0)\n }\n alphas[0] = 0\n mnCal = mn + Number.EPSILON\n }\n for (let j = 0; j < nvtx; j++) {\n const v = scaleFlip * layer.values[j + frameOffset]\n if (v < mnCal) {\n continue\n }\n let v255 = Math.round((v - mn) * scale255)\n if (v255 < 0 && layer.isTransparentBelowCalMin) {\n continue\n }\n v255 = Math.max(0.0, v255)\n v255 = Math.min(255.0, v255)\n let opa = alphas[v255]\n v255 *= 4\n let vtx = j * 28 + 24 // posNormClr is 28 bytes stride, RGBA color at offset 24,\n if (this.f32PerVertex !== 7) {\n vtx = j * 20 + 16\n }\n if (layer.isAdditiveBlend) {\n const j4 = j * 4\n // sum red, green and blue layers\n additiveRGBA[j4 + 0] = additiveBlend(additiveRGBA[j4 + 0], lut[v255 + 0])\n additiveRGBA[j4 + 1] = additiveBlend(additiveRGBA[j4 + 1], lut[v255 + 1])\n additiveRGBA[j4 + 2] = additiveBlend(additiveRGBA[j4 + 2], lut[v255 + 2])\n additiveRGBA[j4 + 3] = additiveBlend(additiveRGBA[j4 + 3], 255.0)\n } else {\n if (borders[j]) {\n opa = layer.outlineBorder\n if (layer.outlineBorder < 0) {\n u8[vtx + 0] = 0\n u8[vtx + 1] = 0\n u8[vtx + 2] = 0\n continue\n }\n }\n u8[vtx + 0] = lerp(u8[vtx + 0], lut[v255 + 0], opa)\n u8[vtx + 1] = lerp(u8[vtx + 1], lut[v255 + 1], opa)\n u8[vtx + 2] = lerp(u8[vtx + 2], lut[v255 + 2], opa)\n }\n }\n } // blendColormap()\n\n // internal function filters mesh to identify which color of triangulated mesh vertices\n updateMesh(gl: WebGL2RenderingContext): void {\n if (this.offsetPt0) {\n this.updateFibers(gl)\n return // fiber not mesh\n }\n if (this.hasConnectome) {\n // this.updateConnectome(gl)\n return // connectome not mesh\n }\n if (!this.pts || !this.tris || !this.rgba255) {\n log.warn('underspecified mesh')\n return\n }\n function lerp(x: number, y: number, a: number): number {\n // https://www.khronos.org/registry/OpenGL-Refpages/gl4/html/mix.xhtml\n return x * (1 - a) + y * a\n }\n const posNormClr = this.generatePosNormClr(this.pts, this.tris, this.rgba255)\n const nvtx = this.pts.length / 3\n const u8 = new Uint8Array(posNormClr.buffer) // Each vertex has 7 components: PositionXYZ, NormalXYZ, RGBA32\n // create emission values\n // let posNormClrEmission = posNormClr.slice();\n let maxAdditiveBlend = 0\n const additiveRGBA = new Uint8Array(nvtx * 4) // emission\n let tris = this.tris\n if (this.layers && this.layers.length > 0) {\n for (let i = 0; i < this.layers.length; i++) {\n const layer = this.layers[i]\n const opacity = layer.opacity\n if (opacity <= 0.0 || layer.cal_min > layer.cal_max) {\n continue\n }\n if (layer.outlineBorder === undefined) {\n layer.outlineBorder = 0\n }\n if (layer.isAdditiveBlend === undefined) {\n layer.isAdditiveBlend = false\n }\n // build a label colormap\n if (layer.colormapLabel && (layer.colormapLabel as ColorMap).R && !(layer.colormapLabel as LUT).lut) {\n // convert colormap JSON to RGBA LUT\n layer.colormapLabel = cmapper.makeLabelLut(layer.colormapLabel as ColorMap, 255, layer.global_max)\n }\n if (layer.colormapLabel && (layer.colormapLabel as LUT).lut) {\n const colormapLabel = layer.colormapLabel as LUT\n let minv = 0\n if (layer.colormapLabel.min) {\n minv = layer.colormapLabel.min\n }\n let lut = colormapLabel.lut\n const nLabel = Math.floor(lut.length / 4)\n if (layer.atlasValues && nLabel > 0 && nLabel === layer.atlasValues.length && layer.colormap) {\n const atlasValues = layer.atlasValues\n let hasNaN = false\n let onlyNaN = true\n for (let j = 0; j < nLabel; j++) {\n if (isNaN(atlasValues[j])) {\n hasNaN = true\n } else {\n onlyNaN = false\n }\n }\n if (onlyNaN) {\n log.debug(`invisible mesh: all atlasValues are NaN.`)\n return\n }\n if (hasNaN) {\n log.debug(`some vertices have NaN atlasValues (mesh will be decimated).`)\n // First: identify all vertices mapped to NaN\n const nanVtxs = new Array(nvtx).fill(false)\n for (let j = 0; j < nvtx; j++) {\n const v = Math.round(layer.values[j]) - minv\n if (isNaN(atlasValues[v])) {\n nanVtxs[j] = true\n }\n }\n\n // next: find all triangle indices that have NaN vertices\n const nanIdxs = new Array(tris.length).fill(false)\n for (let j = 0; j < tris.length; j++) {\n if (nanVtxs[tris[j]]) {\n nanIdxs[j] = true\n }\n }\n // each triangle has 3 indices\n const trisIn = this.tris\n let nTriOK = 0\n for (let j = 0; j < trisIn.length; j += 3) {\n if (!nanIdxs[j] && !nanIdxs[j + 1] && !nanIdxs[j + 2]) {\n nTriOK++\n }\n }\n if (nTriOK === 0) {\n log.debug(`invisible mesh: all triangles of a vertex with a NaN atlasValue.`)\n }\n tris = new Uint32Array(nTriOK * 3)\n let k = 0\n for (let j = 0; j < trisIn.length; j += 3) {\n if (!nanIdxs[j] && !nanIdxs[j + 1] && !nanIdxs[j + 2]) {\n tris[k++] = trisIn[j]\n tris[k++] = trisIn[j + 1]\n tris[k++] = trisIn[j + 2]\n }\n }\n }\n lut.fill(0) // make all transparent\n lut = this.scalars2RGBA(lut, layer, atlasValues)\n if (layer.useNegativeCmap) {\n lut = this.scalars2RGBA(lut, layer, atlasValues, true)\n }\n } else if (layer.atlasValues) {\n log.warn(`Expected ${nLabel} atlasValues but got ${layer.atlasValues.length} for mesh layer`)\n }\n // create labels for legend\n if (layer.showLegend && nLabel === layer.colormapLabel.labels.length) {\n layer.labels = []\n for (let j = 0; j < nLabel; j++) {\n const rgba = Array.from(lut.slice(j * 4, j * 4 + 4)).map((v) => v / 255)\n const labelName = layer.colormapLabel.labels[j]\n // xyzMM is the center of mass for the label\n // For folded cortical regions, this point often lies within the volume\n const xyzMM = [0, 0, 0]\n let count = 0\n for (let i = 0; i < nvtx; i++) {\n if (layer.values[i] === j) {\n const idx = i * 3\n xyzMM[0] += this.pts[idx]\n xyzMM[1] += this.pts[idx + 1]\n xyzMM[2] += this.pts[idx + 2]\n count++\n }\n }\n if (count > 0) {\n xyzMM[0] /= count\n xyzMM[1] /= count\n xyzMM[2] /= count\n }\n if (\n rgba[3] === 0 ||\n !labelName || // handles empty string, null, undefined\n labelName.startsWith('_')\n ) {\n continue\n }\n rgba[3] = 1\n const label = new NVLabel3D(\n labelName,\n {\n textColor: rgba,\n bulletScale: 1,\n bulletColor: rgba,\n lineWidth: 0,\n lineColor: rgba,\n textScale: 1.0,\n textAlignment: LabelTextAlignment.LEFT,\n lineTerminator: LabelLineTerminator.NONE\n },\n xyzMM\n )\n layer.labels.push(label)\n log.debug('label for mesh layer:', label)\n } // for each label\n } else {\n delete layer.labels\n }\n const frame = Math.min(Math.max(layer.frame4D, 0), layer.nFrame4D - 1)\n const frameOffset = nvtx * frame\n const rgba8 = new Uint8Array(nvtx * 4)\n let k = 0\n for (let j = 0; j < nvtx; j++) {\n // eslint-disable-next-line\n const v = layer.values[j + frameOffset] - minv\n const idx = 4 * Math.min(Math.max(v, 0), nLabel - 1)\n rgba8[k + 0] = lut[idx + 0]\n rgba8[k + 1] = lut[idx + 1]\n rgba8[k + 2] = lut[idx + 2]\n rgba8[k + 3] = Math.round((lut[idx + 3] / 255) * layer.opacity * 255)\n k += 4\n }\n let opaque = new Array(nvtx).fill(false)\n if (layer.outlineBorder !== 0.0) {\n opaque = NVMeshUtilities.getClusterBoundary(rgba8, this.tris)\n }\n k = 0\n for (let j = 0; j < nvtx; j++) {\n let vtx = j * 28 + 24 // posNormClr is 28 bytes stride, RGBA color at offset 24,\n if (this.f32PerVertex !== 7) {\n vtx = j * 20 + 16\n }\n let opa = rgba8[k + 3] / 255\n if (opaque[j]) {\n opa = layer.outlineBorder\n if (layer.outlineBorder < 0) {\n u8[vtx + 0] = 0\n u8[vtx + 1] = 0\n u8[vtx + 2] = 0\n k += 4\n continue\n }\n }\n u8[vtx + 0] = lerp(u8[vtx + 0], rgba8[k + 0], opa)\n u8[vtx + 1] = lerp(u8[vtx + 1], rgba8[k + 1], opa)\n u8[vtx + 2] = lerp(u8[vtx + 2], rgba8[k + 2], opa)\n k += 4\n } // for each vertex\n continue\n } // if colormapLabel\n if (layer.values instanceof Uint8Array) {\n const rgba8 = new Uint8Array(layer.values.buffer)\n let opaque = new Array(nvtx).fill(true)\n if (layer.outlineBorder !== 0) {\n opaque = NVMeshUtilities.getClusterBoundary(rgba8, this.tris)\n }\n let k = 0\n for (let j = 0; j < layer.values.length; j++) {\n let vtx = j * 28 + 24 // posNormClr is 28 bytes stride, RGBA color at offset 24,\n if (this.f32PerVertex !== 7) {\n vtx = j * 20 + 16\n }\n let opa = opacity\n if (opaque[j]) {\n opa = layer.outlineBorder\n if (layer.outlineBorder < 0) {\n u8[vtx + 0] = 0\n u8[vtx + 1] = 0\n u8[vtx + 2] = 0\n k += 4\n continue\n }\n }\n u8[vtx + 0] = lerp(u8[vtx + 0], rgba8[k + 0], opa)\n u8[vtx + 1] = lerp(u8[vtx + 1], rgba8[k + 1], opa)\n u8[vtx + 2] = lerp(u8[vtx + 2], rgba8[k + 2], opa)\n k += 4\n }\n continue\n }\n if (layer.useNegativeCmap) {\n layer.cal_min = Math.max(Number.EPSILON, layer.cal_min)\n layer.cal_max = Math.max(layer.cal_min + 0.000001, layer.cal_max)\n }\n if (layer.isTransparentBelowCalMin === undefined) {\n layer.isTransparentBelowCalMin = true\n }\n const lut = cmapper.colormap(layer.colormap, layer.colormapInvert)\n if (layer.isAdditiveBlend) {\n maxAdditiveBlend++\n }\n this.blendColormap(u8, additiveRGBA, layer, layer.cal_min, layer.cal_max, lut)\n if (layer.useNegativeCmap) {\n const neglut = cmapper.colormap(layer.colormapNegative, layer.colormapInvert)\n let mn = layer.cal_min\n let mx = layer.cal_max\n if (isFinite(layer.cal_minNeg) && isFinite(layer.cal_minNeg)) {\n mn = -layer.cal_minNeg\n mx = -layer.cal_maxNeg\n }\n this.blendColormap(u8, additiveRGBA, layer, mn, mx, neglut, true)\n }\n }\n }\n if (maxAdditiveBlend > 0) {\n for (let j = 0; j < nvtx; j++) {\n let vtx = j * 28 + 24 // posNormClr is 28 bytes stride, RGBA color at offset 24,\n if (this.f32PerVertex !== 7) {\n vtx = j * 20 + 16\n }\n const v = j * 4 // additiveRGBA is 4 bytes stride, RGBA color at offset 0,\n const opacity = Math.min(maxAdditiveBlend, additiveRGBA[v + 3] / 255)\n if (opacity <= 0) {\n continue\n }\n function modulate(x: number, y: number): number {\n return Math.min(x * y * (1 / 255), 255.0)\n }\n u8[vtx + 0] = modulate(u8[vtx + 0], additiveRGBA[v + 0])\n u8[vtx + 1] = modulate(u8[vtx + 1], additiveRGBA[v + 1])\n u8[vtx + 2] = modulate(u8[vtx + 2], additiveRGBA[v + 2])\n u8[vtx + 0] = lerp(u8[vtx + 0], additiveRGBA[v + 0], opacity)\n u8[vtx + 1] = lerp(u8[vtx + 1], additiveRGBA[v + 1], opacity)\n u8[vtx + 2] = lerp(u8[vtx + 2], additiveRGBA[v + 2], opacity)\n }\n } // isAdditiveBlend\n // generate webGL buffers and vao\n gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer)\n gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, Uint32Array.from(tris), gl.STATIC_DRAW)\n gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer)\n // issue1129\n // gl.bufferData(gl.ARRAY_BUFFER, Float32Array.from(posNormClr), gl.STATIC_DRAW)\n gl.bufferData(gl.ARRAY_BUFFER, u8, gl.STATIC_DRAW)\n this.indexCount = tris.length\n this.vertexCount = this.pts.length\n } // updateMesh()\n\n // internal function filters mesh to identify which color of triangulated mesh vertices\n reverseFaces(gl: WebGL2RenderingContext): void {\n if (this.offsetPt0) {\n return\n } // fiber not mesh\n if (this.hasConnectome) {\n return\n } // connectome not mesh\n const tris = this.tris || [] // TODO tris should probably be assigned in the constructor\n for (let j = 0; j < tris.length; j += 3) {\n const tri = tris[j]\n tris[j] = tris[j + 1]\n tris[j + 1] = tri\n }\n this.updateMesh(gl) // apply the new properties...\n }\n\n hierarchicalOrder(): number {\n const V0 = 12\n const F0 = 20\n const nF = this.tris.length / 3\n const order = Math.log(nF / F0) / Math.log(4)\n // Sanity checks\n if (nF !== Math.pow(4, order) * F0) {\n return NaN\n }\n const nV = this.pts.length / 3\n if (nV !== Math.pow(4, order) * (V0 - 2) + 2) {\n return NaN\n }\n // next checks are in case FreeSurfer was optimized with more local face indices\n // for an example see BrainMesh_ICBM152.lh.mz3\n for (let i = 0; i < 15; i += 3) {\n if (this.tris[i] !== 0) {\n return NaN\n }\n }\n for (let i = 15; i < 24; i += 3) {\n if (this.tris[i] !== 3) {\n return NaN\n }\n }\n for (let i = 24; i < 30; i += 3) {\n if (this.tris[i] !== 4) {\n return NaN\n }\n }\n return order\n }\n\n decimateFaces(n: number, ntarget: number): void {\n let fac = this.tris\n // Constants for the icosahedron\n const V0 = 12\n const F0 = 20\n for (let j = n - 1; j >= ntarget; j--) {\n const nVjprev = Math.pow(4, j + 1) * (V0 - 2) + 2\n const nVj = Math.pow(4, j) * (V0 - 2) + 2\n const nFjprev = fac.length / 3 // = 4^(j+1)*F0\n const nFj = Math.pow(4, j) * F0\n\n log.info(`order ${j + 1} -> ${j} vertices ${nVjprev} -> ${nVj} faces ${nFjprev} -> ${nFj}`)\n\n const remap = Array.from({ length: nVjprev }, (_, i) => i + 1)\n\n for (let i = 0; i < nFjprev; i++) {\n const v1 = fac[3 * i]\n const v2 = fac[3 * i + 1]\n const v3 = fac[3 * i + 2]\n remap[v1 - 1] = Math.min(remap[v1 - 1], v2, v3)\n }\n\n const facJ = new Uint32Array(nFj * 3)\n for (let i = 0; i < nFj; i++) {\n facJ[3 * i] = remap[fac[3 * i] - 1]\n facJ[3 * i + 1] = remap[fac[3 * i + 1] - 1]\n facJ[3 * i + 2] = remap[fac[3 * i + 2] - 1]\n }\n fac = facJ\n }\n this.tris = new Uint32Array(fac)\n }\n\n // internal function simplifies FreeSurfer triangulated mesh and overlays\n decimateHierarchicalMesh(gl: WebGL2RenderingContext, order: number = 4): boolean {\n const inputOrder = this.hierarchicalOrder()\n if (isNaN(inputOrder)) {\n log.warn('Unable to decimate mesh: it does not have a hierarchical structure')\n return false\n }\n if (order >= inputOrder) {\n log.warn(`Unable to decimate mesh: input order (${inputOrder}) must be larger than downsampled order (${order})`)\n return false\n }\n const inputVLength = this.pts.length / 3\n const V0 = 12\n const nV = Math.pow(4, order) * (V0 - 2) + 2\n this.pts = new Float32Array(this.pts.slice(0, nV * 3))\n this.decimateFaces(inputOrder, order)\n if (this.layers && this.layers.length > 0) {\n for (let i = 0; i < this.layers.length; i++) {\n const layer = this.layers[i]\n if (layer.values instanceof Float32Array || layer.values.length !== inputVLength) {\n layer.values = new Float32Array(layer.values.slice(0, nV))\n } else {\n log.warn(`decimation logic needs to be updated`)\n }\n }\n }\n this.updateMesh(gl) // apply the new properties...\n return true\n }\n\n // adjust attributes of a mesh layer. invoked by niivue.setMeshLayerProperty()\n // TODO this method is a bit too generic\n async setLayerProperty(id: number, key: keyof NVMeshLayer, val: number | string | boolean, gl: WebGL2RenderingContext): Promise<void> {\n const layer = this.layers[id]\n if (!layer || !(key in layer)) {\n log.warn('mesh does not have property ', key, ' for layer ', layer)\n return\n }\n if (key === 'colormapLabel') {\n if (typeof val === 'object') {\n // assume JSON\n layer[key] = cmapper.makeLabelLut(val, 255, layer.global_max)\n } else if (typeof val === 'string') {\n // assume URL\n const cmap = await cmapper.makeLabelLutFromUrl(val)\n layer[key] = cmap\n this.updateMesh(gl) // apply the new properties...\n return\n } else {\n log.error('colormapLabel requires a string or object')\n }\n } else {\n // @ts-expect-error TODO generic property access\n layer[key] = val\n }\n this.updateMesh(gl) // apply the new properties...\n }\n\n // adjust mesh attributes. invoked by niivue.setMeshProperty(()\n // TODO this method is too generic\n setProperty(key: keyof this, val: number | string | boolean | Uint8Array | number[] | ColorMap | LegacyConnectome | Float32Array, gl: WebGL2RenderingContext): void {\n if (!(key in this)) {\n console.warn('Mesh does not have property:', key, this)\n return\n }\n /* if (typeof val !== 'number' && typeof val !== 'string' && typeof val !== 'boolean' && !Array.isArray(val)) {\n console.warn('Invalid value type. Expected number, numbers, string, or boolean but received:', typeof val)\n return\n } */\n ;(this as any)[key] = val // TypeScript safety workaround\n this.updateMesh(gl) // Apply the new properties\n }\n\n // Each streamline vertex has color, normal and position attributes\n // Interleaved Vertex Data https://developer.apple.com/library/archive/documentation/3DDrawing/Conceptual/OpenGLES_ProgrammingGuide/TechniquesforWorkingwithVertexData/TechniquesforWorkingwithVertexData.html\n generatePosNormClr(pts: Float32Array, tris: Uint32Array, rgba255: Uint8Array): Float32Array {\n if (pts.length < 3 || rgba255.length < 4) {\n log.error('Catastrophic failure generatePosNormClr()')\n log.debug('this', this)\n log.debug('pts', pts)\n log.debug('rgba', rgba255)\n }\n const norms = NVMeshUtilities.generateNormals(pts, tris)\n const npt = pts.length / 3\n const isPerVertexColors = npt === rgba255.length / 4\n // n32\n const f32PerVertex = this.f32PerVertex // 7 if NormalXYZ is 3 floats, 5 if normalXYZ is packed into rgb32\n const f32 = new Float32Array(npt * f32PerVertex) // Each vertex has 7 components: PositionXYZ, NormalXYZ, RGBA32\n const u8 = new Uint8Array(f32.buffer) // Each vertex has 7 components: PositionXYZ, NormalXYZ, RGBA32\n let p = 0 // input position\n let c = 0 // input color\n let f = 0 // output float32 location (position and normals)\n let u = (f32PerVertex - 1) * 4 // output uint8 location (colors), offset 24 as after 3*position+3*normal\n for (let i = 0; i < npt; i++) {\n f32[f + 0] = pts[p + 0]\n f32[f + 1] = pts[p + 1]\n f32[f + 2] = pts[p + 2]\n if (f32PerVertex !== 7) {\n u8[u - 4] = norms[p + 0] * 127\n u8[u - 3] = norms[p + 1] * 127\n u8[u - 2] = norms[p + 2] * 127\n } else {\n f32[f + 3] = norms[p + 0]\n f32[f + 4] = norms[p + 1]\n f32[f + 5] = norms[p + 2]\n }\n u8[u] = rgba255[c + 0]\n u8[u + 1] = rgba255[c + 1]\n u8[u + 2] = rgba255[c + 2]\n u8[u + 3] = rgba255[c + 3]\n if (isPerVertexColors) {\n c += 4\n }\n p += 3 // read 3 input components: XYZ\n f += f32PerVertex // write 7 output components: 3*Position, 3*Normal, 1*RGBA\n u += f32PerVertex * 4 // stride of 28 bytes\n }\n return f32\n }\n\n // wrapper to read meshes, tractograms and connectomes regardless of format\n static async readMesh(buffer: ArrayBuffer, name: string, gl: WebGL2RenderingContext, opacity = 1.0, rgba255 = new Uint8Array([255, 255, 255, 255]), visible = true): Promise<NVMesh> {\n let tris: Uint32Array = new Uint32Array([])\n let pts: Float32Array = new Float32Array([])\n let anatomicalStructurePrimary = ''\n let obj: TCK | TRACT | TT | TRX | TRK | GII | MZ3 | X3D | VTK | DefaultMeshType\n const re = /(?:\\.([^.]+))?$/\n let ext = re.exec(name)![1]\n ext = ext.toUpperCase()\n if (ext === 'GZ') {\n ext = re.exec(name.slice(0, -3))![1] // img.trk.gz -> img.trk\n ext = ext.toUpperCase()\n }\n if (ext === 'JCON') {\n // return NVMesh.loadConnectomeFromJSON(JSON.parse(new TextDecoder().decode(buffer)), gl, name, opacity)\n log.error('you should never see this message: load using nvconnectome not nvmesh')\n }\n if (ext === 'JSON') {\n // return NVMesh.loadConnectomeFromFreeSurfer(JSON.parse(new TextDecoder().decode(buffer)), gl, name, opacity)\n log.error('you should never see this message: load using nvconnectome not nvmesh')\n }\n rgba255[3] = Math.max(1, rgba255[3])\n if (ext === 'TCK' || ext === 'TRK' || ext === 'TT' || ext === 'TRX' || ext === 'TRACT') {\n if (ext === 'TCK') {\n obj = NVMeshLoaders.readTCK(buffer)\n } else if (ext === 'TRACT') {\n obj = NVMeshLoaders.readTRACT(buffer)\n } else if (ext === 'TT') {\n obj = await NVMeshLoaders.readTT(buffer)\n } else if (ext === 'TRX') {\n obj = await NVMeshLoaders.readTRX(buffer)\n } else {\n obj = await NVMeshLoaders.readTRK(buffer)\n }\n if (typeof obj === 'undefined') {\n const pts = new Float32Array([0, 0, 0, 0, 0, 0])\n const offsetPt0 = new Uint32Array([0])\n obj = { pts, offsetPt0 }\n log.error('Creating empty tracts')\n }\n rgba255[3] = 0.0\n return new NVMesh(\n obj.pts,\n obj.offsetPt0,\n name,\n rgba255, // colormap,\n opacity, // opacity,\n visible, // visible,\n gl,\n 'inferno',\n (obj as TRX).dpg || null,\n (obj as TRX).dps || null,\n (obj as TRX).dpv || null,\n (obj as TRX).groups\n )\n } // is fibers\n if (ext === 'GII') {\n obj = await NVMeshLoaders.readGII(buffer)\n } else if (ext === 'MZ3') {\n obj = await NVMeshLoaders.readMZ3(buffer)\n if (!('positions' in obj)) {\n log.warn('MZ3 does not have positions (statistical overlay?)')\n }\n } else if (ext === 'ASC') {\n obj = NVMeshLoaders.readASC(buffer)\n } else if (ext === 'DFS') {\n obj = NVMeshLoaders.readDFS(buffer)\n } else if (ext === 'BYU' || ext === 'G') {\n obj = NVMeshLoaders.readGEO(buffer)\n } else if (ext === 'GEO') {\n obj = NVMeshLoaders.readGEO(buffer, true)\n } else if (ext === 'ICO' || ext === 'TRI') {\n obj = NVMeshLoaders.readICO(buffer)\n } else if (ext === 'OFF') {\n obj = NVMeshLoaders.readOFF(buffer)\n } else if (ext === 'NV') {\n obj = NVMeshLoaders.readNV(buffer)\n } else if (ext === 'OBJ') {\n obj = await NVMeshLoaders.readOBJ(buffer)\n } else if (ext === 'PLY') {\n obj = NVMeshLoaders.readPLY(buffer)\n } else if (ext === 'WRL') {\n obj = NVMeshLoaders.readWRL(buffer)\n } else if (ext === 'X3D') {\n obj = NVMeshLoaders.readX3D(buffer)\n } else if (ext === 'FIB' || ext === 'VTK') {\n obj = NVMeshLoaders.readVTK(buffer)\n if ('offsetPt0' in obj) {\n // VTK files used both for meshes and streamlines\n rgba255[3] = 0.0\n return new NVMesh(\n obj.pts,\n obj.offsetPt0,\n name,\n rgba255, // colormap,\n opacity, // opacity,\n visible, // visible,\n gl,\n 'inferno'\n )\n } // if streamlines, not mesh\n } else if (ext === 'SRF') {\n obj = await NVMeshLoaders.readSRF(buffer)\n } else if (ext === 'STL') {\n obj = NVMeshLoaders.readSTL(buffer)\n } else {\n obj = NVMeshLoaders.readFreeSurfer(buffer)\n } // freesurfer hail mary\n if ((obj as GII).anatomicalStructurePrimary) {\n anatomicalStructurePrimary = (obj as GII).anatomicalStructurePrimary\n }\n if (obj instanceof Float32Array) {\n throw new Error('fatal: unknown mesh type loaded')\n }\n\n if (!('positions' in obj)) {\n throw new Error('positions not loaded')\n }\n if (!obj.indices) {\n throw new Error('indices not loaded')\n }\n\n pts = obj.positions\n tris = obj.indices\n\n if ('rgba255' in obj && obj.rgba255.length > 0) {\n // e.g. x3D format\n // rgba255 = Array.from(obj.rgba255)\n rgba255 = obj.rgba255\n }\n if ('colors' in obj && obj.colors && obj.colors.length === pts.length) {\n const n = pts.length / 3\n rgba255 = new Uint8Array(n * 4)\n let c = 0\n let k = 0\n for (let i = 0; i < n; i++) {\n // convert ThreeJS unit RGB to RGBA255\n rgba255[k++] = obj.colors[c] * 255 // red\n rgba255[k++] = obj.colors[c + 1] * 255 // green\n rgba255[k++] = obj.colors[c + 2] * 255 // blue\n rgba255[k++] = 255 // alpha\n c += 3\n } // for i: each vertex\n } // obj includes colors\n const npt = pts.length / 3\n const ntri = tris.length / 3\n if (ntri < 1 || npt < 3) {\n throw new Error('Mesh should have at least one triangle and three vertices')\n }\n rgba255[3] = Math.max(1, rgba255[3]) // mesh not streamline\n const nvm = new NVMesh(\n pts,\n tris,\n name,\n rgba255, // colormap,\n opacity, // opacity,\n visible, // visible,\n gl,\n null, // connectome\n null, // dpg\n null, // dps\n null, // dpv\n null, // groups\n true, // colorbarVisible\n anatomicalStructurePrimary\n )\n if ('scalars' in obj && obj.scalars.length > 0) {\n const newLayer = await NVMeshLoaders.readLayer(name, buffer, nvm, opacity, 'gray')\n if (typeof newLayer === 'undefined') {\n log.warn('readLayer() failed to convert scalars')\n } else {\n nvm.layers.push(newLayer)\n nvm.updateMesh(gl)\n }\n }\n return nvm\n }\n\n static async loadLayer(layer: NVMeshLayer, nvmesh: NVMesh): Promise<void> {\n let buffer = new Uint8Array().buffer\n\n function base64ToArrayBuffer(base64: string): ArrayBuffer {\n const binary_string = window.atob(base64)\n const len = binary_string.length\n const bytes = new Uint8Array(len)\n for (let i = 0; i < len; i++) {\n bytes[i] = binary_string.charCodeAt(i)\n }\n return bytes.buffer\n }\n\n if (layer.base64 !== undefined) {\n // populate buffer with base64 if exists\n buffer = base64ToArrayBuffer(layer.base64)\n } else {\n if (!layer.url) {\n throw new Error('layer: missing url')\n }\n // fetch url otherwise\n const response = await fetch(layer.url, { headers: layer.headers })\n if (!response.ok) {\n throw Error(response.statusText)\n }\n buffer = await response.arrayBuffer()\n }\n\n let layerName: string\n let urlParts: string[] = []\n if (layer.name && layer.name !== '') {\n layerName = layer.name\n } else {\n if (!layer.url) {\n throw new Error('layer: missing url')\n }\n // urlParts = layer.url.split(\"/\");\n // layerName = urlParts.slice(-1)[0];\n try {\n // if a full url like https://domain/path/file.nii.gz?query=filter\n // parse the url and get the pathname component without the query\n urlParts = new URL(layer.url).pathname.split('/')\n } catch (e) {\n // if a relative url then parse the path (assuming no query)\n urlParts = layer.url.split('/')\n } finally {\n layerName = urlParts.slice(-1)[0]\n }\n }\n if (layerName.indexOf('?') > -1) {\n layerName = layerName.slice(0, layerName.indexOf('?')) // remove query string if any\n }\n\n let opacity = 0.5\n if ('opacity' in layer) {\n opacity = layer.opacity\n }\n let colormap = 'warm'\n if ('colormap' in layer) {\n colormap = layer.colormap!\n }\n let colormapNegative = 'winter'\n if ('colormapNegative' in layer) {\n colormapNegative = layer.colormapNegative!\n }\n let useNegativeCmap = false\n if ('useNegativeCmap' in layer) {\n useNegativeCmap = layer.useNegativeCmap!\n }\n let cal_min: number | null = null\n if ('cal_min' in layer) {\n cal_min = layer.cal_min\n }\n let cal_max: number | null = null\n if ('cal_max' in layer) {\n cal_max = layer.cal_max\n }\n\n const newLayer = await NVMeshLoaders.readLayer(layerName, buffer, nvmesh, opacity, colormap, colormapNegative, useNegativeCmap, cal_min, cal_max)\n if (newLayer) {\n nvmesh.layers.push(newLayer)\n }\n }\n\n /**\n * factory function to load and return a new NVMesh instance from a given URL\n */\n static async loadFromUrl({\n url = '',\n headers = {},\n gl,\n name = '',\n opacity = 1.0,\n rgba255 = [255, 255, 255, 255],\n visible = true,\n layers = [],\n buffer = new ArrayBuffer(0),\n meshShaderIndex = 0\n }: Partial<LoadFromUrlParams> = {}): Promise<NVMesh> {\n let urlParts = url.split('/') // split url parts at slash\n if (name === '') {\n try {\n // if a full url like https://domain/path/file.nii.gz?query=filter\n // parse the url and get the pathname component without the query\n urlParts = new URL(url).pathname.split('/')\n } catch (e) {\n // if a relative url then parse the path (assuming no query)\n urlParts = url.split('/')\n }\n name = urlParts.slice(-1)[0] // name will be last part of url (e.g. some/url/image.nii.gz --> image.nii.gz\n if (name.indexOf('?') > -1) {\n name = name.slice(0, name.indexOf('?')) // remove query string if any\n }\n }\n if (url === '') {\n throw Error('url must not be empty')\n }\n if (!gl) {\n throw Error('gl context is null')\n }\n let buff\n if (buffer.byteLength > 0) {\n buff = buffer\n } else {\n // TRX format is special (its a zip archive of multiple files)\n const response = await fetch(url, { headers })\n if (!response.ok) {\n throw Error(response.statusText)\n }\n buff = await response.arrayBuffer()\n }\n const nvmesh = await this.readMesh(buff, name, gl, opacity, new Uint8Array(rgba255), visible)\n\n // Set mesh shader index if provided\n if (meshShaderIndex !== undefined) {\n nvmesh.meshShaderIndex = meshShaderIndex\n }\n\n if (!layers || layers.length < 1) {\n return nvmesh\n }\n\n for (let i = 0; i < layers.length; i++) {\n await NVMesh.loadLayer(layers[i], nvmesh)\n }\n\n // apply the new properties\n nvmesh.updateMesh(gl)\n return nvmesh\n }\n\n // not included in public docs\n // loading Nifti files\n static async readFileAsync(file: Blob): Promise<ArrayBuffer> {\n return new Promise((resolve, reject) => {\n const reader = new FileReader()\n\n reader.onload = (): void => {\n resolve(reader.result as ArrayBuffer)\n }\n\n reader.onerror = reject\n\n reader.readAsArrayBuffer(file)\n })\n }\n\n /**\n * factory function to load and return a new NVMesh instance from a file in the browser\n *\n * @returns NVMesh instance\n */\n static async loadFromFile({ file, gl, name = '', opacity = 1.0, rgba255 = [255, 255, 255, 255], visible = true, layers = [] }: Partial<LoadFromFileParams> = {}): Promise<NVMesh> {\n if (!file) {\n throw new Error('file must be set')\n }\n if (!gl) {\n throw new Error('rendering context must be set')\n }\n\n const buffer = await NVMesh.readFileAsync(file)\n const nvmesh = await NVMesh.readMesh(buffer, name, gl, opacity, new Uint8Array(rgba255), visible)\n\n if (!layers || layers.length < 1) {\n return nvmesh\n }\n\n for (let i = 0; i < layers.length; i++) {\n await NVMesh.loadLayer(layers[i], nvmesh)\n }\n\n // apply the new properties\n nvmesh.updateMesh(gl)\n return nvmesh\n }\n\n /**\n * load and return a new NVMesh instance from a base64 encoded string\n */\n async loadFromBase64({ base64, gl, name = '', opacity = 1.0, rgba255 = [255, 255, 255, 255], visible = true, layers = [] }: Partial<LoadFromBase64Params> = {}): Promise<NVMesh> {\n if (!base64) {\n throw new Error('base64 must bet set')\n }\n if (!gl) {\n throw new Error('rendering context must be set')\n }\n\n // https://stackoverflow.com/questions/21797299/convert-base64-string-to-arraybuffer\n function base64ToArrayBuffer(base64: string): ArrayBuffer {\n const binary_string = window.atob(base64)\n const len = binary_string.length\n const bytes = new Uint8Array(len)\n for (let i = 0; i < len; i++) {\n bytes[i] = binary_string.charCodeAt(i)\n }\n return bytes.buffer\n }\n\n const buffer = base64ToArrayBuffer(base64)\n const nvmesh = await NVMesh.readMesh(buffer, name, gl, opacity, new Uint8Array(rgba255), visible)\n\n if (!layers || layers.length < 1) {\n return nvmesh\n }\n for (let i = 0; i < layers.length; i++) {\n await NVMesh.loadLayer(layers[i], nvmesh)\n }\n\n // apply new properties\n nvmesh.updateMesh(gl)\n return nvmesh\n }\n}\n","import { mat4, vec3, vec4 } from 'gl-matrix'\n\ntype Geometry = {\n vertexBuffer: WebGLBuffer\n indexBuffer: WebGLBuffer\n indexCount: number\n vao: WebGLVertexArrayObject | null\n}\n\n/**\n * Represents a 3D object rendered with WebGL, including geometry, transformations, and rendering state.\n * Used internally by Niivue for rendering meshes and crosshairs.\n */\nexport class NiivueObject3D {\n static BLEND = 1\n static CULL_FACE = 2\n static CULL_FRONT = 4\n static CULL_BACK = 8\n static ENABLE_DEPTH_TEST = 16\n\n sphereIdx: number[] = []\n sphereVtx: number[] = []\n renderShaders: number[] = []\n isVisible = true\n isPickable = true\n vertexBuffer: WebGLVertexArrayObject\n indexCount: number\n indexBuffer: WebGLVertexArrayObject | null\n vao: WebGLVertexArrayObject | null\n mode: number\n glFlags = 0\n id: number\n colorId: [number, number, number, number]\n\n modelMatrix = mat4.create()\n scale = [1, 1, 1]\n position = [0, 0, 0]\n rotation = [0, 0, 0]\n rotationRadians = 0.0\n\n extentsMin: number[] = []\n extentsMax: number[] = []\n\n // TODO needed through NVImage\n furthestVertexFromOrigin?: number\n originNegate?: vec3\n fieldOfViewDeObliqueMM?: vec3\n\n // TODO needed through crosshairs in NiiVue\n mm?: vec4\n\n constructor(id: number, vertexBuffer: WebGLBuffer, mode: number, indexCount: number, indexBuffer: WebGLVertexArrayObject | null = null, vao: WebGLVertexArrayObject | null = null) {\n this.vertexBuffer = vertexBuffer\n this.indexCount = indexCount\n this.indexBuffer = indexBuffer\n this.vao = vao\n this.mode = mode\n\n this.id = id\n this.colorId = [((id >> 0) & 0xff) / 255.0, ((id >> 8) & 0xff) / 255.0, ((id >> 16) & 0xff) / 255.0, ((id >> 24) & 0xff) / 255.0]\n }\n\n static generateCrosshairs = function (gl: WebGL2RenderingContext, id: number, xyzMM: vec4, xyzMin: vec3, xyzMax: vec3, radius: number, sides = 20, gap = 0): NiivueObject3D {\n const geometry = NiivueObject3D.generateCrosshairsGeometry(gl, xyzMM, xyzMin, xyzMax, radius, sides, gap)\n return new NiivueObject3D(id, geometry.vertexBuffer, gl.TRIANGLES, geometry.indexCount, geometry.indexBuffer, geometry.vao)\n }\n\n // not included in public docs\n static generateCrosshairsGeometry = function (gl: WebGL2RenderingContext, xyzMM: vec4, xyzMin: vec3, xyzMax: vec3, radius: number, sides = 20, gap = 0): Geometry {\n const vertices: number[] = []\n const indices: number[] = []\n const gapX = radius * gap\n if (gapX <= 0) {\n // left-right\n let start = vec3.fromValues(xyzMin[0], xyzMM[1], xyzMM[2])\n let dest = vec3.fromValues(xyzMax[0], xyzMM[1], xyzMM[2])\n NiivueObject3D.makeCylinder(vertices, indices, start, dest, radius, sides)\n // anterior-posterior\n start = vec3.fromValues(xyzMM[0], xyzMin[1], xyzMM[2])\n dest = vec3.fromValues(xyzMM[0], xyzMax[1], xyzMM[2])\n NiivueObject3D.makeCylinder(vertices, indices, start, dest, radius, sides)\n // superior-inferior\n start = vec3.fromValues(xyzMM[0], xyzMM[1], xyzMin[2])\n dest = vec3.fromValues(xyzMM[0], xyzMM[1], xyzMax[2])\n NiivueObject3D.makeCylinder(vertices, indices, start, dest, radius, sides)\n } else {\n // left-right\n let start = vec3.fromValues(xyzMin[0], xyzMM[1], xyzMM[2])\n let dest = vec3.fromValues(xyzMM[0] - gapX, xyzMM[1], xyzMM[2])\n NiivueObject3D.makeCylinder(vertices, indices, start, dest, radius, sides, false)\n start = vec3.fromValues(xyzMM[0] + gapX, xyzMM[1], xyzMM[2])\n dest = vec3.fromValues(xyzMax[0], xyzMM[1], xyzMM[2])\n NiivueObject3D.makeCylinder(vertices, indices, start, dest, radius, sides, false)\n // anterior-posterior\n start = vec3.fromValues(xyzMM[0], xyzMin[1], xyzMM[2])\n dest = vec3.fromValues(xyzMM[0], xyzMM[1] - gapX, xyzMM[2])\n NiivueObject3D.makeCylinder(vertices, indices, start, dest, radius, sides, false)\n start = vec3.fromValues(xyzMM[0], xyzMM[1] + gapX, xyzMM[2])\n dest = vec3.fromValues(xyzMM[0], xyzMax[1], xyzMM[2])\n NiivueObject3D.makeCylinder(vertices, indices, start, dest, radius, sides, false)\n // superior-inferior\n start = vec3.fromValues(xyzMM[0], xyzMM[1], xyzMin[2])\n dest = vec3.fromValues(xyzMM[0], xyzMM[1], xyzMM[2] - gapX)\n NiivueObject3D.makeCylinder(vertices, indices, start, dest, radius, sides, false)\n start = vec3.fromValues(xyzMM[0], xyzMM[1], xyzMM[2] + gapX)\n dest = vec3.fromValues(xyzMM[0], xyzMM[1], xyzMax[2])\n NiivueObject3D.makeCylinder(vertices, indices, start, dest, radius, sides, false)\n }\n // console.log('i:',indices.length / 3, 'v:',vertices.length / 3);\n const vertexBuffer = gl.createBuffer()\n if (vertexBuffer === null) {\n throw new Error('could not instantiate vertex buffer')\n }\n\n gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer)\n gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW)\n\n // index buffer allocated in parent class\n const indexBuffer = gl.createBuffer()\n if (indexBuffer === null) {\n throw new Error('could not instantiate index buffer')\n }\n gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer)\n gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint32Array(indices), gl.STATIC_DRAW)\n\n const vao = gl.createVertexArray()\n gl.bindVertexArray(vao)\n gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer)\n gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer)\n // vertex position: 3 floats X,Y,Z\n gl.enableVertexAttribArray(0)\n gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 0, 0)\n gl.bindVertexArray(null) // https://stackoverflow.com/questions/43904396/are-we-not-allowed-to-bind-gl-array-buffer-and-vertex-attrib-array-to-0-in-webgl\n\n return {\n vertexBuffer,\n indexBuffer,\n indexCount: indices.length,\n vao\n }\n }\n\n static getFirstPerpVector = function (v1: vec3): vec3 {\n const v2 = vec3.fromValues(0.0, 0.0, 0.0)\n if (v1[0] === 0.0) {\n v2[0] = 1.0\n } else if (v1[1] === 0.0) {\n v2[1] = 1.0\n } else if (v1[2] === 0.0) {\n v2[2] = 1.0\n } else {\n // If xyz is all set, we set the z coordinate as first and second argument .\n // As the scalar product must be zero, we add the negated sum of x and y as third argument\n v2[0] = v1[2] // scalp = z*x\n v2[1] = v1[2] // scalp = z*(x+y)\n v2[2] = -(v1[0] + v1[1]) // scalp = z*(x+y)-z*(x+y) = 0\n vec3.normalize(v2, v2)\n }\n return v2\n }\n\n static subdivide = function (verts: number[], faces: number[]): void {\n // Subdivide each triangle into four triangles, pushing verts to the unit sphere\"\"\"\n let nv = verts.length / 3\n let nf = faces.length / 3\n const n = nf\n const vNew = vec3.create()\n const nNew = vec3.create()\n for (let faceIndex = 0; faceIndex < n; faceIndex++) {\n // setlength(verts, nv + 3);\n const fx = faces[faceIndex * 3 + 0]\n const fy = faces[faceIndex * 3 + 1]\n const fz = faces[faceIndex * 3 + 2]\n const vx = vec3.fromValues(verts[fx * 3 + 0], verts[fx * 3 + 1], verts[fx * 3 + 2])\n const vy = vec3.fromValues(verts[fy * 3 + 0], verts[fy * 3 + 1], verts[fy * 3 + 2])\n const vz = vec3.fromValues(verts[fz * 3 + 0], verts[fz * 3 + 1], verts[fz * 3 + 2])\n vec3.add(vNew, vx, vy)\n vec3.normalize(nNew, vNew)\n verts.push(...nNew)\n\n vec3.add(vNew, vy, vz)\n vec3.normalize(nNew, vNew)\n verts.push(...nNew)\n\n vec3.add(vNew, vx, vz)\n vec3.normalize(nNew, vNew)\n verts.push(...nNew)\n // Split the current triangle into four smaller triangles:\n let face = [nv, nv + 1, nv + 2]\n faces.push(...face)\n face = [fx, nv, nv + 2]\n faces.push(...face)\n face = [nv, fy, nv + 1]\n faces.push(...face)\n faces[faceIndex * 3 + 0] = nv + 2\n faces[faceIndex * 3 + 1] = nv + 1\n faces[faceIndex * 3 + 2] = fz\n nf = nf + 3\n nv = nv + 3\n }\n }\n\n static weldVertices = function (verts: number[], faces: number[]): number[] {\n // unify identical vertices\n const nv = verts.length / 3\n // yikes: bubble sort! TO DO: see Surfice for more efficient solution\n let nUnique = 0 // first vertex is unique\n // var remap = new Array();\n const remap = new Int32Array(nv)\n for (let i = 0; i < nv - 1; i++) {\n if (remap[i] !== 0) {\n continue\n } // previously tested\n remap[i] = nUnique\n let v = i * 3\n const x = verts[v]\n const y = verts[v + 1]\n const z = verts[v + 2]\n for (let j = i + 1; j < nv; j++) {\n v += 3\n if (x === verts[v] && y === verts[v + 1] && z === verts[v + 2]) {\n remap[j] = nUnique\n }\n }\n nUnique++ // another new vertex\n } // for i\n if (nUnique === nv) {\n return verts\n }\n // console.log('welding vertices removed redundant positions ', nv, '->', nUnique);\n const nf = faces.length\n for (let f = 0; f < nf; f++) {\n faces[f] = remap[faces[f]]\n }\n const vtx = verts.slice(0, nUnique * 3 - 1)\n for (let i = 0; i < nv - 1; i++) {\n const v = i * 3\n const r = remap[i] * 3\n vtx[r] = verts[v]\n vtx[r + 1] = verts[v + 1]\n vtx[r + 2] = verts[v + 2]\n }\n return vtx\n }\n\n static makeSphere = function (vertices: number[], indices: number[], radius: number, origin: vec3 | vec4 = [0, 0, 0]): void {\n let vtx = [\n 0.0, 0.0, 1.0, 0.894, 0.0, 0.447, 0.276, 0.851, 0.447, -0.724, 0.526, 0.447, -0.724, -0.526, 0.447, 0.276, -0.851, 0.447, 0.724, 0.526, -0.447, -0.276, 0.851, -0.447, -0.894, 0.0, -0.447,\n -0.276, -0.851, -0.447, 0.724, -0.526, -0.447, 0.0, 0.0, -1.0\n ]\n // let idx = new Uint16Array([\n const idx = [\n 0, 1, 2, 0, 2, 3, 0, 3, 4, 0, 4, 5, 0, 5, 1, 7, 6, 11, 8, 7, 11, 9, 8, 11, 10, 9, 11, 6, 10, 11, 6, 2, 1, 7, 3, 2, 8, 4, 3, 9, 5, 4, 10, 1, 5, 6, 7, 2, 7, 8, 3, 8, 9, 4, 9, 10, 5, 10, 6, 1\n ]\n NiivueObject3D.subdivide(vtx, idx)\n NiivueObject3D.subdivide(vtx, idx)\n vtx = NiivueObject3D.weldVertices(vtx, idx)\n\n for (let i = 0; i < vtx.length; i++) {\n vtx[i] = vtx[i] * radius\n }\n const nvtx = vtx.length / 3\n let j = 0\n for (let i = 0; i < nvtx; i++) {\n vtx[j] = vtx[j] + origin[0]\n j++\n vtx[j] = vtx[j] + origin[1]\n j++\n vtx[j] = vtx[j] + origin[2]\n j++\n }\n const idx0 = Math.floor(vertices.length / 3) // first new vertex will be AFTER previous vertices\n for (let i = 0; i < idx.length; i++) {\n idx[i] = idx[i] + idx0\n }\n\n indices.push(...idx)\n vertices.push(...vtx)\n }\n\n static makeCylinder = function (vertices: number[], indices: number[], start: vec3, dest: vec3, radius: number, sides = 20, endcaps = true): void {\n if (sides < 3) {\n sides = 3\n } // prism is minimal 3D cylinder\n const v1 = vec3.create()\n vec3.subtract(v1, dest, start)\n vec3.normalize(v1, v1) // principle axis of cylinder\n const v2 = NiivueObject3D.getFirstPerpVector(v1) // a unit length vector orthogonal to v1\n // Get the second perp vector by cross product\n const v3 = vec3.create()\n vec3.cross(v3, v1, v2) // a unit length vector orthogonal to v1 and v2\n vec3.normalize(v3, v3)\n let num_v = 2 * sides\n let num_f = 2 * sides\n if (endcaps) {\n num_f += 2 * sides\n num_v += 2\n }\n const idx0 = Math.floor(vertices.length / 3) // first new vertex will be AFTER previous vertices\n const idx = new Uint32Array(num_f * 3)\n const vtx = new Float32Array(num_v * 3)\n function setV(i: number, vec3: vec3): void {\n vtx[i * 3 + 0] = vec3[0]\n vtx[i * 3 + 1] = vec3[1]\n vtx[i * 3 + 2] = vec3[2]\n }\n function setI(i: number, a: number, b: number, c: number): void {\n idx[i * 3 + 0] = a + idx0\n idx[i * 3 + 1] = b + idx0\n idx[i * 3 + 2] = c + idx0\n }\n const startPole = 2 * sides\n const destPole = startPole + 1\n if (endcaps) {\n setV(startPole, start)\n setV(destPole, dest)\n }\n const pt1 = vec3.create()\n const pt2 = vec3.create()\n for (let i = 0; i < sides; i++) {\n const c = Math.cos((i / sides) * 2 * Math.PI)\n const s = Math.sin((i / sides) * 2 * Math.PI)\n pt1[0] = radius * (c * v2[0] + s * v3[0])\n pt1[1] = radius * (c * v2[1] + s * v3[1])\n pt1[2] = radius * (c * v2[2] + s * v3[2])\n vec3.add(pt2, start, pt1)\n setV(i, pt2)\n vec3.add(pt2, dest, pt1)\n setV(i + sides, pt2)\n let nxt = 0\n if (i < sides - 1) {\n nxt = i + 1\n }\n setI(i * 2, i, nxt, i + sides)\n setI(i * 2 + 1, nxt, nxt + sides, i + sides)\n if (endcaps) {\n setI(sides * 2 + i, i, startPole, nxt)\n setI(sides * 2 + i + sides, destPole, i + sides, nxt + sides)\n }\n }\n indices.push(...idx)\n vertices.push(...vtx)\n }\n\n static makeColoredCylinder = function (\n vertices: number[],\n indices: number[],\n colors: number[],\n start: vec3,\n dest: vec3,\n radius: number,\n rgba255 = [192, 0, 0, 255],\n sides = 20,\n endcaps = false\n ): void {\n let nv = vertices.length / 3\n NiivueObject3D.makeCylinder(vertices, indices, start, dest, radius, sides, endcaps)\n nv = vertices.length / 3 - nv\n const clrs = []\n for (let i = 0; i < nv * 4 - 1; i += 4) {\n clrs[i] = rgba255[0]\n clrs[i + 1] = rgba255[1]\n clrs[i + 2] = rgba255[2]\n clrs[i + 3] = rgba255[3]\n }\n colors.push(...clrs)\n }\n\n static makeColoredSphere = function (vertices: number[], indices: number[], colors: number[], radius: number, origin: vec3 | vec4 = [0, 0, 0], rgba255 = [0, 0, 192, 255]): void {\n let nv = vertices.length / 3\n NiivueObject3D.makeSphere(vertices, indices, radius, origin)\n nv = vertices.length / 3 - nv\n const clrs = []\n for (let i = 0; i < nv * 4 - 1; i += 4) {\n clrs[i] = rgba255[0]\n clrs[i + 1] = rgba255[1]\n clrs[i + 2] = rgba255[2]\n clrs[i + 3] = rgba255[3]\n }\n colors.push(...clrs)\n }\n}\n","import { vec3 } from 'gl-matrix'\nimport { LUT } from '@/colortables'\n\ntype Extents = {\n mxDx: number\n extentsMin: number | number[]\n extentsMax: number | number[]\n}\n\n/**\n * Utilities class for common mesh functions\n */\nexport class NVMeshUtilities {\n static getClusterBoundaryU8(u8: Uint8Array, faces: number[] | Uint32Array): boolean[] {\n // assume all vertices are not near a border\n const border = new Array(u8.length).fill(false)\n const binary = new Array(u8.length).fill(false)\n for (let i = 0; i < u8.length; i++) {\n if (u8[i] > 0) {\n binary[i] = true\n }\n }\n const nTri = faces.length / 3\n let j = 0\n // interior: a triangle where all three vertices are the same color\n // else, all three vertices are on a border\n for (let i = 0; i < nTri; i++) {\n const v0 = faces[j]\n const v1 = faces[j + 1]\n const v2 = faces[j + 2]\n j += 3\n if (binary[v0] === binary[v1] && binary[v0] === binary[v2] && binary[v1] === binary[v2]) {\n continue\n }\n border[v0] = true\n border[v1] = true\n border[v2] = true\n }\n return border\n }\n\n static async gzip(data: Uint8Array): Promise<Uint8Array> {\n const stream = new CompressionStream('gzip')\n const writer = stream.writable.getWriter()\n writer.write(data).catch(console.error)\n const closePromise = writer.close().catch(console.error)\n const response = new Response(stream.readable)\n const result = new Uint8Array(await response.arrayBuffer())\n await closePromise // Ensure close happens eventually\n return result\n }\n\n static createMZ3(\n vertices: Float32Array,\n indices: Uint32Array,\n compress: boolean = false,\n colors: Uint8Array | null = null,\n scalars: Float32Array | null = null,\n colormapLabel: LUT | null = null\n ): ArrayBuffer {\n // ATTR bit flags (per mz3 spec)\n const IS_FACE = 1 // 0x01\n const IS_VERT = 2 // 0x02\n const IS_RGBA = 4 // 0x04\n const IS_SCALAR = 8 // 0x08\n const IS_LOOKUP = 64 // 0x40\n\n const MAGIC = 23117 // 0x4D5A\n\n const nface = indices ? indices.length / 3 : 0\n const nvert = vertices ? vertices.length / 3 : 0\n\n // Basic validation (keeps behavior similar to original)\n if (!Number.isInteger(nface) || nface < 0) {\n throw new Error('indices length must be a multiple of 3')\n }\n if (!Number.isInteger(nvert) || nvert < 0) {\n throw new Error('vertices length must be a multiple of 3')\n }\n\n const isFace = indices && indices.length > 0\n const isVert = vertices && vertices.length > 0\n const isRGBA = colors instanceof Uint8Array && colors.length === nvert * 4\n\n // Prepare lookup (colormapLabel) bytes if provided\n let lookupBytes: Uint8Array | null = null\n let nskip = 0\n if (colormapLabel != null) {\n const lookupJson = typeof colormapLabel === 'string' ? colormapLabel : JSON.stringify(colormapLabel)\n lookupBytes = new TextEncoder().encode(lookupJson)\n nskip = lookupBytes.byteLength\n }\n\n // scalars: must be Float32Array and its length must be positive multiple of nvert\n let isScalars = false\n let nScalarLayers = 0\n if (scalars != null) {\n if (!(scalars instanceof Float32Array)) {\n throw new Error('scalars must be a Float32Array or null/undefined')\n }\n if (nvert === 0) {\n throw new Error('cannot have scalars without vertices (nvert === 0)')\n }\n if (scalars.length > 0 && scalars.length % nvert === 0) {\n isScalars = true\n nScalarLayers = scalars.length / nvert\n } else {\n throw new Error(`scalars.length (${scalars.length}) must be a positive integer multiple of nvert (${nvert})`)\n }\n }\n\n // Build ATTR bitfield (minimal change from original: use bit flags)\n let attr = 0\n if (isFace) {\n attr |= IS_FACE\n }\n if (isVert) {\n attr |= IS_VERT\n }\n if (isRGBA) {\n attr |= IS_RGBA\n }\n if (isScalars) {\n attr |= IS_SCALAR\n }\n if (lookupBytes) {\n attr |= IS_LOOKUP\n }\n\n // header and block sizes\n const headerSize = 16\n\n const indexSize = isFace ? nface * 3 * 4 : 0 // Uint32 indices\n const vertexSize = isVert ? nvert * 3 * 4 : 0 // Float32 vertices\n const colorSize = isRGBA ? nvert * 4 : 0 // RGBA uint8 per vertex\n const scalarSize = isScalars ? nScalarLayers * nvert * 4 : 0 // NSCALAR * NVERT * 4 bytes (Float32)\n\n const totalSize = headerSize + nskip + indexSize + vertexSize + colorSize + scalarSize\n\n const buffer = new ArrayBuffer(totalSize)\n const writer = new DataView(buffer)\n\n // Write header (all little-endian)\n let pos = 0\n writer.setUint16(pos, MAGIC, true)\n pos += 2\n writer.setUint16(pos, attr, true)\n pos += 2\n writer.setUint32(pos, nface, true)\n pos += 4\n writer.setUint32(pos, nvert, true)\n pos += 4\n writer.setUint32(pos, nskip, true)\n pos += 4\n // pos === headerSize\n\n // Helper cursor\n let offset = headerSize\n\n // Write NSKIP (lookup JSON) directly after header if present\n if (nskip > 0 && lookupBytes) {\n new Uint8Array(buffer, offset, lookupBytes.length).set(lookupBytes)\n offset += nskip\n }\n\n // Write indices\n if (isFace) {\n new Uint32Array(buffer, offset, indices.length).set(indices)\n offset += indexSize\n }\n\n // Write vertices\n if (isVert) {\n new Float32Array(buffer, offset, vertices.length).set(vertices)\n offset += vertexSize\n }\n\n // Write colors (RGBA)\n if (isRGBA) {\n new Uint8Array(buffer, offset, colors!.length).set(colors!)\n offset += colorSize\n }\n\n // Write scalars (if any)\n if (isScalars) {\n new Float32Array(buffer, offset, scalars!.length).set(scalars!)\n offset += scalarSize\n }\n\n // sanity check\n if (offset !== totalSize) {\n throw new Error(`mz3 internal size mismatch: offset ${offset} !== totalSize ${totalSize}`)\n }\n\n if (compress) {\n throw new Error('Call async createMZ3Async() for compression')\n }\n\n return buffer\n }\n\n static async createMZ3Async(\n vertices: Float32Array,\n indices: Uint32Array,\n compress: boolean = false,\n colors: Uint8Array | null = null,\n scalars: Float32Array | null = null,\n colormapLabel: LUT | null = null\n ): Promise<ArrayBuffer> {\n const buffer = this.createMZ3(vertices, indices, compress, colors, scalars, colormapLabel)\n if (compress) {\n return await this.gzip(new Uint8Array(buffer))\n }\n return buffer\n }\n\n static createOBJ(vertices: Float32Array, indices: Uint32Array): ArrayBuffer {\n // generate binary OBJ format mesh\n // n.b. widespread support, but large and slow due to ASCII\n // https://en.wikipedia.org/wiki/Wavefront_.obj_file\n let objContent = ''\n // Add vertices to OBJ content\n for (let i = 0; i < vertices.length; i += 3) {\n objContent += `v ${vertices[i]} ${vertices[i + 1]} ${vertices[i + 2]}\\n`\n }\n // Add faces to OBJ content (OBJ indices start at 1, not 0)\n for (let i = 0; i < indices.length; i += 3) {\n objContent += `f ${indices[i] + 1} ${indices[i + 1] + 1} ${indices[i + 2] + 1}\\n`\n }\n // Encode the OBJ content as an ArrayBuffer\n const encoder = new TextEncoder()\n const arrayBuffer = encoder.encode(objContent).buffer\n return arrayBuffer\n }\n\n static createSTL(vertices: Float32Array, indices: Uint32Array): ArrayBuffer {\n // generate binary STL format mesh\n // n.b. inefficient and slow as vertices are not reused\n // https://en.wikipedia.org/wiki/STL_(file_format)#Binary\n const numTriangles = indices.length / 3\n const bufferLength = 84 + numTriangles * 50\n const arrayBuffer = new ArrayBuffer(bufferLength)\n const dataView = new DataView(arrayBuffer)\n // Write header (80 bytes)\n for (let i = 0; i < 80; i++) {\n dataView.setUint8(i, 0)\n }\n // Write number of triangles (4 bytes)\n dataView.setUint32(80, numTriangles, true)\n let offset = 84\n for (let i = 0; i < indices.length; i += 3) {\n const i0 = indices[i] * 3\n const i1 = indices[i + 1] * 3\n const i2 = indices[i + 2] * 3\n // Normal vector (12 bytes, set to zero)\n dataView.setFloat32(offset, 0, true) // Normal X\n dataView.setFloat32(offset + 4, 0, true) // Normal Y\n dataView.setFloat32(offset + 8, 0, true) // Normal Z\n offset += 12\n // Vertex 1 (12 bytes)\n dataView.setFloat32(offset, vertices[i0], true) // Vertex 1 X\n dataView.setFloat32(offset + 4, vertices[i0 + 1], true) // Vertex 1 Y\n dataView.setFloat32(offset + 8, vertices[i0 + 2], true) // Vertex 1 Z\n offset += 12\n // Vertex 2 (12 bytes)\n dataView.setFloat32(offset, vertices[i1], true) // Vertex 2 X\n dataView.setFloat32(offset + 4, vertices[i1 + 1], true) // Vertex 2 Y\n dataView.setFloat32(offset + 8, vertices[i1 + 2], true) // Vertex 2 Z\n offset += 12\n // Vertex 3 (12 bytes)\n dataView.setFloat32(offset, vertices[i2], true) // Vertex 3 X\n dataView.setFloat32(offset + 4, vertices[i2 + 1], true) // Vertex 3 Y\n dataView.setFloat32(offset + 8, vertices[i2 + 2], true) // Vertex 3 Z\n offset += 12\n // Attribute byte count (2 bytes, set to zero)\n dataView.setUint16(offset, 0, true)\n offset += 2\n }\n return arrayBuffer\n }\n\n static downloadArrayBuffer(buffer: ArrayBuffer, filename: string): void {\n const blob = new Blob([buffer], { type: 'application/octet-stream' })\n const url = URL.createObjectURL(blob)\n const a = document.createElement('a')\n a.href = url\n a.download = filename\n document.body.appendChild(a)\n a.style.display = 'none'\n a.click()\n setTimeout(() => {\n document.body.removeChild(a)\n URL.revokeObjectURL(url)\n }, 0)\n }\n\n static async saveMesh(vertices: Float32Array, indices: Uint32Array, filename: string = '.mz3', compress: boolean = false): Promise<ArrayBuffer> {\n let buff = new ArrayBuffer(0)\n if (/\\.obj$/i.test(filename)) {\n buff = this.createOBJ(vertices, indices)\n } else if (/\\.stl$/i.test(filename)) {\n buff = this.createSTL(vertices, indices)\n } else {\n if (!/\\.mz3$/i.test(filename)) {\n filename += '.mz3'\n }\n buff = await this.createMZ3Async(vertices, indices, compress)\n }\n if (filename.length > 4) {\n this.downloadArrayBuffer(buff, filename)\n }\n return buff\n }\n\n static getClusterBoundary(rgba8: Uint8Array, faces: number[] | Uint32Array): boolean[] {\n const rgba32 = new Uint32Array(rgba8.buffer)\n // assume all vertices are not near a border\n const border = new Array(rgba32.length).fill(false)\n const nTri = faces.length / 3\n let j = 0\n // interior: a triangle where all three vertices are the same color\n // else, all three vertices are on a border\n for (let i = 0; i < nTri; i++) {\n const v0 = faces[j]\n const v1 = faces[j + 1]\n const v2 = faces[j + 2]\n j += 3\n if (rgba32[v0] === rgba32[v1] && rgba32[v0] === rgba32[v2] && rgba32[v1] === rgba32[v2]) {\n continue\n }\n border[v0] = true\n border[v1] = true\n border[v2] = true\n }\n return border\n }\n\n // return spatial extremes for vertices\n static getExtents(pts: number[] | Float32Array): Extents {\n if ((!ArrayBuffer.isView(pts) && !Array.isArray(pts)) || pts.length < 3) {\n return { mxDx: 0.0, extentsMin: 0.0, extentsMax: 0.0 }\n }\n\n // each vertex has 3 coordinates: XYZ\n let mxDx = 0.0\n const mn = vec3.fromValues(pts[0], pts[1], pts[2])\n const mx = vec3.fromValues(pts[0], pts[1], pts[2])\n for (let i = 0; i < pts.length; i += 3) {\n const v = vec3.fromValues(pts[i], pts[i + 1], pts[i + 2])\n mxDx = Math.max(mxDx, vec3.len(v))\n vec3.min(mn, mn, v)\n vec3.max(mx, mx, v)\n }\n const extentsMin = [mn[0], mn[1], mn[2]]\n const extentsMax = [mx[0], mx[1], mx[2]]\n return { mxDx, extentsMin, extentsMax }\n }\n\n // determine vector orthogonal to plane defined by triangle\n // triangle winding determines front/back face\n static generateNormals(pts: number[] | Float32Array, tris: number[] | Uint32Array): Float32Array {\n // from https://github.com/rii-mango/Papaya\n /*\nCopyright (c) 2012-2015, RII-UTHSCSA\nAll rights reserved.\n\nTHIS PRODUCT IS NOT FOR CLINICAL USE.\n\nRedistribution and use in source and binary forms, with or without modification, are permitted provided that the\nfollowing conditions are met:\n\n - Redistributions of source code must retain the above copyright notice, this list of conditions and the following\n disclaimer.\n\n - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following\n disclaimer in the documentation and/or other materials provided with the distribution.\n\n - Neither the name of the RII-UTHSCSA nor the names of its contributors may be used to endorse or promote products\n derived from this software without specific prior written permission.\n\n THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES,\n INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\n DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\n SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,\n WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n*/\n const normal = []\n // nn = [],\n let ctr\n const normalsDataLength = pts.length\n let qx\n let qy\n let qz\n let px\n let py\n let pz\n let index1\n let index2\n let index3\n\n const norms = new Float32Array(normalsDataLength)\n const numIndices = tris.length\n for (ctr = 0; ctr < numIndices; ctr += 3) {\n index1 = tris[ctr] * 3\n index2 = tris[ctr + 1] * 3\n index3 = tris[ctr + 2] * 3\n\n const p1 = [pts[index1], pts[index1 + 1], pts[index1 + 2]]\n const p2 = [pts[index2], pts[index2 + 1], pts[index2 + 2]]\n const p3 = [pts[index3], pts[index3 + 1], pts[index3 + 2]]\n\n qx = p2[0] - p1[0]\n qy = p2[1] - p1[1]\n qz = p2[2] - p1[2]\n px = p3[0] - p1[0]\n py = p3[1] - p1[1]\n pz = p3[2] - p1[2]\n\n normal[0] = py * qz - pz * qy\n normal[1] = pz * qx - px * qz\n normal[2] = px * qy - py * qx\n\n norms[index1] += normal[0]\n norms[index1 + 1] += normal[1]\n norms[index1 + 2] += normal[2]\n\n norms[index2] += normal[0]\n norms[index2 + 1] += normal[1]\n norms[index2 + 2] += normal[2]\n\n norms[index3] += normal[0]\n norms[index3 + 1] += normal[1]\n norms[index3 + 2] += normal[2]\n }\n for (ctr = 0; ctr < normalsDataLength; ctr += 3) {\n normal[0] = -1 * norms[ctr]\n normal[1] = -1 * norms[ctr + 1]\n normal[2] = -1 * norms[ctr + 2]\n let len = normal[0] * normal[0] + normal[1] * normal[1] + normal[2] * normal[2]\n if (len > 0) {\n len = 1.0 / Math.sqrt(len)\n normal[0] *= len\n normal[1] *= len\n normal[2] *= len\n }\n norms[ctr] = normal[0]\n norms[ctr + 1] = normal[1]\n norms[ctr + 2] = normal[2]\n }\n return norms\n }\n}\n","import { mat4, vec4, vec3 } from 'gl-matrix'\nimport { log } from '@/logger'\nimport { NVUtilities, Zip } from '@/nvutilities'\nimport { ColorMap, LUT, cmapper } from '@/colortables'\nimport { NiivueObject3D } from '@/niivue-object3D'\nimport { NVMesh, NVMeshLayer, NVMeshLayerDefaults } from '@/nvmesh'\nimport {\n ANNOT,\n DefaultMeshType,\n GII,\n // Layer,\n MGH,\n MZ3,\n SmpMap,\n TCK,\n TRACT,\n TRK,\n TT,\n TRX,\n VTK,\n X3D,\n XmlTag,\n AnyNumberArray,\n ValuesArray\n} from '@/nvmesh-types'\n\nconst utiltiesLogger = log\n\n/**\n * Class to load different mesh formats\n * @ignore\n */\nexport class NVMeshLoaders {\n // read undocumented AFNI tract.niml format streamlines\n static readTRACT(buffer: ArrayBuffer): TRACT {\n const len = buffer.byteLength\n if (len < 20) {\n throw new Error('File too small to be niml.tract: bytes = ' + len)\n }\n const reader = new DataView(buffer)\n const bytes = new Uint8Array(buffer)\n let pos = 0\n function readStr(): string {\n // read until right angle bracket \">\"\n while (pos < len && bytes[pos] !== 60) {\n pos++\n } // start with \"<\"\n const startPos = pos\n while (pos < len && bytes[pos] !== 62) {\n pos++\n }\n pos++ // skip EOLN\n if (pos - startPos < 1) {\n return ''\n }\n return new TextDecoder().decode(buffer.slice(startPos, pos - 1)).trim()\n }\n\n let line = readStr() // 1st line: signature '<network'\n function readNumericTag(TagName: string): number {\n // Tag 'Dim1' will return 3 for Dim1=\"3\"\n const pos = line.indexOf(TagName)\n if (pos < 0) {\n return 0\n }\n const spos = line.indexOf('\"', pos) + 1\n const epos = line.indexOf('\"', spos)\n const str = line.slice(spos, epos)\n return parseInt(str)\n }\n const n_tracts = readNumericTag('N_tracts=')\n if (!line.startsWith('<network') || n_tracts < 1) {\n log.warn('This is not a valid niml.tract file ' + line)\n }\n let npt = 0\n const offsetPt0 = []\n offsetPt0.push(npt) // 1st streamline starts at 0\n const pts = []\n const bundleTags = []\n for (let t = 0; t < n_tracts; t++) {\n line = readStr() // <tracts ...\n const new_tracts = readNumericTag('ni_dimen=')\n const bundleTag = readNumericTag('Bundle_Tag=')\n const isLittleEndian = line.includes('binary.lsbfirst')\n for (let i = 0; i < new_tracts; i++) {\n // let id = reader.getUint32(pos, isLittleEndian);\n pos += 4\n const new_pts = reader.getUint32(pos, isLittleEndian) / 3\n pos += 4\n for (let j = 0; j < new_pts; j++) {\n pts.push(reader.getFloat32(pos, isLittleEndian))\n pos += 4\n pts.push(-reader.getFloat32(pos, isLittleEndian))\n pos += 4\n pts.push(reader.getFloat32(pos, isLittleEndian))\n pos += 4\n }\n npt += new_pts\n offsetPt0.push(npt)\n bundleTags.push(bundleTag) // each streamline associated with tract\n }\n line = readStr() // </tracts>\n }\n const dps = []\n dps.push({\n id: 'tract',\n vals: Float32Array.from(bundleTags)\n })\n\n return {\n pts: new Float32Array(pts),\n offsetPt0: new Uint32Array(offsetPt0),\n dps\n }\n } // readTRACT()\n\n // https://dsi-studio.labsolver.org/doc/cli_data.html\n // https://brain.labsolver.org/hcp_trk_atlas.html\n static async readTT(buffer: ArrayBuffer): Promise<TT> {\n // Read a Matlab V4 file, n.b. does not support modern versions\n // https://www.mathworks.com/help/pdf_doc/matlab/matfile_format.pdf\n let offsetPt0 = new Uint32Array(0)\n let pts = new Float32Array(0)\n const mat = await NVUtilities.readMatV4(buffer)\n if (!('trans_to_mni' in mat)) {\n throw new Error(\"TT format file must have 'trans_to_mni'\")\n }\n if (!('voxel_size' in mat)) {\n throw new Error(\"TT format file must have 'voxel_size'\")\n }\n if (!('track' in mat)) {\n throw new Error(\"TT format file must have 'track'\")\n }\n let trans_to_mni = mat4.create()\n const m = mat.trans_to_mni\n trans_to_mni = mat4.fromValues(m[0], m[1], m[2], m[3], m[4], m[5], m[6], m[7], m[8], m[9], m[10], m[11], m[12], m[13], m[14], m[15])\n mat4.transpose(trans_to_mni, trans_to_mni)\n // unlike TRK, TT uses voxel centers, not voxel corners\n function parse_tt(track: Float64Array | Float32Array | Uint32Array | Uint16Array | Uint8Array | Int32Array | Int16Array | Int8Array): void {\n const dv = new DataView(track.buffer)\n const pos = []\n let nvert3 = 0\n let i = 0\n while (i < track.length) {\n pos.push(i)\n const newpts = dv.getUint32(i, true)\n i = i + newpts + 13\n nvert3 += newpts\n }\n offsetPt0 = new Uint32Array(pos.length + 1)\n pts = new Float32Array(nvert3)\n let npt = 0\n for (let i = 0; i < pos.length; i++) {\n offsetPt0[i] = npt / 3\n let p = pos[i]\n const sz = dv.getUint32(p, true) / 3\n let x = dv.getInt32(p + 4, true)\n let y = dv.getInt32(p + 8, true)\n let z = dv.getInt32(p + 12, true)\n p += 16\n pts[npt++] = x\n pts[npt++] = y\n pts[npt++] = z\n for (let j = 2; j <= sz; j++) {\n x = x + dv.getInt8(p++)\n y = y + dv.getInt8(p++)\n z = z + dv.getInt8(p++)\n pts[npt++] = x\n pts[npt++] = y\n pts[npt++] = z\n }\n } // for each streamline\n for (let i = 0; i < npt; i++) {\n pts[i] = pts[i] / 32.0\n }\n let v = 0\n for (let i = 0; i < npt / 3; i++) {\n const pos = vec4.fromValues(pts[v], pts[v + 1], pts[v + 2], 1)\n vec4.transformMat4(pos, pos, trans_to_mni)\n pts[v++] = pos[0]\n pts[v++] = pos[1]\n pts[v++] = pos[2]\n }\n offsetPt0[pos.length] = npt / 3 // solve fence post problem, offset for final streamline\n } // parse_tt()\n parse_tt(mat.track)\n return {\n pts,\n offsetPt0\n }\n } // readTT\n\n /**\n * Assemble dpg from a map-of-groups into a ValuesArray ordered by groups[].\n * Missing group data or tags are padded with NaN to maintain group alignment.\n *\n * @param dpgMap - map from groupId -> ValuesArray (entries for that group)\n * @param groups - ValuesArray describing groups; defines the result ordering\n * @returns ValuesArray - one entry per unique tag found across all groups\n * @throws Error if \"groups\" is empty or missing\n */\n static assembleDpgFromMap(dpgMap: Record<string, ValuesArray>, groups: ValuesArray): ValuesArray {\n if (!Array.isArray(groups) || groups.length === 0) {\n throw new Error('assembleDpgFromMap: \"groups\" is empty or missing; cannot assemble dpg.')\n }\n\n // 1. Identify all unique tags across all existing groups in dpgMap\n const allTags = new Set<string>()\n for (const gid in dpgMap) {\n dpgMap[gid].forEach((entry) => allTags.add(entry.id))\n }\n\n const result: ValuesArray = []\n\n // 2. For each unique tag found in the dpgMap...\n for (const tag of allTags) {\n const perGroupArrays: Float32Array[] = []\n let totalLen = 0\n\n // 3. Iterate through every group defined in the TRX header\n for (let gi = 0; gi < groups.length; gi++) {\n const gid = String(groups[gi].id)\n const entries = dpgMap[gid] || [] // Handle missing group in map\n const entry = entries.find((e) => e.id === tag)\n\n if (entry) {\n // Use actual data if it exists\n const vals = entry.vals instanceof Float32Array ? entry.vals : Float32Array.from(entry.vals as Iterable<number>)\n perGroupArrays.push(vals)\n totalLen += vals.length\n } else {\n // If tag is missing for this group, fill with NaN (assuming scalar dpg)\n // Note: TRX dpg is typically 1 value per group.\n const fallback = new Float32Array([NaN])\n perGroupArrays.push(fallback)\n totalLen += fallback.length\n }\n }\n\n // 4. Concatenate results for the tag\n const merged = new Float32Array(totalLen)\n let off = 0\n for (const arr of perGroupArrays) {\n merged.set(arr, off)\n off += arr.length\n }\n\n result.push({ id: tag, vals: merged })\n }\n\n return result\n }\n\n // read TRX format tractogram\n // https://github.com/tee-ar-ex/trx-spec/blob/master/specifications.md\n static async readTRX(buffer: ArrayBuffer): Promise<TRX> {\n // Javascript does not support float16, so we convert to float32\n // https://stackoverflow.com/questions/5678432/decompressing-half-precision-floats-in-javascript\n function decodeFloat16(binary: number): number {\n 'use strict'\n const exponent = (binary & 0x7c00) >> 10\n const fraction = binary & 0x03ff\n return (binary >> 15 ? -1 : 1) * (exponent ? (exponent === 0x1f ? (fraction ? NaN : Infinity) : Math.pow(2, exponent - 15) * (1 + fraction / 0x400)) : 6.103515625e-5 * (fraction / 0x400))\n } // decodeFloat16()\n let noff = 0\n let npt = 0\n let pts = new Float32Array([])\n const offsetPt0: number[] = []\n const dpgMap: Record<string, ValuesArray> = {}\n const groups = []\n const dps = []\n const dpv = []\n let header = []\n let isOverflowUint64 = false\n const zip = new Zip(buffer)\n for (let i = 0; i < zip.entries.length; i++) {\n const entry = zip.entries[i]\n if (entry.uncompressedSize === 0) {\n continue // e.g. folder\n }\n\n const parts = entry.fileName.split('/')\n const fname = parts.slice(-1)[0] // my.trx/dpv/fx.float32 -> fx.float32\n if (fname.startsWith('.')) {\n continue\n }\n const dname = parts.slice(-3)[0] // my.trx/dpg/ARC_L/z.float32 -> dpg\n const pname = parts.slice(-2)[0] // my.trx/dpv/fx.float32 -> dpv\n const tag = fname.split('.')[0] // \"positions.3.float16 -> \"positions\"\n const data = await entry.extract()\n // const data = await NVUtilities.zipInflate(buffer, entry.startsAt, entry.compressedSize, entry.uncompressedSize, entry.compressionMethod )\n // console.log(`entry ${pname} ${fname} ${tag} : ${data.length}`)\n if (fname.includes('header.json')) {\n const jsonString = new TextDecoder().decode(data)\n header = JSON.parse(jsonString)\n continue\n }\n\n // next read arrays for all possible datatypes: int8/16/32/64 uint8/16/32/64 float16/32/64\n let nval = 0\n let vals: AnyNumberArray = []\n if (fname.endsWith('.uint64') || fname.endsWith('.int64')) {\n // javascript does not have 64-bit integers! read lower 32-bits\n // note for signed int64 we only read unsigned bytes\n // for both signed and unsigned, generate an error if any value is out of bounds\n // one alternative might be to convert to 64-bit double that has a flintmax of 2^53.\n nval = data.length / 8 // 8 bytes per 64bit input\n vals = new Uint32Array(nval)\n const u32 = new Uint32Array(data.buffer)\n let j = 0\n for (let i = 0; i < nval; i++) {\n vals[i] = u32[j]\n if (u32[j + 1] !== 0) {\n isOverflowUint64 = true\n }\n j += 2\n }\n } else if (fname.endsWith('.uint32')) {\n vals = new Uint32Array(data.buffer)\n } else if (fname.endsWith('.uint16')) {\n vals = new Uint16Array(data.buffer)\n } else if (fname.endsWith('.uint8')) {\n vals = new Uint8Array(data.buffer)\n } else if (fname.endsWith('.int32')) {\n vals = new Int32Array(data.buffer)\n } else if (fname.endsWith('.int16')) {\n vals = new Int16Array(data.buffer)\n } else if (fname.endsWith('.int8')) {\n vals = new Int8Array(data.buffer)\n } else if (fname.endsWith('.float64')) {\n vals = new Float64Array(data.buffer)\n } else if (fname.endsWith('.float32')) {\n vals = new Float32Array(data.buffer)\n } else if (fname.endsWith('.float16')) {\n // javascript does not have 16-bit floats! Convert to 32-bits\n nval = data.length / 2 // 2 bytes per 16bit input\n vals = new Float32Array(nval)\n const u16 = new Uint16Array(data.buffer)\n const lut = new Float32Array(65536)\n for (let i = 0; i < 65536; i++) {\n lut[i] = decodeFloat16(i)\n }\n for (let i = 0; i < nval; i++) {\n vals[i] = lut[u16[i]]\n }\n } else {\n continue\n } // not a data array\n nval = vals.length\n // next: read data_per_group\n if (pname.includes('groups')) {\n groups.push({\n id: tag,\n vals: Float32Array.from(vals.slice())\n })\n continue\n }\n if (dname.includes('dpg')) {\n const key = String(pname) // be defensive; ensure key is a string\n if (!dpgMap[key]) {\n dpgMap[key] = []\n }\n dpgMap[key].push({\n id: tag,\n vals: Float32Array.from(vals.slice())\n })\n continue\n }\n // next: read data_per_vertex\n if (pname.includes('dpv')) {\n dpv.push({\n id: tag,\n vals: Float32Array.from(vals.slice())\n })\n continue\n }\n // next: read data_per_streamline\n if (pname.includes('dps')) {\n dps.push({\n id: tag,\n vals: Float32Array.from(vals.slice())\n })\n continue\n }\n // Next: read offsets: Always uint64\n if (fname.startsWith('offsets.')) {\n // javascript does not have 64-bit integers! read lower 32-bits\n noff = nval // 8 bytes per 64bit input\n // we need to solve the fence post problem, so we can not use slice\n for (let i = 0; i < nval; i++) {\n offsetPt0[i] = vals[i]\n }\n }\n if (fname.startsWith('positions.3.')) {\n npt = nval // 4 bytes per 32bit input\n pts = new Float32Array(vals)\n }\n }\n if (noff === 0 || npt === 0) {\n throw new Error('Failure reading TRX format (no offsets or points).')\n }\n if (isOverflowUint64) {\n // TODO use BigInt\n throw new Error('Too many vertices: JavaScript does not support 64 bit integers')\n }\n let dpg = []\n if (groups.length > 0 && dpgMap && Object.keys(dpgMap).length > 0) {\n dpg = this.assembleDpgFromMap(dpgMap, groups)\n }\n offsetPt0[noff] = npt / 3 // solve fence post problem, offset for final streamline\n return {\n pts,\n offsetPt0: new Uint32Array(offsetPt0),\n dpg,\n dps,\n dpv,\n groups,\n header\n }\n } // readTRX()\n\n // issue1426 MRtrix data per streamline as ASCII text\n static readTXT(buffer: ArrayBuffer, n_count = 0): Float32Array {\n // Decode ASCII (or UTF-8) bytes into a string\n const text = new TextDecoder('utf-8').decode(buffer)\n // Split into lines, handling any line endings: \\r\\n, \\r, or \\n\n const lines = text.split(/\\r?\\n|\\r/).filter((l) => l.trim().length > 0)\n // If n_count not specified, use number of lines\n if (n_count <= 0) {\n n_count = lines.length\n }\n const vals = new Float32Array(n_count)\n // Parse each line into a float\n for (let i = 0; i < n_count && i < lines.length; i++) {\n const v = parseFloat(lines[i].trim())\n vals[i] = Number.isFinite(v) ? v : 0.0\n }\n\n return vals\n }\n\n // read mrtrix tsf format Track Scalar Files - these are are DPV\n // https://mrtrix.readthedocs.io/en/dev/getting_started/image_data.html#track-scalar-file-format-tsf\n static readTSF(buffer: ArrayBuffer, n_vert = 0): Float32Array {\n const vals = new Float32Array(n_vert)\n const len = buffer.byteLength\n if (len < 20) {\n throw new Error('File too small to be TSF: bytes = ' + len)\n }\n const bytes = new Uint8Array(buffer)\n let pos = 0\n function readStr(): string {\n while (pos < len && bytes[pos] === 10) {\n pos++\n } // skip blank lines\n const startPos = pos\n while (pos < len && bytes[pos] !== 10) {\n pos++\n }\n pos++ // skip EOLN\n if (pos - startPos < 1) {\n return ''\n }\n return new TextDecoder().decode(buffer.slice(startPos, pos - 1))\n }\n let line = readStr() // 1st line: signature 'mrtrix tracks'\n if (!line.includes('mrtrix track scalars')) {\n throw new Error('Not a valid TSF file')\n }\n let offset = -1 // \"file: offset\" is REQUIRED\n while (pos < len && !line.includes('END')) {\n line = readStr()\n if (line.toLowerCase().startsWith('file:')) {\n offset = parseInt(line.split(' ').pop()!)\n }\n if (line.toLowerCase().startsWith('datatype:') && !line.endsWith('Float32LE')) {\n throw new Error('Only supports TSF files with Float32LE')\n }\n }\n if (offset < 20) {\n throw new Error('Not a valid TSF file (missing file offset)')\n }\n pos = offset\n const reader = new DataView(buffer)\n // read and transform vertex positions\n let npt = 0\n while (pos + 4 <= len && npt < n_vert) {\n const ptx = reader.getFloat32(pos, true)\n pos += 4\n if (!isFinite(ptx)) {\n // both NaN and Infinity are not finite\n if (!isNaN(ptx)) {\n // terminate if infinity\n break\n }\n } else {\n vals[npt++] = ptx\n }\n }\n return vals\n } // readTSF\n\n // read mrtrix tck format streamlines\n // https://mrtrix.readthedocs.io/en/latest/getting_started/image_data.html#tracks-file-format-tck\n static readTCK(buffer: ArrayBuffer): TCK {\n const len = buffer.byteLength\n if (len < 20) {\n throw new Error('File too small to be TCK: bytes = ' + len)\n }\n const bytes = new Uint8Array(buffer)\n let pos = 0\n function readStr(): string {\n while (pos < len && bytes[pos] === 10) {\n pos++\n } // skip blank lines\n const startPos = pos\n while (pos < len && bytes[pos] !== 10) {\n pos++\n }\n pos++ // skip EOLN\n if (pos - startPos < 1) {\n return ''\n }\n return new TextDecoder().decode(buffer.slice(startPos, pos - 1))\n }\n let line = readStr() // 1st line: signature 'mrtrix tracks'\n if (!line.includes('mrtrix tracks')) {\n throw new Error('Not a valid TCK file')\n }\n let offset = -1 // \"file: offset\" is REQUIRED\n while (pos < len && !line.includes('END')) {\n line = readStr()\n if (line.toLowerCase().startsWith('file:')) {\n offset = parseInt(line.split(' ').pop()!)\n }\n }\n if (offset < 20) {\n throw new Error('Not a valid TCK file (missing file offset)')\n }\n pos = offset\n const reader = new DataView(buffer)\n // read and transform vertex positions\n let npt = 0\n // over-provision offset array to store number of segments\n let offsetPt0 = new Uint32Array(len / (4 * 4))\n let noffset = 0\n // over-provision points array to store vertex positions\n let npt3 = 0\n let pts = new Float32Array(len / 4)\n offsetPt0[0] = 0 // 1st streamline starts at 0\n while (pos + 12 < len) {\n const ptx = reader.getFloat32(pos, true)\n pos += 4\n const pty = reader.getFloat32(pos, true)\n pos += 4\n const ptz = reader.getFloat32(pos, true)\n pos += 4\n if (!isFinite(ptx)) {\n // both NaN and Infinity are not finite\n offsetPt0[noffset++] = npt\n if (!isNaN(ptx)) {\n // terminate if infinity\n break\n }\n } else {\n pts[npt3++] = ptx\n pts[npt3++] = pty\n pts[npt3++] = ptz\n npt++\n }\n }\n // resize offset/vertex arrays that were initially over-provisioned\n pts = pts.slice(0, npt3)\n offsetPt0 = offsetPt0.slice(0, noffset)\n return {\n pts,\n offsetPt0\n }\n } // readTCK()\n\n // not included in public docs\n // read trackvis trk format streamlines\n // http://trackvis.org/docs/?subsect=fileformat\n static async readTRK(buffer: ArrayBuffer): Promise<TRK> {\n // https://brain.labsolver.org/hcp_trk_atlas.html\n // https://github.com/xtk/X/tree/master/io\n // in practice, always little endian\n let reader = new DataView(buffer)\n let magic = reader.getUint32(0, true) // 'TRAC'\n if (magic !== 1128354388) {\n // e.g. TRK.gz\n let raw\n if (magic === 4247762216) {\n // e.g. TRK.zstd\n // raw = fzstd.decompress(new Uint8Array(buffer));\n // raw = new Uint8Array(raw);\n throw new Error('zstd TRK decompression is not supported')\n } else {\n raw = await NVUtilities.decompress(new Uint8Array(buffer))\n }\n buffer = raw.buffer\n reader = new DataView(buffer)\n magic = reader.getUint32(0, true) // 'TRAC'\n }\n const vers = reader.getUint32(992, true) // 2\n const hdr_sz = reader.getUint32(996, true) // 1000\n if (vers > 2 || hdr_sz !== 1000 || magic !== 1128354388) {\n throw new Error('Not a valid TRK file')\n }\n const n_scalars = reader.getInt16(36, true)\n const dpv = []\n // data_per_vertex\n for (let i = 0; i < n_scalars; i++) {\n const arr = new Uint8Array(buffer.slice(38 + i * 20, 58 + i * 20))\n const str = new TextDecoder().decode(arr).split('\\0').shift()\n dpv.push({\n id: str!.trim(), // TODO can we guarantee this?\n vals: [] as number[]\n })\n }\n const voxel_sizeX = reader.getFloat32(12, true)\n const voxel_sizeY = reader.getFloat32(16, true)\n const voxel_sizeZ = reader.getFloat32(20, true)\n const zoomMat = mat4.fromValues(1 / voxel_sizeX, 0, 0, -0.5, 0, 1 / voxel_sizeY, 0, -0.5, 0, 0, 1 / voxel_sizeZ, -0.5, 0, 0, 0, 1)\n const n_properties = reader.getInt16(238, true)\n const dps = []\n // data_per_streamline\n for (let i = 0; i < n_properties; i++) {\n const arr = new Uint8Array(buffer.slice(240 + i * 20, 260 + i * 20))\n const str = new TextDecoder().decode(arr).split('\\0').shift()\n dps.push({\n id: str!.trim(), // TODO can we guarantee this?\n vals: [] as number[]\n })\n }\n const mat = mat4.create()\n for (let i = 0; i < 16; i++) {\n mat[i] = reader.getFloat32(440 + i * 4, true)\n }\n if (mat[15] === 0.0) {\n // vox_to_ras[3][3] is 0, it means the matrix is not recorded\n log.warn('TRK vox_to_ras not set')\n mat4.identity(mat)\n }\n const vox2mmMat = mat4.create()\n mat4.mul(vox2mmMat, zoomMat, mat)\n let i32 = null\n let f32 = null\n i32 = new Int32Array(buffer.slice(hdr_sz))\n f32 = new Float32Array(i32.buffer)\n const ntracks = i32.length\n if (ntracks < 1) {\n throw new Error('Empty TRK file.')\n }\n // read and transform vertex positions\n let i = 0\n let npt = 0\n // pre-allocate and over-provision offset array\n let offsetPt0 = new Uint32Array(i32.length / 4)\n let noffset = 0\n // pre-allocate and over-provision vertex positions array\n let pts = new Float32Array(i32.length)\n let npt3 = 0\n while (i < ntracks) {\n const n_pts = i32[i]\n i = i + 1 // read 1 32-bit integer for number of points in this streamline\n offsetPt0[noffset++] = npt\n for (let j = 0; j < n_pts; j++) {\n const ptx = f32[i + 0]\n const pty = f32[i + 1]\n const ptz = f32[i + 2]\n i += 3 // read 3 32-bit floats for XYZ position\n pts[npt3++] = ptx * vox2mmMat[0] + pty * vox2mmMat[1] + ptz * vox2mmMat[2] + vox2mmMat[3]\n pts[npt3++] = ptx * vox2mmMat[4] + pty * vox2mmMat[5] + ptz * vox2mmMat[6] + vox2mmMat[7]\n pts[npt3++] = ptx * vox2mmMat[8] + pty * vox2mmMat[9] + ptz * vox2mmMat[10] + vox2mmMat[11]\n if (n_scalars > 0) {\n for (let s = 0; s < n_scalars; s++) {\n dpv[s].vals.push(f32[i])\n i++\n }\n }\n npt++\n } // for j: each point in streamline\n if (n_properties > 0) {\n for (let j = 0; j < n_properties; j++) {\n dps[j].vals.push(f32[i])\n i++\n }\n }\n } // for each streamline: while i < n_count\n // output uses static float32 not dynamic number[]\n const dps32 = []\n // data_per_streamline\n for (let i = 0; i < dps.length; i++) {\n dps32.push({\n id: dps[i].id,\n vals: Float32Array.from(dps[i].vals)\n })\n }\n const dpv32 = []\n for (let i = 0; i < dpv.length; i++) {\n dpv32.push({\n id: dpv[i].id,\n vals: Float32Array.from(dpv[i].vals)\n })\n }\n // add 'first index' as if one more line was added (fence post problem)\n offsetPt0[noffset++] = npt\n // resize offset/vertex arrays that were initially over-provisioned\n pts = pts.slice(0, npt3)\n offsetPt0 = offsetPt0.slice(0, noffset)\n return {\n pts,\n offsetPt0,\n dps: dps32,\n dpv: dpv32\n }\n } // readTRK()\n\n // read legacy VTK text format file\n static readTxtVTK(buffer: ArrayBuffer): VTK {\n const enc = new TextDecoder('utf-8')\n const txt = enc.decode(buffer)\n const lines = txt.split('\\n')\n const n = lines.length\n if (n < 7 || !lines[0].startsWith('# vtk DataFile')) {\n throw new Error('Invalid VTK image')\n }\n if (!lines[2].startsWith('ASCII')) {\n throw new Error('Not ASCII VTK mesh')\n }\n let pos = 3\n while (lines[pos].length < 1) {\n pos++\n } // skip blank lines\n if (!lines[pos].includes('POLYDATA')) {\n throw new Error('Not ASCII VTK polydata')\n }\n pos++\n while (lines[pos].length < 1) {\n pos++\n } // skip blank lines\n if (!lines[pos].startsWith('POINTS')) {\n throw new Error('Not VTK POINTS')\n }\n let items = lines[pos].trim().split(/\\s+/)\n const nvert = parseInt(items[1]) // POINTS 10261 float\n const nvert3 = nvert * 3\n const positions = new Float32Array(nvert * 3)\n let v = 0\n while (v < nvert * 3) {\n pos++\n const str = lines[pos].trim()\n const pts = str.trim().split(/\\s+/)\n for (let i = 0; i < pts.length; i++) {\n if (v >= nvert3) {\n break\n }\n positions[v] = parseFloat(pts[i])\n v++\n }\n }\n const tris = []\n pos++\n while (lines[pos].length < 1) {\n pos++\n } // skip blank lines\n if (lines[pos].startsWith('METADATA')) {\n while (lines[pos].length > 1) {\n pos++\n } // skip until blank line\n pos++\n }\n items = lines[pos].trim().split(/\\s+/)\n pos++\n if (items[0].includes('LINES')) {\n const n_count = parseInt(items[1])\n if (n_count < 1) {\n throw new Error('Corrupted VTK ASCII')\n }\n let str = lines[pos].trim()\n const offsetPt0 = []\n let pts: number[] = []\n if (str.startsWith('OFFSETS')) {\n // 'new' line style https://discourse.vtk.org/t/upcoming-changes-to-vtkcellarray/2066\n pos++\n let c = 0\n while (c < n_count) {\n str = lines[pos].trim()\n pos++\n const items = str.trim().split(/\\s+/)\n for (let i = 0; i < items.length; i++) {\n offsetPt0[c] = parseInt(items[i])\n c++\n if (c >= n_count) {\n break\n }\n } // for each line\n } // while offset array not filled\n pts = Array.from(positions)\n } else {\n // classic line style https://www.visitusers.org/index.php?title=ASCII_VTK_Files\n let npt = 0\n offsetPt0[0] = 0 // 1st streamline starts at 0\n let asciiInts: number[] = []\n let asciiIntsPos = 0\n\n function lineToInts(): void {\n // VTK can save one array across multiple ASCII lines\n str = lines[pos].trim()\n const items = str.trim().split(/\\s+/)\n asciiInts = []\n for (let i = 0; i < items.length; i++) {\n asciiInts.push(parseInt(items[i]))\n }\n asciiIntsPos = 0\n pos++\n }\n\n lineToInts()\n for (let c = 0; c < n_count; c++) {\n if (asciiIntsPos >= asciiInts.length) {\n lineToInts()\n }\n const numPoints = asciiInts[asciiIntsPos++]\n npt += numPoints\n offsetPt0[c + 1] = npt\n for (let i = 0; i < numPoints; i++) {\n if (asciiIntsPos >= asciiInts.length) {\n lineToInts()\n }\n const idx = asciiInts[asciiIntsPos++] * 3\n pts.push(positions[idx + 0]) // X\n pts.push(positions[idx + 1]) // Y\n pts.push(positions[idx + 2]) // Z\n } // for numPoints: number of segments in streamline\n } // for n_count: number of streamlines\n }\n return {\n pts: Float32Array.from(pts),\n offsetPt0: Uint32Array.from(offsetPt0)\n }\n } else if (items[0].includes('TRIANGLE_STRIPS')) {\n const nstrip = parseInt(items[1])\n for (let i = 0; i < nstrip; i++) {\n const str = lines[pos].trim()\n pos++\n const vs = str.trim().split(/\\s+/)\n const ntri = parseInt(vs[0]) - 2 // -2 as triangle strip is creates pts - 2 faces\n let k = 1\n for (let t = 0; t < ntri; t++) {\n if (t % 2) {\n // preserve winding order\n tris.push(parseInt(vs[k + 2]))\n tris.push(parseInt(vs[k + 1]))\n tris.push(parseInt(vs[k]))\n } else {\n tris.push(parseInt(vs[k]))\n tris.push(parseInt(vs[k + 1]))\n tris.push(parseInt(vs[k + 2]))\n }\n k += 1\n } // for each triangle\n } // for each strip\n } else if (items[0].includes('POLYGONS')) {\n const npoly = parseInt(items[1])\n for (let i = 0; i < npoly; i++) {\n const str = lines[pos].trim()\n pos++\n const vs = str.trim().split(/\\s+/)\n const ntri = parseInt(vs[0]) - 2 // e.g. 3 for triangle\n const fx = parseInt(vs[1])\n let fy = parseInt(vs[2])\n for (let t = 0; t < ntri; t++) {\n const fz = parseInt(vs[3 + t])\n tris.push(fx)\n tris.push(fy)\n tris.push(fz)\n fy = fz\n }\n }\n } else {\n throw new Error('Unsupported ASCII VTK datatype ' + items[0])\n }\n const indices = new Uint32Array(tris)\n return {\n positions,\n indices\n }\n } // readTxtVTK()\n\n // read mesh overlay to influence vertex colors\n static async readLayer(\n name: string = '',\n buffer: ArrayBuffer,\n nvmesh: NVMesh,\n opacity = 0.5,\n colormap = 'warm',\n colormapNegative = 'winter',\n useNegativeCmap = false,\n cal_min: number | null = null,\n cal_max: number | null = null,\n outlineBorder = 0\n ): Promise<NVMeshLayer | undefined> {\n const layer: NVMeshLayer = {\n ...NVMeshLayerDefaults,\n colormapInvert: false,\n colormapType: 0, // COLORMAP_TYPE.MIN_TO_MAX\n isTransparentBelowCalMin: true,\n isAdditiveBlend: false,\n colorbarVisible: true,\n colormapLabel: null\n }\n const isReadColortables = true\n const re = /(?:\\.([^.]+))?$/\n let ext = re.exec(name)![1] // TODO can we guarantee this?\n ext = ext.toUpperCase()\n if (ext === 'GZ') {\n ext = re.exec(name.slice(0, -3))![1] // img.trk.gz -> img.trk\n ext = ext.toUpperCase()\n }\n const n_vert = nvmesh.vertexCount / 3 // each vertex has XYZ component\n if (nvmesh.offsetPt0) {\n // for streamlines, we can only read .tsf and txt files\n // typescript hell commences for one liner\n // const tag = name.split('/')!.pop()!.split('.')!.slice(0, -1).join('.')!\n const splitResult = name.split('/')\n let tag = 'Unknown'\n if (splitResult.length > 1) {\n const tag1 = splitResult.pop()\n if (tag1) {\n tag = tag.split('.').slice(0, -1).join('.')\n }\n }\n if (ext === 'TXT') {\n const n_count = nvmesh.offsetPt0.length - 1\n const vals = NVMeshLoaders.readTXT(buffer, n_count)\n if (vals.length !== n_count) {\n throw new Error(`TXT file has ${vals.length} items, expected one per streamline (${n_count}).`)\n }\n if (!nvmesh.dps) {\n nvmesh.dps = []\n }\n const mn = vals.reduce((acc, current) => Math.min(acc, current))\n const mx = vals.reduce((acc, current) => Math.max(acc, current))\n nvmesh.dps.push({\n id: tag,\n vals: Float32Array.from(vals.slice()),\n global_min: mn,\n global_max: mx,\n cal_min: mn,\n cal_max: mx\n })\n return layer\n }\n if (ext === 'NII') {\n const vals = (await NVMeshLoaders.readNII(buffer, nvmesh.pts, '')) as Float32Array\n // const npt = nvmesh.pts.length / 3\n const mn = vals.reduce((acc, current) => Math.min(acc, current))\n const mx = vals.reduce((acc, current) => Math.max(acc, current))\n nvmesh.dpv.push({\n id: tag,\n vals: Float32Array.from(vals.slice()),\n global_min: mn,\n global_max: mx,\n cal_min: mn,\n cal_max: mx\n })\n // console.log(`>>> ${tag} got ${vals.length} expected ${nvmesh.pts.length / 3}`)\n return layer\n }\n if (ext !== 'TSF') {\n throw new Error('readLayer for streamlines only supports TSF and TXT files.')\n }\n const npt = nvmesh.pts.length / 3\n // return to readable javascript\n const vals = NVMeshLoaders.readTSF(buffer, npt)\n if (!nvmesh.dpv) {\n nvmesh.dpv = []\n }\n const mn = vals.reduce((acc, current) => Math.min(acc, current))\n const mx = vals.reduce((acc, current) => Math.max(acc, current))\n nvmesh.dpv.push({\n id: tag,\n vals: Float32Array.from(vals.slice()),\n global_min: mn,\n global_max: mx,\n cal_min: mn,\n cal_max: mx\n })\n return layer\n }\n if (n_vert < 3) {\n log.error('n_vert < 3 in layer')\n return\n }\n if (ext === 'MZ3') {\n const obj = await NVMeshLoaders.readMZ3(buffer, n_vert)\n layer.values = obj.scalars\n if ('colormapLabel' in obj) {\n layer.colormapLabel = obj.colormapLabel\n }\n } else if (ext === 'ANNOT') {\n if (!isReadColortables) {\n // TODO: bogus ANNOT return type\n layer.values = NVMeshLoaders.readANNOT(buffer, n_vert) as unknown as Float32Array\n } else {\n const obj = NVMeshLoaders.readANNOT(buffer, n_vert, true)\n if (!(obj instanceof Uint32Array)) {\n layer.values = obj.scalars\n layer.colormapLabel = obj.colormapLabel\n } // unable to decode colormapLabel\n else {\n layer.values = obj\n }\n }\n } else if (ext === 'CRV' || ext === 'CURV' || ext === 'THICKNESS' || ext === 'AREA') {\n layer.values = NVMeshLoaders.readCURV(buffer, n_vert)\n layer.isTransparentBelowCalMin = false\n } else if (ext === 'GII') {\n const obj = await NVMeshLoaders.readGII(buffer, n_vert)\n layer.values = obj.scalars // colormapLabel\n layer.colormapLabel = obj.colormapLabel\n } else if (ext === 'MGH' || ext === 'MGZ') {\n if (!isReadColortables) {\n layer.values = (await NVMeshLoaders.readMGH(buffer, n_vert)) as number[]\n } else {\n const obj = await NVMeshLoaders.readMGH(buffer, n_vert, true)\n if ('scalars' in obj) {\n layer.values = obj.scalars\n layer.colormapLabel = obj.colormapLabel\n } // unable to decode colormapLabel\n else {\n layer.values = obj\n }\n }\n } else if (ext === 'NII') {\n layer.values = (await NVMeshLoaders.readNII(buffer, nvmesh.pts, nvmesh.anatomicalStructurePrimary)) as Float32Array\n } else if (ext === 'SMP') {\n layer.values = await NVMeshLoaders.readSMP(buffer, n_vert)\n } else if (ext === 'STC') {\n layer.values = NVMeshLoaders.readSTC(buffer, n_vert)\n } else if (NVMeshLoaders.isCurv(buffer)) {\n // Unknown layer overlay format - hail mary assume FreeSurfer\n layer.values = NVMeshLoaders.readCURV(buffer, n_vert)\n layer.isTransparentBelowCalMin = false\n } else {\n log.warn('Unknown layer overlay format ' + name)\n return layer\n }\n if (!layer.values) {\n log.error('no values in layer')\n return\n }\n layer.nFrame4D = layer.values.length / n_vert\n layer.frame4D = 0\n layer.outlineBorder = outlineBorder\n // determine global min..max\n let mn = layer.values[0]\n let mx = layer.values[0]\n for (let i = 0; i < layer.values.length; i++) {\n mn = Math.min(mn, layer.values[i])\n mx = Math.max(mx, layer.values[i])\n }\n layer.global_min = mn\n layer.global_max = mx\n layer.cal_min = cal_min || 0\n if (!cal_min) {\n layer.cal_min = mn\n }\n layer.cal_max = cal_max || 0\n if (!cal_max) {\n layer.cal_max = mx\n }\n layer.cal_minNeg = NaN\n layer.cal_maxNeg = NaN\n layer.opacity = opacity\n layer.colormap = colormap\n layer.colormapNegative = colormapNegative\n layer.useNegativeCmap = useNegativeCmap\n return layer\n } // readLayer()\n\n // read brainvoyager smp format file\n // https://support.brainvoyager.com/brainvoyager/automation-development/84-file-formats/40-the-format-of-smp-files\n static async readSMP(buffer: ArrayBuffer, n_vert: number): Promise<Float32Array> {\n const len = buffer.byteLength\n let reader = new DataView(buffer)\n let vers = reader.getUint16(0, true)\n if (vers > 5) {\n // assume gzip\n const raw = await NVUtilities.decompress(new Uint8Array(buffer))\n reader = new DataView(raw.buffer)\n vers = reader.getUint16(0, true)\n buffer = raw.buffer\n }\n if (vers > 5) {\n log.error('Unsupported or invalid BrainVoyager SMP version ' + vers)\n }\n const nvert = reader.getUint32(2, true)\n if (nvert !== n_vert) {\n log.error('SMP file has ' + nvert + ' vertices, background mesh has ' + n_vert)\n }\n const nMaps = reader.getUint16(6, true)\n\n const scalars = new Float32Array(nvert * nMaps)\n const maps = []\n let pos = 9\n function readStr(): string {\n const startPos = pos\n while (pos < len && reader.getUint8(pos) !== 0) {\n pos++\n }\n pos++ // skip null termination\n return new TextDecoder().decode(buffer.slice(startPos, pos - 1))\n } // readStr: read variable length string\n\n // read Name of SRF\n const _filenameSRF = readStr()\n\n for (let i = 0; i < nMaps; i++) {\n const m: Partial<SmpMap> = {}\n m.mapType = reader.getUint32(pos, true)\n pos += 4\n // Read additional values only if a lag map\n if (vers >= 3 && m.mapType === 3) {\n m.nLags = reader.getUint32(pos, true)\n pos += 4\n m.mnLag = reader.getUint32(pos, true)\n pos += 4\n m.mxLag = reader.getUint32(pos, true)\n pos += 4\n m.ccOverlay = reader.getUint32(pos, true)\n pos += 4\n }\n m.clusterSize = reader.getUint32(pos, true)\n pos += 4\n m.clusterCheck = reader.getUint8(pos)\n pos += 1\n m.critThresh = reader.getFloat32(pos, true)\n pos += 4\n m.maxThresh = reader.getFloat32(pos, true)\n pos += 4\n if (vers >= 4) {\n m.includeValuesGreaterThreshMax = reader.getUint32(pos, true)\n pos += 4\n }\n m.df1 = reader.getUint32(pos, true)\n pos += 4\n m.df2 = reader.getUint32(pos, true)\n pos += 4\n if (vers >= 5) {\n m.posNegFlag = reader.getUint32(pos, true)\n pos += 4\n } else {\n m.posNegFlag = 3\n }\n m.cortexBonferroni = reader.getUint32(pos, true)\n pos += 4\n m.posMinRGB = [0, 0, 0]\n m.posMaxRGB = [0, 0, 0]\n m.negMinRGB = [0, 0, 0]\n m.negMaxRGB = [0, 0, 0]\n if (vers >= 2) {\n m.posMinRGB[0] = reader.getUint8(pos)\n pos++\n m.posMinRGB[1] = reader.getUint8(pos)\n pos++\n m.posMinRGB[2] = reader.getUint8(pos)\n pos++\n m.posMaxRGB[0] = reader.getUint8(pos)\n pos++\n m.posMaxRGB[1] = reader.getUint8(pos)\n pos++\n m.posMaxRGB[2] = reader.getUint8(pos)\n pos++\n if (vers >= 4) {\n m.negMinRGB[0] = reader.getUint8(pos)\n pos++\n m.negMinRGB[1] = reader.getUint8(pos)\n pos++\n m.negMinRGB[2] = reader.getUint8(pos)\n pos++\n m.negMaxRGB[0] = reader.getUint8(pos)\n pos++\n m.negMaxRGB[1] = reader.getUint8(pos)\n pos++\n m.negMaxRGB[2] = reader.getUint8(pos)\n pos++\n } // vers >= 4\n m.enableSMPColor = reader.getUint8(pos)\n pos++\n if (vers >= 4) {\n m.lut = readStr()\n }\n m.colorAlpha = reader.getFloat32(pos, true)\n pos += 4\n } // vers >= 2\n m.name = readStr()\n const scalarsNew = new Float32Array(buffer, pos, nvert)\n scalars.set(scalarsNew, i * nvert)\n pos += nvert * 4\n maps.push(m)\n } // for i to nMaps\n return scalars\n } // readSMP()\n\n // read mne stc format file, not to be confused with brainvoyager stc format\n // https://github.com/mne-tools/mne-python/blob/main/mne/source_estimate.py#L211-L365\n static readSTC(buffer: ArrayBuffer, n_vert: number): Float32Array {\n // https://github.com/fahsuanlin/fhlin_toolbox/blob/400cb73cda4880d9ad7841d9dd68e4e9762976bf/codes/inverse_read_stc.m\n // let len = buffer.byteLength;\n const reader = new DataView(buffer)\n // first 12 bytes are header\n // let epoch_begin_latency = reader.getFloat32(0, false);\n // let sample_period = reader.getFloat32(4, false);\n const n_vertex = reader.getInt32(8, false)\n if (n_vertex !== n_vert) {\n throw new Error('Overlay has ' + n_vertex + ' vertices, expected ' + n_vert)\n }\n // next 4*n_vertex bytes are vertex IDS\n let pos = 12 + n_vertex * 4\n // next 4 bytes reports number of volumes/time points\n const n_time = reader.getUint32(pos, false)\n pos += 4\n const f32 = new Float32Array(n_time * n_vertex)\n // reading all floats with .slice() would be faster, but lets handle endian-ness\n for (let i = 0; i < n_time * n_vertex; i++) {\n f32[i] = reader.getFloat32(pos, false)\n pos += 4\n }\n return f32\n } // readSTC()\n\n static isCurv(buffer: ArrayBuffer): boolean {\n const view = new DataView(buffer) // ArrayBuffer to dataview\n // ALWAYS big endian\n const sig0 = view.getUint8(0)\n const sig1 = view.getUint8(1)\n const sig2 = view.getUint8(2)\n if (sig0 !== 255 || sig1 !== 255 || sig2 !== 255) {\n utiltiesLogger.debug('Unable to recognize file type: does not appear to be FreeSurfer format.')\n return false\n }\n return true\n }\n\n // read freesurfer curv big-endian format\n // https://github.com/bonilhamusclab/MRIcroS/blob/master/%2BfileUtils/%2Bpial/readPial.m\n // http://www.grahamwideman.com/gw/brain/fs/surfacefileformats.htm\n static readCURV(buffer: ArrayBuffer, n_vert: number): Float32Array {\n const view = new DataView(buffer) // ArrayBuffer to dataview\n // ALWAYS big endian\n const sig0 = view.getUint8(0)\n const sig1 = view.getUint8(1)\n const sig2 = view.getUint8(2)\n const n_vertex = view.getUint32(3, false)\n // let num_f = view.getUint32(7, false);\n const n_time = view.getUint32(11, false)\n if (sig0 !== 255 || sig1 !== 255 || sig2 !== 255) {\n utiltiesLogger.debug('Unable to recognize file type: does not appear to be FreeSurfer format.')\n }\n if (n_vert !== n_vertex) {\n throw new Error('CURV file has different number of vertices ( ' + n_vertex + ')than mesh (' + n_vert + ')')\n }\n if (buffer.byteLength < 15 + 4 * n_vertex * n_time) {\n throw new Error('CURV file smaller than specified')\n }\n const f32 = new Float32Array(n_time * n_vertex)\n let pos = 15\n // reading all floats with .slice() would be faster, but lets handle endian-ness\n for (let i = 0; i < n_time * n_vertex; i++) {\n f32[i] = view.getFloat32(pos, false)\n pos += 4\n }\n let mn = f32[0]\n let mx = f32[0]\n for (let i = 0; i < f32.length; i++) {\n mn = Math.min(mn, f32[i])\n mx = Math.max(mx, f32[i])\n }\n // normalize\n const scale = 1.0 / (mx - mn)\n for (let i = 0; i < f32.length; i++) {\n f32[i] = 1.0 - (f32[i] - mn) * scale\n }\n return f32\n } // readCURV()\n\n // read freesurfer Annotation file provides vertex colors\n // https://surfer.nmr.mgh.harvard.edu/fswiki/LabelsClutsAnnotationFiles\n static readANNOT(buffer: ArrayBuffer, n_vert: number, isReadColortables = false): ANNOT {\n const view = new DataView(buffer) // ArrayBuffer to dataview\n // ALWAYS big endian\n const n_vertex = view.getUint32(0, false)\n const n_vertexDecimated = this.decimateLayerVertices(n_vertex, n_vert)\n if (n_vert !== n_vertexDecimated) {\n throw new Error('ANNOT file has different number of vertices than mesh')\n }\n if (buffer.byteLength < 4 + 8 * n_vertex) {\n throw new Error('ANNOT file smaller than specified')\n }\n let pos = 0\n // reading all floats with .slice() would be faster, but lets handle endian-ness\n const rgba32 = new Uint32Array(n_vertex)\n for (let i = 0; i < n_vertex; i++) {\n const idx = view.getUint32((pos += 4), false)\n rgba32[idx] = view.getUint32((pos += 4), false)\n }\n if (!isReadColortables) {\n // only read label colors, ignore labels\n return rgba32\n }\n let tag = 0\n try {\n tag = view.getInt32((pos += 4), false)\n } catch (error) {\n return rgba32\n }\n const TAG_OLD_COLORTABLE = 1\n if (tag !== TAG_OLD_COLORTABLE) {\n // undocumented old format\n return rgba32\n }\n const ctabversion = view.getInt32((pos += 4), false)\n if (ctabversion > 0) {\n // undocumented old format\n return rgba32\n }\n const maxstruc = view.getInt32((pos += 4), false)\n const len = view.getInt32((pos += 4), false)\n pos += len\n const num_entries = view.getInt32((pos += 4), false)\n if (num_entries < 1) {\n // undocumented old format\n return rgba32\n }\n // preallocate lookuptable\n const LUT = {\n R: Array(maxstruc).fill(0),\n G: Array(maxstruc).fill(0),\n B: Array(maxstruc).fill(0),\n A: Array(maxstruc).fill(0),\n I: Array(maxstruc).fill(0),\n labels: Array(maxstruc).fill('')\n }\n for (let i = 0; i < num_entries; i++) {\n const struc = view.getInt32((pos += 4), false)\n const labelLen = view.getInt32((pos += 4), false)\n pos += 4\n let txt = ''\n for (let c = 0; c < labelLen; c++) {\n const val = view.getUint8(pos++)\n if (val === 0) {\n break\n }\n txt += String.fromCharCode(val)\n }\n pos -= 4\n const R = view.getInt32((pos += 4), false)\n const G = view.getInt32((pos += 4), false)\n const B = view.getInt32((pos += 4), false)\n const A = view.getInt32((pos += 4), false)\n if (struc < 0 || struc >= maxstruc) {\n log.warn('annot entry out of range')\n continue\n }\n LUT.R[struc] = R\n LUT.G[struc] = G\n LUT.B[struc] = B\n LUT.A[struc] = A\n LUT.I[struc] = (A << 24) + (B << 16) + (G << 8) + R\n LUT.labels[struc] = txt\n }\n // start issue1455: if ALL labels are transparent assume ALL opaque\n let isAllAlphaZero = true\n for (let i = 0; i < maxstruc; i++) {\n if (LUT.A[i] > 0) {\n isAllAlphaZero = false\n }\n }\n if (isAllAlphaZero) {\n for (let i = 0; i < maxstruc; i++) {\n LUT.A[i] = 255\n }\n }\n // end issue1455\n const scalars = new Float32Array(n_vertex)\n scalars.fill(-1)\n let nError = 0\n for (let i = 0; i < n_vert; i++) {\n const RGB = rgba32[i]\n for (let c = 0; c < maxstruc; c++) {\n if (LUT.I[c] === RGB) {\n scalars[i] = c\n break\n }\n } // for c\n if (scalars[i] < 0) {\n nError++\n scalars[i] = 0\n }\n }\n if (nError > 0) {\n log.error(`annot vertex colors do not match ${nError} of ${n_vertex} vertices.`)\n }\n for (let i = 0; i < maxstruc; i++) {\n LUT.I[i] = i\n }\n const colormapLabel = cmapper.makeLabelLut(LUT)\n return {\n scalars,\n colormapLabel\n }\n } // readANNOT()\n\n // read BrainNet viewer format\n // https://www.nitrc.org/projects/bnv/\n static readNV(buffer: ArrayBuffer): DefaultMeshType {\n // n.b. clockwise triangle winding, indexed from 1\n const len = buffer.byteLength\n const bytes = new Uint8Array(buffer)\n let pos = 0\n function readStr(): string {\n while (pos < len && bytes[pos] === 10) {\n pos++\n } // skip blank lines\n const startPos = pos\n while (pos < len && bytes[pos] !== 10) {\n pos++\n }\n pos++ // skip EOLN\n if (pos - startPos < 1) {\n return ''\n }\n return new TextDecoder().decode(buffer.slice(startPos, pos - 1))\n }\n let nvert = 0 // 173404 346804\n let ntri = 0\n let v = 0\n let t = 0\n let positions: Float32Array\n let indices: Uint32Array\n while (pos < len) {\n const line = readStr()\n if (line.startsWith('#')) {\n continue\n }\n const items = line.trim().split(/\\s+/)\n if (nvert < 1) {\n nvert = parseInt(items[0])\n positions = new Float32Array(nvert * 3)\n continue\n }\n if (v < nvert * 3) {\n positions![v] = parseFloat(items[0])\n positions![v + 1] = parseFloat(items[1])\n positions![v + 2] = parseFloat(items[2])\n v += 3\n continue\n }\n if (ntri < 1) {\n ntri = parseInt(items[0])\n indices = new Uint32Array(ntri * 3)\n continue\n }\n if (t >= ntri * 3) {\n break\n }\n indices![t + 2] = parseInt(items[0]) - 1\n indices![t + 1] = parseInt(items[1]) - 1\n indices![t + 0] = parseInt(items[2]) - 1\n t += 3\n }\n return {\n positions: positions!,\n indices: indices!\n }\n } // readNV()\n\n // read ASCII Patch File format\n // https://afni.nimh.nih.gov/pub/dist/doc/htmldoc/demos/Bootcamp/CD.html#cd\n // http://www.grahamwideman.com/gw/brain/fs/surfacefileformats.htm\n static readASC(buffer: ArrayBuffer): DefaultMeshType {\n const len = buffer.byteLength\n const bytes = new Uint8Array(buffer)\n let pos = 0\n function readStr(): string {\n while (pos < len && bytes[pos] === 10) {\n pos++\n } // skip blank lines\n const startPos = pos\n while (pos < len && bytes[pos] !== 10) {\n pos++\n }\n pos++ // skip EOLN\n if (pos - startPos < 1) {\n return ''\n }\n return new TextDecoder().decode(buffer.slice(startPos, pos - 1))\n }\n let line = readStr() // 1st line: '#!ascii version of lh.pial'\n if (!line.startsWith('#!ascii')) {\n log.warn('Invalid ASC mesh')\n }\n line = readStr() // 1st line: signature\n let items = line.trim().split(/\\s+/)\n const nvert = parseInt(items[0]) // 173404 346804\n const ntri = parseInt(items[1])\n const positions = new Float32Array(nvert * 3)\n let j = 0\n for (let i = 0; i < nvert; i++) {\n line = readStr() // 1st line: signature\n items = line.trim().split(/\\s+/)\n positions[j] = parseFloat(items[0])\n positions[j + 1] = parseFloat(items[1])\n positions[j + 2] = parseFloat(items[2])\n j += 3\n }\n const indices = new Uint32Array(ntri * 3)\n j = 0\n for (let i = 0; i < ntri; i++) {\n line = readStr() // 1st line: signature\n items = line.trim().split(/\\s+/)\n indices[j] = parseInt(items[0])\n indices[j + 1] = parseInt(items[1])\n indices[j + 2] = parseInt(items[2])\n j += 3\n }\n return {\n positions,\n indices\n }\n } // readASC()\n\n // read legacy VTK format\n static readVTK(buffer: ArrayBuffer): VTK {\n const len = buffer.byteLength\n if (len < 20) {\n throw new Error('File too small to be VTK: bytes = ' + buffer.byteLength)\n }\n const bytes = new Uint8Array(buffer)\n let pos = 0\n function readStr(isSkipBlank = true): string {\n if (isSkipBlank) {\n while (pos < len && bytes[pos] === 10) {\n pos++\n }\n } // skip blank lines\n const startPos = pos\n while (pos < len && bytes[pos] !== 10) {\n pos++\n }\n pos++ // skip EOLN\n if (pos - startPos < 1) {\n return ''\n }\n return new TextDecoder().decode(buffer.slice(startPos, pos - 1))\n }\n let line = readStr() // 1st line: signature\n if (!line.startsWith('# vtk DataFile')) {\n throw new Error('Invalid VTK mesh')\n }\n line = readStr(false) // 2nd line comment, n.b. MRtrix stores empty line\n line = readStr() // 3rd line ASCII/BINARY\n if (line.startsWith('ASCII')) {\n return NVMeshLoaders.readTxtVTK(buffer)\n } else if (!line.startsWith('BINARY')) {\n throw new Error('Invalid VTK image, expected ASCII or BINARY ' + line)\n }\n line = readStr() // 5th line \"DATASET POLYDATA\"\n if (!line.includes('POLYDATA')) {\n throw new Error('Only able to read VTK POLYDATA ' + line)\n }\n line = readStr() // 6th line \"POINTS 10261 float\"\n if (!line.includes('POINTS') || (!line.includes('double') && !line.includes('float'))) {\n log.warn('Only able to read VTK float or double POINTS' + line)\n }\n const isFloat64 = line.includes('double')\n let items = line.trim().split(/\\s+/)\n const nvert = parseInt(items[1]) // POINTS 10261 float\n const nvert3 = nvert * 3\n const positions = new Float32Array(nvert3)\n const reader = new DataView(buffer)\n if (isFloat64) {\n for (let i = 0; i < nvert3; i++) {\n positions[i] = reader.getFloat64(pos, false)\n pos += 8\n }\n } else {\n for (let i = 0; i < nvert3; i++) {\n positions[i] = reader.getFloat32(pos, false)\n pos += 4\n }\n }\n line = readStr() // Type, \"LINES 11885 \"\n items = line.trim().split(/\\s+/)\n const tris = []\n if (items[0].includes('LINES')) {\n const n_count = parseInt(items[1])\n // tractogaphy data: detect if borked by DiPy\n const posOK = pos\n line = readStr() // borked files \"OFFSETS vtktypeint64\"\n if (line.startsWith('OFFSETS')) {\n let isInt64 = false\n if (line.includes('int64')) {\n isInt64 = true\n }\n const offsetPt0 = new Uint32Array(n_count)\n if (isInt64) {\n let isOverflowInt32 = false\n for (let c = 0; c < n_count; c++) {\n let idx = reader.getInt32(pos, false)\n if (idx !== 0) {\n isOverflowInt32 = true\n }\n pos += 4\n idx = reader.getInt32(pos, false)\n pos += 4\n offsetPt0[c] = idx\n }\n if (isOverflowInt32) {\n log.warn('int32 overflow: JavaScript does not support int64')\n }\n } else {\n for (let c = 0; c < n_count; c++) {\n const idx = reader.getInt32(pos, false)\n pos += 4\n offsetPt0[c] = idx\n }\n }\n const pts = positions\n return {\n pts,\n offsetPt0\n }\n }\n pos = posOK // valid VTK file\n let npt = 0\n const offsetPt0 = []\n const pts = []\n offsetPt0.push(npt) // 1st streamline starts at 0\n for (let c = 0; c < n_count; c++) {\n const numPoints = reader.getInt32(pos, false)\n pos += 4\n npt += numPoints\n offsetPt0.push(npt)\n for (let i = 0; i < numPoints; i++) {\n const idx = reader.getInt32(pos, false) * 3\n pos += 4\n pts.push(positions[idx + 0])\n pts.push(positions[idx + 1])\n pts.push(positions[idx + 2])\n } // for numPoints: number of segments in streamline\n } // for n_count: number of streamlines\n return {\n pts: Float32Array.from(pts),\n offsetPt0: Uint32Array.from(offsetPt0)\n }\n } else if (items[0].includes('TRIANGLE_STRIPS')) {\n const nstrip = parseInt(items[1])\n for (let i = 0; i < nstrip; i++) {\n const ntri = reader.getInt32(pos, false) - 2 // -2 as triangle strip is creates pts - 2 faces\n pos += 4\n for (let t = 0; t < ntri; t++) {\n if (t % 2) {\n // preserve winding order\n tris.push(reader.getInt32(pos + 8, false))\n tris.push(reader.getInt32(pos + 4, false))\n tris.push(reader.getInt32(pos, false))\n } else {\n tris.push(reader.getInt32(pos, false))\n tris.push(reader.getInt32(pos + 4, false))\n tris.push(reader.getInt32(pos + 8, false))\n }\n pos += 4\n } // for each triangle\n pos += 8\n } // for each strip\n } else if (items[0].includes('POLYGONS')) {\n const npoly = parseInt(items[1])\n const byteOffsetAfterPoly = pos\n const maybeOffsetsLine = readStr()\n if (maybeOffsetsLine.startsWith('OFFSETS')) {\n let isInt64 = maybeOffsetsLine.includes('int64')\n const offset = new Uint32Array(npoly)\n let is32bitOverflow = false\n for (let i = 0; i < npoly; i++) {\n if (isInt64) {\n if (reader.getInt32(pos, false) !== 0) {\n is32bitOverflow = true\n }\n pos += 4\n } // skip high 32 bits\n offset[i] = reader.getInt32(pos, false)\n pos += 4\n }\n if (!Number.isSafeInteger(npoly) || npoly >= 2147483648 || is32bitOverflow) {\n throw new Error(`values exceed 2GB limit`)\n }\n const connLine = readStr()\n if (!connLine.startsWith('CONNECTIVITY')) {\n throw new Error('Expected CONNECTIVITY after OFFSETS')\n }\n isInt64 = connLine.includes('int64')\n const numIndices = offset[npoly - 1]\n const connectivity = new Uint32Array(numIndices)\n for (let i = 0; i < numIndices; i++) {\n if (isInt64) {\n pos += 4\n }\n connectivity[i] = reader.getInt32(pos, false)\n pos += 4\n }\n\n for (let i = 0; i < npoly; i++) {\n const start = i === 0 ? 0 : offset[i - 1]\n const end = offset[i]\n for (let t = 1; t < end - start - 1; t++) {\n tris.push(connectivity[start])\n tris.push(connectivity[start + t])\n tris.push(connectivity[start + t + 1])\n }\n }\n } else {\n // Classic binary VTK format: rewind and parse as before\n pos = byteOffsetAfterPoly\n for (let i = 0; i < npoly; i++) {\n const ntri = reader.getInt32(pos, false) - 2\n if (i === 0 && ntri > 65535) {\n throw new Error('Invalid VTK binary polygons using little-endian data (MRtrix)')\n }\n pos += 4\n const fx = reader.getInt32(pos, false)\n pos += 4\n let fy = reader.getInt32(pos, false)\n pos += 4\n for (let t = 0; t < ntri; t++) {\n const fz = reader.getInt32(pos, false)\n pos += 4\n tris.push(fx, fy, fz)\n fy = fz\n }\n }\n }\n } else {\n throw new Error('Unsupported binary VTK datatype ' + items[0])\n }\n const indices = new Uint32Array(tris)\n return {\n positions,\n indices\n }\n } // readVTK()\n\n static readWRL(buffer: ArrayBuffer): DefaultMeshType {\n const wrlText = new TextDecoder('utf-8').decode(buffer)\n const coordRegex = /coord\\s+Coordinate\\s*\\{\\s*point\\s*\\[([\\s\\S]*?)\\]/\n const indexRegex = /coordIndex\\s*\\[([\\s\\S]*?)\\]/\n const colorRegex = /color\\s+Color\\s*\\{\\s*color\\s*\\[([\\s\\S]*?)\\]/\n const coordMatch = coordRegex.exec(wrlText)\n const indexMatch = indexRegex.exec(wrlText)\n const colorMatch = colorRegex.exec(wrlText)\n\n if (!coordMatch || !indexMatch) {\n throw new Error('Invalid WRL file: Could not find vertices or indices.')\n }\n // Extract vertex positions\n const positions = new Float32Array(\n coordMatch[1]\n .trim()\n .split(/[\\s,]+/)\n .map(Number)\n )\n // Extract per-vertex colors (if they exist)\n let colors: Float32Array | null = null\n if (colorMatch) {\n colors = new Float32Array(\n colorMatch[1]\n .trim()\n .split(/[\\s,]+/)\n .map(Number)\n )\n const nVert = positions.length / 3\n if (colors.length !== nVert * 3) {\n console.warn(`Unexpected color count: expected ${nVert * 3}, got ${colors.length}`)\n colors = null // Ignore malformed color data\n }\n }\n // Extract triangle indices (ignoring `-1` separators)\n const indices = new Uint32Array(\n indexMatch[1]\n .trim()\n .split(/[\\s,]+/)\n .map(Number)\n .filter((v) => v !== -1)\n )\n return { positions, indices, colors }\n } // readWRL()\n\n // read brainsuite DFS format\n // http://brainsuite.org/formats/dfs/\n static readDFS(buffer: ArrayBuffer): DefaultMeshType {\n // Does not play with other formats: vertex positions do not use Aneterior Commissure as origin\n const reader = new DataView(buffer)\n const magic = reader.getUint32(0, true) // \"DFS_\"\n const LE = reader.getUint16(4, true) // \"LE\"\n if (magic !== 1599292996 || LE !== 17740) {\n log.warn('Not a little-endian brainsuite DFS mesh')\n }\n const hdrBytes = reader.getUint32(12, true)\n // var mdoffset = reader.getUint32(16, true);\n // var pdoffset = reader.getUint32(20, true);\n const nface = reader.getUint32(24, true) // number of triangles\n const nvert = reader.getUint32(28, true)\n // var nStrips = reader.getUint32(32, true); //deprecated\n // var stripSize = reader.getUint32(36, true); //deprecated\n // var normals = reader.getUint32(40, true);\n // var uvStart = reader.getUint32(44, true);\n const vcoffset = reader.getUint32(48, true) // vertexColor offset\n // var precision = reader.getUint32(52, true);\n // float64 orientation[4][4]; //4x4 matrix, affine transformation to world coordinates*)\n let pos = hdrBytes\n const indices = new Uint32Array(buffer, pos, nface * 3)\n pos += nface * 3 * 4\n const positions = new Float32Array(buffer, pos, nvert * 3)\n // oops, triangle winding opposite of CCW convention\n for (let i = 0; i < nvert * 3; i += 3) {\n const tmp = positions[i]\n positions[i] = positions[i + 1]\n positions[i + 1] = tmp\n }\n let colors\n if (vcoffset >= 0) {\n colors = new Float32Array(buffer, vcoffset, nvert * 3)\n }\n return {\n positions,\n indices,\n colors\n }\n }\n\n // read surfice MZ3 format\n // https://github.com/neurolabusc/surf-ice/tree/master/mz3\n static async readMZ3(buffer: ArrayBuffer, n_vert = 0): Promise<MZ3> {\n if (buffer.byteLength < 20) {\n throw new Error('File too small to be mz3: bytes = ' + buffer.byteLength)\n }\n let reader = new DataView(buffer)\n let _buffer = buffer\n\n // Check for gzip\n let magic = reader.getUint16(0, true)\n if (magic === 35615 || magic === 8075) {\n const raw = await NVUtilities.decompress(new Uint8Array(buffer))\n reader = new DataView(raw.buffer)\n magic = reader.getUint16(0, true)\n _buffer = raw.buffer\n }\n const attr = reader.getUint16(2, true)\n const nface = reader.getUint32(4, true)\n let nvert = reader.getUint32(8, true)\n const nskip = reader.getUint32(12, true)\n utiltiesLogger.debug('MZ3 magic %d attr %d face %d vert %d skip %d', magic, attr, nface, nvert, nskip)\n if (magic !== 23117) {\n throw new Error('Invalid MZ3 file')\n }\n const isFace = (attr & 1) !== 0\n const isVert = (attr & 2) !== 0\n const isRGBA = (attr & 4) !== 0\n let isSCALAR = (attr & 8) !== 0\n const isDOUBLE = (attr & 16) !== 0\n const isAOMAP = (attr & 32) !== 0\n const isLOOKUP = (attr & 64) !== 0\n utiltiesLogger.debug(`isFace=${isFace} isVert=${isVert} isRGBA=${isRGBA} isSCALAR=${isSCALAR} isDOUBLE=${isDOUBLE} isAOMAP=${isAOMAP} isLOOKUP=${isLOOKUP}`)\n if (attr > 127) {\n throw new Error('Unsupported future version of MZ3 file')\n }\n\n let bytesPerScalar = 4\n if (isDOUBLE) {\n bytesPerScalar = 8\n }\n\n let NSCALAR = 0\n if (n_vert > 0 && !isFace && nface < 1 && !isRGBA) {\n isSCALAR = true\n }\n if (isSCALAR) {\n const nv = n_vert || nvert\n const FSizeWoScalars = 16 + nskip + (isFace ? nface * 12 : 0) + (isVert ? nv * 12 : 0) + (isRGBA ? nv * 4 : 0)\n const scalarFloats = Math.floor((_buffer.byteLength - FSizeWoScalars) / bytesPerScalar)\n if (nvert !== n_vert && scalarFloats % n_vert === 0) {\n nvert = n_vert\n }\n NSCALAR = Math.floor(scalarFloats / nvert)\n if (NSCALAR < 1) {\n log.warn('Corrupt MZ3: file reports NSCALAR but not enough bytes')\n isSCALAR = false\n }\n }\n\n if (nvert < 3 && n_vert < 3) {\n throw new Error('Not a mesh MZ3 file (maybe scalar)')\n }\n if (n_vert > 0 && n_vert !== nvert) {\n log.warn('Layer has ' + nvert + 'vertices, but background mesh has ' + n_vert)\n }\n\n let filepos = 16 + nskip\n const view = new DataView(_buffer)\n\n let indices: Uint32Array | null = null\n if (isFace) {\n indices = new Uint32Array(nface * 3)\n for (let i = 0; i < nface * 3; i++) {\n indices[i] = view.getUint32(filepos, true)\n filepos += 4\n }\n }\n\n let positions: Float32Array | null = null\n if (isVert) {\n positions = new Float32Array(nvert * 3)\n for (let i = 0; i < nvert * 3; i++) {\n positions[i] = view.getFloat32(filepos, true)\n filepos += 4\n }\n }\n let colors: Float32Array | null = null\n if (isRGBA) {\n colors = new Float32Array(nvert * 3)\n for (let i = 0; i < nvert; i++) {\n for (let j = 0; j < 3; j++) {\n colors[i * 3 + j] = view.getUint8(filepos++) / 255\n }\n filepos++ // skip Alpha\n }\n }\n let scalars = new Float32Array()\n if (isSCALAR && NSCALAR > 0) {\n if (isDOUBLE) {\n const flt64 = new Float64Array(NSCALAR * nvert)\n for (let i = 0; i < NSCALAR * nvert; i++) {\n flt64[i] = view.getFloat64(filepos, true)\n filepos += 8\n }\n scalars = Float32Array.from(flt64)\n } else {\n scalars = new Float32Array(NSCALAR * nvert)\n for (let i = 0; i < NSCALAR * nvert; i++) {\n scalars[i] = view.getFloat32(filepos, true)\n filepos += 4\n }\n }\n }\n if (n_vert > 0 && isLOOKUP && isSCALAR) {\n // Read NSKIP bytes after 16-byte header\n const decoder = new TextDecoder('utf-8')\n const jsonBytes = new Uint8Array(_buffer, 16, nskip)\n const jsonText = decoder.decode(jsonBytes)\n const colormap = JSON.parse(jsonText)\n const colormapLabel = cmapper.makeLabelLut(colormap)\n return { scalars, colormapLabel }\n }\n if (n_vert > 0 && isRGBA && isSCALAR) {\n let mx = scalars[0]\n for (let i = 0; i < nvert; i++) {\n mx = Math.max(mx, scalars[i])\n }\n const Labels: ColorMap = { R: [], G: [], B: [], A: [], I: [], labels: [] }\n for (let i = 0; i <= mx; i++) {\n for (let v = 0; v < nvert; v++) {\n if (i === scalars[v]) {\n const v3 = v * 3\n Labels.I.push(i)\n Labels.R.push(colors![v3] * 255)\n Labels.G.push(colors![v3 + 1] * 255)\n Labels.B.push(colors![v3 + 2] * 255)\n Labels.A.push(255)\n Labels.labels!.push(`${i}`)\n break\n }\n }\n }\n const colormapLabel = cmapper.makeLabelLut(Labels)\n return { scalars, colormapLabel }\n }\n if (n_vert > 0) {\n return { scalars }\n }\n return { positions, indices, scalars, colors }\n }\n\n // read PLY format\n // https://en.wikipedia.org/wiki/PLY_(file_format)\n static readPLY(buffer: ArrayBuffer): DefaultMeshType {\n const len = buffer.byteLength\n const bytes = new Uint8Array(buffer)\n let pos = 0\n function readStr(): string {\n while (pos < len && bytes[pos] === 10) {\n pos++\n } // skip blank lines\n const startPos = pos\n while (pos < len && bytes[pos] !== 10) {\n pos++\n }\n pos++ // skip EOLN\n if (pos - startPos < 1) {\n return ''\n }\n return new TextDecoder().decode(buffer.slice(startPos, pos - 1))\n }\n let line = readStr() // 1st line: magic 'ply'\n if (!line.startsWith('ply')) {\n throw new Error('Not a valid PLY file')\n }\n line = readStr() // 2nd line: format 'format binary_little_endian 1.0'\n const isAscii = line.includes('ascii')\n function dataTypeBytes(str: string): number {\n if (str === 'char' || str === 'uchar' || str === 'int8' || str === 'uint8') {\n return 1\n }\n if (str === 'short' || str === 'ushort' || str === 'int16' || str === 'uint16') {\n return 2\n }\n if (str === 'int' || str === 'uint' || str === 'int32' || str === 'uint32' || str === 'float' || str === 'float32') {\n return 4\n }\n if (str === 'double') {\n return 8\n }\n throw new Error('Unknown data type: ' + str)\n }\n const isLittleEndian = line.includes('binary_little_endian')\n let nvert = 0\n let vertIsDouble = false\n let vertStride = 0 // e.g. if each vertex stores xyz as float32 and rgb as uint8, stride is 15\n let indexStrideBytes = 0 // \"list uchar int vertex_indices\" has stride 1 + 3 * 4\n let indexCountBytes = 0 // if \"property list uchar int vertex_index\" this is 1 (uchar)\n let indexBytes = 0 // if \"property list uchar int vertex_index\" this is 4 (int)\n let indexPaddingBytes = 0\n let nIndexPadding = 0\n let nface = 0\n const vertexProps: string[] = []\n let currVertPropOffset = 0\n let redOffset = -1\n let greenOffset = -1\n let blueOffset = -1\n let hasColors = false\n while (pos < len && !line.startsWith('end_header')) {\n line = readStr()\n if (line.startsWith('comment')) {\n continue\n }\n // line = line.replaceAll('\\t', ' '); // ?are tabs valid white space?\n let items = line.split(/\\s/)\n if (line.startsWith('element vertex')) {\n nvert = parseInt(items[items.length - 1])\n // read vertex properties:\n line = readStr()\n items = line.split(/\\s/)\n currVertPropOffset = 0\n vertexProps.length = 0\n while (line.startsWith('property')) {\n const datatype = items[1]\n const propName = items[2]\n // record property name (for ASCII token mapping)\n vertexProps.push(propName)\n // record offsets for color components (for binary parsing)\n if (propName === 'red') {\n redOffset = currVertPropOffset\n hasColors = true\n } else if (propName === 'green') {\n greenOffset = currVertPropOffset\n hasColors = true\n } else if (propName === 'blue') {\n blueOffset = currVertPropOffset\n hasColors = true\n }\n if (propName === 'x' && datatype.startsWith('double')) {\n vertIsDouble = true\n } else if (propName === 'x' && !datatype.startsWith('float')) {\n log.error('Error: expect ply xyz to be float or double: ' + line)\n }\n const bytes = dataTypeBytes(datatype)\n vertStride += bytes\n currVertPropOffset += bytes\n line = readStr()\n items = line.split(/\\s/)\n }\n }\n if (line.startsWith('element face')) {\n nface = parseInt(items[items.length - 1])\n // read face properties:\n line = readStr()\n items = line.split(/\\s/)\n while (line.startsWith('property')) {\n if (items[1] === 'list') {\n indexCountBytes = dataTypeBytes(items[2])\n indexBytes = dataTypeBytes(items[3])\n indexStrideBytes += indexCountBytes + 3 * indexBytes // e.g. \"uchar int\" is 1 + 3 * 4 bytes\n } else {\n const bytes = dataTypeBytes(items[1])\n indexStrideBytes += bytes\n if (indexBytes === 0) {\n // this index property is BEFORE the list\n indexPaddingBytes += bytes\n nIndexPadding++\n }\n }\n line = readStr()\n items = line.split(/\\s/)\n }\n }\n } // while reading all lines of header\n if (isAscii) {\n if (nface < 1) {\n log.error(`Malformed ply format: faces ${nface} `)\n }\n const positions = new Float32Array(nvert * 3)\n let colors: Float32Array | undefined\n // find ascii token indices for x,y,z and rgb if present\n const idxX = vertexProps.indexOf('x')\n const idxY = vertexProps.indexOf('y')\n const idxZ = vertexProps.indexOf('z')\n const idxR = vertexProps.indexOf('red')\n const idxG = vertexProps.indexOf('green')\n const idxB = vertexProps.indexOf('blue')\n if (idxR !== -1 && idxG !== -1 && idxB !== -1) {\n colors = new Float32Array(nvert * 3)\n hasColors = true\n }\n let v = 0\n for (let i = 0; i < nvert; i++) {\n line = readStr()\n const items = line.split(/\\s/)\n // read xyz using property indices (fallback to 0,1,2 if not found)\n const tx = idxX >= 0 ? parseFloat(items[idxX]) : parseFloat(items[0])\n const ty = idxY >= 0 ? parseFloat(items[idxY]) : parseFloat(items[1])\n const tz = idxZ >= 0 ? parseFloat(items[idxZ]) : parseFloat(items[2])\n positions[v] = tx\n positions[v + 1] = ty\n positions[v + 2] = tz\n if (hasColors && colors) {\n const rr = idxR >= 0 ? parseInt(items[idxR]) : 0\n const gg = idxG >= 0 ? parseInt(items[idxG]) : 0\n const bb = idxB >= 0 ? parseInt(items[idxB]) : 0\n const vi = i * 3\n colors[vi] = rr / 255.0\n colors[vi + 1] = gg / 255.0\n colors[vi + 2] = bb / 255.0\n }\n v += 3\n }\n let indices = new Uint32Array(nface * 3)\n let f = 0\n for (let i = 0; i < nface; i++) {\n line = readStr()\n const items = line.split(/\\s/)\n const nTri = parseInt(items[nIndexPadding]) - 2\n if (nTri < 1) {\n break\n } // error\n if (f + nTri * 3 > indices.length) {\n const c = new Uint32Array(indices.length + indices.length)\n c.set(indices)\n indices = c.slice()\n }\n const idx0 = parseInt(items[nIndexPadding + 1])\n let idx1 = parseInt(items[nIndexPadding + 2])\n for (let j = 0; j < nTri; j++) {\n const idx2 = parseInt(items[nIndexPadding + 3 + j])\n indices[f + 0] = idx0\n indices[f + 1] = idx1\n indices[f + 2] = idx2\n idx1 = idx2\n f += 3\n }\n }\n if (indices.length !== f) {\n indices = indices.slice(0, f)\n }\n const out: DefaultMeshType = {\n positions,\n indices\n }\n if (hasColors) {\n // colors was created only when we detected rgb properties\n out.colors = typeof colors !== 'undefined' ? colors : undefined\n }\n return out\n } // if isAscii\n if (vertStride < 12 || indexCountBytes < 1 || indexBytes < 1 || nface < 1) {\n log.warn(`Malformed ply format: stride ${vertStride} count ${indexCountBytes} iBytes ${indexBytes} iStrideBytes ${indexStrideBytes} iPadBytes ${indexPaddingBytes} faces ${nface}`)\n }\n const reader = new DataView(buffer)\n let positions\n let colors: Float32Array | undefined\n if (hasColors) {\n colors = new Float32Array(nvert * 3)\n }\n if (pos % 4 === 0 && vertStride === 12 && isLittleEndian) {\n // optimization: vertices only store xyz position as float\n // n.b. start offset of Float32Array must be a multiple of 4\n positions = new Float32Array(buffer, pos, nvert * 3)\n pos += nvert * vertStride\n // if colors are present they wouldn't be in this optimized path (vertStride would be >12)\n } else {\n positions = new Float32Array(nvert * 3)\n let v = 0\n for (let i = 0; i < nvert; i++) {\n if (vertIsDouble) {\n positions[v] = reader.getFloat64(pos, isLittleEndian)\n positions[v + 1] = reader.getFloat64(pos + 8, isLittleEndian)\n positions[v + 2] = reader.getFloat64(pos + 16, isLittleEndian)\n } else {\n positions[v] = reader.getFloat32(pos, isLittleEndian)\n positions[v + 1] = reader.getFloat32(pos + 4, isLittleEndian)\n positions[v + 2] = reader.getFloat32(pos + 8, isLittleEndian)\n }\n if (hasColors && colors) {\n // read uchar rgb at recorded offsets (if set)\n const base = pos\n const r = redOffset >= 0 ? reader.getUint8(base + redOffset) : 0\n const g = greenOffset >= 0 ? reader.getUint8(base + greenOffset) : 0\n const b = blueOffset >= 0 ? reader.getUint8(base + blueOffset) : 0\n const vi = i * 3\n colors[vi] = r / 255.0\n colors[vi + 1] = g / 255.0\n colors[vi + 2] = b / 255.0\n }\n v += 3\n pos += vertStride\n }\n }\n const indices = new Uint32Array(nface * 3) // assume triangular mesh: pre-allocation optimization\n let isTriangular = true\n let j = 0\n if (indexCountBytes === 1 && indexBytes === 4 && indexStrideBytes === 13) {\n // default mode: \"list uchar int vertex_indices\" without other properties\n for (let i = 0; i < nface; i++) {\n const nIdx = reader.getUint8(pos)\n pos += indexCountBytes\n if (nIdx !== 3) {\n isTriangular = false\n }\n indices[j] = reader.getUint32(pos, isLittleEndian)\n pos += 4\n indices[j + 1] = reader.getUint32(pos, isLittleEndian)\n pos += 4\n indices[j + 2] = reader.getUint32(pos, isLittleEndian)\n pos += 4\n j += 3\n }\n } else {\n // not 1:4 index data\n let startPos = pos\n for (let i = 0; i < nface; i++) {\n pos = startPos + indexPaddingBytes\n let nIdx = 0\n if (indexCountBytes === 1) {\n nIdx = reader.getUint8(pos)\n } else if (indexCountBytes === 2) {\n nIdx = reader.getUint16(pos, isLittleEndian)\n } else if (indexCountBytes === 4) {\n nIdx = reader.getUint32(pos, isLittleEndian)\n }\n pos += indexCountBytes\n if (nIdx !== 3) {\n isTriangular = false\n }\n for (let k = 0; k < 3; k++) {\n if (indexBytes === 1) {\n indices[j] = reader.getUint8(pos)\n } else if (indexBytes === 2) {\n indices[j] = reader.getUint16(pos, isLittleEndian)\n } else if (indexBytes === 4) {\n indices[j] = reader.getUint32(pos, isLittleEndian)\n }\n j++\n pos += indexBytes\n }\n startPos += indexStrideBytes\n } // for each face\n } // if not 1:4 datatype\n if (!isTriangular) {\n log.warn('Only able to read PLY meshes limited to triangles.')\n }\n const out: DefaultMeshType = {\n positions,\n indices\n }\n if (hasColors && typeof colors !== 'undefined') {\n out.colors = colors\n }\n return out\n } // readPLY()\n\n // FreeSurfer can convert meshes to ICO/TRI format text files\n // https://github.com/dfsp-spirit/freesurferformats/blob/434962608108c75d4337d5e7a5096e3bd4ee6ee6/R/read_fs_surface.R#L1090\n // detect TRI format that uses same extension\n // http://paulbourke.net/dataformats/tri/\n static readICO(buffer: ArrayBuffer): DefaultMeshType {\n const enc = new TextDecoder('utf-8')\n const txt = enc.decode(buffer)\n const lines = txt.split('\\n')\n let header = lines[0].trim().split(/\\s+/)\n // read line 0: header\n // FreeSurfer header has one item: [0]'num_verts'\n // Bourke header has 2 items: [0]'num_verts', [1]'num_faces'\n if (header.length > 1) {\n log.warn('This is not a valid FreeSurfer ICO/TRI mesh.')\n }\n const num_v = parseInt(header[0])\n // read vertices: each line has 4 values: index, x, y, z\n const positions = new Float32Array(num_v * 3)\n // let v = 0;\n let line = 1 // line 0 is header\n for (let i = 0; i < num_v; i++) {\n const items = lines[line].trim().split(/\\s+/)\n line++\n // idx is indexed from 1, not 0\n let idx = parseInt(items[0]) - 1\n const x = parseFloat(items[1])\n const y = parseFloat(items[2])\n const z = parseFloat(items[3])\n if (idx < 0 || idx >= num_v) {\n log.error('ICO vertices corrupted')\n break\n }\n idx *= 3\n positions[idx] = x\n positions[idx + 1] = y\n positions[idx + 2] = z\n } // read all vertices\n // read faces\n header = lines[line].trim().split(/\\s+/)\n line++\n const num_f = parseInt(header[0])\n const indices = new Uint32Array(num_f * 3)\n for (let i = 0; i < num_f; i++) {\n const items = lines[line].trim().split(/\\s+/)\n line++\n // all values indexed from 1, not 0\n let idx = parseInt(items[0]) - 1\n const x = parseInt(items[1]) - 1\n const y = parseInt(items[2]) - 1\n const z = parseInt(items[3]) - 1\n if (idx < 0 || idx >= num_f) {\n log.error('ICO indices corrupted')\n break\n }\n idx *= 3\n indices[idx] = x\n indices[idx + 1] = y\n indices[idx + 2] = z\n } // read all faces\n // FreeSurfer seems to enforce clockwise winding: reverse to CCW\n for (let j = 0; j < indices.length; j += 3) {\n const tri = indices[j]\n indices[j] = indices[j + 1]\n indices[j + 1] = tri\n }\n return {\n positions,\n indices\n }\n } // readICO()\n\n // While BYU and FreeSurfer GEO are related\n // - BYU can have multiple parts\n // - BYU faces not always triangular\n // http://www.grahamwideman.com/gw/brain/fs/surfacefileformats.htm#GeoFile\n // http://www.eg-models.de/formats/Format_Byu.html\n // https://github.com/dfsp-spirit/freesurferformats/blob/dafaf88a601dac90fa3c9aae4432f003f5344546/R/read_fs_surface.R#L924\n // https://github.com/dfsp-spirit/freesurferformats/blob/434962608108c75d4337d5e7a5096e3bd4ee6ee6/R/read_fs_surface.R#L1144\n // n.b. AFNI uses the '.g' extension for this format 'ConvertSurface -i_gii L.surf.gii -o_byu L'\n static readGEO(buffer: ArrayBuffer, isFlipWinding = false): DefaultMeshType {\n const enc = new TextDecoder('utf-8')\n const txt = enc.decode(buffer)\n const lines = txt.split('\\n')\n const header = lines[0].trim().split(/\\s+/)\n // read line 0: header\n // header[0]='nparts', [1]'npoints/vertices', [2]'npolys/faces', [3]'nconnects'\n const num_p = parseInt(header[0])\n let num_v = parseInt(header[1])\n let num_f = parseInt(header[2])\n const num_c = parseInt(header[3])\n if (num_p > 1 || num_c !== num_f * 3) {\n log.warn('Multi-part BYU/GEO header or not a triangular mesh.')\n }\n // skip line 1: it is redundant (contains number of faces once more)\n // next read the vertices (points)\n const pts = []\n num_v *= 3 // each vertex has three components (x,y,z)\n let v = 0\n let line = 2 // line 0 and 1 are header\n while (v < num_v) {\n const items = lines[line].trim().split(/\\s+/)\n line++\n for (let i = 0; i < items.length; i++) {\n pts.push(parseFloat(items[i]))\n v++\n if (v >= num_v) {\n break\n }\n } // for each item\n } // read all vertices\n // next read faces (triangles)\n const t: number[] = []\n num_f *= 3 // each triangle has three vertices (i,j,k)\n let f = 0\n while (f < num_f) {\n const items = lines[line].trim().split(/\\s+/)\n line++\n for (let i = 0; i < items.length; i++) {\n t.push(Math.abs(parseInt(items[i])) - 1)\n f++\n if (f >= num_f) {\n break\n }\n } // for each item\n } // read all faces\n // FreeSurfer seems to enforce clockwise winding: reverse to CCW\n if (isFlipWinding) {\n for (let j = 0; j < t.length; j += 3) {\n const tri = t[j]\n t[j] = t[j + 1]\n t[j + 1] = tri\n }\n }\n // return results\n const positions = new Float32Array(pts)\n const indices = new Uint32Array(t)\n return {\n positions,\n indices\n }\n } // readGEO()\n\n // read OFF format\n // https://en.wikipedia.org/wiki/OFF_(file_format)\n static readOFF(buffer: ArrayBuffer): DefaultMeshType {\n const enc = new TextDecoder('utf-8')\n const txt = enc.decode(buffer)\n const lines = txt.split('\\n')\n // var n = lines.length;\n const pts = []\n const t = []\n let i = 0\n // first line signature \"OFF\", but R freesurfer package uses \"# OFF\"\n if (!lines[i].includes('OFF')) {\n log.warn('File does not start with OFF')\n } else {\n i++\n }\n let items = lines[i].trim().split(/\\s+/)\n const num_v = parseInt(items[0])\n const num_f = parseInt(items[1])\n i++\n for (let j = 0; j < num_v; j++) {\n const str = lines[i]\n items = str.trim().split(/\\s+/)\n pts.push(parseFloat(items[0]))\n pts.push(parseFloat(items[1]))\n pts.push(parseFloat(items[2]))\n i++\n }\n for (let j = 0; j < num_f; j++) {\n const str = lines[i]\n items = str.trim().split(/\\s+/)\n const n = parseInt(items[0])\n if (n !== 3) {\n log.warn('Only able to read OFF files with triangular meshes')\n }\n t.push(parseInt(items[1]))\n t.push(parseInt(items[2]))\n t.push(parseInt(items[3]))\n i++\n }\n const positions = new Float32Array(pts)\n const indices = new Uint32Array(t)\n return {\n positions,\n indices\n }\n } // readOFF()\n\n static readOBJMNI(buffer: ArrayBuffer): DefaultMeshType {\n // Support MNI 'P'olygon mesh format\n // n.b. uses same .obj extension as WaveFront OBJ meshes\n // https://bigbrain.loris.ca/main.php?test_name=brainsurfaces\n // http://www.bic.mni.mcgill.ca/users/mishkin/mni_obj_format.pdf\n // https://pages.stat.wisc.edu/~mchung/softwares/mesh/mesh.html\n // https://github.com/aces/brainbrowser/tree/master\n const enc = new TextDecoder('utf-8')\n const txt = enc.decode(buffer)\n const items = txt.trim().split(/\\s*,\\s*|\\s+/)\n if (items.length < 1 || items[0] !== 'P') {\n log.warn('This is not a valid MNI OBJ mesh.')\n }\n let j = 6\n const nVert = parseInt(items[j++])\n const nVertX3 = nVert * 3\n const positions = new Float32Array(nVertX3)\n for (let i = 0; i < nVertX3; i++) {\n positions[i] = parseFloat(items[j++])\n }\n j += nVertX3\n const nTri = parseInt(items[j++])\n const colour_flag = parseInt(items[j++])\n if (nTri < 1 || colour_flag < 0 || colour_flag > 2) {\n log.warn('This is not a valid MNI OBJ mesh.')\n }\n let num_c = 1\n if (colour_flag === 1) {\n num_c = nTri\n } else if (colour_flag === 1) {\n num_c = nVert\n }\n j += num_c * 4\n j += nTri\n const nTriX3 = nTri * 3\n const indices = new Uint32Array(nTriX3)\n for (let i = 0; i < nTriX3; i++) {\n indices[i] = parseInt(items[j++])\n }\n return {\n positions,\n indices\n }\n } // readOBJMNI()\n\n static async readOBJ(buffer: ArrayBuffer): Promise<DefaultMeshType> {\n // WaveFront OBJ format\n const headerBytes = new Uint8Array(buffer, 0, 2)\n if (headerBytes[0] === 0x1f && headerBytes[1] === 0x8b) {\n // gzip signature 0x1F8B in little and big endian\n buffer = await NVUtilities.decompressToBuffer(new Uint8Array(buffer))\n }\n const enc = new TextDecoder('utf-8')\n const txt = enc.decode(buffer)\n if (txt[0] === 'P') {\n return this.readOBJMNI(buffer)\n }\n const lines = txt.split('\\n')\n const n = lines.length\n const pts = []\n const tris = []\n for (let i = 0; i < n; i++) {\n const str = lines[i]\n if (str[0] === 'v' && str[1] === ' ') {\n // 'v ' but not 'vt' or 'vn'\n const items = str.trim().split(/\\s+/)\n pts.push(parseFloat(items[1]))\n pts.push(parseFloat(items[2]))\n pts.push(parseFloat(items[3]))\n // v 0 -0.5 -0\n }\n if (str[0] === 'f') {\n const items = str.trim().split(/\\s+/)\n const new_t = items.length - 3 // number of new triangles created\n if (new_t < 1) {\n break\n } // error\n let tn = items[1].split('/')\n const t0 = parseInt(tn[0]) - 1 // first vertex\n tn = items[2].split('/')\n let tprev = parseInt(tn[0]) - 1 // previous vertex\n for (let j = 0; j < new_t; j++) {\n tn = items[3 + j].split('/')\n const tcurr = parseInt(tn[0]) - 1 // current vertex\n tris.push(t0)\n tris.push(tprev)\n tris.push(tcurr)\n tprev = tcurr\n }\n }\n } // for all lines\n const positions = new Float32Array(pts)\n const indices = new Uint32Array(tris)\n // next vertex indices are supposed to be from 1, not 0\n let min = indices[0]\n let max = indices[0]\n for (let i = 1; i < indices.length; i++) {\n if (indices[i] < min) {\n min = indices[i]\n }\n if (indices[i] > max) {\n max = indices[i]\n }\n }\n if (max - min + 1 > positions.length / 3) {\n throw new Error('Not a valid OBJ file')\n }\n for (let i = 0; i < indices.length; i++) {\n indices[i] -= min\n }\n return {\n positions,\n indices\n }\n } // readOBJ()\n\n // read FreeSurfer big endian format\n static readFreeSurfer(buffer: ArrayBuffer): DefaultMeshType {\n const bytes = new Uint8Array(buffer)\n if (bytes[0] === 35 && bytes[1] === 33 && bytes[2] === 97) {\n return NVMeshLoaders.readASC(buffer) // \"#!ascii version\"\n }\n const view = new DataView(buffer) // ArrayBuffer to dataview\n const sig0 = view.getUint32(0, false)\n const sig1 = view.getUint32(4, false)\n if (sig0 !== 4294966883 || sig1 !== 1919246708) {\n utiltiesLogger.debug('Unable to recognize file type: does not appear to be FreeSurfer format.')\n }\n let offset = 0\n while (view.getUint8(offset) !== 10) {\n offset++\n }\n offset += 2\n let nv = view.getUint32(offset, false) // number of vertices\n offset += 4\n let nf = view.getUint32(offset, false) // number of faces\n offset += 4\n nv *= 3 // each vertex has 3 positions: XYZ\n const positions = new Float32Array(nv)\n for (let i = 0; i < nv; i++) {\n positions[i] = view.getFloat32(offset, false)\n offset += 4\n }\n nf *= 3 // each triangle face indexes 3 triangles\n const indices = new Uint32Array(nf)\n for (let i = 0; i < nf; i++) {\n indices[i] = view.getUint32(offset, false)\n offset += 4\n }\n // read undocumented footer\n // https://github.com/nipy/nibabel/blob/8fea2a8e50aaf4d8b0d4bfff7a21b132914120ee/nibabel/freesurfer/io.py#L58C5-L58C9\n const head0 = view.getUint32(offset, false)\n offset += 4\n let isHeadOK = head0 === 20\n if (!isHeadOK) {\n // read two more int32s\n const head1 = view.getUint32(offset, false)\n offset += 4\n const head2 = view.getUint32(offset, false)\n offset += 4\n isHeadOK = head0 === 2 && head1 === 0 && head2 === 20\n }\n if (!isHeadOK) {\n log.warn('Unknown FreeSurfer Mesh extension code.')\n } else {\n const footer = new TextDecoder().decode(buffer.slice(offset)).trim()\n const strings = footer.split('\\n')\n for (let s = 0; s < strings.length; s++) {\n if (!strings[s].startsWith('cras')) {\n continue\n }\n const cras = strings[s].split('=')[1].trim()\n const FreeSurferTranlate = cras.split(' ').map(Number)\n const nvert = Math.floor(positions.length / 3)\n let i = 0\n for (let v = 0; v < nvert; v++) {\n positions[i] += FreeSurferTranlate[0]\n i++\n positions[i] += FreeSurferTranlate[1]\n i++\n positions[i] += FreeSurferTranlate[2]\n i++\n }\n }\n }\n return {\n positions,\n indices\n }\n } // readFreeSurfer()\n\n // read brainvoyager SRF format\n // https://support.brainvoyager.com/brainvoyager/automation-development/84-file-formats/344-users-guide-2-3-the-format-of-srf-files\n static async readSRF(buffer: ArrayBuffer): Promise<DefaultMeshType> {\n const bytes = new Uint8Array(buffer)\n if (bytes[0] === 35 && bytes[1] === 33 && bytes[2] === 97) {\n // .srf also used for freesurfer https://brainder.org/research/brain-for-blender/\n return NVMeshLoaders.readASC(buffer) // \"#!ascii version\"\n }\n if (bytes[0] === 31 && bytes[1] === 139) {\n // handle .srf.gz\n const raw = await NVUtilities.decompress(new Uint8Array(buffer))\n buffer = raw.buffer\n }\n const reader = new DataView(buffer)\n const ver = reader.getFloat32(0, true)\n const nVert = reader.getUint32(8, true)\n const nTri = reader.getUint32(12, true)\n const oriX = reader.getFloat32(16, true)\n const oriY = reader.getFloat32(20, true)\n const oriZ = reader.getFloat32(24, true)\n const positions = new Float32Array(nVert * 3)\n // BrainVoyager does not use Talairach coordinates for XYZ!\n // read X component of each vertex\n let pos = 28\n let j = 1 // BrainVoyager X is Talairach Y\n for (let i = 0; i < nVert; i++) {\n positions[j] = -reader.getFloat32(pos, true) + oriX\n j += 3 // read one of 3 components: XYZ\n pos += 4 // read one float32\n }\n // read Y component of each vertex\n j = 2 // BrainVoyager Y is Talairach Z\n for (let i = 0; i < nVert; i++) {\n positions[j] = -reader.getFloat32(pos, true) + oriY\n j += 3 // read one of 3 components: XYZ\n pos += 4 // read one float32\n }\n // read Z component of each vertex\n j = 0 // BrainVoyager Z is Talairach X\n for (let i = 0; i < nVert; i++) {\n positions[j] = -reader.getFloat32(pos, true) + oriZ\n j += 3 // read one of 3 components: XYZ\n pos += 4 // read one float32\n }\n // not sure why normals are stored, does bulk up file size\n pos = 28 + 4 * 6 * nVert // each vertex has 6 float32s: XYZ for position and normal\n // read concave and convex colors:\n const rVex = reader.getFloat32(pos, true)\n const gVex = reader.getFloat32(pos + 4, true)\n const bVex = reader.getFloat32(pos + 8, true)\n const rCave = reader.getFloat32(pos + 16, true)\n const gCave = reader.getFloat32(pos + 20, true)\n const bCave = reader.getFloat32(pos + 24, true)\n pos += 8 * 4 // skip 8 floats (RGBA convex/concave)\n // read per-vertex colors\n const colors = new Float32Array(nVert * 3)\n const colorsIdx = new Uint32Array(buffer, pos, nVert)\n j = 0 // convert RGBA -> RGB\n for (let i = 0; i < nVert; i++) {\n const c = colorsIdx[i]\n if (c > 1056964608) {\n colors[j + 0] = ((c >> 16) & 0xff) / 255\n colors[j + 1] = ((c >> 8) & 0xff) / 255\n colors[j + 2] = (c & 0xff) / 255\n }\n if (c === 0) {\n // convex\n colors[j + 0] = rVex\n colors[j + 1] = gVex\n colors[j + 2] = bVex\n }\n if (c === 1) {\n // concave\n colors[j + 0] = rCave\n colors[j + 1] = gCave\n colors[j + 2] = bCave\n }\n j += 3\n }\n pos += nVert * 4 // MeshColor, sequence of color indices\n // not sure why nearest neighbors are stored, slower and bigger files\n for (let i = 0; i < nVert; i++) {\n const nNearest = reader.getUint32(pos, true)\n pos += 4 + 4 * nNearest\n }\n const indices = new Uint32Array(nTri * 3)\n for (let i = 0; i < nTri * 3; i++) {\n indices[i] = reader.getInt32(pos, true)\n pos += 4\n }\n if (ver !== 4) {\n log.warn('Not valid SRF')\n }\n\n return {\n positions,\n indices,\n colors\n }\n } // readSRF()\n\n // read STL ASCII format file\n // http://paulbourke.net/dataformats/stl/\n static readTxtSTL(buffer: ArrayBuffer): DefaultMeshType {\n const enc = new TextDecoder('utf-8')\n const txt = enc.decode(buffer)\n const lines = txt.split('\\n')\n if (!lines[0].startsWith('solid')) {\n throw new Error('Not a valid STL file')\n }\n const pts = []\n for (let i = 1; i < lines.length; i++) {\n if (!lines[i].includes('vertex')) {\n continue\n }\n const items = lines[i].trim().split(/\\s+/)\n for (let j = 1; j < items.length; j++) {\n pts.push(parseFloat(items[j]))\n }\n }\n const npts = Math.floor(pts.length / 3) // each vertex has x,y,z\n if (npts * 3 !== pts.length) {\n throw new Error('Unable to parse ASCII STL file.')\n }\n const positions = new Float32Array(pts)\n const indices = new Uint32Array(npts)\n for (let i = 0; i < npts; i++) {\n indices[i] = i\n }\n return {\n positions,\n indices\n }\n } // readTxtSTL()\n\n // read STL format, nb this format does not reuse vertices\n // https://en.wikipedia.org/wiki/STL_(file_format)\n static readSTL(buffer: ArrayBuffer): DefaultMeshType {\n if (buffer.byteLength < 80 + 4 + 50) {\n throw new Error('File too small to be STL: bytes = ' + buffer.byteLength)\n }\n const reader = new DataView(buffer)\n const sig = reader.getUint32(0, true)\n if (sig === 1768714099) {\n return NVMeshLoaders.readTxtSTL(buffer)\n }\n const ntri = reader.getUint32(80, true)\n const ntri3 = 3 * ntri\n if (buffer.byteLength < 80 + 4 + ntri * 50) {\n throw new Error('STL file too small to store triangles = ' + ntri)\n }\n const indices = new Uint32Array(ntri3)\n const positions = new Float32Array(ntri3 * 3)\n let pos = 80 + 4 + 12\n let v = 0 // vertex\n for (let i = 0; i < ntri; i++) {\n for (let j = 0; j < 9; j++) {\n positions[v] = reader.getFloat32(pos, true)\n v += 1\n pos += 4\n }\n pos += 14 // 50 bytes for triangle, only 36 used for position\n }\n for (let i = 0; i < ntri3; i++) {\n indices[i] = i\n }\n return {\n positions,\n indices\n }\n } // readSTL()\n\n static decimateLayerVertices(nVertLayer: number, nVertMesh: number): number {\n // downsample layer vertices if the mesh has been decimated\n if (nVertLayer % nVertMesh === 0) {\n return nVertLayer\n }\n const V0 = 12\n const orderLayer = Math.round(Math.log((nVertLayer - 2) / (V0 - 2)) / Math.log(4))\n const orderMesh = Math.round(Math.log((nVertMesh - 2) / (V0 - 2)) / Math.log(4))\n // sanity check\n const nVLayer = Math.pow(4, orderLayer) * (V0 - 2) + 2\n const nVMesh = Math.pow(4, orderMesh) * (V0 - 2) + 2\n if (nVLayer !== nVertLayer || nVMesh !== nVertMesh) {\n return nVertLayer\n }\n return nVertMesh\n }\n\n // read NIfTI2 format with embedded CIfTI\n // this variation very specific to connectome workbench\n // https://brainder.org/2015/04/03/the-nifti-2-file-format/\n static async readNII2(buffer: ArrayBuffer, n_vert = 0, anatomicalStructurePrimary = ''): Promise<Int32Array | Float32Array | Int16Array | Uint8Array> {\n let scalars: Float32Array | Int32Array | Int16Array | Uint8Array = new Float32Array()\n const len = buffer.byteLength\n let isLittleEndian = true\n const reader = new DataView(buffer)\n let magic = reader.getUint16(0, isLittleEndian)\n if (magic === 469893120) {\n isLittleEndian = false\n magic = reader.getUint16(0, isLittleEndian)\n }\n if (magic !== 540) {\n throw new Error('Not a valid NIfTI-2 dataset')\n }\n const voxoffset = Number(reader.getBigInt64(168, isLittleEndian))\n const scl_slope = reader.getFloat64(176, isLittleEndian)\n const scl_inter = reader.getFloat64(184, isLittleEndian)\n if (scl_slope !== 1 || scl_inter !== 0) {\n log.warn('ignoring scale slope and intercept')\n }\n const intent_code = reader.getUint32(504, isLittleEndian)\n const datatype = reader.getUint16(12, isLittleEndian)\n if (datatype !== 2 && datatype !== 4 && datatype !== 8 && datatype !== 16) {\n throw new Error('Unsupported NIfTI datatype ' + datatype)\n }\n let nvert = 1\n const dim = [1, 1, 1, 1, 1, 1, 1, 1]\n for (let i = 1; i < 8; i++) {\n dim[i] = Math.max(Number(reader.getBigInt64(16 + i * 8, isLittleEndian)), 1)\n nvert *= dim[i]\n }\n if (intent_code >= 3000 && intent_code <= 3099 && voxoffset > 580) {\n // CIFTI ConnDenseScalar\n let indexOffset = 0\n let indexCount = 0\n let surfaceNumberOfVertices = 0\n let brainStructure = ''\n let vertexIndices: Uint32Array = new Uint32Array()\n const bytes = new Uint8Array(buffer)\n let pos = 552\n\n function readStrX(): string {\n while (pos < len && bytes[pos] === 10) {\n pos++\n } // skip blank lines\n const startPos = pos\n while (pos < len && bytes[pos] !== 10) {\n pos++\n }\n pos++ // skip EOLN\n if (pos - startPos < 1) {\n return ''\n }\n return new TextDecoder().decode(buffer.slice(startPos, pos - 1)).trim()\n }\n\n function readStr(): string {\n // concatenate lines to return tag <...>\n let line = readStrX()\n if (!line.startsWith('<') || line.endsWith('>')) {\n return line\n }\n while (pos < len && !line.endsWith('>')) {\n line += readStrX()\n }\n return line\n }\n let line: string\n\n function readNumericTag(TagName: string, asString = false): string | number {\n // Tag 'Dim1' will return 3 for Dim1=\"3\"\n const tpos = line.indexOf(TagName)\n if (tpos < 0) {\n return 1\n }\n const spos = line.indexOf('\"', tpos) + 1\n const epos = line.indexOf('\"', spos)\n const str = line.slice(spos, epos)\n if (asString) {\n return str\n }\n return parseInt(str)\n } // readNumericTag\n\n const nFrame4D = dim[5] // number of timepoints/frames per vertex\n const scalars = new Float32Array(n_vert * nFrame4D)\n\n // eslint-disable-next-line no-unmodified-loop-condition -- pos is modified within readStr\n while (pos < len) {\n line = readStr()\n if (line.includes('</CIFTI>')) {\n break\n }\n if (line.includes('<BrainModel')) {\n const nv = readNumericTag('SurfaceNumberOfVertices=') as number\n const bStruct = (readNumericTag('BrainStructure=', true) as string).toUpperCase()\n if (nv % n_vert !== 0) {\n continue\n }\n // a single CIfTI file can contain multiple structures, but only one structure per mesh\n // The big kludge: try to find CIfTI structure that matches GIfTI mesh\n let isMatch = false\n if (anatomicalStructurePrimary.includes('CORTEX') && bStruct.includes('CORTEX')) {\n isMatch = true\n }\n // to do: other anatomy: cerebellum\n if (!isMatch) {\n continue\n }\n isMatch = false\n if (anatomicalStructurePrimary.includes('LEFT') && bStruct.includes('LEFT')) {\n isMatch = true\n }\n if (anatomicalStructurePrimary.includes('RIGHT') && bStruct.includes('RIGHT')) {\n isMatch = true\n }\n if (!isMatch) {\n continue\n }\n surfaceNumberOfVertices = nv\n indexOffset = readNumericTag('IndexOffset=') as number\n indexCount = readNumericTag('IndexCount=') as number\n brainStructure = bStruct\n if (!line.includes('<VertexIndices>')) {\n line = readStr()\n }\n if (!line.startsWith('<VertexIndices>') || !line.endsWith('</VertexIndices>')) {\n log.warn('Unable to find CIfTI <VertexIndices>')\n return scalars\n }\n line = line.slice(15, -16)\n const items = line.trim().split(/\\s+/)\n if (items.length < indexCount) {\n log.error('Error parsing VertexIndices')\n }\n vertexIndices = new Uint32Array(indexCount)\n for (let i = 0; i < indexCount; i++) {\n vertexIndices[i] = parseInt(items[i])\n }\n } // read <BrainModel\n } // while (pos < len) or reached </CIFTI>\n\n if (surfaceNumberOfVertices === 0 || vertexIndices.length === 0) {\n log.warn('Unable to find CIfTI structure that matches the mesh.')\n return scalars\n }\n if (datatype !== 16) {\n log.warn('Only able to read float32 CIfTI (only known datatype).')\n return scalars\n }\n\n const vals = new Float32Array(indexCount * nFrame4D)\n const off = voxoffset + nFrame4D * indexOffset * 4\n for (let i = 0; i < indexCount * nFrame4D; i++) {\n vals[i] = reader.getFloat32(off + i * 4, isLittleEndian)\n }\n // }\n let j = 0\n\n for (let i = 0; i < indexCount; i++) {\n for (let f = 0; f < nFrame4D; f++) {\n scalars[vertexIndices[i] + f * n_vert] = vals[j]\n j++\n }\n }\n log.debug('CIfTI diagnostics', surfaceNumberOfVertices, brainStructure, indexOffset, indexCount, indexOffset, anatomicalStructurePrimary)\n //\n return scalars\n } // is CIfTI\n nvert = this.decimateLayerVertices(nvert, n_vert)\n if (nvert % n_vert !== 0) {\n throw new Error('Vertices in layer (' + nvert + ') is not a multiple of number of vertices (' + n_vert + ')')\n }\n if (isLittleEndian) {\n // block read native endian\n if (datatype === 16) {\n scalars = new Float32Array(buffer, voxoffset, nvert)\n } else if (datatype === 8) {\n scalars = new Int32Array(buffer, voxoffset, nvert)\n } else if (datatype === 4) {\n scalars = new Int16Array(buffer, voxoffset, nvert)\n }\n } else {\n // if isLittleEndian\n if (datatype === 16) {\n scalars = new Float32Array(nvert)\n for (let i = 0; i < nvert; i++) {\n scalars[i] = reader.getFloat32(voxoffset + i * 4, isLittleEndian)\n }\n } else if (datatype === 8) {\n scalars = new Int32Array(nvert)\n for (let i = 0; i < nvert; i++) {\n scalars[i] = reader.getInt32(voxoffset + i * 4, isLittleEndian)\n }\n } else if (datatype === 4) {\n scalars = new Int16Array(nvert)\n for (let i = 0; i < nvert; i++) {\n scalars[i] = reader.getInt16(voxoffset + i * 2, isLittleEndian)\n }\n }\n } // if isLittleEndian else big end\n if (datatype === 2) {\n scalars = new Uint8Array(buffer, voxoffset, nvert)\n }\n return scalars\n } // readNII2()\n\n // read NIfTI1/2 as vertex colors\n // https://brainder.org/2012/09/23/the-nifti-file-format/#:~:text=In%20the%20nifti%20format%2C%20the,seventh%2C%20are%20for%20other%20uses.\n static async readNII(buffer: ArrayBuffer, pts: Float32Array, anatomicalStructurePrimary = ''): Promise<Float32Array | Uint8Array | Int32Array | Int16Array> {\n const n_mesh_vert = pts.length / 3\n // TODO clean up number types\n let scalars: Float32Array | Int32Array | Int16Array | Uint8Array = new Float32Array()\n let isLittleEndian = true\n let reader = new DataView(buffer)\n let magic = reader.getUint16(0, isLittleEndian)\n if (magic === 540 || magic === 469893120) {\n return NVMeshLoaders.readNII2(buffer, n_mesh_vert, anatomicalStructurePrimary)\n }\n if (magic === 23553) {\n isLittleEndian = false\n magic = reader.getUint16(0, isLittleEndian)\n }\n if (magic !== 348) {\n // gzip signature 0x1F8B in little and big endian\n const raw = await NVUtilities.decompress(new Uint8Array(buffer))\n reader = new DataView(raw.buffer)\n buffer = raw.buffer\n magic = reader.getUint16(0, isLittleEndian)\n if (magic === 540 || magic === 469893120) {\n return NVMeshLoaders.readNII2(buffer, n_mesh_vert, anatomicalStructurePrimary)\n }\n if (magic === 23553) {\n isLittleEndian = false\n magic = reader.getUint16(0, isLittleEndian)\n }\n }\n if (magic !== 348) {\n log.error('Not a valid NIfTI image.')\n }\n const voxoffset = reader.getFloat32(108, isLittleEndian)\n const scl_slope = reader.getFloat32(112, isLittleEndian)\n const scl_inter = reader.getFloat32(116, isLittleEndian)\n const qform_code = reader.getUint16(252, isLittleEndian)\n const sform_code = reader.getUint16(254, isLittleEndian)\n const datatype = reader.getUint16(70, isLittleEndian)\n if (datatype !== 2 && datatype !== 4 && datatype !== 8 && datatype !== 16) {\n throw new Error('Unsupported NIfTI datatype ' + datatype)\n }\n const sform = mat4.create()\n for (let i = 0; i < 12; i++) {\n sform[i] = reader.getFloat32(280 + i * 4, isLittleEndian)\n }\n let n_vox = 1\n const dim = new Array(8)\n for (let i = 0; i < 8; i++) {\n dim[i] = reader.getUint16(40 + i * 2, isLittleEndian)\n if (i < 1) {\n continue\n }\n n_vox *= dim[i] || 1\n }\n let is3D = false\n let nvertD = this.decimateLayerVertices(n_vox, n_mesh_vert)\n if (nvertD % n_mesh_vert !== 0) {\n if (dim[0] >= 3 && dim[1] > 1 && dim[2] > 1 && dim[3] > 1) {\n is3D = true\n nvertD = dim[1] * dim[2] * dim[3]\n } else {\n throw new Error('Voxels in layer (' + n_vox + ') is not a multiple of number of vertices (' + n_mesh_vert + ')')\n }\n }\n n_vox = nvertD\n if (isLittleEndian) {\n // block read native endian\n if (datatype === 16) {\n scalars = new Float32Array(buffer, voxoffset, n_vox)\n } else if (datatype === 8) {\n scalars = new Int32Array(buffer, voxoffset, n_vox)\n } else if (datatype === 4) {\n scalars = new Int16Array(buffer, voxoffset, n_vox)\n }\n } else {\n // if isLittleEndian\n if (datatype === 16) {\n scalars = new Float32Array(n_vox)\n for (let i = 0; i < n_vox; i++) {\n scalars[i] = reader.getFloat32(voxoffset + i * 4, isLittleEndian)\n }\n } else if (datatype === 8) {\n scalars = new Int32Array(n_vox)\n for (let i = 0; i < n_vox; i++) {\n scalars[i] = reader.getInt32(voxoffset + i * 4, isLittleEndian)\n }\n } else if (datatype === 4) {\n scalars = new Int16Array(n_vox)\n for (let i = 0; i < n_vox; i++) {\n scalars[i] = reader.getInt16(voxoffset + i * 2, isLittleEndian)\n }\n }\n } // if isLittleEndian else big end\n if (datatype === 2) {\n scalars = new Uint8Array(buffer, voxoffset, n_vox)\n }\n if (Number.isFinite(scl_slope) && Number.isFinite(scl_inter) && scl_slope !== 0 && (scl_slope !== 1 || scl_inter !== 0)) {\n const f32 = new Float32Array(n_vox)\n for (let i = 0; i < n_vox; i++) {\n f32[i] = scalars[i] * scl_slope + scl_inter\n }\n scalars = f32\n }\n let mn = scalars[0]\n let mx = mn\n for (let i = 0; i < n_vox; i++) {\n if (isNaN(scalars[i])) {\n scalars[i] = 0.0\n }\n mn = Math.min(mn, scalars[i])\n mx = Math.max(mx, scalars[i])\n }\n log.debug(`Layer Range ${mn}..${mx} slope ${scl_slope} inter ${scl_inter}`)\n if (is3D) {\n const f32 = new Float32Array(n_mesh_vert)\n // Sample voxel intensities at mesh vertices\n log.warn('Sampling voxel intensities at mesh vertices (assumes precise alignment).')\n if (qform_code > sform_code || sform_code <= 0) {\n log.warn(`Requires valid sform (sform_code = ${sform_code})`)\n }\n const sformInv = mat4.create()\n mat4.invert(sformInv, sform)\n function transformMat4homogeneous(xyz: number[], m: mat4): number[] {\n const out = [0, 0, 0]\n out[0] = m[0] * xyz[0] + m[1] * xyz[1] + m[2] * xyz[2] + m[3]\n out[1] = m[4] * xyz[0] + m[5] * xyz[1] + m[6] * xyz[2] + m[7]\n out[2] = m[8] * xyz[0] + m[9] * xyz[1] + m[10] * xyz[2] + m[11]\n return out\n }\n const nx = dim[1]\n const ny = dim[2]\n const nz = dim[3]\n const nxy = nx * ny\n let j = 0\n let k = 0\n let nOK = 0\n while (k < n_mesh_vert) {\n const xyz = [pts[j], pts[j + 1], pts[j + 2]]\n const ijk = transformMat4homogeneous(xyz, sformInv)\n j += 3\n k += 1\n // todo read voxel scalar\n const ixf = Math.floor(ijk[0])\n const iyf = Math.floor(ijk[1])\n const izf = Math.floor(ijk[2])\n const ixc = Math.ceil(ijk[0])\n const iyc = Math.ceil(ijk[1])\n const izc = Math.ceil(ijk[2])\n // bounds check: valid ranges are 0..(nx-1), 0..(ny-1), 0..(nz-1)\n if (ixf < 0 || ixc >= nx || iyf < 0 || iyc >= ny || izf < 0 || izc >= nz) {\n continue\n }\n const base = ixf + iyf * nx + izf * nxy\n const vs = new Float32Array(8)\n vs[0] = scalars[base]\n vs[1] = scalars[base + 1]\n vs[2] = scalars[base + nx]\n vs[3] = scalars[base + nx + 1]\n vs[4] = scalars[base + nxy]\n vs[5] = scalars[base + nxy + 1]\n vs[6] = scalars[base + nxy + nx]\n vs[7] = scalars[base + nxy + nx + 1]\n // choose max (unrolled for speed)\n let maxv = vs[0]\n let minv = maxv\n for (let i = 1; i < 8; i++) {\n maxv = Math.max(maxv, vs[i])\n minv = Math.min(minv, vs[i])\n }\n if (Math.abs(minv) > maxv) {\n maxv = minv\n }\n f32[k - 1] = maxv\n nOK++\n }\n\n const fractionOK = nOK / n_mesh_vert\n if (fractionOK < 0.1) {\n log.warn(`${nOK} of ${n_mesh_vert} vertices in range (${(fractionOK * 100).toFixed(1)}%)`)\n }\n scalars = f32\n }\n return scalars\n } // readNII();\n\n // read MGH format as vertex colors (not voxel-based image)\n // https://surfer.nmr.mgh.harvard.edu/fswiki/FsTutorial/MghFormat\n static async readMGH(buffer: ArrayBuffer, n_vert = 0, isReadColortables = false): Promise<MGH> {\n let reader = new DataView(buffer)\n let raw = buffer\n if (reader.getUint8(0) === 31 && reader.getUint8(1) === 139) {\n const decompressed = await NVUtilities.decompress(new Uint8Array(buffer))\n raw = new ArrayBuffer(decompressed.byteLength)\n new Uint8Array(raw).set(new Uint8Array(decompressed))\n reader = new DataView(decompressed.buffer)\n }\n const version = reader.getInt32(0, false)\n const width = Math.max(1, reader.getInt32(4, false))\n const height = Math.max(1, reader.getInt32(8, false))\n const depth = Math.max(1, reader.getInt32(12, false))\n const nframes = Math.max(1, reader.getInt32(16, false))\n const mtype = reader.getInt32(20, false)\n let voxoffset = 284 // ALWAYS fixed header size\n const isLittleEndian = false // ALWAYS byte order is BIG ENDIAN\n if (version !== 1 || mtype < 0 || mtype > 4) {\n log.warn('Not a valid MGH file')\n }\n let nvert = width * height * depth * nframes\n let scalars: AnyNumberArray = []\n nvert = this.decimateLayerVertices(nvert, n_vert)\n if (nvert % n_vert !== 0) {\n log.warn('Vertices in layer (' + nvert + ') is not a multiple of number of vertices (' + n_vert + ')')\n return scalars\n }\n if (mtype === 3) {\n scalars = new Float32Array(nvert)\n for (let i = 0; i < nvert; i++) {\n scalars[i] = reader.getFloat32(voxoffset + i * 4, isLittleEndian)\n }\n } else if (mtype === 1) {\n scalars = new Int32Array(nvert)\n for (let i = 0; i < nvert; i++) {\n scalars[i] = reader.getInt32(voxoffset + i * 4, isLittleEndian)\n }\n } else if (mtype === 4) {\n scalars = new Int16Array(nvert)\n for (let i = 0; i < nvert; i++) {\n scalars[i] = reader.getInt16(voxoffset + i * 2, isLittleEndian)\n }\n } else if (mtype === 0) {\n scalars = new Uint8Array(buffer, voxoffset, nvert)\n }\n if (!isReadColortables) {\n return scalars\n }\n // next: read footer\n let bytesPerVertex = 4\n if (mtype === 4) {\n bytesPerVertex = 2\n }\n if (mtype === 0) {\n bytesPerVertex = 1\n }\n voxoffset += bytesPerVertex * nvert\n voxoffset += 4 * 4 // skip TR, FlipAngle, TE, TI, FOV\n const TAG_OLD_COLORTABLE = 1\n const TAG_OLD_USEREALRAS = 2\n // const TAG_CMDLINE = 3;\n // const TAG_USEREALRAS = 4;\n // const TAG_COLORTABLE = 5;\n // const TAG_GCAMORPH_GEOM = 10;\n // const TAG_GCAMORPH_TYPE = 11;\n // const TAG_GCAMORPH_LABELS = 12;\n const TAG_OLD_SURF_GEOM = 20\n // const TAG_SURF_GEOM = 21;\n const TAG_OLD_MGH_XFORM = 30\n // const TAG_MGH_XFORM = 31;\n // const TAG_GROUP_AVG_SURFACE_AREA = 32;\n // const TAG_AUTO_ALIGN = 33;\n // const TAG_SCALAR_DOUBLE = 40;\n // const TAG_PEDIR = 41;\n // const TAG_MRI_FRAME = 42;\n // const TAG_FIELDSTRENGTH = 43;\n // const TAG_ORIG_RAS2VOX = 44;\n const nBytes = raw.byteLength\n let colormapLabel: LUT\n\n while (voxoffset < nBytes - 8) {\n // let vx = voxoffset;\n const tagType = reader.getInt32((voxoffset += 4), isLittleEndian)\n let plen = 0\n switch (tagType) {\n case TAG_OLD_MGH_XFORM:\n // doesn't include null\n plen = reader.getInt32((voxoffset += 4), isLittleEndian) - 1\n break\n case TAG_OLD_SURF_GEOM: // these don't take lengths at all\n case TAG_OLD_USEREALRAS:\n plen = 0\n break\n case TAG_OLD_COLORTABLE:\n plen = 0\n // CTABreadFromBinary()\n {\n let version = reader.getInt32((voxoffset += 4), isLittleEndian)\n if (version > 0) {\n log.warn('unsupported CTABreadFromBinaryV1')\n return scalars\n }\n version = -version\n if (version !== 2) {\n log.warn('CTABreadFromBinary: unknown version')\n return scalars\n }\n // CTABreadFromBinaryV2() follows\n const nentries = reader.getInt32((voxoffset += 4), isLittleEndian)\n if (nentries < 0) {\n log.warn('CTABreadFromBinaryV2: nentries was ', nentries)\n return scalars\n }\n // skip the file name\n const len = reader.getInt32((voxoffset += 4), isLittleEndian)\n voxoffset += len\n const num_entries_to_read = reader.getInt32((voxoffset += 4), isLittleEndian)\n if (num_entries_to_read < 0) {\n return scalars\n }\n // Allocate our table.\n const Labels: ColorMap = { R: [], G: [], B: [], A: [], I: [], labels: [] }\n for (let i = 0; i < num_entries_to_read; i++) {\n const structure = reader.getInt32((voxoffset += 4), isLittleEndian)\n const labelLen = reader.getInt32((voxoffset += 4), isLittleEndian)\n let pos = voxoffset + 4\n let txt = ''\n for (let c = 0; c < labelLen; c++) {\n const val = reader.getUint8(pos++)\n if (val === 0) {\n break\n }\n txt += String.fromCharCode(val)\n } // for labelLen\n voxoffset += labelLen\n const R = reader.getInt32((voxoffset += 4), isLittleEndian)\n const G = reader.getInt32((voxoffset += 4), isLittleEndian)\n const B = reader.getInt32((voxoffset += 4), isLittleEndian)\n const A = 255 - reader.getInt32((voxoffset += 4), isLittleEndian)\n Labels.I.push(structure)\n Labels.R.push(R)\n Labels.G.push(G)\n Labels.B.push(B)\n Labels.A.push(A)\n Labels.labels!.push(txt)\n // break\n } // for num_entries_to_read\n colormapLabel = cmapper.makeLabelLut(Labels)\n }\n break\n default:\n plen = reader.getInt32((voxoffset += 8), isLittleEndian)\n }\n voxoffset += plen\n }\n return {\n scalars,\n colormapLabel: colormapLabel! // TODO can we guarantee this?\n }\n } // readMGH()\n\n // read X3D format mesh\n // https://en.wikipedia.org/wiki/X3D\n static readX3D(buffer: ArrayBuffer): X3D {\n // n.b. only plain text \".x3d\", not binary \".x3db\"\n // beware: The values of XML attributes are delimited by either single or double quotes\n const len = buffer.byteLength\n if (len < 20) {\n throw new Error('File too small to be X3D: bytes = ' + len)\n }\n const bytes = new Uint8Array(buffer)\n let pos = 0\n function readStr(): string {\n while (pos < len && bytes[pos] !== 60) {\n pos++\n }\n const startP = pos\n while (pos < len && bytes[pos] !== 62) {\n pos++\n }\n const endP = pos\n return new TextDecoder().decode(buffer.slice(startP, endP + 1)).trim()\n }\n let line = readStr() // detect XML signature: '<?xml version=...'\n function readStringTag(TagName: string): string {\n // Tag 'DEF' will return l3 for DEF='l3'\n const fpos = line.indexOf(TagName + '=')\n if (fpos < 0) {\n return ''\n }\n const delimiter = line[fpos + TagName.length + 1]\n const spos = line.indexOf(delimiter, fpos) + 1\n const epos = line.indexOf(delimiter, spos)\n return line.slice(spos, epos)\n }\n function readNumericTag(TagName: string): number | number[] {\n // Tag 'Dim1' will return 3 for Dim1=\"3\"\n const fpos = line.indexOf(TagName + '=')\n if (fpos < 0) {\n return 1\n }\n const delimiter = line[fpos + TagName.length + 1]\n const spos = line.indexOf(delimiter, fpos) + 1\n const epos = line.indexOf(delimiter, spos)\n let str = line.slice(spos, epos).trim()\n str = str.replace(/,\\s*$/, '')\n const items = str.trim().split(/\\s*,\\s*|\\s+/)\n if (items.length < 2) {\n return parseFloat(str)\n }\n let ret = new Array(items.length)\n let j = 0\n for (let i = 0; i < items.length; i++) {\n const v = parseFloat(items[i])\n if (!isFinite(v)) {\n continue\n }\n ret[j] = v\n j++\n }\n ret = ret.slice(0, j)\n return ret\n }\n if (!line.includes('xml version')) {\n log.warn('Not a X3D image')\n }\n let positions: number[] = []\n let indices: number[] = []\n let rgba255: number[] = []\n let color: number[] = []\n let translation: vec4 = [0, 0, 0, 0]\n let rotation = [0, 0, 0, 0]\n let rgba = [255, 255, 255, 255]\n let rgbaGlobal = [255, 255, 255, 255]\n const appearanceStyles: Record<string, number[]> = {}\n function readAppearance(): void {\n if (!line.endsWith('/>')) {\n if (line.startsWith('<Appearance>')) {\n // eslint-disable-next-line no-unmodified-loop-condition -- modified within readStr\n while (pos < len && !line.endsWith('</Appearance>')) {\n line += readStr()\n }\n } else {\n // eslint-disable-next-line no-unmodified-loop-condition -- modified within readStr\n while (pos < len && !line.endsWith('/>')) {\n line += readStr()\n }\n }\n }\n const ref = readStringTag('USE')\n if (ref.length > 1) {\n if (ref in appearanceStyles) {\n rgba = appearanceStyles[ref as keyof typeof appearanceStyles] as number[]\n } else {\n log.warn('Unable to find DEF for ' + ref)\n }\n return\n }\n const diffuseColor = readNumericTag('diffuseColor') as number[]\n if (diffuseColor.length < 3) {\n return\n }\n rgba[0] = Math.round(diffuseColor[0] * 255)\n rgba[1] = Math.round(diffuseColor[1] * 255)\n rgba[2] = Math.round(diffuseColor[2] * 255)\n const def = readStringTag('DEF')\n if (def.length < 1) {\n return\n }\n appearanceStyles[def] = rgba\n }\n // eslint-disable-next-line no-unmodified-loop-condition -- modified within readStr\n while (pos < len) {\n line = readStr()\n rgba = rgbaGlobal.slice()\n if (line.startsWith('<Transform')) {\n translation = readNumericTag('translation') as vec4\n rotation = readNumericTag('rotation') as number[]\n }\n if (line.startsWith('<Appearance')) {\n readAppearance()\n rgbaGlobal = rgba.slice()\n }\n if (line.startsWith('<Shape')) {\n let radius = 1.0\n let height = 1.0\n let coordIndex: number[] = []\n let point: number[] = []\n\n // eslint-disable-next-line no-unmodified-loop-condition -- modified within readAppearance\n while (pos < len) {\n line = readStr()\n if (line.startsWith('<Appearance')) {\n readAppearance()\n }\n if (line.startsWith('</Shape')) {\n break\n }\n if (line.startsWith('<Sphere')) {\n radius = readNumericTag('radius') as number\n height = -1.0\n }\n if (line.startsWith('<Cylinder')) {\n radius = readNumericTag('radius') as number\n height = readNumericTag('height') as number\n }\n if (line.startsWith('<IndexedFaceSet')) {\n height = -2\n // https://www.web3d.org/specifications/X3Dv4Draft/ISO-IEC19775-1v4-CD/Part01/components/geometry3D.html#IndexedFaceSet\n coordIndex = readNumericTag('coordIndex') as number[]\n }\n if (line.startsWith('<IndexedTriangleSet')) {\n height = -7\n coordIndex = readNumericTag('index') as number[]\n }\n if (line.startsWith('<IndexedTriangleStripSet')) {\n height = -3\n // https://www.web3d.org/specifications/X3Dv4Draft/ISO-IEC19775-1v4-CD/Part01/components/geometry3D.html#IndexedFaceSet\n coordIndex = readNumericTag('index') as number[]\n }\n if (line.startsWith('<Coordinate')) {\n point = readNumericTag('point') as number[]\n const rem = point.length % 3\n if (rem !== 0) {\n point = point.slice(0, -rem)\n }\n } // Coordinate point\n if (line.startsWith('<Color')) {\n color = readNumericTag('color') as number[]\n }\n if (line.startsWith('<Box')) {\n height = -4\n log.warn('Unsupported x3d shape: Box')\n }\n if (line.startsWith('<Cone')) {\n height = -5\n log.warn('Unsupported x3d shape: Cone')\n }\n if (line.startsWith('<ElevationGrid')) {\n height = -6\n log.warn('Unsupported x3d shape: ElevationGrid')\n }\n }\n if (height < -3 && height !== -7) {\n // cone, box, elevation grid\n // unsupported\n } else if (height < -1) {\n // indexed triangle mesh or strip\n if (coordIndex.length < 1 || point.length < 3 || point.length === undefined) {\n log.warn('Indexed mesh must specify indices and points')\n break\n }\n const idx0 = Math.floor(positions.length / 3) // first new vertex will be AFTER previous vertices\n let j = 2\n if (height === -7) {\n indices = [...indices, ...coordIndex]\n } else if (height === -2) {\n let triStart = 0\n while (j < coordIndex.length) {\n if (coordIndex[j] >= 0) {\n indices.push(coordIndex[triStart] + idx0)\n indices.push(coordIndex[j - 1] + idx0)\n indices.push(coordIndex[j - 0] + idx0)\n j += 1\n } else {\n j += 3\n triStart = j - 2\n }\n }\n } else {\n while (j < coordIndex.length) {\n if (coordIndex[j] >= 0) {\n indices.push(coordIndex[j - 2] + idx0)\n indices.push(coordIndex[j - 1] + idx0)\n indices.push(coordIndex[j - 0] + idx0)\n j += 1\n } else {\n // coordIndex[j] === -1, next polygon\n j += 3\n }\n }\n }\n // n.b. positions.push(...point) can generate \"Maximum call stack size exceeded\"\n positions = [...positions, ...point]\n const npt = Math.floor(point.length / 3)\n const rgbas = Array(npt).fill(rgba).flat()\n if (color.length === npt * 3) {\n // colors are rgb 0..1, rgbas are RGBA 0..255\n let c3 = 0\n let c4 = 0\n for (let i = 0; i < npt; i++) {\n for (let j2 = 0; j2 < 3; j2++) {\n rgbas[c4] = Math.round(color[c3] * 255.0)\n c3++\n c4++\n }\n c4++\n }\n }\n rgba255 = [...rgba255, ...rgbas]\n } else if (height < 0.0) {\n // sphere\n NiivueObject3D.makeColoredSphere(positions, indices, rgba255, radius, translation, rgba)\n } else {\n // https://www.andre-gaschler.com/rotationconverter/\n const r = mat4.create() // rotation mat4x4\n mat4.fromRotation(r, rotation[3], [rotation[0], rotation[1], rotation[2]])\n const pti = vec4.fromValues(0, -height * 0.5, 0, 1)\n const ptj = vec4.fromValues(0, +height * 0.5, 0, 1)\n vec4.transformMat4(pti, pti, r)\n vec4.transformMat4(ptj, ptj, r)\n vec4.add(pti, pti, translation)\n vec4.add(ptj, ptj, translation)\n const pti3 = vec3.fromValues(pti[0], pti[1], pti[2])\n const ptj3 = vec3.fromValues(ptj[0], ptj[1], ptj[2])\n // https://www.web3d.org/specifications/X3Dv4Draft/ISO-IEC19775-1v4-CD/Part01/components/geometry3D.html#Cylinder\n NiivueObject3D.makeColoredCylinder(positions, indices, rgba255, pti3, ptj3, radius, rgba)\n }\n } // while <shape\n }\n return {\n positions: Float32Array.from(positions),\n indices: Uint32Array.from(indices),\n rgba255: Uint8Array.from(rgba255)\n }\n } // readX3D()\n\n // read GIfTI format mesh\n // https://www.nitrc.org/projects/gifti/\n static async readGII(buffer: ArrayBuffer, n_vert = 0): Promise<GII> {\n let len = buffer.byteLength\n if (len < 20) {\n throw new Error('File too small to be GII: bytes = ' + len)\n }\n let chars = new TextDecoder('ascii').decode(buffer)\n if (chars[0].charCodeAt(0) === 31) {\n // raw GIFTI saved as .gii.gz is smaller than gz GIFTI due to base64 overhead\n const raw = await NVUtilities.decompress(new Uint8Array(buffer))\n buffer = raw.buffer\n chars = new TextDecoder('ascii').decode(raw.buffer)\n }\n let pos = 0\n function readXMLtag(): XmlTag {\n let isEmptyTag = true\n let startPos = pos\n while (isEmptyTag) {\n // while (pos < len && chars[pos] === 10) pos++; //skip blank lines\n while (pos < len && chars[pos] !== '<') {\n pos++\n } // find tag start symbol: '<' e.g. \"<tag>\"\n startPos = pos\n while (pos < len && chars[pos] !== '>') {\n pos++\n } // find tag end symbol: '>' e.g. \"<tag>\"\n isEmptyTag = chars[pos - 1] === '/' // empty tag ends \"/>\" e.g. \"<br/>\"\n if (startPos + 1 < len && chars[startPos + 1] === '/') {\n // skip end tag \"</\"\n pos += 1\n isEmptyTag = true\n }\n // let endTagPos = pos;\n if (pos >= len) {\n break\n }\n }\n const tagString = new TextDecoder().decode(buffer.slice(startPos + 1, pos)).trim()\n const startTag = tagString.split(' ')[0].trim()\n // ignore declarations https://stackoverflow.com/questions/60801060/what-does-mean-in-xml\n const contentStartPos = pos\n let contentEndPos = pos\n let endPos = pos\n if (chars[startPos + 1] !== '?' && chars[startPos + 1] !== '!') {\n // ignore declarations \"<?\" and \"<!\"\n const endTag = '</' + startTag + '>'\n contentEndPos = chars.indexOf(endTag, contentStartPos)\n endPos = contentEndPos + endTag.length - 1\n }\n // <name>content</name>\n // a b c d\n // a: startPos\n // b: contentStartPos\n // c: contentEndPos\n // d: endPos\n return {\n name: tagString,\n startPos,\n contentStartPos,\n contentEndPos,\n endPos\n } //, 'startTagLastPos': startTagLastPos, 'endTagFirstPos': endTagFirstPos, 'endTagLastPos': endTagLastPos];\n }\n let tag = readXMLtag()\n if (!tag.name.startsWith('?xml')) {\n throw new Error('readGII: Invalid XML file')\n }\n while (!tag.name.startsWith('GIFTI') && tag.endPos < len) {\n tag = readXMLtag()\n }\n if (!tag.name.startsWith('GIFTI') || tag.contentStartPos === tag.contentEndPos) {\n throw new Error('readGII: XML file does not include GIFTI tag')\n }\n len = tag.contentEndPos // only read contents of GIfTI tag\n let positions = new Float32Array()\n let indices = new Uint32Array()\n let scalars = new Float32Array()\n let anatomicalStructurePrimary = ''\n let isIdx = false\n let isPts = false\n let isVectors = false\n let isColMajor = false\n let Dims = [1, 1, 1]\n const FreeSurferTranlate = [0, 0, 0] // https://gist.github.com/alexisthual/f0b2f9eb2a67b8f61798f2c138dda981\n let dataType = 0\n // let isLittleEndian = true;\n let isGzip = false\n let isASCII = false\n let nvert = 0\n // FreeSurfer versions after 20221225 disambiguate if transform has been applied\n // \"./mris_convert --to-scanner\" store raw vertex positions in scanner space, so transforms should be ignored.\n // FreeSurfer versions after 20221225 report that the transform is applied by reporting:\n // <DataSpace><![CDATA[NIFTI_XFORM_SCANNER_ANAT\n let isDataSpaceScanner = false\n tag.endPos = tag.contentStartPos // read the children of the 'GIFTI' tag\n let line = ''\n function readNumericTag(TagName: string, isFloat = false): number {\n // Tag 'Dim1' will return 3 for Dim1=\"3\"\n const pos = line.indexOf(TagName)\n if (pos < 0) {\n return 1\n }\n const spos = line.indexOf('\"', pos) + 1\n const epos = line.indexOf('\"', spos)\n const str = line.slice(spos, epos)\n if (isFloat) {\n return parseFloat(str)\n } else {\n return parseInt(str)\n }\n }\n function readBracketTag(TagName: string): string {\n const pos = line.indexOf(TagName)\n if (pos < 0) {\n return ''\n }\n const spos = pos + TagName.length\n const epos = line.indexOf(']', spos)\n return line.slice(spos, epos)\n }\n const Labels: ColorMap = { R: [], G: [], B: [], A: [], I: [], labels: [] }\n while (tag.endPos < len && tag.name.length > 1) {\n tag = readXMLtag()\n if (tag.name.startsWith('Label Key')) {\n line = tag.name\n Labels.I.push(readNumericTag('Key='))\n Labels.R.push(Math.round(255 * readNumericTag('Red=', true)))\n Labels.G.push(Math.round(255 * readNumericTag('Green=', true)))\n Labels.B.push(Math.round(255 * readNumericTag('Blue=', true)))\n Labels.A.push(Math.round(255 * readNumericTag('Alpha', true)))\n line = new TextDecoder().decode(buffer.slice(tag.contentStartPos + 1, tag.contentEndPos)).trim()\n Labels.labels!.push(readBracketTag('<![CDATA['))\n }\n if (tag.name.trim() === 'Data') {\n if (isVectors) {\n continue\n }\n line = new TextDecoder().decode(buffer.slice(tag.contentStartPos + 1, tag.contentEndPos)).trim()\n // Data can be on one to three lines...\n let datBin: Int32Array | Float32Array | Uint8Array\n if (isASCII) {\n const nvert = Dims[0] * Dims[1] * Dims[2]\n const lines = line.split(/\\s+/) // .split(/[ ,]+/);\n if (nvert !== lines.length) {\n throw new Error('Unable to parse ASCII GIfTI')\n }\n if (dataType === 2) {\n dataType = 8\n } // UInt8 -> Int32\n if (dataType === 32) {\n dataType = 16\n } // float64 -> float32\n if (dataType === 8) {\n datBin = new Int32Array(nvert)\n for (let v = 0; v < nvert; v++) {\n datBin[v] = parseInt(lines[v])\n }\n }\n if (dataType === 16) {\n datBin = new Float32Array(nvert)\n for (let v = 0; v < nvert; v++) {\n datBin[v] = parseFloat(lines[v])\n }\n }\n } else if (typeof Buffer === 'undefined') {\n // raw.gii\n function base64ToUint8(base64: string): Uint8Array {\n const binary_string = atob(base64)\n const len = binary_string.length\n const bytes = new Uint8Array(len)\n for (let i = 0; i < len; i++) {\n bytes[i] = binary_string.charCodeAt(i)\n }\n return bytes\n }\n if (isGzip) {\n const datZ = base64ToUint8(line.slice())\n datBin = await NVUtilities.decompress(new Uint8Array(datZ))\n } else {\n datBin = base64ToUint8(line.slice())\n }\n } else {\n // if Buffer not defined\n if (isGzip) {\n const datZ = Buffer.from(line.slice(), 'base64')\n datBin = await NVUtilities.decompress(new Uint8Array(datZ))\n } else {\n datBin = Buffer.from(line.slice(), 'base64')\n }\n }\n if (isPts) {\n if (dataType !== 16) {\n log.warn('expect positions as FLOAT32')\n }\n positions = new Float32Array(datBin!.buffer) // TODO can we guarantee this?\n if (isColMajor) {\n const tmp = positions.slice()\n const np = tmp.length / 3\n let j = 0\n for (let p = 0; p < np; p++) {\n for (let i = 0; i < 3; i++) {\n positions[j] = tmp[i * np + p]\n j++\n }\n }\n } // isColMajor\n } else if (isIdx) {\n if (dataType !== 8) {\n log.warn('expect indices as INT32')\n }\n indices = new Uint32Array(datBin!.buffer)\n if (isColMajor) {\n const tmp = indices.slice()\n const np = tmp.length / 3\n let j = 0\n for (let p = 0; p < np; p++) {\n for (let i = 0; i < 3; i++) {\n indices[j] = tmp[i * np + p]\n j++\n }\n }\n } // isColMajor\n } else {\n // not position or indices: assume scalars NIFTI_INTENT_NONE\n nvert = Dims[0] * Dims[1] * Dims[2]\n if (n_vert !== 0) {\n if (nvert % n_vert !== 0) {\n log.warn('Number of vertices in scalar overlay (' + nvert + ') does not match mesh (' + n_vert + ')')\n }\n }\n function Float32Concat(first: Float32Array, second: Float32Array): Float32Array {\n const firstLength = first.length\n const result = new Float32Array(firstLength + second.length)\n result.set(first)\n result.set(second, firstLength)\n return result\n } // Float32Concat()\n let scalarsNew\n if (dataType === 2) {\n const scalarsInt = new Uint8Array(datBin!.buffer)\n scalarsNew = Float32Array.from(scalarsInt)\n } else if (dataType === 8) {\n const scalarsInt = new Int32Array(datBin!.buffer)\n scalarsNew = Float32Array.from(scalarsInt)\n } else if (dataType === 16) {\n scalarsNew = new Float32Array(datBin!.buffer)\n } else if (dataType === 32) {\n const scalarFloat = new Float64Array(datBin!.buffer)\n scalarsNew = Float32Array.from(scalarFloat)\n } else {\n throw new Error(`Invalid dataType: ${dataType}`)\n }\n scalars = Float32Concat(scalars as Float32Array, scalarsNew)\n }\n continue\n }\n if (tag.name.trim() === 'DataSpace') {\n line = new TextDecoder().decode(buffer.slice(tag.contentStartPos + 1, tag.contentEndPos)).trim()\n if (line.includes('NIFTI_XFORM_SCANNER_ANAT')) {\n isDataSpaceScanner = true\n }\n }\n if (tag.name.trim() === 'MD') {\n line = new TextDecoder().decode(buffer.slice(tag.contentStartPos + 1, tag.contentEndPos)).trim()\n if (line.includes('AnatomicalStructurePrimary') && line.includes('CDATA[')) {\n anatomicalStructurePrimary = readBracketTag('<Value><![CDATA[').toUpperCase()\n // this.AnatomicalStructurePrimary = anatomicalStructurePrimary\n }\n if (line.includes('VolGeom') && line.includes('CDATA[')) {\n let e = -1\n if (line.includes('VolGeomC_R')) {\n e = 0\n }\n if (line.includes('VolGeomC_A')) {\n e = 1\n }\n if (line.includes('VolGeomC_S')) {\n e = 2\n }\n if (e < 0) {\n continue\n }\n FreeSurferTranlate[e] = parseFloat(readBracketTag('<Value><![CDATA['))\n }\n }\n // read DataArray properties\n if (!tag.name.startsWith('DataArray')) {\n continue\n }\n line = tag.name\n Dims = [1, 1, 1]\n isGzip = line.includes('Encoding=\"GZipBase64Binary\"')\n isASCII = line.includes('Encoding=\"ASCII\"')\n isIdx = line.includes('Intent=\"NIFTI_INTENT_TRIANGLE\"')\n isPts = line.includes('Intent=\"NIFTI_INTENT_POINTSET\"')\n isVectors = line.includes('Intent=\"NIFTI_INTENT_VECTOR\"')\n isColMajor = line.includes('ArrayIndexingOrder=\"ColumnMajorOrder\"')\n // isLittleEndian = line.includes('Endian=\"LittleEndian\"');\n if (line.includes('DataType=\"NIFTI_TYPE_UINT8\"')) {\n dataType = 2\n } // DT_UINT8\n if (line.includes('DataType=\"NIFTI_TYPE_INT32\"')) {\n dataType = 8\n } // DT_INT32\n if (line.includes('DataType=\"NIFTI_TYPE_FLOAT32\"')) {\n dataType = 16\n } // DT_FLOAT32\n if (line.includes('DataType=\"NIFTI_TYPE_FLOAT64\"')) {\n dataType = 32\n } // DT_FLOAT64\n Dims[0] = readNumericTag('Dim0=')\n Dims[1] = readNumericTag('Dim1=')\n Dims[2] = readNumericTag('Dim2=')\n }\n let colormapLabel\n if (Labels.I.length > 1) {\n // issue1441\n const hasAlpha = Labels.A.some((a) => a > 0)\n if (!hasAlpha) {\n Labels.A.fill(255)\n }\n colormapLabel = cmapper.makeLabelLut(Labels)\n }\n if (n_vert > 0) {\n return { scalars, colormapLabel, anatomicalStructurePrimary }\n }\n if (positions!.length > 2 && !isDataSpaceScanner && (FreeSurferTranlate[0] !== 0 || FreeSurferTranlate[1] !== 0 || FreeSurferTranlate[2] !== 0)) {\n nvert = Math.floor(positions!.length / 3)\n let i = 0\n for (let v = 0; v < nvert; v++) {\n positions![i] += FreeSurferTranlate[0]\n i++\n positions[i] += FreeSurferTranlate[1]\n i++\n positions[i] += FreeSurferTranlate[2]\n i++\n }\n } // issue416: apply FreeSurfer translation\n return {\n positions,\n indices,\n scalars,\n colormapLabel,\n anatomicalStructurePrimary\n } // MatrixData\n } // readGII()\n}\n","// src/utils/legacy-migrate.ts\n/**\n * Migration utility for older nvdocument shapes.\n *\n * Exports:\n * - migrateLegacyDocument(documentData) -> normalized DocumentData\n * - normalizeMeshForRehydrate(mesh) -> normalized mesh object suitable for rehydrateMeshes\n */\n\nimport { deserialize } from '@ungap/structured-clone'\nimport { DocumentData } from '@/nvdocument' // keep types aligned with your project\n\n// --- helpers --------------------------------------------------------------\n\nfunction isNumeric(v: any): boolean {\n return typeof v === 'number' && Number.isFinite(v)\n}\n\nfunction stabilizeNumber(n: number, decimals = 12): number {\n if (!isNumeric(n)) {\n return n\n }\n const mag = Math.max(1, Math.abs(n))\n const eps = 1e-9 * mag\n const rounded = Number(n.toFixed(decimals))\n if (Math.abs(n - rounded) <= eps) {\n return rounded\n }\n return n\n}\n\nfunction stabilizeArray(arr: any[], decimals = 12): any[] {\n return arr.map((v) => {\n if (Array.isArray(v)) {\n return stabilizeArray(v, decimals)\n }\n if (isNumeric(v)) {\n return stabilizeNumber(v, decimals)\n }\n return v\n })\n}\n\n// Encode special numeric values into strings for safe JSON roundtrip\nfunction specialNumberReplacer(_key: string, value: any): any {\n if (typeof value === 'number') {\n if (Number.isNaN(value)) {\n return 'NaN'\n }\n if (value === Infinity) {\n return 'infinity'\n }\n if (value === -Infinity) {\n return '-infinity'\n }\n return value\n }\n return value\n}\n\n/**\n * Try to extract an Array<number> from a value produced by structured-clone\n * or various typed-array serializations.\n */\nexport function extractNumberArray(maybe: any): number[] | undefined {\n if (maybe === undefined || maybe === null) {\n return undefined\n }\n if (Array.isArray(maybe)) {\n return maybe.slice()\n }\n // typed arrays / ArrayBufferView\n if (ArrayBuffer.isView(maybe)) {\n try {\n return Array.from(maybe as unknown as Iterable<number>)\n } catch (_) {\n /* fallthrough */\n }\n }\n if (typeof maybe === 'object') {\n if (Array.isArray((maybe as any).data)) {\n return (maybe as any).data.slice()\n }\n if (maybe.buffer && Array.isArray(maybe.buffer.data)) {\n return maybe.buffer.data.slice()\n }\n // numeric-keyed object like { \"0\": v0, \"1\": v1, length: N }\n const numericKeys = Object.keys(maybe)\n .filter((k) => /^\\d+$/.test(k))\n .map((k) => Number(k))\n .sort((a, b) => a - b)\n if (numericKeys.length > 0) {\n try {\n return numericKeys.map((i) => maybe[String(i)])\n } catch (_) {\n /* ignore */\n }\n }\n for (const candidateKey of ['_data', 'source', 'values', 'elements', 'items']) {\n if (Array.isArray((maybe as any)[candidateKey])) {\n return (maybe as any)[candidateKey].slice()\n }\n }\n }\n try {\n const arr = Array.from(maybe as unknown as Iterable<number>)\n if (Array.isArray(arr)) {\n return arr\n }\n } catch (_) {\n /* ignore */\n }\n return undefined\n}\n\n// --- per-mesh normalization -----------------------------------------------\n\n/**\n * Convert a single parsed mesh object from legacy shapes to the modern shape\n * used by rehydrateMeshes. This is intentionally defensive and non-destructive.\n */\nexport function normalizeMeshForRehydrate(meshIn: any): any {\n if (!meshIn || typeof meshIn !== 'object') {\n return meshIn\n }\n const m: any = { ...(meshIn || {}) }\n\n // 1) pts (vertex positions)\n if ((m.pts === undefined || m.pts === null) && (m.vertices !== undefined || m.positions !== undefined || m.verts !== undefined)) {\n const candidates = [m.vertices, m.positions, m.verts]\n for (const c of candidates) {\n const arr = extractNumberArray(c)\n if (arr && arr.length > 0) {\n m.pts = stabilizeArray(arr)\n break\n }\n }\n } else if (m.pts != null && !Array.isArray(m.pts) && ArrayBuffer.isView(m.pts)) {\n const arr = extractNumberArray(m.pts)\n if (arr) {\n m.pts = stabilizeArray(arr)\n }\n } else if (Array.isArray(m.pts)) {\n m.pts = stabilizeArray(m.pts)\n }\n\n // 2) tris (triangle indices)\n if ((m.tris === undefined || m.tris === null) && (m.indices !== undefined || m.faces !== undefined || m.cells !== undefined)) {\n const candidates = [m.indices, m.faces, m.cells]\n for (const c of candidates) {\n const arr = extractNumberArray(c)\n if (arr && arr.length > 0) {\n m.tris = arr\n break\n }\n }\n } else if (m.tris != null && !Array.isArray(m.tris) && ArrayBuffer.isView(m.tris)) {\n const arr = extractNumberArray(m.tris)\n if (arr) {\n m.tris = arr\n }\n }\n\n // 3) rgba255 (colors)\n if ((m.rgba255 === undefined || m.rgba255 === null) && (m.rgba !== undefined || m.color !== undefined || m.colors !== undefined)) {\n const candidates = [m.rgba, m.color, m.colors]\n for (const c of candidates) {\n const arr = extractNumberArray(c)\n if (arr && arr.length > 0) {\n m.rgba255 = arr\n break\n }\n }\n } else if (m.rgba255 != null && !Array.isArray(m.rgba255) && ArrayBuffer.isView(m.rgba255)) {\n const arr = extractNumberArray(m.rgba255)\n if (arr) {\n m.rgba255 = arr\n }\n }\n\n // 4) offsetPt0 (legacy fiber storage), fiber metadata left intact\n if (m.offsetPt0 != null && !Array.isArray(m.offsetPt0)) {\n const arr = extractNumberArray(m.offsetPt0)\n if (arr) {\n m.offsetPt0 = arr\n }\n }\n\n // 5) nodes/edges to plain arrays with shallow clones for objects\n if (Array.isArray(m.nodes)) {\n m.nodes = m.nodes.length > 0 && typeof m.nodes[0] === 'object' ? m.nodes.map((n: any) => ({ ...n })) : m.nodes.slice()\n }\n if (Array.isArray(m.edges)) {\n m.edges = m.edges.length > 0 && typeof m.edges[0] === 'object' ? m.edges.map((e: any) => ({ ...e })) : m.edges.slice()\n }\n\n // 6) normalize layers: rename legacy keys and extract values arrays\n if (Array.isArray(m.layers)) {\n m.layers = m.layers.map((layer: any) => {\n if (!layer || typeof layer !== 'object') {\n return layer\n }\n const lcopy: any = { ...(layer || {}) }\n\n // Normalize legacy colormap keys to modern names, but prefer any already-present modern keys.\n if ('colorMap' in lcopy) {\n if (!('colormap' in lcopy)) {\n lcopy.colormap = lcopy.colorMap\n }\n // remove legacy key\n delete lcopy.colorMap\n }\n if ('colorMapNegative' in lcopy) {\n if (!('colormapNegative' in lcopy)) {\n lcopy.colormapNegative = lcopy.colorMapNegative\n }\n // remove legacy key\n delete lcopy.colorMapNegative\n }\n\n // decode numeric strings if present (lightweight)\n for (const k of ['global_min', 'global_max', 'cal_min', 'cal_max', 'cal_minNeg', 'cal_maxNeg']) {\n if (typeof lcopy[k] === 'string') {\n const v = lcopy[k]\n if (v === 'infinity') {\n lcopy[k] = Infinity\n } else if (v === '-infinity') {\n lcopy[k] = -Infinity\n } else if (v === 'NaN') {\n lcopy[k] = NaN\n } else {\n const n = Number(v)\n if (!Number.isNaN(n)) {\n lcopy[k] = n\n }\n }\n }\n }\n\n if (lcopy.values != null && !Array.isArray(lcopy.values)) {\n const ev = extractNumberArray(lcopy.values)\n if (ev) {\n lcopy.values = stabilizeArray(ev)\n }\n } else if (Array.isArray(lcopy.values)) {\n lcopy.values = stabilizeArray(lcopy.values)\n }\n\n if (lcopy.atlasValues != null && !Array.isArray(lcopy.atlasValues)) {\n const ev = extractNumberArray(lcopy.atlasValues)\n if (ev) {\n lcopy.atlasValues = ev\n }\n }\n\n return lcopy\n })\n }\n\n return m\n}\n\n// --- meshesString normalization ------------------------------------------\n\nfunction normalizeMeshesString(meshesString: any): string | undefined {\n if (typeof meshesString !== 'string') {\n return undefined\n }\n const trimmed = meshesString.trim()\n if (trimmed.length === 0) {\n return meshesString\n }\n try {\n const parsed = JSON.parse(meshesString)\n\n // Detect structured-clone serialized form: array of small objects with numeric keys 0 and 1\n const looksLikeStructuredClone = Array.isArray(parsed) && parsed.length > 0 && typeof parsed[0] === 'object' && parsed[0] !== null && '0' in parsed[0] && '1' in parsed[0]\n\n if (looksLikeStructuredClone) {\n // Use deserialize to reconstruct original objects, then normalize them\n try {\n const des = deserialize(parsed as unknown as any) as any[] // reconstructs typed arrays / objects\n if (Array.isArray(des)) {\n const normalized = des.map((mx: any) => normalizeMeshForRehydrate(mx))\n return JSON.stringify(normalized, specialNumberReplacer)\n }\n // fallback: if deserialize produced something unexpected, continue below\n } catch (deserErr) {\n // if deserialize fails, fall through to legacy parsing below\n console.warn('legacy-migrate: structured-clone deserialize failed', deserErr)\n }\n }\n\n // If not structured-clone or deserialize didn't work, handle the usual JSON array case\n if (!Array.isArray(parsed)) {\n // try to locate inner array if serialize() produced wrapper\n const maybeArrayCandidates = ['value', 'data', '0']\n for (const c of maybeArrayCandidates) {\n if (parsed && Array.isArray((parsed as any)[c])) {\n const arr = (parsed as any)[c]\n const norm = arr.map((mx: any) => normalizeMeshForRehydrate(mx))\n return JSON.stringify(norm, specialNumberReplacer)\n }\n }\n return meshesString\n }\n const normalized = parsed.map((mx: any) => normalizeMeshForRehydrate(mx))\n return JSON.stringify(normalized, specialNumberReplacer)\n } catch (e) {\n return meshesString\n }\n}\n\n// --- document-level migration -------------------------------------------\n\n/**\n * Top-level migrateLegacyDocument function.\n * Takes a DocumentData-ish object and returns a normalized DocumentData object (shallow clone).\n */\nexport function migrateLegacyDocument(input: DocumentData | any | undefined): DocumentData {\n if (!input || typeof input !== 'object') {\n return input as DocumentData\n }\n\n const doc: any = { ...(input || {}) }\n\n // normalize root-level legacy colormap names\n if ('colorMap' in doc && !('colormap' in doc)) {\n doc.colormap = doc.colorMap\n }\n if ('colorMapNegative' in doc && !('colormapNegative' in doc)) {\n doc.colormapNegative = doc.colorMapNegative\n }\n\n // imageOptionsArray: convert typed-array-ish colormap fields and decode numeric strings\n if (Array.isArray(doc.imageOptionsArray)) {\n doc.imageOptionsArray = doc.imageOptionsArray.map((io: any) => {\n if (!io || typeof io !== 'object') {\n return io\n }\n const copy = { ...(io || {}) }\n for (const k of ['cal_min', 'cal_max', 'cal_minNeg', 'cal_maxNeg']) {\n if (typeof copy[k] === 'string') {\n if (copy[k] === 'infinity') {\n copy[k] = Infinity\n } else if (copy[k] === '-infinity') {\n copy[k] = -Infinity\n } else if (copy[k] === 'NaN') {\n copy[k] = NaN\n } else {\n const n = Number(copy[k])\n if (!Number.isNaN(n)) {\n copy[k] = n\n }\n }\n }\n }\n if (copy.colormap && !Array.isArray(copy.colormap)) {\n const arr = extractNumberArray(copy.colormap)\n if (arr) {\n copy.colormap = arr\n }\n }\n if (copy.colormapNegative && !Array.isArray(copy.colormapNegative)) {\n const arr = extractNumberArray(copy.colormapNegative)\n if (arr) {\n copy.colormapNegative = arr\n }\n }\n return copy\n })\n }\n\n // meshesString normalization\n if (typeof doc.meshesString === 'string' && doc.meshesString.trim().length > 0) {\n const normalized = normalizeMeshesString(doc.meshesString)\n if (typeof normalized === 'string') {\n doc.meshesString = normalized\n }\n }\n\n // Ensure labels/connectomes/encodedImageBlobs are plain arrays where possible\n if (doc.labels && !Array.isArray(doc.labels)) {\n try {\n doc.labels = Array.from(doc.labels)\n } catch (_) {\n /* leave as-is */\n }\n }\n if (doc.connectomes && !Array.isArray(doc.connectomes)) {\n try {\n doc.connectomes = Array.from(doc.connectomes)\n } catch (_) {\n /* leave as-is */\n }\n }\n if (doc.encodedImageBlobs && !Array.isArray(doc.encodedImageBlobs)) {\n try {\n doc.encodedImageBlobs = Array.from(doc.encodedImageBlobs)\n } catch (_) {\n /* leave as-is */\n }\n }\n\n return doc as DocumentData\n}\n\nexport default migrateLegacyDocument\n","import { SLICE_TYPE } from '@/nvdocument'\nimport { TypedVoxelArray } from '@/nvimage'\n\ninterface DrawingDimensions {\n dimX: number\n dimY: number\n dimZ: number\n}\n\ninterface InterpolationOptions {\n intensityWeight?: number\n binaryThreshold?: number\n intensitySigma?: number\n applySmoothingToSlices?: boolean\n useIntensityGuided?: boolean\n sliceType?: SLICE_TYPE\n}\n\n/**\n * Find the first and last slices containing drawing data along a given axis\n * @param sliceType - The slice orientation (AXIAL, CORONAL, or SAGITTAL)\n * @param drawBitmap - The drawing bitmap data\n * @param dims - The volume dimensions\n * @returns Object containing first and last slice indices, or null if no data found\n */\nexport function findBoundarySlices(sliceType: SLICE_TYPE, drawBitmap: Uint8Array, dims: DrawingDimensions): { first: number; last: number } | null {\n const { dimX, dimY, dimZ } = dims\n\n let axisSize: number\n if (sliceType === SLICE_TYPE.AXIAL) {\n axisSize = dimZ\n } else if (sliceType === SLICE_TYPE.CORONAL) {\n axisSize = dimY\n } else if (sliceType === SLICE_TYPE.SAGITTAL) {\n axisSize = dimX\n } else {\n return null\n }\n\n let firstSliceWithData = -1\n let lastSliceWithData = -1\n\n for (let slice = 0; slice < axisSize; slice++) {\n let hasData = false\n\n if (sliceType === SLICE_TYPE.AXIAL) {\n // Check axial slice (XY plane at Z=slice)\n const offset = slice * dimX * dimY\n for (let i = 0; i < dimX * dimY; i++) {\n if (drawBitmap[offset + i] > 0) {\n hasData = true\n break\n }\n }\n } else if (sliceType === SLICE_TYPE.CORONAL) {\n // Check coronal slice (XZ plane at Y=slice)\n for (let z = 0; z < dimZ; z++) {\n for (let x = 0; x < dimX; x++) {\n const idx = x + slice * dimX + z * dimX * dimY\n if (drawBitmap[idx] > 0) {\n hasData = true\n break\n }\n }\n if (hasData) {\n break\n }\n }\n } else if (sliceType === SLICE_TYPE.SAGITTAL) {\n // Check sagittal slice (YZ plane at X=slice)\n for (let z = 0; z < dimZ; z++) {\n for (let y = 0; y < dimY; y++) {\n const idx = slice + y * dimX + z * dimX * dimY\n if (drawBitmap[idx] > 0) {\n hasData = true\n break\n }\n }\n if (hasData) {\n break\n }\n }\n }\n\n if (hasData) {\n if (firstSliceWithData === -1) {\n firstSliceWithData = slice\n }\n lastSliceWithData = slice\n }\n }\n\n if (firstSliceWithData === -1 || lastSliceWithData === -1) {\n return null\n }\n\n return { first: firstSliceWithData, last: lastSliceWithData }\n}\n\n/**\n * Extract a single slice from 3D volume data\n * @param sliceIndex - Index of the slice to extract\n * @param sliceType - The slice orientation (AXIAL, CORONAL, or SAGITTAL)\n * @param drawBitmap - The drawing bitmap data\n * @param dims - The volume dimensions\n * @returns Float32Array containing the slice data\n */\nexport function extractSlice(sliceIndex: number, sliceType: SLICE_TYPE, drawBitmap: Uint8Array, dims: DrawingDimensions): Float32Array {\n const { dimX, dimY, dimZ } = dims\n\n let sliceData: Float32Array\n if (sliceType === SLICE_TYPE.AXIAL) {\n // Extract axial slice (XY plane at Z=sliceIndex)\n sliceData = new Float32Array(dimX * dimY)\n const offset = sliceIndex * dimX * dimY\n for (let i = 0; i < dimX * dimY; i++) {\n sliceData[i] = drawBitmap[offset + i]\n }\n } else if (sliceType === SLICE_TYPE.CORONAL) {\n // Extract coronal slice (XZ plane at Y=sliceIndex)\n sliceData = new Float32Array(dimX * dimZ)\n for (let z = 0; z < dimZ; z++) {\n for (let x = 0; x < dimX; x++) {\n const srcIdx = x + sliceIndex * dimX + z * dimX * dimY\n const dstIdx = x + z * dimX\n sliceData[dstIdx] = drawBitmap[srcIdx]\n }\n }\n } else if (sliceType === SLICE_TYPE.SAGITTAL) {\n // Extract sagittal slice (YZ plane at X=sliceIndex)\n sliceData = new Float32Array(dimY * dimZ)\n for (let z = 0; z < dimZ; z++) {\n for (let y = 0; y < dimY; y++) {\n const srcIdx = sliceIndex + y * dimX + z * dimX * dimY\n const dstIdx = y + z * dimY\n sliceData[dstIdx] = drawBitmap[srcIdx]\n }\n }\n } else {\n throw new Error('Invalid slice type')\n }\n\n return sliceData\n}\n\n/**\n * Extract intensity slice from image data\n * @param sliceIndex - Index of the slice to extract\n * @param sliceType - The slice orientation\n * @param imageData - The image intensity data\n * @param dims - The volume dimensions\n * @param maxVal - Maximum value for normalization\n * @returns Float32Array containing normalized intensity values\n */\nexport function extractIntensitySlice(sliceIndex: number, sliceType: SLICE_TYPE, imageData: TypedVoxelArray, dims: DrawingDimensions, maxVal: number): Float32Array {\n const { dimX, dimY, dimZ } = dims\n\n let sliceData: TypedVoxelArray\n if (sliceType === SLICE_TYPE.AXIAL) {\n // Extract axial slice (XY plane at Z=sliceIndex)\n sliceData = new Float32Array(dimX * dimY)\n const offset = sliceIndex * dimX * dimY\n for (let i = 0; i < dimX * dimY; i++) {\n sliceData[i] = imageData[offset + i] / maxVal\n }\n } else if (sliceType === SLICE_TYPE.CORONAL) {\n // Extract coronal slice (XZ plane at Y=sliceIndex)\n sliceData = new Float32Array(dimX * dimZ)\n for (let z = 0; z < dimZ; z++) {\n for (let x = 0; x < dimX; x++) {\n const srcIdx = x + sliceIndex * dimX + z * dimX * dimY\n const dstIdx = x + z * dimX\n sliceData[dstIdx] = imageData[srcIdx] / maxVal\n }\n }\n } else if (sliceType === SLICE_TYPE.SAGITTAL) {\n // Extract sagittal slice (YZ plane at X=sliceIndex)\n sliceData = new Float32Array(dimY * dimZ)\n for (let z = 0; z < dimZ; z++) {\n for (let y = 0; y < dimY; y++) {\n const srcIdx = sliceIndex + y * dimX + z * dimX * dimY\n const dstIdx = y + z * dimY\n sliceData[dstIdx] = imageData[srcIdx] / maxVal\n }\n }\n } else {\n throw new Error('Invalid slice type')\n }\n\n return sliceData\n}\n\n/**\n * Insert a color mask into the drawing bitmap at a specific slice\n * @param mask - The mask data to insert\n * @param sliceIndex - Index of the slice\n * @param sliceType - The slice orientation\n * @param drawBitmap - The drawing bitmap to modify\n * @param dims - The volume dimensions\n * @param binaryThreshold - Threshold for binary conversion\n * @param color - Color value to use\n */\nexport function insertColorMask(mask: TypedVoxelArray, sliceIndex: number, sliceType: SLICE_TYPE, drawBitmap: Uint8Array, dims: DrawingDimensions, binaryThreshold: number, color: number): void {\n const { dimX, dimY, dimZ } = dims\n\n if (sliceType === SLICE_TYPE.AXIAL) {\n // Insert axial slice (XY plane at Z=sliceIndex)\n const offset = sliceIndex * dimX * dimY\n for (let i = 0; i < mask.length; i++) {\n if (mask[i] >= binaryThreshold) {\n drawBitmap[offset + i] = color\n }\n }\n } else if (sliceType === SLICE_TYPE.CORONAL) {\n // Insert coronal slice (XZ plane at Y=sliceIndex)\n for (let z = 0; z < dimZ; z++) {\n for (let x = 0; x < dimX; x++) {\n const srcIdx = x + z * dimX\n const dstIdx = x + sliceIndex * dimX + z * dimX * dimY\n if (mask[srcIdx] >= binaryThreshold) {\n drawBitmap[dstIdx] = color\n }\n }\n }\n } else if (sliceType === SLICE_TYPE.SAGITTAL) {\n // Insert sagittal slice (YZ plane at X=sliceIndex)\n for (let z = 0; z < dimZ; z++) {\n for (let y = 0; y < dimY; y++) {\n const srcIdx = y + z * dimY\n const dstIdx = sliceIndex + y * dimX + z * dimX * dimY\n if (mask[srcIdx] >= binaryThreshold) {\n drawBitmap[dstIdx] = color\n }\n }\n }\n } else {\n throw new Error('Invalid slice type')\n }\n}\n\n/**\n * Smooth a 2D slice using a simple 3x3 kernel\n * @param slice - The slice data to smooth\n * @param width - Width of the slice\n * @param height - Height of the slice\n */\nexport function smoothSlice(slice: Float32Array, width: number, height: number): void {\n if (width < 3 || height < 3) {\n return\n }\n\n const temp = new Float32Array(slice.length)\n\n // Smooth in X direction\n for (let y = 0; y < height; y++) {\n for (let x = 0; x < width; x++) {\n const idx = x + y * width\n if (x === 0 || x === width - 1) {\n temp[idx] = slice[idx]\n } else {\n temp[idx] = (slice[idx - 1] + 2 * slice[idx] + slice[idx + 1]) * 0.25\n }\n }\n }\n\n // Smooth in Y direction\n for (let y = 0; y < height; y++) {\n for (let x = 0; x < width; x++) {\n const idx = x + y * width\n if (y === 0 || y === height - 1) {\n slice[idx] = temp[idx]\n } else {\n slice[idx] = (temp[idx - width] + 2 * temp[idx] + temp[idx + width]) * 0.25\n }\n }\n }\n}\n\n/**\n * Calculate intensity-based weight for interpolation\n * @param intensity1 - Intensity from first slice\n * @param intensity2 - Intensity from second slice\n * @param targetIntensity - Target intensity to compare against\n * @param intensitySigma - Sigma parameter for weight calculation\n * @returns Weight value between 0 and 1\n */\nexport function calculateIntensityWeight(intensity1: number, intensity2: number, targetIntensity: number, intensitySigma: number): number {\n const diff1 = Math.abs(targetIntensity - intensity1)\n const diff2 = Math.abs(targetIntensity - intensity2)\n\n const weight1 = Math.exp((-diff1 * diff1) / (2 * intensitySigma * intensitySigma))\n const weight2 = Math.exp((-diff2 * diff2) / (2 * intensitySigma * intensitySigma))\n\n const totalWeight = weight1 + weight2\n if (totalWeight < 1e-6) {\n return 0.5\n }\n\n return weight1 / totalWeight\n}\n\n/**\n * Perform geometric interpolation between two slices\n * @param sliceLow - Lower slice data\n * @param sliceHigh - Higher slice data\n * @param z - Current slice index\n * @param sliceIndexLow - Lower slice index\n * @param sliceIndexHigh - Higher slice index\n * @param interpolatedSlice - Output array for interpolated values\n */\nexport function doGeometricInterpolation(sliceLow: Float32Array, sliceHigh: Float32Array, z: number, sliceIndexLow: number, sliceIndexHigh: number, interpolatedSlice: Float32Array): void {\n const fracHigh = (z - sliceIndexLow) / (sliceIndexHigh - sliceIndexLow)\n const fracLow = 1 - fracHigh\n\n for (let i = 0; i < sliceLow.length; i++) {\n interpolatedSlice[i] = sliceLow[i] * fracLow + sliceHigh[i] * fracHigh\n }\n}\n\n/**\n * Perform intensity-guided interpolation between two slices\n * @param sliceLow - Lower slice data\n * @param sliceHigh - Higher slice data\n * @param z - Current slice index\n * @param sliceIndexLow - Lower slice index\n * @param sliceIndexHigh - Higher slice index\n * @param interpolatedSlice - Output array for interpolated values\n * @param opts - Interpolation options\n * @param intensityLow - Intensity data for lower slice\n * @param intensityHigh - Intensity data for higher slice\n * @param targetIntensity - Target intensity data\n */\nexport function doIntensityGuidedInterpolation(\n sliceLow: Float32Array,\n sliceHigh: Float32Array,\n z: number,\n sliceIndexLow: number,\n sliceIndexHigh: number,\n interpolatedSlice: Float32Array,\n opts: { intensityWeight: number; intensitySigma: number },\n intensityLow: Float32Array,\n intensityHigh: Float32Array,\n targetIntensity: Float32Array\n): void {\n const baseFracHigh = (z - sliceIndexLow) / (sliceIndexHigh - sliceIndexLow)\n const baseFracLow = 1 - baseFracHigh\n\n for (let i = 0; i < sliceLow.length; i++) {\n if (sliceLow[i] > 0 || sliceHigh[i] > 0) {\n const intensityWeight = calculateIntensityWeight(intensityLow[i], intensityHigh[i], targetIntensity[i], opts.intensitySigma)\n\n const alpha = opts.intensityWeight\n const combinedWeightLow = alpha * intensityWeight + (1 - alpha) * baseFracLow\n const combinedWeightHigh = 1 - combinedWeightLow\n\n interpolatedSlice[i] = sliceLow[i] * combinedWeightLow + sliceHigh[i] * combinedWeightHigh\n } else {\n interpolatedSlice[i] = sliceLow[i] * baseFracLow + sliceHigh[i] * baseFracHigh\n }\n }\n}\n\n/**\n * Main interpolation function for mask slices\n * @param drawBitmap - The drawing bitmap to modify\n * @param dims - The volume dimensions\n * @param imageData - Optional image intensity data for guided interpolation\n * @param maxVal - Maximum value for intensity normalization\n * @param sliceIndexLow - Lower slice index (optional)\n * @param sliceIndexHigh - Higher slice index (optional)\n * @param options - Interpolation options\n * @param refreshDrawingCallback - Callback to refresh the drawing\n */\nexport function interpolateMaskSlices(\n drawBitmap: Uint8Array,\n dims: DrawingDimensions,\n imageData: TypedVoxelArray,\n maxVal: number,\n sliceIndexLow: number | undefined,\n sliceIndexHigh: number | undefined,\n options: InterpolationOptions,\n refreshDrawingCallback: () => void\n): void {\n const { dimX, dimY, dimZ } = dims\n\n // Determine slice type (default to axial)\n const sliceType = options.sliceType ?? SLICE_TYPE.AXIAL\n\n // Get dimensions based on slice type\n let sliceWidth: number, sliceHeight: number, maxSliceIndex: number\n if (sliceType === SLICE_TYPE.AXIAL) {\n sliceWidth = dimX\n sliceHeight = dimY\n maxSliceIndex = dimZ - 1\n } else if (sliceType === SLICE_TYPE.CORONAL) {\n sliceWidth = dimX\n sliceHeight = dimZ\n maxSliceIndex = dimY - 1\n } else if (sliceType === SLICE_TYPE.SAGITTAL) {\n sliceWidth = dimY\n sliceHeight = dimZ\n maxSliceIndex = dimX - 1\n } else {\n throw new Error('Invalid slice type. Must be AXIAL, CORONAL, or SAGITTAL')\n }\n\n // Set default options\n const opts = {\n intensityWeight: options.intensityWeight ?? 0.7,\n binaryThreshold: options.binaryThreshold ?? 0.375,\n intensitySigma: options.intensitySigma ?? 0.1,\n applySmoothingToSlices: options.applySmoothingToSlices ?? true,\n useIntensityGuided: options.useIntensityGuided ?? true\n }\n\n // If slice indices are provided, validate them\n if (sliceIndexLow !== undefined && sliceIndexHigh !== undefined) {\n if (sliceIndexLow >= sliceIndexHigh) {\n throw new Error('Low slice index must be less than high slice index')\n }\n if (sliceIndexLow < 0 || sliceIndexHigh > maxSliceIndex) {\n throw new Error(`Slice indices out of bounds [0, ${maxSliceIndex}]`)\n }\n }\n\n // Find all unique colors across all slices\n const colorRanges = new Map<number, { min: number; max: number }>()\n\n // Scan through all slices to find color ranges\n for (let sliceIdx = 0; sliceIdx <= maxSliceIndex; sliceIdx++) {\n const slice = extractSlice(sliceIdx, sliceType, drawBitmap, dims)\n\n for (let i = 0; i < slice.length; i++) {\n const color = slice[i]\n if (color > 0) {\n if (!colorRanges.has(color)) {\n colorRanges.set(color, { min: sliceIdx, max: sliceIdx })\n } else {\n const range = colorRanges.get(color)!\n range.min = Math.min(range.min, sliceIdx)\n range.max = Math.max(range.max, sliceIdx)\n }\n }\n }\n }\n\n // Process each color independently with its own slice range\n for (const [color, range] of colorRanges) {\n // Use provided indices or color-specific range\n const colorSliceLow = sliceIndexLow !== undefined ? Math.max(sliceIndexLow, range.min) : range.min\n const colorSliceHigh = sliceIndexHigh !== undefined ? Math.min(sliceIndexHigh, range.max) : range.max\n\n // Skip if range is invalid or too small\n if (colorSliceLow >= colorSliceHigh || colorSliceHigh - colorSliceLow < 2) {\n continue\n }\n\n // Extract boundary slices for this color\n const sliceLow = extractSlice(colorSliceLow, sliceType, drawBitmap, dims)\n const sliceHigh = extractSlice(colorSliceHigh, sliceType, drawBitmap, dims)\n\n // Create binary masks for this color\n const colorMaskLow = new Float32Array(sliceLow.length)\n const colorMaskHigh = new Float32Array(sliceHigh.length)\n\n for (let i = 0; i < sliceLow.length; i++) {\n colorMaskLow[i] = sliceLow[i] === color ? 1 : 0\n colorMaskHigh[i] = sliceHigh[i] === color ? 1 : 0\n }\n\n // Apply smoothing if enabled\n if (opts.applySmoothingToSlices) {\n smoothSlice(colorMaskLow, sliceWidth, sliceHeight)\n smoothSlice(colorMaskHigh, sliceWidth, sliceHeight)\n }\n\n // Interpolate between boundary slices for this color\n for (let z = colorSliceLow + 1; z < colorSliceHigh; z++) {\n const colorInterpolated = new Float32Array(sliceWidth * sliceHeight)\n\n if (opts.useIntensityGuided && imageData) {\n // Intensity-guided interpolation\n const intensityLow = extractIntensitySlice(colorSliceLow, sliceType, imageData, dims, maxVal)\n const intensityHigh = extractIntensitySlice(colorSliceHigh, sliceType, imageData, dims, maxVal)\n const targetIntensity = extractIntensitySlice(z, sliceType, imageData, dims, maxVal)\n\n doIntensityGuidedInterpolation(colorMaskLow, colorMaskHigh, z, colorSliceLow, colorSliceHigh, colorInterpolated, opts, intensityLow, intensityHigh, targetIntensity)\n } else {\n // Geometric interpolation\n doGeometricInterpolation(colorMaskLow, colorMaskHigh, z, colorSliceLow, colorSliceHigh, colorInterpolated)\n }\n\n // Insert interpolated values for this color\n insertColorMask(colorInterpolated, z, sliceType, drawBitmap, dims, opts.binaryThreshold, color)\n }\n }\n\n // Update the drawing texture\n refreshDrawingCallback()\n}\n"],"mappings":"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AAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IACxE;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACxE;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IACxE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACxE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IACrE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAG;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IACrE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA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ACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAAA,EACA,GAAK;AAAA,IACH;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACxE;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAG;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IACpE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACxE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACtE;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAG;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACrE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACpE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACvE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IACxE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACxE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACrE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IACpE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,EAC3B;AAAA,EACA,GAAK;AAAA,IACH;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACxE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IACrE;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACxE;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACrE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACrE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACrE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACpE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACrE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,EAC5D;AAAA,EACA,GAAK;AAAA,IACH;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAG;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IACvE;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACvE;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACvE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACzE;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACvE;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IACtE;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACpE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAG;AAAA,IAAG;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IACpE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACxE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACpE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IACxE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACxE;AAAA,EACF;AAAA,EACA,GAAK;AAAA,IACH;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACpE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,EACF;AAAA,EACA,GAAK;AAAA,IACH;AAAA,IAAG;AAAA,IAAG;AAAA,IAAG;AAAA,IAAG;AAAA,IAAG;AAAA,IAAG;AAAA,IAAG;AAAA,IAAG;AAAA,IAAG;AAAA,IAAG;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACtE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACxE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AA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AK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,EACzD;AAAA,EACA,GAAK;AAAA,IACH;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,EAC3B;AAAA,EACA,GAAK;AAAA,IACH;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,EACF;AAAA,EACA,GAAK;AAAA,IACH;AAAA,IAAG;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACvE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,EAClC;AAAA,EACA,GAAK;AAAA,IACH;AAAA,IAAG;AAAA,IAAG;AAAA,IAAG;AAAA,IAAG;AAAA,IAAG;AAAA,IAAG;AAAA,IAAG;AAAA,IAAG;AAAA,IAAG;AAAA,IAAG;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACtE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACxE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA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IAAI;AAAA,IACtE;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IACzE;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IACtE;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACxE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IACrE;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAG;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IACtE;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACpE;AAAA,IAAK;AAAA,IAAI;AAAA,IAAG;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IACzE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACxE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IACpE;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAG;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IACpE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAG;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,EAChD;AAAA,EACA,GAAK;AAAA,IACH;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IACvE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IACrE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACrE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IACvE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IACzE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACpE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IACtE;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IACxE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IACvE;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IACtE;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,EACvC;AAAA,EACA,GAAK;AAAA,IACH;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACvE;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAG;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IACvE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACxE;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAI;AAAA,IACxE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAG;AAAA,IAAG;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IACvE;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IACxE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IACzE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAG;AAAA,IAAG;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IACvE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,EACtC;AAAA,EACA,GAAK;AAAA,IACH;AAAA,IAAG;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACvE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,EAClC;AAAA,EACA,GAAK;AAAA,IACH;AAAA,IAAG;AAAA,IAAG;AAAA,IAAG;AAAA,IAAG;AAAA,IAAG;AAAA,IAAG;AAAA,IAAG;AAAA,IAAG;AAAA,IAAG;AAAA,IAAG;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACtE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACxE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAA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AA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IACvE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAG;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IACrE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAG;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IACvE;AAAA,IAAG;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IACzE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACvE;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IACpE;AAAA,IAAK;AAAA,IAAG;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAG;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACzE;AAAA,IAAG;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,EAC3B;AAAA,EACA,GAAK;AAAA,IACH;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAG;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAG;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAI;AAAA,IAAG;AAAA,IAAI;AAAA,IAAK;AAAA,IACtE;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IACvE;AAAA,IAAI;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IACrE;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IACxE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IACxE;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAG;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IACpE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAG;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAG;AAAA,IACxE;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAG;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACxE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IACrE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAG;AAAA,IACxE;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAC1E;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,EAChB;AAAA,EACA,GAAK;AAAA,IACH;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAI;AAAA,IACvE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAG;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IACzE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IACtE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAG;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAC1E;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IACzE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IACrE;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IACzE;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IACrE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAG;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IACtE;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAG;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAI;AAAA,IACvE;AAAA,IAAK;AAAA,IAAG;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAI;AAAA,IACtE;AAAA,IAAG;AAAA,IAAI;AAAA,IAAG;AAAA,IAAK;AAAA,IAAK;AAAA,EACtB;AAAA,EACA,GAAK;AAAA,IACH;AAAA,IAAG;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACvE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,EAClC;AAAA,EACA,GAAK;AAAA,IACH;AAAA,IAAG;AAAA,IAAG;AAAA,IAAG;AAAA,IAAG;AAAA,IAAG;AAAA,IAAG;AAAA,IAAG;AAAA,IAAG;AAAA,IAAG;AAAA,IAAG;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACtE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IACxE;AAAA,IAAI;AAAA,IAAI;AAAA,IAAI;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACxE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IACtE;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;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","names":["vec3","vec4","vec3","readHeaderAsync","uuidv4","vec3","ImageType","vec4","NIFTI1","mat4","vec3","vec4","NIFTI1","NIFTI1","NIFTI1","NIFTI1","NIFTI1","vec3","NIFTI1","mat4","NIFTI1","NIFTI1","mat3","mat4","vec3","NIFTI1","NIFTI1","mat4","vec3","vec4","mat3","mat4","vec3","vec4","readHeaderAsync","readHeaderAsync","NIFTI1","mat4","zarr","vec3","vec4","uuidv4","mat4","vec3","vec3","mat4","vec4","vec3","vec3","vec4","slice"]}
|