Skip to content

Commit

Permalink
Merge pull request #1096 from silx-kit/tiles-update
Browse files Browse the repository at this point in the history
TiledHeatmapMesh: added supports of scene transforms
  • Loading branch information
t20100 authored May 10, 2022
2 parents 8a20458 + 3d3eb9a commit 13a6f00
Show file tree
Hide file tree
Showing 4 changed files with 224 additions and 107 deletions.
75 changes: 73 additions & 2 deletions apps/storybook/src/TiledHeatmapMesh.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
TilesApi,
ResetZoomButton,
SelectToZoom,
useAxisSystemContext,
} from '@h5web/lib';
import type {
Domain,
Expand All @@ -19,6 +20,7 @@ import type { Meta, Story } from '@storybook/react/types-6-0';
import { clamp } from 'lodash';
import ndarray from 'ndarray';
import type { NdArray } from 'ndarray';
import type { ReactNode } from 'react';
import { createFetchStore } from 'react-suspense-fetch';
import type { Vector2 } from 'three';

Expand Down Expand Up @@ -130,7 +132,7 @@ interface TiledHeatmapStoryProps extends TiledHeatmapMeshProps {
}

const Template: Story<TiledHeatmapStoryProps> = (args) => {
const { abscissaConfig, api, ordinateConfig, ...tiledHeatmapProps } = args;
const { abscissaConfig, ordinateConfig, ...tiledHeatmapProps } = args;

return (
<VisCanvas
Expand All @@ -145,7 +147,11 @@ const Template: Story<TiledHeatmapStoryProps> = (args) => {
<Zoom />
<SelectToZoom keepRatio modifierKey="Control" />
<ResetZoomButton />
<TiledHeatmapMesh api={api} {...tiledHeatmapProps} />
<group
scale={[abscissaConfig.flip ? -1 : 1, ordinateConfig.flip ? -1 : 1, 1]}
>
<TiledHeatmapMesh {...tiledHeatmapProps} />
</group>
</VisCanvas>
);
};
Expand Down Expand Up @@ -216,6 +222,71 @@ FlippedAxes.args = {
},
};

function LinearAxesGroup(props: { children: ReactNode }) {
const { children } = props;
const { abscissaConfig, ordinateConfig, visSize } = useAxisSystemContext();
const { width, height } = visSize;
const sx =
((abscissaConfig.flip ? -1 : 1) * width) /
(abscissaConfig.visDomain[1] - abscissaConfig.visDomain[0]);
const sy =
((ordinateConfig.flip ? -1 : 1) * height) /
(ordinateConfig.visDomain[1] - ordinateConfig.visDomain[0]);
const x = 0.5 * (abscissaConfig.visDomain[0] + abscissaConfig.visDomain[1]);
const y = 0.5 * (ordinateConfig.visDomain[0] + ordinateConfig.visDomain[1]);

return (
<group position={[-x * sx, -y * sy, 0]} scale={[sx, sy, 1]}>
{children}
</group>
);
}

export const WithTransforms: Story<TiledHeatmapStoryProps> = (args) => {
const { abscissaConfig, api, ordinateConfig, ...tiledHeatmapProps } = args;
const { baseLayerSize } = api;
const size = { width: 1, height: baseLayerSize.height / baseLayerSize.width };

return (
<VisCanvas
abscissaConfig={abscissaConfig}
ordinateConfig={ordinateConfig}
visRatio={Math.abs(
(abscissaConfig.visDomain[1] - abscissaConfig.visDomain[0]) /
(ordinateConfig.visDomain[1] - ordinateConfig.visDomain[0])
)}
>
<Pan />
<Zoom />
<SelectToZoom keepRatio modifierKey="Control" />
<ResetZoomButton />
<LinearAxesGroup>
<group position={[1, 1, 0]} rotation={[0, 0, Math.PI / 4]}>
<TiledHeatmapMesh api={api} {...tiledHeatmapProps} size={size} />
</group>
<group position={[-1, 1, 0]} scale={[2, 2, 1]}>
<TiledHeatmapMesh api={api} {...tiledHeatmapProps} size={size} />
</group>
</LinearAxesGroup>
</VisCanvas>
);
};
WithTransforms.args = {
api: halfMandelbrotApi,
abscissaConfig: {
visDomain: [-2, 1.5],
isIndexAxis: true,
showGrid: false,
flip: false,
},
ordinateConfig: {
visDomain: [0, 2],
isIndexAxis: true,
showGrid: false,
flip: false,
},
};

export default {
title: 'Experimental/TiledHeatmapMesh',
component: TiledHeatmapMesh,
Expand Down
77 changes: 52 additions & 25 deletions packages/lib/src/vis/tiles/TiledHeatmapMesh.tsx
Original file line number Diff line number Diff line change
@@ -1,71 +1,98 @@
import { useThree } from '@react-three/fiber';
import { clamp, range } from 'lodash';
import { useRef } from 'react';
import type { Object3D } from 'three';

import { getInterpolator } from '../heatmap/utils';
import { useCameraState } from '../hooks';
import type { Size } from '../models';
import { useAxisSystemContext } from '../shared/AxisSystemProvider';
import TiledLayer from './TiledLayer';
import type { TilesApi } from './api';
import type { ColorMapProps } from './models';
import { getScaledVisibleDomains } from './utils';
import {
getObject3DVisibleBox,
getObject3DPixelSize,
getNdcToObject3DMatrix,
scaleToLayer,
} from './utils';

interface Props extends ColorMapProps {
api: TilesApi;
displayLowerResolutions?: boolean;
qualityFactor?: number;
size?: Size;
}

function TiledHeatmapMesh(props: Props) {
const {
api,
displayLowerResolutions = true,
qualityFactor = 1, // 0: Lower quality, less fetch; 1: Best quality
size,
...colorMapProps
} = props;
const { baseLayerIndex, baseLayerSize } = api;

const canvasSize = useThree((state) => state.size);
const { visSize } = useAxisSystemContext();
const meshSize = size ?? visSize;

const { xVisibleDomain, yVisibleDomain } = useCameraState(
(...args) => getScaledVisibleDomains(...args, baseLayerSize),
[baseLayerSize]
);
const groupRef = useRef<Object3D>(null);

const itemsPerPixel = Math.max(
1,
Math.abs(xVisibleDomain[1] - xVisibleDomain[0]) / canvasSize.width,
Math.abs(yVisibleDomain[1] - yVisibleDomain[0]) / canvasSize.height
const ndcToLocalMatrix = useCameraState(
(camera) => getNdcToObject3DMatrix(camera, groupRef),
[]
);
const visibleBox = getObject3DVisibleBox(ndcToLocalMatrix);

const roundingOffset = 1 - clamp(qualityFactor, 0, 1);
const subsamplingLevel = Math.min(
Math.floor(Math.log2(itemsPerPixel) + roundingOffset),
baseLayerIndex
);
const currentLayerIndex = baseLayerIndex - subsamplingLevel;
const bounds = scaleToLayer(visibleBox, baseLayerSize, meshSize);

let layers: number[] = [];
if (!bounds.isEmpty()) {
const pixelSize = getObject3DPixelSize(ndcToLocalMatrix, canvasSize);
const dataPointsPerPixel = Math.max(
1,
(pixelSize.x / meshSize.width) * baseLayerSize.width,
(pixelSize.y / meshSize.height) * baseLayerSize.height
);

// displayLowerResolutions selects which levels of detail layers are displayed:
// true: lower resolution layers displayed behind the current one
// false: only current level of detail layer is displayed
const layers = displayLowerResolutions
? range(currentLayerIndex + 1)
: [currentLayerIndex];
const roundingOffset = 1 - clamp(qualityFactor, 0, 1);
const subsamplingLevel = Math.min(
Math.floor(Math.log2(dataPointsPerPixel) + roundingOffset),
baseLayerIndex
);
const currentLayerIndex = baseLayerIndex - subsamplingLevel;

// displayLowerResolutions selects which levels of detail layers are displayed:
// true: lower resolution layers displayed behind the current one
// false: only current level of detail layer is displayed
layers = displayLowerResolutions
? range(currentLayerIndex + 1)
: [currentLayerIndex];
}

const { colorMap, invertColorMap = false } = colorMapProps;

return (
<>
<group ref={groupRef}>
<mesh position={[0, 0, -0.1]}>
<planeGeometry args={[visSize.width, visSize.height]} />
<planeGeometry args={[meshSize.width, meshSize.height]} />
<meshBasicMaterial
color={getInterpolator(colorMap, invertColorMap)(0)}
/>
</mesh>
{layers.map((layer) => (
<TiledLayer key={layer} api={api} layer={layer} {...colorMapProps} />
<TiledLayer
key={layer}
api={api}
layer={layer}
meshSize={meshSize}
visibleBox={visibleBox}
{...colorMapProps}
/>
))}
</>
</group>
);
}

Expand Down
74 changes: 31 additions & 43 deletions packages/lib/src/vis/tiles/TiledLayer.tsx
Original file line number Diff line number Diff line change
@@ -1,69 +1,57 @@
import { sum } from 'lodash';
import { Suspense } from 'react';
import { LinearFilter, NearestFilter, Vector2 } from 'three';
import type { Box3 } from 'three';

import { useCameraState } from '../hooks';
import { useAxisSystemContext } from '../shared/AxisSystemProvider';
import type { Size } from '../models';
import Tile from './Tile';
import type { TilesApi } from './api';
import type { ColorMapProps } from './models';
import {
getTileOffsets,
getScaledVisibleDomains,
sortTilesByDistanceTo,
} from './utils';
import { getTileOffsets, scaleToLayer, sortTilesByDistanceTo } from './utils';

interface Props extends ColorMapProps {
api: TilesApi;
layer: number;
meshSize: Size;
visibleBox: Box3;
}

function TiledLayer(props: Props) {
const { api, layer, ...colorMapProps } = props;
const { api, layer, meshSize, visibleBox, ...colorMapProps } = props;

const { baseLayerIndex, numLayers, tileSize } = api;
const layerSize = api.layerSizes[layer];

const { abscissaConfig, ordinateConfig, visSize } = useAxisSystemContext();
const { xVisibleDomain, yVisibleDomain } = useCameraState(
(...args) => getScaledVisibleDomains(...args, layerSize),
[layerSize]
);

const tileOffsets = getTileOffsets(xVisibleDomain, yVisibleDomain, tileSize);
if (visibleBox.isEmpty()) {
return null;
}

const bounds = scaleToLayer(visibleBox, layerSize, meshSize);
const tileOffsets = getTileOffsets(bounds, tileSize);
// Sort tiles from closest to vis center to farthest away
const center = new Vector2(sum(xVisibleDomain) / 2, sum(yVisibleDomain) / 2);
sortTilesByDistanceTo(tileOffsets, tileSize, center);
sortTilesByDistanceTo(tileOffsets, tileSize, bounds.getCenter(new Vector2()));

return (
// Transforms to handle axes flip and use level of details layer array coordinates
// Transforms to use level of details layer array coordinates
<group
scale={[abscissaConfig.flip ? -1 : 1, ordinateConfig.flip ? -1 : 1, 1]}
position={[-meshSize.width / 2, -meshSize.height / 2, layer / numLayers]}
scale={[
meshSize.width / layerSize.width,
meshSize.height / layerSize.height,
1,
]}
>
<group
position={[-visSize.width / 2, -visSize.height / 2, layer / numLayers]}
scale={[
visSize.width / layerSize.width,
visSize.height / layerSize.height,
1,
]}
>
{tileOffsets.map((offset) => (
<Suspense key={`${offset.x},${offset.y}`} fallback={null}>
<Tile
api={api}
layer={layer}
x={offset.x}
y={offset.y}
{...colorMapProps}
magFilter={
layer === baseLayerIndex ? NearestFilter : LinearFilter
}
/>
</Suspense>
))}
</group>
{tileOffsets.map((offset) => (
<Suspense key={`${offset.x},${offset.y}`} fallback={null}>
<Tile
api={api}
layer={layer}
x={offset.x}
y={offset.y}
{...colorMapProps}
magFilter={layer === baseLayerIndex ? NearestFilter : LinearFilter}
/>
</Suspense>
))}
</group>
);
}
Expand Down
Loading

0 comments on commit 13a6f00

Please sign in to comment.