molstar
Version:
A comprehensive macromolecular library.
378 lines (377 loc) • 18.4 kB
JavaScript
"use strict";
/**
* Copyright (c) 2020-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.SliceRepresentationProvider = exports.SliceParams = void 0;
exports.getSliceParams = getSliceParams;
exports.createImage = createImage;
exports.SliceVisual = SliceVisual;
exports.SliceRepresentation = SliceRepresentation;
const param_definition_1 = require("../../mol-util/param-definition");
const image_1 = require("../../mol-geo/geometry/image/image");
const volume_1 = require("../../mol-model/volume");
const representation_1 = require("./representation");
const location_iterator_1 = require("../../mol-geo/util/location-iterator");
const location_1 = require("../../mol-model/location");
const loci_1 = require("../../mol-model/loci");
const int_1 = require("../../mol-data/int");
const util_1 = require("../../mol-geo/util");
const color_1 = require("../../mol-util/color");
const color_2 = require("../../mol-theme/color");
const number_packing_1 = require("../../mol-util/number-packing");
const util_2 = require("./util");
const vec3_1 = require("../../mol-math/linear-algebra/3d/vec3");
const quat_1 = require("../../mol-math/linear-algebra/3d/quat");
const misc_1 = require("../../mol-math/misc");
const mat4_1 = require("../../mol-math/linear-algebra/3d/mat4");
const interpolate_1 = require("../../mol-math/interpolate");
exports.SliceParams = {
...image_1.Image.Params,
quality: { ...image_1.Image.Params.quality, isEssential: false },
dimension: param_definition_1.ParamDefinition.MappedStatic('x', {
x: param_definition_1.ParamDefinition.Numeric(0, { min: 0, max: 0, step: 1 }, { immediateUpdate: true }),
y: param_definition_1.ParamDefinition.Numeric(0, { min: 0, max: 0, step: 1 }, { immediateUpdate: true }),
z: param_definition_1.ParamDefinition.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_1.Volume.IsoValueParam,
mode: param_definition_1.ParamDefinition.Select('grid', param_definition_1.ParamDefinition.arrayToOptions(['grid', 'frame']), { 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.' }),
offset: param_definition_1.ParamDefinition.Numeric(0, { min: -1, max: 1, step: 0.01 }, { isEssential: true, immediateUpdate: true, hideIf: p => p.mode !== 'frame', description: 'Relative offset from center.' }),
axis: param_definition_1.ParamDefinition.Select('a', param_definition_1.ParamDefinition.arrayToOptions(['a', 'b', 'c']), { isEssential: true, hideIf: p => p.mode !== 'frame', description: 'Axis of the frame.' }),
rotation: param_definition_1.ParamDefinition.Group({
axis: param_definition_1.ParamDefinition.Vec3(vec3_1.Vec3.create(1, 0, 0), {}, { description: 'Axis of rotation' }),
angle: param_definition_1.ParamDefinition.Numeric(0, { min: -180, max: 180, step: 1 }, { immediateUpdate: true, description: 'Axis rotation angle in Degrees' }),
}, { isExpanded: true, hideIf: p => p.mode !== 'frame' }),
};
function getSliceParams(ctx, volume) {
const p = param_definition_1.ParamDefinition.clone(exports.SliceParams);
const dim = volume.grid.cells.space.dimensions;
p.dimension = param_definition_1.ParamDefinition.MappedStatic('x', {
x: param_definition_1.ParamDefinition.Numeric(0, { min: 0, max: dim[0] - 1, step: 1 }, { immediateUpdate: true }),
y: param_definition_1.ParamDefinition.Numeric(0, { min: 0, max: dim[1] - 1, step: 1 }, { immediateUpdate: true }),
z: param_definition_1.ParamDefinition.Numeric(0, { min: 0, max: dim[2] - 1, step: 1 }, { immediateUpdate: true }),
}, { isEssential: true, hideIf: p => p.mode !== 'grid' });
p.isoValue = volume_1.Volume.createIsoValueParam(volume_1.Volume.IsoValue.absolute(volume.grid.stats.min), volume.grid.stats);
return p;
}
async function createImage(ctx, volume, key, theme, props, image) {
if (props.mode === 'frame') {
return createFrameImage(ctx, volume, key, theme, props, image);
}
else {
return createGridImage(ctx, volume, key, theme, props, image);
}
}
//
async function createFrameImage(ctx, volume, key, theme, props, image) {
const { offset, isoValue } = props;
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
: () => (0, color_1.Color)(0xffffff);
const { size, major, minor, normal, center, trim, resolution } = getFrame(volume, props);
const scaleFactor = 1 / resolution;
const scale = vec3_1.Vec3.create(size[0], size[1], 1);
const offsetDir = vec3_1.Vec3.setMagnitude((0, vec3_1.Vec3)(), normal, size[2] / 2);
const width = Math.floor(size[1] * scaleFactor);
const height = Math.floor(size[0] * scaleFactor);
const m = mat4_1.Mat4.identity();
const v = (0, vec3_1.Vec3)();
const anchor = (0, vec3_1.Vec3)();
vec3_1.Vec3.add(v, center, major);
mat4_1.Mat4.targetTo(m, center, v, minor);
vec3_1.Vec3.scaleAndAdd(anchor, center, offsetDir, offset);
mat4_1.Mat4.setTranslation(m, anchor);
mat4_1.Mat4.mul(m, m, mat4_1.Mat4.rotY90);
mat4_1.Mat4.scale(m, m, scale);
const imageArray = new Uint8Array(width * height * 4);
const groupArray = new Uint8Array(width * height * 4);
const valueArray = new Float32Array(width * height);
const [mx, my, mz] = space.dimensions;
const gridToCartn = volume_1.Grid.getGridToCartesianTransform(volume.grid);
const cartnToGrid = mat4_1.Mat4.invert((0, mat4_1.Mat4)(), gridToCartn);
const gridCoords = (0, vec3_1.Vec3)();
const pl = (0, location_iterator_1.PositionLocation)((0, vec3_1.Vec3)(), (0, vec3_1.Vec3)());
const getTrilinearlyInterpolated = volume_1.Grid.makeGetTrilinearlyInterpolated(volume.grid, 'none');
let i = 0;
for (let ih = 0; ih < height; ++ih) {
for (let iw = 0; iw < width; ++iw) {
const y = ((0, interpolate_1.clamp)(iw + 0.5, 0, width - 1) / width) - 0.5;
const x = ((0, interpolate_1.clamp)(ih + 0.5, 0, height - 1) / height) - 0.5;
vec3_1.Vec3.set(v, x, -y, 0);
vec3_1.Vec3.transformMat4(v, v, m);
vec3_1.Vec3.copy(gridCoords, v);
vec3_1.Vec3.transformMat4(gridCoords, gridCoords, cartnToGrid);
const ix = Math.trunc(gridCoords[0]);
const iy = Math.trunc(gridCoords[1]);
const iz = Math.trunc(gridCoords[2]);
vec3_1.Vec3.copy(pl.position, v);
const c = color(pl, false);
color_1.Color.toArray(c, imageArray, i);
const val = (0, interpolate_1.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) {
(0, number_packing_1.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
]);
(0, util_1.transformPositionArray)(m, corners, 0, 4);
const isoLevel = (0, interpolate_1.clamp)((0, interpolate_1.normalize)(volume_1.Volume.IsoValue.toAbsolute(isoValue, stats).absoluteValue, min, max), 0, 1);
return image_1.Image.create(imageTexture, corners, groupTexture, valueTexture, trim, isoLevel, image);
}
function getFrame(volume, props) {
const { axis, rotation, mode } = props;
const gridToCartn = volume_1.Grid.getGridToCartesianTransform(volume.grid);
const cartnToGrid = mat4_1.Mat4.invert((0, mat4_1.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_1.Vec3.create(a, 0, 0);
const dirB = vec3_1.Vec3.create(0, b, 0);
const dirC = vec3_1.Vec3.create(0, 0, c);
const resolution = Math.max(a, b, c) / Math.max(nx, ny, nz);
const min = vec3_1.Vec3.create(0, 0, 0);
const max = vec3_1.Vec3.create(a, b, c);
vec3_1.Vec3.transformMat4(min, min, gridToCartn);
vec3_1.Vec3.transformMat4(max, max, gridToCartn);
const center = vec3_1.Vec3.center((0, vec3_1.Vec3)(), max, min);
const size = (0, vec3_1.Vec3)();
const major = (0, vec3_1.Vec3)();
const minor = (0, vec3_1.Vec3)();
const normal = (0, vec3_1.Vec3)();
if (axis === 'c') {
vec3_1.Vec3.set(size, a, b, c);
vec3_1.Vec3.copy(major, dirA);
vec3_1.Vec3.copy(minor, dirB);
vec3_1.Vec3.copy(normal, dirC);
}
else if (axis === 'b') {
vec3_1.Vec3.set(size, a, c, b);
vec3_1.Vec3.copy(major, dirA);
vec3_1.Vec3.copy(normal, dirB);
vec3_1.Vec3.copy(minor, dirC);
}
else {
vec3_1.Vec3.set(size, b, c, a);
vec3_1.Vec3.copy(normal, dirA);
vec3_1.Vec3.copy(major, dirB);
vec3_1.Vec3.copy(minor, dirC);
}
if (rotation.angle !== 0) {
const ra = (0, vec3_1.Vec3)();
vec3_1.Vec3.scaleAndAdd(ra, ra, dirA, rotation.axis[0]);
vec3_1.Vec3.scaleAndAdd(ra, ra, dirB, rotation.axis[1]);
vec3_1.Vec3.scaleAndAdd(ra, ra, dirC, rotation.axis[2]);
vec3_1.Vec3.normalize(ra, ra);
const rm = mat4_1.Mat4.fromRotation((0, mat4_1.Mat4)(), (0, misc_1.degToRad)(rotation.angle), ra);
vec3_1.Vec3.transformDirection(major, major, rm);
vec3_1.Vec3.transformDirection(minor, minor, rm);
vec3_1.Vec3.transformDirection(normal, normal, rm);
}
if (mode === 'frame') {
const r = vec3_1.Vec3.distance(min, max);
const s = vec3_1.Vec3.distance(min, max) * Math.SQRT2;
vec3_1.Vec3.set(size, s, s, r);
}
vec3_1.Vec3.transformDirection(major, major, gridToCartn);
vec3_1.Vec3.transformDirection(minor, minor, gridToCartn);
vec3_1.Vec3.transformDirection(normal, normal, gridToCartn);
const trim = {
type: 3,
center: vec3_1.Vec3.create(a / 2, b / 2, c / 2),
scale: vec3_1.Vec3.create(a, b, c),
rotation: quat_1.Quat.identity(),
transform: cartnToGrid,
};
return { size, major, minor, normal, center, trim, resolution };
}
//
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
: () => (0, color_1.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 = volume_1.Grid.getGridToCartesianTransform(volume.grid);
const l = (0, location_iterator_1.PositionLocation)((0, vec3_1.Vec3)(), (0, vec3_1.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_1.Vec3.set(l.position, ix, iy, iz);
vec3_1.Vec3.transformMat4(l.position, l.position, gridToCartn);
color_1.Color.toArray(color(l, false), imageArray, i);
const val = (0, interpolate_1.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 = volume_1.Grid.getGridToCartesianTransform(volume.grid);
(0, util_1.transformPositionArray)(transform, corners, 0, 4);
const trim = image_1.Image.createEmptyTrim();
const isoLevel = (0, interpolate_1.clamp)((0, interpolate_1.normalize)(volume_1.Volume.IsoValue.toAbsolute(isoValue, stats).absoluteValue, min, max), 0, 1);
return image_1.Image.create(imageTexture, corners, groupTexture, valueTexture, trim, isoLevel, image);
}
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) {
(0, number_packing_1.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_1.Volume.Cell.Loci(volume, int_1.SortedArray.ofUnsortedArray(groupArray));
}
else {
// TODO: get exact groups
return volume_1.Volume.Loci(volume);
}
}
function getSliceLoci(pickingId, volume, key, props, id) {
const { objectId, groupId } = pickingId;
if (id === objectId) {
const granularity = volume_1.Volume.PickingGranularity.get(volume);
if (granularity === 'volume') {
return volume_1.Volume.Loci(volume);
}
if (granularity === 'object') {
return getLoci(volume, props);
}
else {
return volume_1.Volume.Cell.Loci(volume, int_1.Interval.ofSingleton(groupId));
}
}
return loci_1.EmptyLoci;
}
function eachSlice(loci, volume, key, props, apply) {
return (0, util_2.eachVolumeLoci)(loci, volume, undefined, apply);
}
//
function SliceVisual(materialId) {
return (0, representation_1.VolumeVisual)({
defaultProps: param_definition_1.ParamDefinition.getDefaultValues(exports.SliceParams),
createGeometry: createImage,
createLocationIterator: (volume) => (0, location_iterator_1.LocationIterator)(volume.grid.cells.data.length, 1, 1, () => location_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_1.Vec3.equals(newProps.rotation.axis, currentProps.rotation.axis) ||
newProps.rotation.angle !== currentProps.rotation.angle ||
newProps.offset !== currentProps.offset ||
newProps.axis !== currentProps.axis ||
!volume_1.Volume.IsoValue.areSame(newProps.isoValue, currentProps.isoValue, volume.grid.stats) ||
!color_2.ColorTheme.areEqual(newTheme.color, currentTheme.color));
},
geometryUtils: image_1.Image.Utils
}, materialId);
}
function SliceRepresentation(ctx, getParams) {
return (0, representation_1.VolumeRepresentation)('Slice', ctx, getParams, SliceVisual, getLoci);
}
exports.SliceRepresentationProvider = (0, representation_1.VolumeRepresentationProvider)({
name: 'slice',
label: 'Slice',
description: 'Slice of volume rendered as image with interpolation.',
factory: SliceRepresentation,
getParams: getSliceParams,
defaultValues: param_definition_1.ParamDefinition.getDefaultValues(exports.SliceParams),
defaultColorTheme: { name: 'uniform' },
defaultSizeTheme: { name: 'uniform' },
isApplicable: (volume) => !volume_1.Volume.isEmpty(volume) && !volume_1.Volume.Segmentation.get(volume)
});