mirror of
https://github.com/molstar/molstar.git
synced 2026-06-04 13:30:24 +08:00
Optimize slice marking for hover
This commit is contained in:
78
src/mol-repr/volume/_spec/slice.spec.ts
Normal file
78
src/mol-repr/volume/_spec/slice.spec.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author OpenAI
|
||||
*/
|
||||
|
||||
import { OrderedSet, Interval } from '../../../mol-data/int';
|
||||
import { Grid, Volume } from '../../../mol-model/volume';
|
||||
import { Mat4 } from '../../../mol-math/linear-algebra';
|
||||
import { CustomProperties } from '../../../mol-model/custom-property';
|
||||
import { applySliceObjectLoci, applySlicePixelIntervals } from '../slice';
|
||||
|
||||
function createTestVolume(instanceCount: number, periodic = false, stats: Grid['stats'] = Grid.One.stats): Volume {
|
||||
return {
|
||||
grid: periodic ? { ...Grid.One, periodicity: 'xyz', stats } : { ...Grid.One, stats },
|
||||
instances: Array.from({ length: instanceCount }, () => ({ transform: Mat4.identity() })),
|
||||
sourceData: { kind: 'test', name: 'test', data: {} } as any,
|
||||
customProperties: new CustomProperties(),
|
||||
_propertyData: Object.create(null),
|
||||
_localPropertyData: Object.create(null),
|
||||
};
|
||||
}
|
||||
|
||||
describe('slice helpers', () => {
|
||||
it('applies object loci as displayed plane intervals for contiguous instances', () => {
|
||||
const volume = createTestVolume(4);
|
||||
const loci = Volume.Loci(volume, OrderedSet.ofBounds(1, 3));
|
||||
const intervals: Array<[number, number]> = [];
|
||||
|
||||
const changed = applySliceObjectLoci(loci, volume, 5, interval => {
|
||||
intervals.push([Interval.start(interval), Interval.end(interval)]);
|
||||
return true;
|
||||
});
|
||||
|
||||
expect(changed).toBe(true);
|
||||
expect(intervals).toEqual([[5, 15]]);
|
||||
});
|
||||
|
||||
it('applies object loci per displayed instance for discontiguous selections', () => {
|
||||
const volume = createTestVolume(4);
|
||||
const loci = Volume.Loci(volume, OrderedSet.ofSortedArray([0, 2] as const));
|
||||
const intervals: Array<[number, number]> = [];
|
||||
|
||||
const changed = applySliceObjectLoci(loci, volume, 5, interval => {
|
||||
intervals.push([Interval.start(interval), Interval.end(interval)]);
|
||||
return true;
|
||||
});
|
||||
|
||||
expect(changed).toBe(true);
|
||||
expect(intervals).toEqual([[0, 5], [10, 15]]);
|
||||
});
|
||||
|
||||
it('collapses periodic object loci to the single displayed slice plane', () => {
|
||||
const volume = createTestVolume(4, true);
|
||||
const loci = Volume.Loci(volume, OrderedSet.ofBounds(1, 3));
|
||||
const intervals: Array<[number, number]> = [];
|
||||
|
||||
const changed = applySliceObjectLoci(loci, volume, 6, interval => {
|
||||
intervals.push([Interval.start(interval), Interval.end(interval)]);
|
||||
return true;
|
||||
});
|
||||
|
||||
expect(changed).toBe(true);
|
||||
expect(intervals).toEqual([[0, 6]]);
|
||||
});
|
||||
|
||||
it('batches contiguous slice pixels into minimal intervals', () => {
|
||||
const intervals: Array<[number, number]> = [];
|
||||
|
||||
const changed = applySlicePixelIntervals([1, 2, 3, 7, 8, 10], 5, interval => {
|
||||
intervals.push([Interval.start(interval), Interval.end(interval)]);
|
||||
return true;
|
||||
});
|
||||
|
||||
expect(changed).toBe(true);
|
||||
expect(intervals).toEqual([[6, 9], [12, 14], [15, 16]]);
|
||||
});
|
||||
});
|
||||
@@ -306,7 +306,6 @@ function getSampledImage(volume: Volume, theme: Theme, info: SamplingInfo, isoVa
|
||||
|
||||
const im = Image.create(imageTexture, corners, groupTexture, valueTexture, trim, isoLevel, image);
|
||||
im.setBoundingSphere(Volume.isPeriodic(volume) ? Volume.getBoundingSphere(volume) : Grid.getBoundingSphere(volume.grid));
|
||||
|
||||
im.meta.mapping = mapping;
|
||||
|
||||
return im;
|
||||
@@ -481,7 +480,6 @@ async function createGridImage(ctx: VisualContext, volume: Volume, key: number,
|
||||
|
||||
const im = Image.create(imageTexture, corners, groupTexture, valueTexture, trim, isoLevel, image);
|
||||
im.setBoundingSphere(Volume.isPeriodic(volume) ? Volume.getBoundingSphere(volume) : Grid.getBoundingSphere(volume.grid));
|
||||
|
||||
im.meta.mapping = mapping;
|
||||
|
||||
return im;
|
||||
@@ -575,6 +573,49 @@ function getSliceLoci(pickingId: PickingId, volume: Volume, _key: number, props:
|
||||
return EmptyLoci;
|
||||
}
|
||||
|
||||
export function applySliceObjectLoci(loci: Volume.Loci, volume: Volume, groupCount: number, apply: (interval: Interval) => boolean) {
|
||||
if (Volume.isLociEmpty(loci) || !Volume.areEquivalent(loci.volume, volume)) return false;
|
||||
|
||||
if (Volume.isPeriodic(volume)) {
|
||||
return apply(Interval.ofBounds(0, groupCount));
|
||||
}
|
||||
|
||||
let changed = false;
|
||||
if (Interval.is(loci.instances)) {
|
||||
const start = Interval.start(loci.instances) * groupCount;
|
||||
const end = Interval.end(loci.instances) * groupCount;
|
||||
if (apply(Interval.ofBounds(start, end))) changed = true;
|
||||
} else {
|
||||
OrderedSet.forEach(loci.instances, instanceIndex => {
|
||||
const offset = instanceIndex * groupCount;
|
||||
if (apply(Interval.ofBounds(offset, offset + groupCount))) changed = true;
|
||||
});
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
export function applySlicePixelIntervals(indices: number[] | undefined, offset: number, apply: (interval: Interval) => boolean) {
|
||||
if (!indices || indices.length === 0) return false;
|
||||
|
||||
let changed = false;
|
||||
let start = indices[0] + offset;
|
||||
let prev = start;
|
||||
|
||||
for (let i = 1, il = indices.length; i < il; ++i) {
|
||||
const value = indices[i] + offset;
|
||||
if (value === prev + 1) {
|
||||
prev = value;
|
||||
continue;
|
||||
}
|
||||
if (apply(Interval.ofBounds(start, prev + 1))) changed = true;
|
||||
start = value;
|
||||
prev = value;
|
||||
}
|
||||
|
||||
if (apply(Interval.ofBounds(start, prev + 1))) changed = true;
|
||||
return changed;
|
||||
}
|
||||
|
||||
function eachSlice(loci: Loci, volume: Volume, key: number, props: SliceProps, apply: (interval: Interval) => boolean, image: Image) {
|
||||
const mapping = image.meta.mapping as SampledImageMapping;
|
||||
if (mapping) {
|
||||
@@ -582,24 +623,23 @@ function eachSlice(loci: Loci, volume: Volume, key: number, props: SliceProps, a
|
||||
const cellCount = volume.grid.cells.data.length;
|
||||
const isPeriodic = Volume.isPeriodic(volume);
|
||||
|
||||
const getIndices = isPeriodic
|
||||
? (instanceIndex: number, groupIndex: number) => {
|
||||
return mapping.index.get(cantorPairing(instanceIndex, groupIndex));
|
||||
}
|
||||
: (instanceIndex: number, groupIndex: number) => {
|
||||
const indices = mapping.index.get(groupIndex);
|
||||
return indices !== undefined ? indices.map(idx => idx + instanceIndex * groupCount) : undefined;
|
||||
};
|
||||
if (Volume.isLoci(loci)) {
|
||||
return applySliceObjectLoci(loci, volume, groupCount, apply);
|
||||
}
|
||||
|
||||
return eachVolumeLoci(loci, volume, undefined, (interval) => {
|
||||
let changed = false;
|
||||
for (let i = Interval.start(interval), il = Interval.end(interval); i < il; ++i) {
|
||||
const instanceIndex = Math.floor(i / cellCount);
|
||||
const groupIndex = i % cellCount;
|
||||
const vs = getIndices(instanceIndex, groupIndex);
|
||||
if (vs !== undefined) {
|
||||
for (const v of vs) {
|
||||
if (apply(Interval.ofSingleton(v))) changed = true;
|
||||
if (isPeriodic) {
|
||||
if (applySlicePixelIntervals(mapping.index.get(cantorPairing(instanceIndex, groupIndex)), 0, apply)) {
|
||||
changed = true;
|
||||
}
|
||||
} else {
|
||||
const offset = instanceIndex * groupCount;
|
||||
if (applySlicePixelIntervals(mapping.index.get(groupIndex), offset, apply)) {
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -654,4 +694,4 @@ export const SliceRepresentationProvider = VolumeRepresentationProvider({
|
||||
defaultColorTheme: { name: 'uniform' },
|
||||
defaultSizeTheme: { name: 'uniform' },
|
||||
isApplicable: (volume: Volume) => !Volume.isEmpty(volume) && !Volume.Segmentation.get(volume),
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user