UNPKG

molstar

Version:

A comprehensive macromolecular library.

419 lines (418 loc) 18.5 kB
/** * Copyright (c) 2020-2025 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Alexander Rose <alexander.rose@weirdbyte.de> */ import { ParamDefinition as PD } from '../../mol-util/param-definition'; import { Image } from '../../mol-geo/geometry/image/image'; import { Grid, Volume } from '../../mol-model/volume'; import { VolumeVisual, VolumeRepresentation, VolumeRepresentationProvider } from './representation'; import { LocationIterator, PositionLocation } from '../../mol-geo/util/location-iterator'; import { NullLocation } from '../../mol-model/location'; import { EmptyLoci } from '../../mol-model/loci'; import { Interval, SortedArray } from '../../mol-data/int'; import { transformPositionArray } from '../../mol-geo/util'; import { Color } from '../../mol-util/color'; import { ColorTheme } from '../../mol-theme/color'; import { packIntToRGBArray } from '../../mol-util/number-packing'; import { eachVolumeLoci } from './util'; import { Vec3 } from '../../mol-math/linear-algebra/3d/vec3'; import { Quat } from '../../mol-math/linear-algebra/3d/quat'; import { degToRad } from '../../mol-math/misc'; import { Mat4 } from '../../mol-math/linear-algebra/3d/mat4'; import { clamp, normalize } from '../../mol-math/interpolate'; import { assertUnreachable } from '../../mol-util/type-helpers'; export const SliceParams = { ...Image.Params, quality: { ...Image.Params.quality, isEssential: false }, dimension: PD.MappedStatic('x', { x: PD.Numeric(0, { min: 0, max: 0, step: 1 }, { immediateUpdate: true }), y: PD.Numeric(0, { min: 0, max: 0, step: 1 }, { immediateUpdate: true }), z: PD.Numeric(0, { min: 0, max: 0, step: 1 }, { immediateUpdate: true }), }, { isEssential: true, hideIf: p => p.mode !== 'grid', description: 'Slice position in grid coordinates.' }), isoValue: Volume.IsoValueParam, mode: PD.Select('grid', PD.arrayToOptions(['grid', 'frame', 'plane']), { description: 'Grid: slice through the volume along the grid axes in integer steps. Frame: slice through the volume along arbitrary axes in any step size. Plane: an arbitrary plane defined by point and normal.' }), offset: PD.Numeric(0, { min: -1, max: 1, step: 0.01 }, { isEssential: true, immediateUpdate: true, hideIf: p => p.mode !== 'frame', description: 'Relative offset from center.' }), axis: PD.Select('a', PD.arrayToOptions(['a', 'b', 'c']), { isEssential: true, hideIf: p => p.mode !== 'frame', description: 'Axis of the frame.' }), rotation: PD.Group({ axis: PD.Vec3(Vec3.create(1, 0, 0), {}, { description: 'Axis of rotation' }), angle: PD.Numeric(0, { min: -180, max: 180, step: 1 }, { immediateUpdate: true, description: 'Axis rotation angle in Degrees' }), }, { isExpanded: true, hideIf: p => p.mode !== 'frame' }), plane: PD.Group({ point: PD.Vec3(Vec3.create(0, 0, 0), {}, { description: 'Plane point' }), normal: PD.Vec3(Vec3.create(1, 0, 0), {}, { description: 'Plane normal' }), }, { isExpanded: true, hideIf: p => p.mode !== 'plane' }), }; export function getSliceParams(ctx, volume) { const p = PD.clone(SliceParams); const dim = volume.grid.cells.space.dimensions; p.dimension = PD.MappedStatic('x', { x: PD.Numeric(0, { min: 0, max: dim[0] - 1, step: 1 }, { immediateUpdate: true }), y: PD.Numeric(0, { min: 0, max: dim[1] - 1, step: 1 }, { immediateUpdate: true }), z: PD.Numeric(0, { min: 0, max: dim[2] - 1, step: 1 }, { immediateUpdate: true }), }, { isEssential: true, hideIf: p => p.mode !== 'grid' }); p.isoValue = Volume.createIsoValueParam(Volume.IsoValue.absolute(volume.grid.stats.min), volume.grid.stats); return p; } export async function createImage(ctx, volume, key, theme, props, image) { switch (props.mode) { case 'frame': return createFrameImage(ctx, volume, key, theme, props, image); case 'grid': return createGridImage(ctx, volume, key, theme, props, image); case 'plane': return createPlaneImage(ctx, volume, key, theme, props, image); default: assertUnreachable(props.mode); } } // function getFrame(volume, props) { const { axis, rotation, mode } = props; const gridToCartn = Grid.getGridToCartesianTransform(volume.grid); const cartnToGrid = Mat4.invert(Mat4(), gridToCartn); const [nx, ny, nz] = volume.grid.cells.space.dimensions; const a = nx - 1; const b = ny - 1; const c = nz - 1; const dirA = Vec3.create(a, 0, 0); const dirB = Vec3.create(0, b, 0); const dirC = Vec3.create(0, 0, c); const resolution = Math.max(a, b, c) / Math.max(nx, ny, nz); const min = Vec3.create(0, 0, 0); const max = Vec3.create(a, b, c); Vec3.transformMat4(min, min, gridToCartn); Vec3.transformMat4(max, max, gridToCartn); const center = Vec3.center(Vec3(), max, min); const size = Vec3(); const major = Vec3(); const minor = Vec3(); const normal = Vec3(); if (axis === 'c') { Vec3.set(size, a, b, c); Vec3.copy(major, dirA); Vec3.copy(minor, dirB); Vec3.copy(normal, dirC); } else if (axis === 'b') { Vec3.set(size, a, c, b); Vec3.copy(major, dirA); Vec3.copy(normal, dirB); Vec3.copy(minor, dirC); } else { Vec3.set(size, b, c, a); Vec3.copy(normal, dirA); Vec3.copy(major, dirB); Vec3.copy(minor, dirC); } if (rotation.angle !== 0) { const ra = Vec3(); Vec3.scaleAndAdd(ra, ra, dirA, rotation.axis[0]); Vec3.scaleAndAdd(ra, ra, dirB, rotation.axis[1]); Vec3.scaleAndAdd(ra, ra, dirC, rotation.axis[2]); Vec3.normalize(ra, ra); const rm = Mat4.fromRotation(Mat4(), degToRad(rotation.angle), ra); Vec3.transformDirection(major, major, rm); Vec3.transformDirection(minor, minor, rm); Vec3.transformDirection(normal, normal, rm); } if (mode === 'frame') { const r = Vec3.distance(min, max); const s = Vec3.distance(min, max) * Math.SQRT2; Vec3.set(size, s, s, r); } Vec3.transformDirection(major, major, gridToCartn); Vec3.transformDirection(minor, minor, gridToCartn); Vec3.transformDirection(normal, normal, gridToCartn); const trim = { type: 3, center: Vec3.create(a / 2, b / 2, c / 2), scale: Vec3.create(a, b, c), rotation: Quat.identity(), transform: cartnToGrid, }; return { size, major, minor, normal, center, trim, resolution }; } function getSampledImage(volume, theme, info, isoValue, trim, image) { const { m, width, height } = info; const { cells: { space }, stats } = volume.grid; const { min, max } = stats; const isUniform = theme.color.granularity === 'uniform'; const color = 'color' in theme.color && theme.color.color ? theme.color.color : () => Color(0xffffff); const v = Vec3(); const gridToCartn = Grid.getGridToCartesianTransform(volume.grid); const cartnToGrid = Mat4.invert(Mat4(), gridToCartn); const [mx, my, mz] = space.dimensions; const imageArray = new Uint8Array(width * height * 4); const groupArray = new Uint8Array(width * height * 4); const valueArray = new Float32Array(width * height); const gridCoords = Vec3(); const pl = PositionLocation(Vec3(), Vec3()); const getTrilinearlyInterpolated = Grid.makeGetTrilinearlyInterpolated(volume.grid, 'none'); let i = 0; for (let ih = 0; ih < height; ++ih) { for (let iw = 0; iw < width; ++iw) { const y = (clamp(iw + 0.5, 0, width - 1) / width) - 0.5; const x = (clamp(ih + 0.5, 0, height - 1) / height) - 0.5; Vec3.set(v, x, -y, 0); Vec3.transformMat4(v, v, m); Vec3.copy(gridCoords, v); Vec3.transformMat4(gridCoords, gridCoords, cartnToGrid); const ix = Math.trunc(gridCoords[0]); const iy = Math.trunc(gridCoords[1]); const iz = Math.trunc(gridCoords[2]); Vec3.copy(pl.position, v); const c = color(pl, false); Color.toArray(c, imageArray, i); const val = normalize(getTrilinearlyInterpolated(v), min, max); if (isUniform) { imageArray[i] *= val; imageArray[i + 1] *= val; imageArray[i + 2] *= val; } valueArray[i / 4] = val; if (ix >= 0 && ix < mx && iy >= 0 && iy < my && iz >= 0 && iz < mz) { packIntToRGBArray(space.dataOffset(ix, iy, iz), groupArray, i); } i += 4; } } const imageTexture = { width, height, array: imageArray, flipY: true }; const groupTexture = { width, height, array: groupArray, flipY: true }; const valueTexture = { width, height, array: valueArray, flipY: true }; const corners = new Float32Array([ -0.5, 0.5, 0, 0.5, 0.5, 0, -0.5, -0.5, 0, 0.5, -0.5, 0 ]); transformPositionArray(m, corners, 0, 4); const isoLevel = clamp(normalize(Volume.IsoValue.toAbsolute(isoValue, stats).absoluteValue, min, max), 0, 1); const im = Image.create(imageTexture, corners, groupTexture, valueTexture, trim, isoLevel, image); im.setBoundingSphere(Volume.getBoundingSphere(volume)); return im; } async function createFrameImage(ctx, volume, key, theme, props, image) { const { offset, isoValue } = props; const { size, major, minor, normal, center, trim, resolution } = getFrame(volume, props); const scaleFactor = 1 / resolution; const scale = Vec3.create(size[0], size[1], 1); const offsetDir = Vec3.setMagnitude(Vec3(), normal, size[2] / 2); const width = Math.floor(size[1] * scaleFactor); const height = Math.floor(size[0] * scaleFactor); const m = Mat4.identity(); const v = Vec3(); const anchor = Vec3(); Vec3.add(v, center, major); Mat4.targetTo(m, center, v, minor); Vec3.scaleAndAdd(anchor, center, offsetDir, offset); Mat4.setTranslation(m, anchor); Mat4.mul(m, m, Mat4.rotY90); Mat4.scale(m, m, scale); const info = { m, width, height }; return getSampledImage(volume, theme, info, isoValue, trim, image); } async function createPlaneImage(ctx, volume, key, theme, props, image) { const { plane: { point, normal }, isoValue } = props; const m = Mat4.fromPlane(Mat4(), normal, point); const gridToCartn = Grid.getGridToCartesianTransform(volume.grid); const cartnToGrid = Mat4.invert(Mat4(), gridToCartn); const [mx, my, mz] = volume.grid.cells.space.dimensions; const a = mx - 1; const b = my - 1; const c = mz - 1; const resolution = Math.max(a, b, c) / Math.max(mx, my, mz); const scaleFactor = 1 / resolution; const s = Vec3.distance(Vec3.create(0, 0, 0), Vec3.create(a, b, c)) * Math.SQRT2; const size = Vec3.set(Vec3(), s, s, s); Mat4.mul(m, m, Mat4.rotY90); Mat4.scale(m, m, size); const width = Math.floor(size[1] * scaleFactor); const height = Math.floor(size[0] * scaleFactor); const trim = { type: 3, center: Vec3.create(a / 2, b / 2, c / 2), scale: Vec3.create(a, b, c), rotation: Quat.identity(), transform: cartnToGrid, }; const info = { m, width, height }; return getSampledImage(volume, theme, info, isoValue, trim, image); } async function createGridImage(ctx, volume, key, theme, props, image) { const { dimension: { name: dim }, isoValue } = props; const { cells: { space, data }, stats } = volume.grid; const { min, max } = stats; const isUniform = theme.color.granularity === 'uniform'; const color = 'color' in theme.color && theme.color.color ? theme.color.color : () => Color(0xffffff); const { width, height, x, y, z, x0, y0, z0, nx, ny, nz } = getSliceInfo(volume.grid, props); const corners = new Float32Array(dim === 'x' ? [x, 0, 0, x, y, 0, x, 0, z, x, y, z] : dim === 'y' ? [0, y, 0, x, y, 0, 0, y, z, x, y, z] : [0, 0, z, 0, y, z, x, 0, z, x, y, z]); const imageArray = new Uint8Array(width * height * 4); const groupArray = getPackedGroupArray(volume.grid, props); const valueArray = new Float32Array(width * height); const gridToCartn = Grid.getGridToCartesianTransform(volume.grid); const l = PositionLocation(Vec3(), Vec3()); let i = 0; for (let iy = y0; iy < ny; ++iy) { for (let ix = x0; ix < nx; ++ix) { for (let iz = z0; iz < nz; ++iz) { Vec3.set(l.position, ix, iy, iz); Vec3.transformMat4(l.position, l.position, gridToCartn); Color.toArray(color(l, false), imageArray, i); const val = normalize(space.get(data, ix, iy, iz), min, max); if (isUniform) { imageArray[i] *= val; imageArray[i + 1] *= val; imageArray[i + 2] *= val; } valueArray[i / 4] = val; i += 4; } } } const imageTexture = { width, height, array: imageArray, flipY: true }; const groupTexture = { width, height, array: groupArray, flipY: true }; const valueTexture = { width, height, array: valueArray, flipY: true }; const transform = Grid.getGridToCartesianTransform(volume.grid); transformPositionArray(transform, corners, 0, 4); const trim = Image.createEmptyTrim(); const isoLevel = clamp(normalize(Volume.IsoValue.toAbsolute(isoValue, stats).absoluteValue, min, max), 0, 1); const im = Image.create(imageTexture, corners, groupTexture, valueTexture, trim, isoLevel, image); im.setBoundingSphere(Volume.getBoundingSphere(volume)); return im; } // function getSliceInfo(grid, props) { const { dimension: { name: dim, params: index } } = props; const { space } = grid.cells; let width, height; let x, y, z; let x0 = 0, y0 = 0, z0 = 0; let [nx, ny, nz] = space.dimensions; if (dim === 'x') { x = index, y = ny - 1, z = nz - 1; width = nz, height = ny; x0 = x, nx = x0 + 1; } else if (dim === 'y') { x = nx - 1, y = index, z = nz - 1; width = nz, height = nx; y0 = y, ny = y0 + 1; } else { x = nx - 1, y = ny - 1, z = index; width = nx, height = ny; z0 = z, nz = z0 + 1; } return { width, height, x, y, z, x0, y0, z0, nx, ny, nz }; } function getPackedGroupArray(grid, props) { const { space } = grid.cells; const { width, height, x0, y0, z0, nx, ny, nz } = getSliceInfo(grid, props); const groupArray = new Uint8Array(width * height * 4); let j = 0; for (let iy = y0; iy < ny; ++iy) { for (let ix = x0; ix < nx; ++ix) { for (let iz = z0; iz < nz; ++iz) { packIntToRGBArray(space.dataOffset(ix, iy, iz), groupArray, j); j += 4; } } } return groupArray; } function getGroupArray(grid, props) { const { space } = grid.cells; const { width, height, x0, y0, z0, nx, ny, nz } = getSliceInfo(grid, props); const groupArray = new Uint32Array(width * height); let j = 0; for (let iy = y0; iy < ny; ++iy) { for (let ix = x0; ix < nx; ++ix) { for (let iz = z0; iz < nz; ++iz) { groupArray[j] = space.dataOffset(ix, iy, iz); j += 1; } } } return groupArray; } function getLoci(volume, props) { // TODO: cache somehow? if (props.mode === 'grid') { const groupArray = getGroupArray(volume.grid, props); return Volume.Cell.Loci(volume, SortedArray.ofUnsortedArray(groupArray)); } else { // TODO: get exact groups return Volume.Loci(volume); } } function getSliceLoci(pickingId, volume, key, props, id) { const { objectId, groupId } = pickingId; if (id === objectId) { const granularity = Volume.PickingGranularity.get(volume); if (granularity === 'volume') { return Volume.Loci(volume); } if (granularity === 'object') { return getLoci(volume, props); } else { return Volume.Cell.Loci(volume, Interval.ofSingleton(groupId)); } } return EmptyLoci; } function eachSlice(loci, volume, key, props, apply) { return eachVolumeLoci(loci, volume, undefined, apply); } // export function SliceVisual(materialId) { return VolumeVisual({ defaultProps: PD.getDefaultValues(SliceParams), createGeometry: createImage, createLocationIterator: (volume) => LocationIterator(volume.grid.cells.data.length, 1, 1, () => NullLocation), getLoci: getSliceLoci, eachLocation: eachSlice, setUpdateState: (state, volume, newProps, currentProps, newTheme, currentTheme) => { state.createGeometry = (newProps.dimension.name !== currentProps.dimension.name || newProps.dimension.params !== currentProps.dimension.params || newProps.mode !== currentProps.mode || !Vec3.equals(newProps.rotation.axis, currentProps.rotation.axis) || newProps.rotation.angle !== currentProps.rotation.angle || newProps.offset !== currentProps.offset || newProps.axis !== currentProps.axis || !Vec3.equals(newProps.plane.point, currentProps.plane.point) || !Vec3.equals(newProps.plane.normal, currentProps.plane.normal) || !Volume.IsoValue.areSame(newProps.isoValue, currentProps.isoValue, volume.grid.stats) || !ColorTheme.areEqual(newTheme.color, currentTheme.color)); }, geometryUtils: Image.Utils }, materialId); } export function SliceRepresentation(ctx, getParams) { return VolumeRepresentation('Slice', ctx, getParams, SliceVisual, getLoci); } export const SliceRepresentationProvider = VolumeRepresentationProvider({ name: 'slice', label: 'Slice', description: 'Slice of volume rendered as image with interpolation.', factory: SliceRepresentation, getParams: getSliceParams, defaultValues: PD.getDefaultValues(SliceParams), defaultColorTheme: { name: 'uniform' }, defaultSizeTheme: { name: 'uniform' }, isApplicable: (volume) => !Volume.isEmpty(volume) && !Volume.Segmentation.get(volume) });