Optimize slice marking for hover

This commit is contained in:
Ludovic Autin
2026-04-12 21:53:08 -07:00
parent 31819dbf16
commit e7da6092aa
2 changed files with 133 additions and 15 deletions

View 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]]);
});
});

View File

@@ -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),
});
});