diff --git a/src/GlobalStates/PlotStore.ts b/src/GlobalStates/PlotStore.ts index def20607..c41ae727 100644 --- a/src/GlobalStates/PlotStore.ts +++ b/src/GlobalStates/PlotStore.ts @@ -67,6 +67,21 @@ type PlotState ={ camera: THREE.Camera | undefined; nativeCRS: string | undefined; destCRS: string | undefined; + colorScale: string; + logConstant: number; + lowclip: string; + highclip: string; + useLowclip: boolean; + useHighclip: boolean; + isCategorical: boolean; + categoricalMode: 'unique' | 'bins'; + numBins: number; + uniqueCategories: number[]; + + setIsCategorical: (isCategorical: boolean) => void; + setCategoricalMode: (categoricalMode: 'unique' | 'bins') => void; + setNumBins: (numBins: number) => void; + setUniqueCategories: (uniqueCategories: number[]) => void; setQuality: (quality: number) => void; setTimeScale: (timeScale : number) =>void; @@ -91,6 +106,12 @@ type PlotState ={ setAnimProg: (animProg: number) => void; setCOffset: (cOffset: number) => void; setCScale: (cScale: number) => void; + setColorScale: (colorScale: string) => void; + setLogConstant: (logConstant: number) => void; + setLowclip: (lowclip: string) => void; + setHighclip: (highclip: string) => void; + setUseLowclip: (useLowclip: boolean) => void; + setUseHighclip: (useHighclip: boolean) => void; setUseFragOpt: (useFragOpt: boolean) => void; setResetCamera: (resetCamera: boolean) => void; setUseCustomColor: (useCustomColor: boolean) => void; @@ -193,6 +214,21 @@ export const usePlotStore = create((set, get) => ({ camera: undefined, nativeCRS: undefined, destCRS: undefined, + colorScale: "identity", + logConstant: 1.0, + lowclip: "#000000", + highclip: "#ffffff", + useLowclip: false, + useHighclip: false, + isCategorical: false, + categoricalMode: 'bins', + numBins: 10, + uniqueCategories: [], + + setIsCategorical: (isCategorical) => set({ isCategorical }), + setCategoricalMode: (categoricalMode) => set({ categoricalMode }), + setNumBins: (numBins) => set({ numBins }), + setUniqueCategories: (uniqueCategories) => set({ uniqueCategories }), setVTransferRange: (vTransferRange) => set({ vTransferRange }), setVTransferScale: (vTransferScale) => set({ vTransferScale }), @@ -219,6 +255,12 @@ export const usePlotStore = create((set, get) => ({ setAnimProg: (animProg) => set({ animProg }), setCOffset: (cOffset) => set({ cOffset }), setCScale: (cScale) => set({ cScale }), + setColorScale: (colorScale) => set({ colorScale }), + setLogConstant: (logConstant) => set({ logConstant }), + setLowclip: (lowclip) => set({ lowclip }), + setHighclip: (highclip) => set({ highclip }), + setUseLowclip: (useLowclip) => set({ useLowclip }), + setUseHighclip: (useHighclip) => set({ useHighclip }), setUseFragOpt: (useFragOpt) => set({ useFragOpt }), setResetCamera: (resetCamera) => set({ resetCamera }), setUseCustomColor: (useCustomColor) => set({ useCustomColor }), diff --git a/src/__tests__/colorScale.test.ts b/src/__tests__/colorScale.test.ts new file mode 100644 index 00000000..cd4a09de --- /dev/null +++ b/src/__tests__/colorScale.test.ts @@ -0,0 +1,163 @@ +import { describe, it, expect } from 'vitest'; +import { applyColorScale, colorScaleToId, exprToGLSL } from '../components/textures/colormap'; + +describe('Color Scale Options', () => { + describe('1. Linear Scale: identity', () => { + it('returns original normalized x directly', () => { + expect(applyColorScale(0.0, 'identity')).toBe(0.0); + expect(applyColorScale(0.25, 'identity')).toBe(0.25); + expect(applyColorScale(0.5, 'identity')).toBe(0.5); + expect(applyColorScale(1.0, 'identity')).toBe(1.0); + }); + }); + + describe('2. Log Scale: log(x)', () => { + it('Test 1: log(x) on range (1.0, 10.0)', () => { + const minVal = 1.0; + const maxVal = 10.0; + const dataRange = maxVal - minVal; + const n = 10; + + const logA = Math.log10(minVal); + const logB = Math.log10(maxVal); + const dataPoints = Array.from({ length: n }, (_, i) => { + const logVal = logA + (i / (n - 1)) * (logB - logA); + return Math.pow(10, logVal); + }); + + const expectedPositions = Array.from({ length: n }, (_, i) => i / (n - 1)); + + dataPoints.forEach((d, i) => { + const x = (d - minVal) / dataRange; + const pos = applyColorScale(x, 'log(x)', 1.0, 0.0001, dataRange, minVal); + expect(pos).toBeCloseTo(expectedPositions[i], 4); + }); + }); + + it('Test 2: log(x) on range (0.001, 1.0)', () => { + const minVal = 0.001; + const maxVal = 1.0; + const dataRange = maxVal - minVal; + const n = 10; + + const logA = Math.log10(minVal); + const logB = Math.log10(maxVal); + const dataPoints = Array.from({ length: n }, (_, i) => { + const logVal = logA + (i / (n - 1)) * (logB - logA); + return Math.pow(10, logVal); + }); + + const expectedPositions = Array.from({ length: n }, (_, i) => i / (n - 1)); + + dataPoints.forEach((d, i) => { + const x = (d - minVal) / dataRange; + const pos = applyColorScale(x, 'log(x)', 1.0, 0.0001, dataRange, minVal); + expect(pos).toBeCloseTo(expectedPositions[i], 4); + }); + }); + + it('Test 3: log(x) on range (0.0, 1000.0) with zero clipping', () => { + const minVal = 0.0; + const maxVal = 1000.0; + const dataRange = maxVal - minVal; + const logEps = 0.001; // v_pos_min = 1.0 + + expect(applyColorScale(0.0, 'log(x)', 1.0, logEps, dataRange, minVal)).toBe(0.0); + expect(applyColorScale(0.001, 'log(x)', 1.0, logEps, dataRange, minVal)).toBeCloseTo(0.0, 4); + expect(applyColorScale(0.01, 'log(x)', 1.0, logEps, dataRange, minVal)).toBeCloseTo(0.3333, 3); + expect(applyColorScale(0.1, 'log(x)', 1.0, logEps, dataRange, minVal)).toBeCloseTo(0.6667, 3); + expect(applyColorScale(1.0, 'log(x)', 1.0, logEps, dataRange, minVal)).toBeCloseTo(1.0, 4); + }); + }); + + describe('3. Standard Log Offset: log(1+x)', () => { + it('expands lower values across the range (0.0, 1000.0)', () => { + const dataRange = 1000.0; + + expect(applyColorScale(0.0, 'log(1+x)', 1.0, 0.0001, dataRange)).toBe(0.0); + expect(applyColorScale(0.01, 'log(1+x)', 1.0, 0.0001, dataRange)).toBeCloseTo(0.3472, 3); + expect(applyColorScale(0.1, 'log(1+x)', 1.0, 0.0001, dataRange)).toBeCloseTo(0.6680, 3); + expect(applyColorScale(1.0, 'log(1+x)', 1.0, 0.0001, dataRange)).toBeCloseTo(1.0, 4); + }); + }); + + describe('4. Custom Constant Log Offset: log(x+c)', () => { + it('matches log(1+x) when c = 1.0', () => { + const dataRange = 1000.0; + + expect(applyColorScale(0.0, 'log(x+c)', 1.0, 0.0001, dataRange)).toBe(0.0); + expect(applyColorScale(0.01, 'log(x+c)', 1.0, 0.0001, dataRange)).toBeCloseTo(0.3472, 3); + expect(applyColorScale(1.0, 'log(x+c)', 1.0, 0.0001, dataRange)).toBeCloseTo(1.0, 4); + }); + + it('expands lower values more aggressively when c = 0.1', () => { + const dataRange = 1000.0; + const c = 0.1; + + expect(applyColorScale(0.0, 'log(x+c)', c, 0.0001, dataRange)).toBe(0.0); + expect(applyColorScale(0.01, 'log(x+c)', c, 0.0001, dataRange)).toBeCloseTo(0.5010, 3); + expect(applyColorScale(1.0, 'log(x+c)', c, 0.0001, dataRange)).toBeCloseTo(1.0, 4); + }); + }); + + describe('5. Sign-Preserving Sqrt: sign(x)*sqrt(abs(x))', () => { + it('expands lower-end values scale-invariantly', () => { + expect(applyColorScale(0.0, 'sign(x)*sqrt(abs(x))')).toBe(0.0); + expect(applyColorScale(0.01, 'sign(x)*sqrt(abs(x))')).toBeCloseTo(0.10, 4); + expect(applyColorScale(0.25, 'sign(x)*sqrt(abs(x))')).toBeCloseTo(0.50, 4); + expect(applyColorScale(1.0, 'sign(x)*sqrt(abs(x))')).toBeCloseTo(1.0, 4); + }); + + it('preserves negative signs for symmetric variables', () => { + expect(applyColorScale(-0.01, 'sign(x)*sqrt(abs(x))')).toBeCloseTo(-0.10, 4); + expect(applyColorScale(-0.25, 'sign(x)*sqrt(abs(x))')).toBeCloseTo(-0.50, 4); + }); + }); + + describe('6. Exponential Transform: exp(x)/100', () => { + it('maps log-space input back to linear colormap coordinates', () => { + const dataRange = 5.0; + + expect(applyColorScale(0.0, 'exp(x)/100', 1.0, 0.0001, dataRange)).toBe(0.0); + expect(applyColorScale(0.5, 'exp(x)/100', 1.0, 0.0001, dataRange)).toBeCloseTo(0.0758, 3); + expect(applyColorScale(1.0, 'exp(x)/100', 1.0, 0.0001, dataRange)).toBeCloseTo(1.0, 4); + }); + }); + + describe('7. Generic Custom Expressions', () => { + it('evaluates piecewise ternary "x > 0 ? x/2 : x"', () => { + const expr = 'x > 0 ? x/2 : x'; + expect(colorScaleToId(expr)).toBe(6); + expect(applyColorScale(0.0, expr)).toBe(0.0); + expect(applyColorScale(0.5, expr)).toBeCloseTo(0.5, 4); + expect(applyColorScale(1.0, expr)).toBe(1.0); + }); + + it('evaluates piecewise ternary "x > 0 ? x*2 : x"', () => { + const expr = 'x > 0 ? x*2 : x'; + expect(colorScaleToId(expr)).toBe(6); + expect(applyColorScale(0.0, expr)).toBe(0.0); + expect(applyColorScale(0.5, expr)).toBeCloseTo(0.5, 4); + expect(applyColorScale(1.0, expr)).toBe(1.0); + }); + + it('evaluates offset expression "x + 10"', () => { + const expr = 'x + 10'; + expect(applyColorScale(0.0, expr)).toBe(0.0); + expect(applyColorScale(0.5, expr)).toBe(0.5); + expect(applyColorScale(1.0, expr)).toBe(1.0); + }); + + it('evaluates power expression "x * x"', () => { + const expr = 'x * x'; + expect(applyColorScale(0.0, expr)).toBe(0.0); + expect(applyColorScale(0.5, expr)).toBe(0.25); + expect(applyColorScale(1.0, expr)).toBe(1.0); + }); + + it('converts JS expressions to float-safe GLSL code', () => { + expect(exprToGLSL('x > 0 ? x/2 : x')).toBe('(val) > 0.0 ? (val)/2.0 : (val)'); + expect(exprToGLSL('x + 10')).toBe('(val) + 10.0'); + }); + }); +}); diff --git a/src/components/computation/shaders/frag.glsl b/src/components/computation/shaders/frag.glsl index 527b1fd9..7de2b62c 100644 --- a/src/components/computation/shaders/frag.glsl +++ b/src/components/computation/shaders/frag.glsl @@ -1,19 +1,35 @@ -uniform sampler2D data; -uniform sampler2D cmap; - -uniform float cOffset; -uniform float cScale; - -in vec2 vUv; -out vec4 Color; - -void main() { - vec4 val = texture(data,vUv); - float d = val.x; - float sampLoc = d == 1. ? d : (d - 0.5)*cScale + 0.5; - sampLoc = d == 1. ? d : min(sampLoc+cOffset,0.99); - vec4 color = texture(cmap, vec2(sampLoc,0.5)); - color.a = val.x > 0.999 ? 0. : 1.; - - Color = color; +uniform sampler2D data; +uniform sampler2D cmap; + +uniform float cOffset; +uniform float cScale; +uniform vec2 threshold; +uniform int colorScale; +uniform float logConstant; +uniform float logEps; +uniform float dataRange; +uniform float minVal; +uniform vec4 lowclip; +uniform vec4 highclip; +uniform bool useLowclip; +uniform bool useHighclip; + +in vec2 vUv; +out vec4 Color; + +// APPLY_COLOR_SCALE + +void main() { + vec4 val = texture(data, vUv); + float d = val.x; + if (d >= 0.999) { + Color = vec4(0.0); + return; + } + + Color = evaluateColorScale( + d, threshold, 0.999, vec3(0.0), 0.0, + cmap, cScale, cOffset, colorScale, logConstant, logEps, + dataRange, minVal, lowclip, highclip, useLowclip, useHighclip + ); } \ No newline at end of file diff --git a/src/components/computation/shaders/index.ts b/src/components/computation/shaders/index.ts index 9f4349cb..cadcaa66 100644 --- a/src/components/computation/shaders/index.ts +++ b/src/components/computation/shaders/index.ts @@ -1,16 +1,20 @@ -import MaxFrag from './Max.glsl' -import MinFrag from './Min.glsl' -import MeanFrag from './Mean.glsl' -import StDevFrag from './StDev.glsl' -import vertShader from './vert.glsl' -import fragShader from './frag.glsl' -import correlateFrag from './Correlation.glsl' -export { - MaxFrag, - MinFrag, - MeanFrag, - StDevFrag, - vertShader, - fragShader, - correlateFrag +import colorPipelineChunk from '@/components/textures/shaders/colorPipeline.glsl'; +import MaxFrag from './Max.glsl' +import MinFrag from './Min.glsl' +import MeanFrag from './Mean.glsl' +import StDevFrag from './StDev.glsl' +import vertShader from './vert.glsl' +import fragShaderRaw from './frag.glsl' +import correlateFrag from './Correlation.glsl' + +const fragShader = fragShaderRaw.replace('// APPLY_COLOR_SCALE', colorPipelineChunk); + +export { + MaxFrag, + MinFrag, + MeanFrag, + StDevFrag, + vertShader, + fragShader, + correlateFrag } \ No newline at end of file diff --git a/src/components/plots/AnalysisWG.tsx b/src/components/plots/AnalysisWG.tsx index d7bccbbf..8dfed432 100644 --- a/src/components/plots/AnalysisWG.tsx +++ b/src/components/plots/AnalysisWG.tsx @@ -154,16 +154,20 @@ const AnalysisWG = ({ setTexture, }: { setTexture: React.Dispatch currentOperation.includes(op)); const isCorrelation = currentOperation.includes('Correlation'); if (needsRescale) { if (!valueScalesOrig) setValueScalesOrig(valueScales); - [minVal, maxVal] = ArrayMinMax(newArray); + const [calculatedMin, calculatedMax] = ArrayMinMax(newArray); + minVal = calculatedMin ?? valueScales.minVal; + maxVal = calculatedMax ?? valueScales.maxVal; } else if (isCorrelation) { if (!valueScalesOrig) setValueScalesOrig(valueScales); - [minVal, maxVal] = [-1, 1]; + minVal = -1; + maxVal = 1; } else { ({ minVal, maxVal } = valueScales); } diff --git a/src/components/plots/DataCube.tsx b/src/components/plots/DataCube.tsx index 0a787b8b..a7768d12 100644 --- a/src/components/plots/DataCube.tsx +++ b/src/components/plots/DataCube.tsx @@ -1,15 +1,14 @@ import { useEffect, useMemo, useRef } from 'react' import * as THREE from 'three' import { vertexShader, fragmentShader, fragOpt, orthoVertex } from '@/components/textures/shaders'; -import { useGlobalStore } from '@/GlobalStates/GlobalStore'; import { usePlotStore } from '@/GlobalStates/PlotStore'; +import { colorScaleToId, exprToGLSL } from '@/components/textures'; import { useShallow } from 'zustand/shallow'; import { invalidate, useFrame } from '@react-three/fiber'; -import { deg2rad } from '@/utils/HelperFuncs'; -import { useCoordBounds } from '@/hooks/useCoordBounds'; import { UVCube } from '@/components/plots' import { ColumnMeshes } from './TransectMeshes'; import { usePaddedTextures } from '@/hooks/usePaddedTextures'; +import { createCommonUniforms, updateCommonUniforms, useCommonPlotState } from '@/utils/plotUniforms'; interface DataCubeProps { volTexture: THREE.Data3DTexture[] | THREE.DataTexture[] | null, @@ -17,66 +16,40 @@ interface DataCubeProps { export const DataCube = ({ volTexture: propVolTexture }: DataCubeProps ) => { const volTexture = usePaddedTextures(propVolTexture); - const {shape, colormap, flipY, textureArrayDepths, remapTexture} = useGlobalStore(useShallow(state=>({ - shape:state.shape, - colormap:state.colormap, - flipY:state.flipY, - textureArrayDepths: state.textureArrayDepths, - remapTexture: state.remapTexture - }))) //We have to useShallow when returning an object instead of a state. I don't fully know the logic yet - const { - valueRange, xRange, yRange, zRange, quality, useOrtho, - animProg, cScale, cOffset, useFragOpt, transparency, maskTexture, maskValue, - nanTransparency, nanColor, vTransferRange, vTransferScale, fillValue} = usePlotStore(useShallow(state => ({ - valueRange: state.valueRange, xRange: state.xRange, - yRange: state.yRange, zRange: state.zRange, - quality: state.quality, useOrtho: state.useOrtho, - animProg: state.animProg, cScale: state.cScale, - cOffset: state.cOffset, useFragOpt: state.useFragOpt, - transparency: state.transparency, - maskTexture: state.maskTexture, - maskValue: state.maskValue, - nanTransparency: state.nanTransparency, - nanColor: state.nanColor, - vTransferRange: state.vTransferRange, - vTransferScale: state.vTransferScale, - fillValue: state.fillValue, + const commonState = useCommonPlotState(); + const { colormap, valueScales, flipY, shape, textureArrayDepths, remapTexture, + animProg, cOffset, cScale, nanColor, nanTransparency, fillValue, valueRange, maskTexture, maskValue, + colorScale, logConstant, lowclip, highclip, useLowclip, useHighclip, latBounds, lonBounds } = commonState; + + const { xRange, yRange, zRange, quality, useOrtho, useFragOpt, transparency, vTransferRange, vTransferScale } = usePlotStore(useShallow(state => ({ + xRange: state.xRange, yRange: state.yRange, zRange: state.zRange, + quality: state.quality, useOrtho: state.useOrtho, useFragOpt: state.useFragOpt, + transparency: state.transparency, vTransferRange: state.vTransferRange, vTransferScale: state.vTransferScale }))) const meshRef = useRef(null!); const aspectRatio = shape.y/shape.x const timeRatio = shape.z/shape.x; - const {lonBounds, latBounds} = useCoordBounds() const shaderMaterial = useMemo(()=>new THREE.ShaderMaterial({ glslVersion: THREE.GLSL3, uniforms: { + ...createCommonUniforms(commonState), modelViewMatrixInverse: { value: new THREE.Matrix4() }, // Used for Orthographic RayMarcher map: { value: volTexture}, - maskTexture: { value: maskTexture }, - maskValue: {value: maskValue }, textureDepths: {value: new THREE.Vector3(textureArrayDepths[2], textureArrayDepths[1], textureArrayDepths[0])}, - cmap:{value: colormap}, remapTexture: { value: remapTexture}, - cOffset:{value: cOffset}, - cScale: {value: cScale}, - threshold: {value: new THREE.Vector2(valueRange[0],valueRange[1])}, scale: {value: shape}, flatBounds:{value: new THREE.Vector4(-xRange[1],-xRange[0],zRange[0] * timeRatio, zRange[1] * timeRatio)}, vertBounds:{value: new THREE.Vector2(yRange[0]*aspectRatio,yRange[1]*aspectRatio)}, - latBounds: {value: new THREE.Vector2(deg2rad(latBounds[0]), deg2rad(latBounds[1]))}, - lonBounds: {value: new THREE.Vector2(deg2rad(lonBounds[0]), deg2rad(lonBounds[1]))}, steps: { value: quality }, - animateProg: {value: animProg}, transparency: {value: transparency}, opacityMag: {value: vTransferScale}, useClipScale: {value: vTransferRange}, - nanAlpha: {value: 1-nanTransparency}, - nanColor: {value: new THREE.Color(nanColor)}, - fillValue: {value: fillValue?? NaN} }, defines: { USE_VORIGIN: 1, USE_VDIRECTION: 1, - ...(remapTexture ? { REPROJECT: true } : {}) + ...(remapTexture ? { REPROJECT: true } : {}), + 'CUSTOM_EXPR(val)': colorScaleToId(colorScale) === 6 ? exprToGLSL(colorScale) : '(val)', }, vertexShader: useOrtho ? orthoVertex : vertexShader, fragmentShader: useFragOpt ? fragOpt : fragmentShader, @@ -89,28 +62,24 @@ export const DataCube = ({ volTexture: propVolTexture }: DataCubeProps ) => { const geometry = useMemo(() => new THREE.BoxGeometry(shape.x, shape.y, shape.z), [shape]); useEffect(() => { if (shaderMaterial) { + updateCommonUniforms(shaderMaterial, commonState); const uniforms = shaderMaterial.uniforms - uniforms.cmap.value = colormap; - uniforms.cOffset.value = cOffset; - uniforms.cScale.value = cScale; - uniforms.threshold.value.set(valueRange[0], valueRange[1]); uniforms.scale.value = shape; uniforms.flatBounds.value.set(-xRange[1], -xRange[0], zRange[0] * timeRatio, zRange[1] * timeRatio); uniforms.vertBounds.value.set(yRange[0] * aspectRatio, yRange[1] * aspectRatio); - uniforms.latBounds.value = new THREE.Vector2(deg2rad(latBounds[0]), deg2rad(latBounds[1])) - uniforms.lonBounds.value = new THREE.Vector2(deg2rad(lonBounds[0]), deg2rad(lonBounds[1])) uniforms.steps.value = quality; - uniforms.animateProg.value = animProg; uniforms.transparency.value = transparency; - uniforms.nanAlpha.value = 1 - nanTransparency; - uniforms.nanColor.value.set(nanColor); uniforms.opacityMag.value = vTransferScale; uniforms.useClipScale.value = vTransferRange; - uniforms.fillValue.value = fillValue?? NaN; - uniforms.maskValue.value = maskValue - invalidate() // Needed because Won't trigger re-render if camera is stationary. + const scaleId = colorScaleToId(colorScale); + const customDef = scaleId === 6 ? exprToGLSL(colorScale) : '(val)'; + if (shaderMaterial.defines['CUSTOM_EXPR(val)'] !== customDef) { + shaderMaterial.defines['CUSTOM_EXPR(val)'] = customDef; + shaderMaterial.needsUpdate = true; + } + invalidate(); // Needed because Won't trigger re-render if camera is stationary. } - }, [shape, colormap, cOffset, cScale, valueRange, xRange, yRange, zRange, aspectRatio, latBounds, lonBounds, quality, animProg, transparency, nanTransparency, nanColor, maskValue, fillValue, vTransferScale, vTransferRange]); + }, [colormap, cOffset, cScale, valueRange, shape, xRange, timeRatio, yRange, aspectRatio, quality, animProg, transparency, nanTransparency, nanColor, vTransferScale, vTransferRange, fillValue, maskValue, latBounds, lonBounds, colorScale, logConstant, valueScales, lowclip, highclip, useLowclip, useHighclip]); useFrame(({camera})=>{ // This calculates InverseModel matrix for the orthographic raymarcher if (!useOrtho || !meshRef.current || !shaderMaterial) return; meshRef.current.modelViewMatrix.multiplyMatrices(camera.matrixWorldInverse, meshRef.current.matrixWorld); diff --git a/src/components/plots/FlatBlocks.tsx b/src/components/plots/FlatBlocks.tsx index f527a86d..2bcfb586 100644 --- a/src/components/plots/FlatBlocks.tsx +++ b/src/components/plots/FlatBlocks.tsx @@ -1,37 +1,28 @@ import React, { useEffect, useMemo } from 'react' import { useAnalysisStore } from '@/GlobalStates/AnalysisStore'; -import { useGlobalStore } from '@/GlobalStates/GlobalStore'; import { usePlotStore } from '@/GlobalStates/PlotStore'; import { useErrorStore } from '@/GlobalStates/ErrorStore'; import { useShallow } from 'zustand/shallow' import * as THREE from 'three' import { flatBlocksVert, sphereBlocksFrag } from '../textures/shaders' import { invalidate } from '@react-three/fiber' -import { deg2rad } from '@/utils/HelperFuncs' -import { useCoordBounds } from '@/hooks/useCoordBounds' import { usePaddedTextures } from '@/hooks/usePaddedTextures'; import { useAxisIndices } from '@/hooks'; +import { colorScaleToId, exprToGLSL } from '@/components/textures'; + +import { createCommonUniforms, updateCommonUniforms, useCommonPlotState } from '@/utils/plotUniforms'; const FlatBlocks = ({textures: propTextures} : {textures: THREE.Data3DTexture[] | THREE.DataTexture[] | null}) => { const textures = usePaddedTextures(propTextures); - const {colormap, isFlat, valueScales, flipY, - dataShape, textureArrayDepths, axisDimArrays, remapTexture} = useGlobalStore(useShallow(state=>({ - colormap: state.colormap, - isFlat: state.isFlat, - valueScales: state.valueScales, - flipY: state.flipY, - dataShape: state.dataShape, - textureArrayDepths: state.textureArrayDepths, - axisDimArrays: state.axisDimArrays, - remapTexture: state.remapTexture - }))) - const { animProg, cOffset, cScale, nanColor, nanTransparency, displacement, fillValue, valueRange, offsetNegatives, rotateFlat, maskTexture, maskValue, - } = usePlotStore(useShallow(state=> ({ - animate: state.animate, animProg: state.animProg, cOffset: state.cOffset, - cScale: state.cScale, nanColor: state.nanColor, nanTransparency: state.nanTransparency, - displacement: state.displacement, valueRange:state.valueRange, sphereResolution: state.sphereResolution, - offsetNegatives: state.offsetNegatives, rotateFlat:state.rotateFlat, - maskTexture:state.maskTexture, maskValue:state.maskValue, fillValue:state.fillValue, + const commonState = useCommonPlotState(); + const { colormap, isFlat, valueScales, flipY, dataShape, textureArrayDepths, axisDimArrays, remapTexture, + animProg, cOffset, cScale, nanColor, nanTransparency, fillValue, valueRange, maskTexture, maskValue, + colorScale, logConstant, lowclip, highclip, useLowclip, useHighclip, latBounds, lonBounds } = commonState; + + const { displacement, offsetNegatives, rotateFlat } = usePlotStore(useShallow(state => ({ + displacement: state.displacement, + offsetNegatives: state.offsetNegatives, + rotateFlat: state.rotateFlat, }))) const {analysisMode, axis} = useAnalysisStore(useShallow(state => ({ analysisMode: state.analysisMode, axis:state.axis @@ -80,33 +71,23 @@ const FlatBlocks = ({textures: propTextures} : {textures: THREE.Data3DTexture[] ); return geo },[width, height]) - const {lonBounds, latBounds} = useCoordBounds() + const shaderMaterial = useMemo(()=>{ const shader = new THREE.ShaderMaterial({ glslVersion: THREE.GLSL3, uniforms: { + ...createCommonUniforms(commonState), map: { value: textures }, remapTexture: { value: remapTexture }, - maskTexture: {value: maskTexture}, - maskValue: {value: maskValue}, - threshold: {value: new THREE.Vector2(valueRange[0],valueRange[1])}, - latBounds: {value: new THREE.Vector2(deg2rad(latBounds[0]), deg2rad(latBounds[1]))}, - lonBounds: {value: new THREE.Vector2(deg2rad(lonBounds[0]), deg2rad(lonBounds[1]))}, aspect: {value: width/height}, textureDepths: {value: new THREE.Vector3(textureArrayDepths[2], textureArrayDepths[1], textureArrayDepths[0])}, - cmap:{value: colormap}, - cOffset:{value: cOffset}, - cScale: {value: cScale}, - animateProg: {value: animProg}, - nanColor: {value: new THREE.Color(nanColor)}, - nanAlpha: {value: 1 - nanTransparency}, displaceZero: {value: offsetNegatives ? 0 : (-valueScales.minVal/(valueScales.maxVal-valueScales.minVal)) }, displacement: {value: displacement}, - fillValue: {value: fillValue?? NaN}, }, defines:{ ...(isFlat ? { IS_FLAT: true } : {}), - ...(remapTexture ? { REPROJECT: true } : {}) + ...(remapTexture ? { REPROJECT: true } : {}), + 'CUSTOM_EXPR(val)': colorScaleToId(colorScale) === 6 ? exprToGLSL(colorScale) : '(val)', }, vertexShader: flatBlocksVert, fragmentShader: sphereBlocksFrag, @@ -121,22 +102,13 @@ const FlatBlocks = ({textures: propTextures} : {textures: THREE.Data3DTexture[] if (shaderMaterial){ const uniforms = shaderMaterial.uniforms; uniforms.map.value = textures; - uniforms.animateProg.value = animProg - uniforms.displaceZero.value = -valueScales.minVal/(valueScales.maxVal-valueScales.minVal) - uniforms.displacement.value = displacement - uniforms.cmap.value = colormap - uniforms.cOffset.value = cOffset - uniforms.cScale.value = cScale - uniforms.threshold.value.set(valueRange[0], valueRange[1]) - uniforms.latBounds.value = new THREE.Vector2(deg2rad(latBounds[0]), deg2rad(latBounds[1])) - uniforms.lonBounds.value = new THREE.Vector2(deg2rad(lonBounds[0]), deg2rad(lonBounds[1])) + updateCommonUniforms(shaderMaterial, commonState); uniforms.displaceZero.value = offsetNegatives ? 0 : (-valueScales.minVal/(valueScales.maxVal-valueScales.minVal)) + uniforms.displacement.value = displacement uniforms.aspect.value = width/height; - uniforms.maskValue.value = maskValue; - uniforms.fillValue.value = fillValue?? NaN; } invalidate(); - },[animProg, valueScales, displacement, colormap, cScale, cOffset, offsetNegatives, valueRange, textures, fillValue, analysisMode, axis, width, height, latBounds, lonBounds, maskValue]) + },[animProg, valueScales, displacement, colormap, cScale, cOffset, offsetNegatives, valueRange, textures, fillValue, analysisMode, axis, width, height, latBounds, lonBounds, maskValue, colorScale, logConstant, lowclip, highclip, useLowclip, useHighclip]) return ( diff --git a/src/components/plots/FlatMap.tsx b/src/components/plots/FlatMap.tsx index de2ff35c..0c73034a 100644 --- a/src/components/plots/FlatMap.tsx +++ b/src/components/plots/FlatMap.tsx @@ -1,268 +1,242 @@ -"use client"; - -import React, {useMemo, useEffect, useRef} from 'react' -import * as THREE from 'three' -import { useAnalysisStore } from '@/GlobalStates/AnalysisStore'; -import { useGlobalStore } from '@/GlobalStates/GlobalStore'; -import { usePlotStore } from '@/GlobalStates/PlotStore'; -import { useZarrStore } from '@/GlobalStates/ZarrStore'; -import { vertShader } from '@/components/computation/shaders' -import { useShallow } from 'zustand/shallow' -import { ThreeEvent } from '@react-three/fiber'; -import { coarsenFlatArray, GetCurrentArray, GetTimeSeries, parseUVCoords, deg2rad } from '@/utils/HelperFuncs'; -import { sampleCRS } from '../textures/ProjectionTexture'; -import { evaluateColorMap } from '@/components/textures'; -import { useCoordBounds } from '@/hooks/useCoordBounds'; -import { flatFrag } from '../textures/shaders'; -import { SquareMeshes } from './TransectMeshes'; -import { usePaddedTextures } from '@/hooks/usePaddedTextures'; -import { useAxisIndices } from '@/hooks'; -interface InfoSettersProps{ - setLoc: React.Dispatch>; - setShowInfo: React.Dispatch>; - val: React.RefObject; - coords: React.RefObject; -} - -const FlatMap = ({textures: propTextures, infoSetters} : {textures : THREE.DataTexture[] | THREE.Data3DTexture[], infoSetters : InfoSettersProps}) => { - const textures = usePaddedTextures(propTextures); - const {setLoc, setShowInfo, val, coords} = infoSetters; - const {flipY, colormap, dimArrays, dimNames, dimUnits, - isFlat, dataShape, textureArrayDepths, strides, remapTexture, shape, - setPlotDim,updateDimCoords, updateTimeSeries} = useGlobalStore(useShallow(state => ({ - flipY: state.flipY, colormap: state.colormap, - dimArrays: state.dimArrays, strides: state.strides, - dimNames:state.dimNames, dimUnits: state.dimUnits, - isFlat: state.isFlat, dataShape: state.dataShape, - textureArrayDepths: state.textureArrayDepths, - remapTexture:state.remapTexture, shape: state.shape, - setPlotDim:state.setPlotDim, - updateDimCoords:state.updateDimCoords, - updateTimeSeries: state.updateTimeSeries - }))) - - const {cScale, cOffset, animProg, nanTransparency, nanColor, - zSlice, ySlice, xSlice, selectTS, fillValue, coarsen, maskTexture, maskValue, valueRange, - getColorIdx, incrementColorIdx} = usePlotStore(useShallow(state => ({ - cOffset: state.cOffset, cScale: state.cScale, - resetAnim: state.resetAnim, animate: state.animate, - animProg: state.animProg, nanTransparency: state.nanTransparency, - nanColor: state.nanColor, zSlice: state.zSlice, - ySlice: state.ySlice, xSlice: state.xSlice, valueRange:state.valueRange, - selectTS: state.selectTS, coarsen: state.coarsen, - maskTexture:state.maskTexture, maskValue:state.maskValue, fillValue: state.fillValue, - getColorIdx: state.getColorIdx, - incrementColorIdx: state.incrementColorIdx - }))) - const {axis, analysisMode, analysisArray} = useAnalysisStore(useShallow(state=> ({ - axis: state.axis, - analysisMode: state.analysisMode, - analysisArray: state.analysisArray - }))) - const {kernelSize, kernelDepth} = useZarrStore(useShallow(state => ({ - kernelSize: state.kernelSize, - kernelDepth: state.kernelDepth, - }))) - - const {xIdx, yIdx, zIdx} = useAxisIndices() - - const dimSlices = useMemo (() => { - let slices = isFlat - ? [ - dimArrays[yIdx]?.slice(ySlice[0], ySlice[1] ? ySlice[1] : undefined) ?? [], - dimArrays[xIdx]?.slice(xSlice[0], xSlice[1] ? xSlice[1] : undefined) ?? [], - ] - : [ - dimArrays[zIdx]?.slice(zSlice[0], zSlice[1] ? zSlice[1] : undefined) ?? [], - dimArrays[yIdx]?.slice(ySlice[0], ySlice[1] ? ySlice[1] : undefined) ?? [], - dimArrays[xIdx]?.slice(xSlice[0], xSlice[1] ? xSlice[1] : undefined ) ?? [], - ] - if (coarsen) slices = slices.map((val, idx) => coarsenFlatArray(val, (idx === 0 && slices.length > 2 ? kernelDepth : kernelSize))) - return slices - } ,[dimArrays, zSlice, ySlice, xSlice, coarsen, kernelDepth, kernelSize, xIdx, yIdx, zIdx]) - const shapeRatio = useMemo(()=> { - if (dataShape.length == 2){ - return shape.y/shape.x - } else if (analysisMode){ - const thisShape = dataShape.filter((_val, idx) => idx != axis) - return thisShape[0]/thisShape[1] - } else { - return shape.y/shape.x - } - }, [axis, shape, dataShape, analysisMode] ) - - const geometry = useMemo(()=>new THREE.PlaneGeometry(2,2*shapeRatio),[shapeRatio]) - const infoRef = useRef(false) - const rotateMap = analysisMode && axis == 2; - const sampleArray = useMemo(()=> analysisMode ? analysisArray : GetCurrentArray(),[analysisMode, analysisArray, textures]) - const analysisDims = useMemo(() => { - if (!analysisMode) return dimSlices; - const fullSlices = [ - dimArrays[zIdx]?.slice(zSlice[0], zSlice[1] ? zSlice[1] : undefined) ?? [], - dimArrays[yIdx]?.slice(ySlice[0], ySlice[1] ? ySlice[1] : undefined) ?? [], - dimArrays[xIdx]?.slice(xSlice[0], xSlice[1] ? xSlice[1] : undefined) ?? [], - ]; - let slices = fullSlices.filter((_, idx) => idx !== axis); - if (coarsen) slices = slices.map((val, idx) => coarsenFlatArray(val, (idx === 0 && slices.length > 2 ? kernelDepth : kernelSize))) - return slices; - }, [analysisMode, dimSlices, dimArrays, zSlice, ySlice, xSlice, axis, coarsen, kernelDepth, kernelSize, xIdx, yIdx, zIdx]) - - const {lonBounds, latBounds} = useCoordBounds() - - useEffect(()=>{ - geometry.dispose() - },[geometry]) - - // ----- MOUSE MOVE ----- // - const eventRef = useRef | null>(null); - const handleMove = (e: ThreeEvent) => { - if (infoRef.current && e.uv) { - let {uv} = e; - if (!uv) return; - setLoc([e.clientX, e.clientY]); - eventRef.current = e; - if (remapTexture){ - const [thisUV, isValid] = sampleCRS(remapTexture, uv.x, flipY ? 1-uv.y: uv.y) // Weird double flippiing of UVs with flipY. Has something to do with how projected data is done. - if (flipY) thisUV.y = 1-thisUV.y - if (isValid) uv = thisUV; - else{ - val.current = NaN; - coords.current = [thisUV.y,thisUV.x] - return; - } - } - - const { x, y } = uv; - const zSliceIdx = dimSlices.length > 2 ? 2 : 1; - const ySliceIdx = dimSlices.length > 2 ? 1 : 0; - const xSize = isFlat ? (analysisMode ? analysisDims[1].length : dimSlices[1].length) : dimSlices[zSliceIdx].length; - const ySize = isFlat ? (analysisMode ? analysisDims[0].length : dimSlices[0].length) : dimSlices[ySliceIdx].length; - - const xId = Math.round(x*xSize-.5) - const yId = Math.round(y*ySize-.5) - let dataIdx = xSize * yId + xId; - dataIdx += isFlat ? 0 : Math.floor((dimSlices[zIdx].length-1) * animProg) * xSize*ySize - const dataVal = sampleArray ? sampleArray[dataIdx] : 0; - val.current = dataVal; - coords.current = [y,x] - } - } - - // ----- TIMESERIES ----- // - function HandleTimeSeries(event: THREE.Intersection){ - const uv = event.uv; - if (!uv) return; - const tsUV = flipY ? new THREE.Vector2(uv.x, 1-uv.y) : uv - let newUV: THREE.Vector2 | undefined; - const normal = new THREE.Vector3(0,0,1) - if (remapTexture){ - const [thisUV, isValid] = sampleCRS(remapTexture, uv.x, flipY ? 1-uv.y: uv.y) // Weird double flippiing of UVs with flipY. Has something to do with how projected data is done. - if (flipY) thisUV.y = 1-thisUV.y - if (isValid) newUV = thisUV; - else{ - return; - } - } - - const tempTS = GetTimeSeries({data:analysisMode ? analysisArray : GetCurrentArray(), shape:dataShape, stride:strides},{uv:newUV ?? tsUV,normal}) - setPlotDim(0) //I think this 2 is only if there are 3-dims. Need to rework the logic - - const coordUV = parseUVCoords({normal:normal,uv:uv}) - let dimCoords = coordUV.map((val,idx)=>val ? dimSlices[idx][Math.round(val*dimSlices[idx].length)] : null) - const thisDimNames = dimNames.filter((_,idx)=> dimCoords[idx] !== null) - const thisDimUnits = dimUnits.filter((_,idx)=> dimCoords[idx] !== null) - dimCoords = dimCoords.filter(val => val !== null) - const tsID = `${dimCoords[0]}_${dimCoords[1]}` - const tsObj = { - color: evaluateColorMap(getColorIdx() / 10, 'Paired'), - data: tempTS, - normal, - uv: tsUV, - } - incrementColorIdx(); - updateTimeSeries({ [tsID] : tsObj}) - const dimObj = { - first:{ - name:thisDimNames[0], - loc:dimCoords[0] ?? 0, - units:thisDimUnits[0] - }, - second:{ - name:thisDimNames[1], - loc:dimCoords[1] ?? 0, - units:thisDimUnits[1] - }, - plot:{ - units:dimUnits[0] - } - } - updateDimCoords({[tsID] : dimObj}) - - } - // ----- SHADER MATERIAL ----- // - const shaderMaterial = useMemo(()=>new THREE.ShaderMaterial({ - glslVersion: THREE.GLSL3, - uniforms:{ - cScale: {value: cScale}, - cOffset: {value: cOffset}, - map : {value: textures}, - remapTexture: { value: remapTexture}, - maskTexture: {value: maskTexture}, - maskValue: {value: maskValue}, - threshold: {value: new THREE.Vector2(valueRange[0],valueRange[1])}, - latBounds: {value: new THREE.Vector2(deg2rad(latBounds[0]), deg2rad(latBounds[1]))}, - lonBounds: {value: new THREE.Vector2(deg2rad(lonBounds[0]), deg2rad(lonBounds[1]))}, - textureDepths: {value: new THREE.Vector3(textureArrayDepths[2], textureArrayDepths[1], textureArrayDepths[0])}, - cmap : { value : colormap}, - animateProg: {value:animProg}, - nanColor: {value : new THREE.Color(nanColor)}, - nanAlpha: {value: 1 - nanTransparency}, - fillValue: {value: fillValue?? NaN}, - }, - defines:{ - ...(isFlat ? { IS_FLAT: true } : {}), - ...(remapTexture ? { REPROJECT: true } : {}) - }, - vertexShader: vertShader, - fragmentShader: flatFrag, - side: THREE.DoubleSide, - }),[isFlat, remapTexture, textures]) - - useEffect(()=>{ - if(shaderMaterial){ - const uniforms = shaderMaterial.uniforms - uniforms.cOffset.value = cOffset; - uniforms.cmap. value = colormap; - uniforms.animateProg.value = animProg; - uniforms.nanColor.value = new THREE.Color(nanColor); - uniforms.nanAlpha.value = 1 - nanTransparency; - uniforms.cScale.value = cScale; - uniforms.threshold.value.set(valueRange[0], valueRange[1]); - uniforms.latBounds.value = new THREE.Vector2(deg2rad(latBounds[0]), deg2rad(latBounds[1])) - uniforms.lonBounds.value = new THREE.Vector2(deg2rad(lonBounds[0]), deg2rad(lonBounds[1])) - uniforms.maskValue.value = maskValue; - uniforms.fillValue.value = fillValue?? NaN - } - },[cScale, cOffset, colormap, animProg, nanColor, nanTransparency, latBounds, lonBounds, fillValue, maskValue, valueRange]) - useEffect(()=>{ - // This is duplicated. Probably shoud just move it to Plot.tsx - useGlobalStore.setState({timeSeries:{}, dimCoords:{}}) - },[remapTexture]) - return ( - <> - - {setShowInfo(true); infoRef.current = true }} - onPointerLeave={()=>{setShowInfo(false); infoRef.current = false }} - onPointerMove={handleMove} - onClick={selectTS && HandleTimeSeries} - /> - - ) -} - -export {FlatMap} +"use client"; + +import React, {useMemo, useEffect, useRef} from 'react' +import * as THREE from 'three' +import { useAnalysisStore } from '@/GlobalStates/AnalysisStore'; +import { useGlobalStore } from '@/GlobalStates/GlobalStore'; +import { usePlotStore } from '@/GlobalStates/PlotStore'; +import { useZarrStore } from '@/GlobalStates/ZarrStore'; +import { vertShader } from '@/components/computation/shaders' +import { useShallow } from 'zustand/shallow' +import { ThreeEvent } from '@react-three/fiber'; +import { coarsenFlatArray, GetCurrentArray, GetTimeSeries, parseUVCoords, deg2rad, getLogEps, parseColorToVec4 } from '@/utils/HelperFuncs'; +import { sampleCRS } from '../textures/ProjectionTexture'; +import { evaluateColorMap, colorScaleToId, exprToGLSL } from '@/components/textures'; +import { flatFrag } from '../textures/shaders'; +import { SquareMeshes } from './TransectMeshes'; +import { usePaddedTextures } from '@/hooks/usePaddedTextures'; +import { useAxisIndices } from '@/hooks'; +import { createCommonUniforms, updateCommonUniforms, useCommonPlotState } from '@/utils/plotUniforms'; + +interface InfoSettersProps{ + setLoc: React.Dispatch>; + setShowInfo: React.Dispatch>; + val: React.RefObject; + coords: React.RefObject; +} + +const FlatMap = ({textures: propTextures, infoSetters} : {textures : THREE.DataTexture[] | THREE.Data3DTexture[], infoSetters : InfoSettersProps}) => { + const textures = usePaddedTextures(propTextures); + const {setLoc, setShowInfo, val, coords} = infoSetters; + const commonState = useCommonPlotState(); + const { colormap, isFlat, valueScales, flipY, dataShape, textureArrayDepths, remapTexture, shape, + animProg, cOffset, cScale, nanColor, nanTransparency, fillValue, valueRange, maskTexture, maskValue, + colorScale, logConstant, lowclip, highclip, useLowclip, useHighclip, latBounds, lonBounds } = commonState; + + const { dimArrays, dimNames, dimUnits, strides, setPlotDim, updateDimCoords, updateTimeSeries } = useGlobalStore(useShallow(state => ({ + dimArrays: state.dimArrays, strides: state.strides, + dimNames: state.dimNames, dimUnits: state.dimUnits, + setPlotDim: state.setPlotDim, + updateDimCoords: state.updateDimCoords, + updateTimeSeries: state.updateTimeSeries + }))) + + const { zSlice, ySlice, xSlice, selectTS, coarsen, + getColorIdx, incrementColorIdx } = usePlotStore(useShallow(state => ({ + resetAnim: state.resetAnim, animate: state.animate, + zSlice: state.zSlice, ySlice: state.ySlice, xSlice: state.xSlice, + selectTS: state.selectTS, coarsen: state.coarsen, + getColorIdx: state.getColorIdx, + incrementColorIdx: state.incrementColorIdx, + }))) + const {axis, analysisMode, analysisArray} = useAnalysisStore(useShallow(state=> ({ + axis: state.axis, + analysisMode: state.analysisMode, + analysisArray: state.analysisArray + }))) + const {kernelSize, kernelDepth} = useZarrStore(useShallow(state => ({ + kernelSize: state.kernelSize, + kernelDepth: state.kernelDepth, + }))) + + const {xIdx, yIdx, zIdx} = useAxisIndices() + + const dimSlices = useMemo (() => { + let slices = isFlat + ? [ + dimArrays[yIdx]?.slice(ySlice[0], ySlice[1] ? ySlice[1] : undefined) ?? [], + dimArrays[xIdx]?.slice(xSlice[0], xSlice[1] ? xSlice[1] : undefined) ?? [], + ] + : [ + dimArrays[zIdx]?.slice(zSlice[0], zSlice[1] ? zSlice[1] : undefined) ?? [], + dimArrays[yIdx]?.slice(ySlice[0], ySlice[1] ? ySlice[1] : undefined) ?? [], + dimArrays[xIdx]?.slice(xSlice[0], xSlice[1] ? xSlice[1] : undefined ) ?? [], + ]; + if (coarsen) slices = slices.map((val, idx) => coarsenFlatArray(val, (idx === 0 && slices.length > 2 ? kernelDepth : kernelSize))) + return slices + } ,[dimArrays, zSlice, ySlice, xSlice, coarsen, kernelDepth, kernelSize, xIdx, yIdx, zIdx]) + + const shapeRatio = useMemo(()=> { + if (dataShape.length == 2){ + return shape.y/shape.x + } else if (analysisMode){ + const thisShape = dataShape.filter((_val, idx) => idx != axis) + return thisShape[0]/thisShape[1] + } else { + return shape.y/shape.x + } + }, [axis, shape, dataShape, analysisMode] ) + + const geometry = useMemo(()=>new THREE.PlaneGeometry(2,2*shapeRatio),[shapeRatio]) + const infoRef = useRef(false) + const rotateMap = analysisMode && axis == 2; + const sampleArray = useMemo(()=> analysisMode ? analysisArray : GetCurrentArray(),[analysisMode, analysisArray, textures]) + const analysisDims = useMemo(() => { + if (!analysisMode) return dimSlices; + const fullSlices = [ + dimArrays[zIdx]?.slice(zSlice[0], zSlice[1] ? zSlice[1] : undefined) ?? [], + dimArrays[yIdx]?.slice(ySlice[0], ySlice[1] ? ySlice[1] : undefined) ?? [], + dimArrays[xIdx]?.slice(xSlice[0], xSlice[1] ? xSlice[1] : undefined) ?? [], + ]; + let slices = fullSlices.filter((_, idx) => idx !== axis); + if (coarsen) slices = slices.map((val, idx) => coarsenFlatArray(val, (idx === 0 && slices.length > 2 ? kernelDepth : kernelSize))) + return slices; + }, [analysisMode, dimSlices, dimArrays, zSlice, ySlice, xSlice, axis, coarsen, kernelDepth, kernelSize, xIdx, yIdx, zIdx]) + + useEffect(()=>{ + return () => { + geometry.dispose() + } + },[geometry]) + + // ----- MOUSE MOVE ----- // + const eventRef = useRef | null>(null); + const handleMove = (e: ThreeEvent) => { + if (infoRef.current && e.uv) { + let {uv} = e; + if (!uv) return; + setLoc([e.clientX, e.clientY]); + eventRef.current = e; + if (remapTexture){ + const [thisUV, isValid] = sampleCRS(remapTexture, uv.x, flipY ? 1-uv.y: uv.y) // Weird double flippiing of UVs with flipY. Has something to do with how projected data is done. + if (flipY) thisUV.y = 1-thisUV.y + if (isValid) uv = thisUV; + else{ + val.current = NaN; + coords.current = [thisUV.y,thisUV.x] + return; + } + } + + const { x, y } = uv; + const zSliceIdx = dimSlices.length > 2 ? 2 : 1; + const ySliceIdx = dimSlices.length > 2 ? 1 : 0; + const xSize = isFlat ? (analysisMode ? analysisDims[1].length : dimSlices[1].length) : dimSlices[zSliceIdx].length; + const ySize = isFlat ? (analysisMode ? analysisDims[0].length : dimSlices[0].length) : dimSlices[ySliceIdx].length; + + const xId = Math.round(x*xSize-.5) + const yId = Math.round(y*ySize-.5) + let dataIdx = xSize * yId + xId; + dataIdx += isFlat ? 0 : Math.floor((dimSlices[zIdx].length-1) * animProg) * xSize*ySize + const dataVal = sampleArray ? sampleArray[dataIdx] : 0; + val.current = dataVal; + coords.current = [y,x] + } + } + + // ----- TIMESERIES ----- // + function HandleTimeSeries(event: THREE.Intersection){ + const uv = event.uv; + if (!uv) return; + const tsUV = flipY ? new THREE.Vector2(uv.x, 1-uv.y) : uv + let newUV: THREE.Vector2 | undefined; + const normal = new THREE.Vector3(0,0,1) + if (remapTexture){ + const [thisUV, isValid] = sampleCRS(remapTexture, uv.x, flipY ? 1-uv.y: uv.y) + if (flipY) thisUV.y = 1-thisUV.y + if (isValid) newUV = thisUV; + else{ + return; + } + } + + const tempTS = GetTimeSeries({data:analysisMode ? analysisArray : GetCurrentArray(), shape:dataShape, stride:strides},{uv:newUV ?? tsUV,normal}) + setPlotDim(0) + + const coordUV = parseUVCoords({normal:normal,uv:uv}) + let dimCoords = coordUV.map((val,idx)=>val ? dimSlices[idx][Math.round(val*dimSlices[idx].length)] : null) + const thisDimNames = dimNames.filter((_,idx)=> dimCoords[idx] !== null) + const thisDimUnits = dimUnits.filter((_,idx)=> dimCoords[idx] !== null) + dimCoords = dimCoords.filter(val => val !== null) + const tsID = `${dimCoords[0]}_${dimCoords[1]}` + const tsObj = { + color: evaluateColorMap(getColorIdx() / 10, 'Paired'), + data: tempTS, + normal, + uv: tsUV, + } + incrementColorIdx(); + updateTimeSeries({ [tsID] : tsObj}) + const dimObj = { + first:{ + name:thisDimNames[0], + loc:dimCoords[0] ?? 0, + units:thisDimUnits[0] + }, + second:{ + name:thisDimNames[1], + loc:dimCoords[1] ?? 0, + units:thisDimUnits[1] + }, + plot:{ + units:dimUnits[0] + } + } + updateDimCoords({[tsID] : dimObj}) + } + + // ----- SHADER MATERIAL ----- // + const shaderMaterial = useMemo(()=>new THREE.ShaderMaterial({ + glslVersion: THREE.GLSL3, + uniforms:{ + ...createCommonUniforms(commonState), + map : {value: textures}, + remapTexture: { value: remapTexture}, + textureDepths: {value: new THREE.Vector3(textureArrayDepths[2], textureArrayDepths[1], textureArrayDepths[0])}, + }, + defines:{ + ...(isFlat ? { IS_FLAT: true } : {}), + ...(remapTexture ? { REPROJECT: true } : {}), + 'CUSTOM_EXPR(val)': colorScaleToId(colorScale) === 6 ? exprToGLSL(colorScale) : '(val)', + }, + vertexShader: vertShader, + fragmentShader: flatFrag, + side: THREE.DoubleSide, + }),[isFlat, remapTexture, textures, commonState]) + + useEffect(()=>{ + if(shaderMaterial){ + updateCommonUniforms(shaderMaterial, commonState); + } + },[cScale, cOffset, colormap, animProg, nanColor, nanTransparency, latBounds, lonBounds, fillValue, maskValue, valueRange, colorScale, logConstant, valueScales, lowclip, highclip, useLowclip, useHighclip]) + useEffect(()=>{ + useGlobalStore.setState({timeSeries:{}, dimCoords:{}}) + },[remapTexture]) + return ( + <> + + {setShowInfo(true); infoRef.current = true }} + onPointerLeave={()=>{setShowInfo(false); infoRef.current = false }} + onPointerMove={handleMove} + onClick={selectTS && HandleTimeSeries} + /> + + ) +} + +export {FlatMap} diff --git a/src/components/plots/PointCloud.tsx b/src/components/plots/PointCloud.tsx index a2cbbb64..0b8ff9eb 100644 --- a/src/components/plots/PointCloud.tsx +++ b/src/components/plots/PointCloud.tsx @@ -4,12 +4,12 @@ import { pointFrag, pointVert } from '@/components/textures/shaders' import { useGlobalStore } from '@/GlobalStates/GlobalStore'; import { usePlotStore } from '@/GlobalStates/PlotStore'; import { useShallow } from 'zustand/shallow'; -import { deg2rad } from '@/utils/HelperFuncs'; -import { useCoordBounds } from '@/hooks/useCoordBounds'; import { UVCube } from './UVCube'; import { ColumnMeshes } from './TransectMeshes'; import { usePaddedTextures } from '@/hooks/usePaddedTextures'; +import { colorScaleToId, exprToGLSL } from '@/components/textures'; +import { createCommonUniforms, updateCommonUniforms, useCommonPlotState } from '@/utils/plotUniforms'; interface PCProps { texture: THREE.Data3DTexture[] | null, @@ -43,31 +43,14 @@ const MappingCube = () =>{ export const PointCloud = ({textures} : {textures:PCProps} )=>{ const { colormap } = textures; const volTexture = usePaddedTextures(textures.texture); - const { flipY, dataShape, remapTexture, textureArrayDepths, shape } = useGlobalStore(useShallow(state=>({ - flipY: state.flipY, - dataShape: state.dataShape, - remapTexture: state.remapTexture, - textureArrayDepths: state.textureArrayDepths, - shape: state.shape - }))) - const {scalePoints, scaleIntensity, pointSize, cScale, cOffset, valueRange, animProg, - timeScale, xRange, yRange, zRange, fillValue, - maskTexture, maskValue, disablePointScale} = usePlotStore(useShallow(state => ({ - scalePoints: state.scalePoints, - scaleIntensity: state.scaleIntensity, - pointSize: state.pointSize, - cScale: state.cScale, - cOffset:state.cOffset, - valueRange: state.valueRange, - animProg: state.animProg, - timeScale: state.timeScale, - xRange: state.xRange, - yRange: state.yRange, - zRange: state.zRange, - fillValue:state.fillValue, - maskTexture: state.maskTexture, - maskValue: state.maskValue, - disablePointScale: state.disablePointScale + const commonState = useCommonPlotState(); + const { valueScales, flipY, dataShape, textureArrayDepths, remapTexture, shape, + animProg, cOffset, cScale, nanColor, nanTransparency, fillValue, valueRange, maskTexture, maskValue, + colorScale, logConstant, lowclip, highclip, useLowclip, useHighclip, latBounds, lonBounds } = commonState; + + const { scalePoints, scaleIntensity, pointSize, timeScale, xRange, yRange, zRange, disablePointScale } = usePlotStore(useShallow(state => ({ + scalePoints: state.scalePoints, scaleIntensity: state.scaleIntensity, pointSize: state.pointSize, + timeScale: state.timeScale, xRange: state.xRange, yRange: state.yRange, zRange: state.zRange, disablePointScale: state.disablePointScale }))) //Extract data and shape from Data3DTexture @@ -109,35 +92,27 @@ export const PointCloud = ({textures} : {textures:PCProps} )=>{ return list; }, [depth, width, height]); - const {lonBounds, latBounds} = useCoordBounds() - const shaderMaterial = useMemo(()=> (new THREE.ShaderMaterial({ glslVersion: THREE.GLSL3, uniforms: { + ...createCommonUniforms({ ...commonState, colormap }), map: { value: volTexture }, remapTexture: { value: remapTexture }, textureDepths: { value: new THREE.Vector3(textureArrayDepths[2], textureArrayDepths[1], textureArrayDepths[0]) }, - maskTexture: {value: maskTexture}, - maskValue: {value: maskValue}, pointSize: {value: pointSize}, - cmap: {value: colormap}, - cOffset: {value: cOffset}, - cScale: {value: cScale}, - valueRange: {value: new THREE.Vector2(valueRange[0], valueRange[1])}, scalePoints:{value: scalePoints}, scaleIntensity: {value: scaleIntensity}, timeScale: {value: depthRatio}, - animateProg: {value: animProg}, aspect: {value: shape.x/shape.y}, shape: {value: new THREE.Vector3(depth, height, width)}, flatBounds:{value: new THREE.Vector4(xRange[0], xRange[1], zRange[0], zRange[1])}, vertBounds:{value: new THREE.Vector2(yRange[0], yRange[1])}, - fillValue: {value: fillValue?? NaN} }, defines: { GLOBAL_SCALE: globalscale*2, ...(remapTexture ? { REPROJECT: true } : {}), - ...(disablePointScale ? { NO_SCALE: true } : {}) + ...(disablePointScale ? { NO_SCALE: true } : {}), + 'CUSTOM_EXPR(val)': colorScaleToId(colorScale) === 6 ? exprToGLSL(colorScale) : '(val)', }, vertexShader: pointVert, fragmentShader:pointFrag, @@ -152,16 +127,12 @@ export const PointCloud = ({textures} : {textures:PCProps} )=>{ const uniforms = shaderMaterial.uniforms; uniforms.map.value = volTexture; shaderMaterial.needsUpdate = true; + updateCommonUniforms(shaderMaterial, { ...commonState, colormap }); uniforms.shape.value.set(depth, height, width); uniforms.pointSize.value = pointSize; - uniforms.cmap.value = colormap; - uniforms.cOffset.value = cOffset; - uniforms.cScale.value = cScale; - uniforms.valueRange.value.set(valueRange[0], valueRange[1]); uniforms.scalePoints.value = scalePoints; uniforms.scaleIntensity.value = scaleIntensity; uniforms.timeScale.value = depthRatio; - uniforms.animateProg.value = animProg; uniforms.flatBounds.value.set( xRange[0], xRange[1], zRange[0], zRange[1] @@ -169,11 +140,9 @@ export const PointCloud = ({textures} : {textures:PCProps} )=>{ uniforms.vertBounds.value.set( yRange[0], yRange[1] ); - uniforms.fillValue.value = fillValue?? NaN - uniforms.maskValue.value = maskValue uniforms.aspect.value = shape.x/shape.y; } - }, [volTexture, depthRatio, depth, height, shape, width, pointSize, colormap, cOffset, cScale, valueRange, scalePoints, scaleIntensity, animProg, xRange, yRange, fillValue, zRange, maskValue]); + }, [volTexture, depthRatio, depth, height, shape, width, pointSize, colormap, cOffset, cScale, valueRange, scalePoints, scaleIntensity, animProg, xRange, yRange, fillValue, zRange, maskValue, colorScale, logConstant, valueScales, lowclip, highclip, useLowclip, useHighclip]); return ( diff --git a/src/components/plots/Sphere.tsx b/src/components/plots/Sphere.tsx index 12c10a91..e022469e 100644 --- a/src/components/plots/Sphere.tsx +++ b/src/components/plots/Sphere.tsx @@ -1,16 +1,17 @@ -import React, {useRef, useMemo, useState, useEffect} from 'react' +import React, { useMemo, useEffect} from 'react' import * as THREE from 'three' import { useAnalysisStore } from '@/GlobalStates/AnalysisStore'; import { useGlobalStore } from '@/GlobalStates/GlobalStore'; import { usePlotStore } from '@/GlobalStates/PlotStore'; import { useShallow } from 'zustand/shallow' import { parseUVCoords, GetTimeSeries, GetCurrentArray, deg2rad } from '@/utils/HelperFuncs'; -import { evaluateColorMap } from '@/components/textures'; -import { useCoordBounds } from '@/hooks/useCoordBounds' +import { evaluateColorMap, colorScaleToId, exprToGLSL } from '@/components/textures'; import { SquareMeshes } from './TransectMeshes'; import { usePaddedTextures } from '@/hooks/usePaddedTextures'; import { useAxisIndices } from '@/hooks'; import { sphereVertex, sphereFrag } from '@/components/textures/shaders' +import { createCommonUniforms, updateCommonUniforms, useCommonPlotState } from '@/utils/plotUniforms'; + function XYZtoRemap(xyz : THREE.Vector3, latBounds: number[], lonBounds : number[]){ const lon = Math.atan2(xyz.z,xyz.x) const lat = Math.asin(xyz.y); @@ -21,50 +22,28 @@ function XYZtoRemap(xyz : THREE.Vector3, latBounds: number[], lonBounds : number export const Sphere = ({textures: propTextures} : {textures: THREE.Data3DTexture[] | THREE.DataTexture[] | null}) => { const textures = usePaddedTextures(propTextures); - const {setPlotDim,updateDimCoords, updateTimeSeries} = useGlobalStore(useShallow(state=>({ - setPlotDim:state.setPlotDim, - updateDimCoords:state.updateDimCoords, - updateTimeSeries: state.updateTimeSeries - }))) - const {analysisMode, analysisArray} = useAnalysisStore(useShallow(state => ({ - analysisMode: state.analysisMode, - analysisArray: state.analysisArray - }))) - const {colormap, isFlat, dimArrays, dimNames, dimUnits, valueScales, - dataShape, strides, flipY, textureArrayDepths, remapTexture} = useGlobalStore(useShallow(state=>({ - colormap: state.colormap, - isFlat: state.isFlat, - dimArrays:state.dimArrays, - dimNames:state.dimNames, - dimUnits:state.dimUnits, - valueScales: state.valueScales, - dataShape: state.dataShape, - strides: state.strides, - flipY: state.flipY, - textureArrayDepths: state.textureArrayDepths, - remapTexture: state.remapTexture + const commonState = useCommonPlotState(); + const { colormap, isFlat, valueScales, flipY, dataShape, textureArrayDepths, remapTexture, + animProg, cOffset, cScale, nanColor, nanTransparency, fillValue, valueRange, maskTexture, maskValue, + colorScale, logConstant, lowclip, highclip, useLowclip, useHighclip, latBounds, lonBounds } = commonState; + + const { dimArrays, dimNames, dimUnits, strides, setPlotDim, updateDimCoords, updateTimeSeries } = useGlobalStore(useShallow(state=>({ + dimArrays: state.dimArrays, dimNames: state.dimNames, dimUnits: state.dimUnits, strides: state.strides, + setPlotDim: state.setPlotDim, updateDimCoords: state.updateDimCoords, updateTimeSeries: state.updateTimeSeries }))) - - const {animate, animProg, cOffset, cScale, valueRange, selectTS, nanColor, nanTransparency, sphereDisplacement, sphereResolution, - zSlice, ySlice, xSlice, fillValue, borderTexture, maskTexture, maskValue, - getColorIdx, incrementColorIdx} = usePlotStore(useShallow(state=> ({ + + const { animate, sphereDisplacement, sphereResolution, zSlice, ySlice, xSlice, selectTS, borderTexture, + getColorIdx, incrementColorIdx } = usePlotStore(useShallow(state=> ({ animate: state.animate, - animProg: state.animProg, - cOffset: state.cOffset, - cScale: state.cScale, - valueRange: state.valueRange, - selectTS: state.selectTS, - nanColor: state.nanColor, - nanTransparency: state.nanTransparency, sphereDisplacement: state.displacement, sphereResolution: state.sphereResolution, - zSlice: state.zSlice, - ySlice: state.ySlice, - xSlice: state.xSlice, - fillValue:state.fillValue, maskValue:state.maskValue, - borderTexture:state.borderTexture, maskTexture:state.maskTexture, - getColorIdx: state.getColorIdx, - incrementColorIdx: state.incrementColorIdx + zSlice: state.zSlice, ySlice: state.ySlice, xSlice: state.xSlice, + selectTS: state.selectTS, borderTexture: state.borderTexture, + getColorIdx: state.getColorIdx, incrementColorIdx: state.incrementColorIdx, + }))) + const {analysisMode, analysisArray} = useAnalysisStore(useShallow(state => ({ + analysisMode: state.analysisMode, + analysisArray: state.analysisArray }))) const {xIdx, yIdx, zIdx} = useAxisIndices() @@ -78,34 +57,23 @@ export const Sphere = ({textures: propTextures} : {textures: THREE.Data3DTexture ]; }, [dimArrays, zIdx, yIdx, xIdx, zSlice, ySlice, xSlice]); - const {lonBounds, latBounds} = useCoordBounds() - const geometry = useMemo(() => new THREE.IcosahedronGeometry(1, sphereResolution), [sphereResolution]); + const shaderMaterial = useMemo(()=>{ const shader = new THREE.ShaderMaterial({ glslVersion: THREE.GLSL3, uniforms: { + ...createCommonUniforms(commonState), map: { value: textures }, remapTexture: { value: remapTexture }, - maskTexture: { value: maskTexture}, - maskValue: { value: maskValue }, - threshold: {value: new THREE.Vector2(valueRange[0],valueRange[1])}, textureDepths: {value: new THREE.Vector3(textureArrayDepths[2], textureArrayDepths[1], textureArrayDepths[0])}, - cmap:{value: colormap}, - cOffset:{value: cOffset}, - cScale: {value: cScale}, - animateProg: {value: animProg}, - latBounds: {value: new THREE.Vector2(deg2rad(latBounds[0]), deg2rad(latBounds[1]))}, - lonBounds: {value: new THREE.Vector2(deg2rad(lonBounds[0]), deg2rad(lonBounds[1]))}, - nanColor: {value: new THREE.Color(nanColor)}, - nanAlpha: {value: 1 - nanTransparency}, displaceZero: {value: -valueScales.minVal/(valueScales.maxVal-valueScales.minVal)}, displacement: {value: sphereDisplacement}, - fillValue: {value: NaN}, }, defines:{ ...(isFlat ? { IS_FLAT: true } : {}), - ...(remapTexture ? { REPROJECT: true } : {}) + ...(remapTexture ? { REPROJECT: true } : {}), + 'CUSTOM_EXPR(val)': colorScaleToId(colorScale) === 6 ? exprToGLSL(colorScale) : '(val)', }, vertexShader: sphereVertex, fragmentShader: sphereFrag, @@ -115,7 +83,7 @@ export const Sphere = ({textures: propTextures} : {textures: THREE.Data3DTexture depthWrite:true, }) return shader - },[isFlat, textures, borderTexture]) + },[isFlat, textures, borderTexture, commonState, sphereDisplacement, textureArrayDepths, valueScales]) const backMaterial = useMemo(()=>{ const mat = shaderMaterial.clone() @@ -132,20 +100,10 @@ export const Sphere = ({textures: propTextures} : {textures: THREE.Data3DTexture } else { delete material.defines.REPROJECT; } - material.needsUpdate = true; - uniforms.cmap.value = colormap - uniforms.maskValue.value = maskValue - uniforms.cOffset.value = cOffset - uniforms.cScale.value = cScale - uniforms.animateProg.value = animProg - uniforms.threshold.value.set(valueRange[0], valueRange[1]) - uniforms.latBounds.value = new THREE.Vector2(deg2rad(latBounds[0]), deg2rad(latBounds[1])) - uniforms.lonBounds.value = new THREE.Vector2(deg2rad(lonBounds[0]), deg2rad(lonBounds[1])) - uniforms.nanColor.value = new THREE.Color(nanColor) - uniforms.nanAlpha.value = 1 - nanTransparency + updateCommonUniforms(material, commonState); uniforms.displaceZero.value = -valueScales.minVal/(valueScales.maxVal-valueScales.minVal) uniforms.displacement.value = sphereDisplacement - uniforms.fillValue.value = fillValue?? NaN + material.needsUpdate = true; } useEffect(()=>{ @@ -155,7 +113,7 @@ export const Sphere = ({textures: propTextures} : {textures: THREE.Data3DTexture if (backMaterial){ updateMaterial(backMaterial) } - },[textures, remapTexture, animProg, colormap, cOffset, cScale, animate, lonBounds, latBounds, nanColor, nanTransparency, sphereDisplacement,valueRange, fillValue, maskValue, valueScales]) + },[textures, remapTexture, animProg, colormap, cOffset, cScale, animate, lonBounds, latBounds, nanColor, nanTransparency, sphereDisplacement,valueRange, fillValue, maskValue, valueScales, colorScale, logConstant, lowclip, highclip, useLowclip, useHighclip]) function HandleTimeSeries(event: THREE.Intersection){ diff --git a/src/components/plots/SphereBlocks.tsx b/src/components/plots/SphereBlocks.tsx index 23ed63da..2364ba46 100644 --- a/src/components/plots/SphereBlocks.tsx +++ b/src/components/plots/SphereBlocks.tsx @@ -1,40 +1,24 @@ import React, { useEffect, useMemo } from 'react' -import { useGlobalStore } from '@/GlobalStates/GlobalStore'; import { usePlotStore } from '@/GlobalStates/PlotStore'; import { useErrorStore } from '@/GlobalStates/ErrorStore'; import { useShallow } from 'zustand/shallow' import * as THREE from 'three' import { sphereBlocksFrag, sphereBlocksVert } from '../textures/shaders' import { invalidate } from '@react-three/fiber' -import { deg2rad } from '@/utils/HelperFuncs' -import { useCoordBounds } from '@/hooks/useCoordBounds' import { usePaddedTextures } from '@/hooks/usePaddedTextures'; +import { colorScaleToId, exprToGLSL } from '@/components/textures'; +import { createCommonUniforms, updateCommonUniforms, useCommonPlotState } from '@/utils/plotUniforms'; + const SphereBlocks = ({textures: propTextures} : {textures: THREE.Data3DTexture[] | THREE.DataTexture[] | null}) => { const textures = usePaddedTextures(propTextures); - const {colormap, isFlat, valueScales, - dataShape, textureArrayDepths, flipY, remapTexture} = useGlobalStore(useShallow(state=>({ - colormap: state.colormap, - isFlat: state.isFlat, - valueScales: state.valueScales, - dataShape: state.dataShape, - textureArrayDepths: state.textureArrayDepths, - flipY: state.flipY, - remapTexture: state.remapTexture - }))) - const { animProg, cOffset, cScale, nanColor, nanTransparency, sphereDisplacement, offsetNegatives, fillValue, valueRange, maskTexture, maskValue} = usePlotStore(useShallow(state=> ({ - animate: state.animate, - animProg: state.animProg, - cOffset: state.cOffset, - cScale: state.cScale, - nanColor: state.nanColor, - nanTransparency: state.nanTransparency, + const commonState = useCommonPlotState(); + const { colormap, isFlat, valueScales, flipY, dataShape, textureArrayDepths, remapTexture, + animProg, cOffset, cScale, nanColor, nanTransparency, fillValue, valueRange, maskTexture, maskValue, + colorScale, logConstant, lowclip, highclip, useLowclip, useHighclip, latBounds, lonBounds } = commonState; + + const { sphereDisplacement, offsetNegatives } = usePlotStore(useShallow(state => ({ sphereDisplacement: state.displacement, - sphereResolution: state.sphereResolution, offsetNegatives: state.offsetNegatives, - fillValue:state.fillValue, - valueRange: state.valueRange, - maskTexture: state.maskTexture, - maskValue: state.maskValue, }))) const count = useMemo(()=>{ @@ -76,33 +60,21 @@ const SphereBlocks = ({textures: propTextures} : {textures: THREE.Data3DTexture[ return geo },[dataShape]) - const {lonBounds, latBounds} = useCoordBounds() - const shaderMaterial = useMemo(()=>{ const shader = new THREE.ShaderMaterial({ glslVersion: THREE.GLSL3, uniforms: { + ...createCommonUniforms(commonState), map: { value: textures }, remapTexture: { value: remapTexture }, - maskTexture: {value: maskTexture}, - maskValue: {value: maskValue}, - threshold: {value: new THREE.Vector2(valueRange[0],valueRange[1])}, textureDepths: {value: new THREE.Vector3(textureArrayDepths[2], textureArrayDepths[1], textureArrayDepths[0])}, - latBounds: {value: new THREE.Vector2(deg2rad(latBounds[0]), deg2rad(latBounds[1]))}, - lonBounds: {value: new THREE.Vector2(deg2rad(lonBounds[0]), deg2rad(lonBounds[1]))}, - cmap:{value: colormap}, - cOffset:{value: cOffset}, - cScale: {value: cScale}, - animateProg: {value: animProg}, - nanColor: {value: new THREE.Color(nanColor)}, - nanAlpha: {value: 1 - nanTransparency}, displaceZero: {value: offsetNegatives ? 0 : (-valueScales.minVal/(valueScales.maxVal-valueScales.minVal))}, displacement: {value: sphereDisplacement}, - fillValue: {value: fillValue?? NaN}, }, defines:{ ...(isFlat ? { IS_FLAT: true } : {}), - ...(remapTexture ? { REPROJECT: true } : {}) + ...(remapTexture ? { REPROJECT: true } : {}), + 'CUSTOM_EXPR(val)': colorScaleToId(colorScale) === 6 ? exprToGLSL(colorScale) : '(val)', }, vertexShader: sphereBlocksVert, fragmentShader: sphereBlocksFrag, @@ -123,22 +95,13 @@ const SphereBlocks = ({textures: propTextures} : {textures: THREE.Data3DTexture[ } else { delete shaderMaterial.defines.REPROJECT; } - shaderMaterial.needsUpdate = true; - uniforms.animateProg.value = animProg - uniforms.displaceZero.value = -valueScales.minVal/(valueScales.maxVal-valueScales.minVal) - uniforms.displacement.value = sphereDisplacement - uniforms.cmap.value = colormap - uniforms.cOffset.value = cOffset - uniforms.cScale.value = cScale - uniforms.threshold.value.set(valueRange[0], valueRange[1]) - uniforms.latBounds.value = new THREE.Vector2(deg2rad(latBounds[0]), deg2rad(latBounds[1])) - uniforms.lonBounds.value = new THREE.Vector2(deg2rad(lonBounds[0]), deg2rad(lonBounds[1])) + updateCommonUniforms(shaderMaterial, commonState); uniforms.displaceZero.value = offsetNegatives ? 0 : (-valueScales.minVal/(valueScales.maxVal-valueScales.minVal)) - uniforms.fillValue.value = fillValue?? NaN - uniforms.maskValue.value = maskValue + uniforms.displacement.value = sphereDisplacement + shaderMaterial.needsUpdate = true; } invalidate(); - },[animProg, valueScales, sphereDisplacement, colormap, cScale, cOffset, latBounds, lonBounds, valueRange, offsetNegatives, textures, remapTexture, maskValue, fillValue]) + },[animProg, valueScales, sphereDisplacement, colormap, cScale, cOffset, latBounds, lonBounds, valueRange, offsetNegatives, textures, remapTexture, maskValue, fillValue, colorScale, logConstant, lowclip, highclip, useLowclip, useHighclip]) const nanMaterial = useMemo(()=>new THREE.MeshBasicMaterial({color:nanColor}),[]) nanMaterial.transparent = true; diff --git a/src/components/plots/plotarea/ThickLine.tsx b/src/components/plots/plotarea/ThickLine.tsx index 92350f72..f1b6ba10 100644 --- a/src/components/plots/plotarea/ThickLine.tsx +++ b/src/components/plots/plotarea/ThickLine.tsx @@ -7,14 +7,8 @@ import { usePlotStore } from '@/GlobalStates/PlotStore'; import { useShallow } from 'zustand/shallow' import vertexShader from '@/components/textures/shaders/thickLineVert.glsl' import { PlotPoints } from './PlotPoints'; -import { useThree } from '@react-three/fiber'; -import { invalidate } from '@react-three/fiber'; - - -function linspace(start: number, stop: number, num: number): number[] { - const step = (stop - start) / (num - 1); - return Array.from({ length: num }, (_, i) => start + step * i); - } +import { useThree, invalidate } from '@react-three/fiber'; +import { linspace } from '@/utils/HelperFuncs'; interface pointSetters{ setPointID:React.Dispatch>, diff --git a/src/components/textures/TextureMakers.tsx b/src/components/textures/TextureMakers.tsx index 8dc739c3..07339158 100644 --- a/src/components/textures/TextureMakers.tsx +++ b/src/components/textures/TextureMakers.tsx @@ -8,10 +8,18 @@ interface Array { shape: number[]; } -function StoreData(array: Array, valueScales?: {maxVal: number, minVal: number}): {minVal: number, maxVal: number}{ +import { detectUniqueCategories } from './colormap'; +import { usePlotStore } from '@/GlobalStates/PlotStore'; + +function StoreData(array: Array, valueScales?: {maxVal: number, minVal: number, minPosVal?: number}): {minVal: number, maxVal: number, minPosVal?: number}{ const { setTextureData} = useGlobalStore.getState() const data = array.data; - const [minVal,maxVal] = valueScales ? [valueScales.minVal, valueScales.maxVal] : ArrayMinMax(data ) + const [minVal, maxVal, minPosVal] = valueScales ? [valueScales.minVal, valueScales.maxVal, valueScales.minPosVal] : ArrayMinMax(data); + + // Auto-detect unique categories in dataset + const uniques = detectUniqueCategories(data as any, 50); + usePlotStore.getState().setUniqueCategories(uniques); + const textureData = new Uint8Array(data.length) const range = (maxVal - minVal) for (let i = 0; i < data.length; i++){ @@ -23,7 +31,7 @@ function StoreData(array: Array, valueScales?: {maxVal: number, minVal: number}) } }; setTextureData(textureData) - return {minVal, maxVal} + return {minVal, maxVal, minPosVal} } export function CreateTexture(shape: number[], data?: Uint8Array) : THREE.DataTexture[] | THREE.Data3DTexture[] | undefined { @@ -75,7 +83,7 @@ export function CreateTexture(shape: number[], data?: Uint8Array) : THREE.DataTe } } -export function ArrayToTexture(array: Array, valueScales?: {maxVal: number, minVal: number}): [ THREE.Data3DTexture[] | THREE.DataTexture[], {minVal: number, maxVal: number}]{ +export function ArrayToTexture(array: Array, valueScales?: {maxVal: number, minVal: number, minPosVal?: number}): [ THREE.Data3DTexture[] | THREE.DataTexture[], {minVal: number, maxVal: number, minPosVal?: number}]{ const scales = StoreData(array, valueScales); const textures = CreateTexture(array.shape) return [textures as THREE.Data3DTexture[] | THREE.DataTexture[], scales]; diff --git a/src/components/textures/colormap.tsx b/src/components/textures/colormap.tsx index 4e42e9c9..16042171 100644 --- a/src/components/textures/colormap.tsx +++ b/src/components/textures/colormap.tsx @@ -1,8 +1,143 @@ import * as THREE from 'three'; -import { colorschemes, get, findColorScheme } from 'color-schemes-js'; +import { colorschemes, get, findColorScheme, resample } from 'color-schemes-js'; export const colormaps = ['magma', 'inferno', 'plasma', 'viridis', 'cividis', 'twilight', 'twilight_shifted', 'turbo', 'Blues', 'BrBG', 'BuGn', 'BuPu', 'CMRmap', 'GnBu', 'Greens', 'Greys', 'OrRd', 'Oranges', 'PRGn', 'PiYG', 'PuBu', 'PuBuGn', 'PuOr', 'PuRd', 'Purples', 'RdBu', 'RdGy', 'RdPu', 'RdYlBu', 'RdYlGn', 'Reds', 'Spectral', 'Wistia', 'YlGn', 'YlGnBu', 'YlOrBr', 'YlOrRd', 'afmhot', 'autumn', 'binary', 'bone', 'brg', 'bwr', 'cool', 'coolwarm', 'copper', 'cubehelix', 'flag', 'gist_earth', 'gist_gray', 'gist_heat', 'gist_ncar', 'gist_rainbow', 'gist_stern', 'gist_yarg', 'gnuplot', 'gnuplot2', 'gray', 'hot', 'hsv', 'jet', 'nipy_spectral', 'ocean', 'pink', 'prism', 'rainbow', 'seismic', 'spring', 'summer', 'terrain', 'winter', 'Accent', 'Dark2', 'Paired', 'Pastel1', 'Pastel2', 'Set1', 'Set2', 'Set3', 'tab10', 'tab20', 'tab20b', 'tab20c']; +export const COLOR_SCALE_OPTIONS = [ + { label: 'x (Linear)', value: 'identity' }, + { label: 'log(x)', value: 'log(x)' }, + { label: 'log(1+x)', value: 'log(1+x)' }, + { label: 'log(x+c)', value: 'log(x+c)' }, + { label: 'sign(x)*sqrt(abs(x))', value: 'sign(x)*sqrt(abs(x))' }, + { label: 'exp(x)/100', value: 'exp(x)/100' }, + { label: 'Custom (Expression)', value: 'custom' }, +] as const; + +const customExprCache = new Map number>(); + +export function evalCustomExprJS(expr: string): ((x: number) => number) | null { + if (!expr || typeof expr !== 'string') return null; + if (customExprCache.has(expr)) return customExprCache.get(expr)!; + + const sanitized = expr + .replace(/\babs\b/g, 'Math.abs') + .replace(/\bsqrt\b/g, 'Math.sqrt') + .replace(/\blog\b/g, 'Math.log') + .replace(/\bexp\b/g, 'Math.exp') + .replace(/\bpow\b/g, 'Math.pow') + .replace(/\bsign\b/g, 'Math.sign'); + + try { + const fn = new Function('x', `return ${sanitized};`) as (x: number) => number; + const test0 = fn(0); + const test1 = fn(1); + if (typeof test0 === 'number' && typeof test1 === 'number' && !isNaN(test0) && !isNaN(test1)) { + customExprCache.set(expr, fn); + return fn; + } + } catch { + return null; + } + return null; +} + +export function exprToGLSL(expr: string): string { + if (!expr || typeof expr !== 'string') return '(val)'; + const valSubbed = expr.replace(/\bx\b/g, '(val)'); + const floatified = valSubbed.replace(/\b(\d+)(?!\.)\b/g, '$1.0'); + return floatified.replace(/\bMath\./g, ''); +} + +export function colorScaleToId(colorScale: string): number { + switch (colorScale) { + case 'identity': return 0; + case 'log(x)': return 1; + case 'log(1+x)': return 2; + case 'log(x+c)': return 3; + case 'sign(x)*sqrt(abs(x))': return 4; + case 'exp(x)/100': return 5; + default: return 6; // Custom generic expression (e.g., "x > 0 ? x/2 : x") + } +} + +export function applyColorScale(x: number, scaleType: string, c = 1.0, logEps = 0.000001, dataRange = 100.0, minVal = 0.0): number { + const safeRange = Math.max(dataRange, 0.000001); + if (scaleType === 'log(x)') { + if (minVal > 0) { + const K = safeRange / minVal; + const clampedX = Math.max(x, 0.0); + const num = Math.log(1.0 + clampedX * K); + const denom = Math.log(1.0 + K); + return denom !== 0 ? num / denom : x; + } else { + const eps = Math.max(logEps, 0.000001); + if (x <= eps) return 0.0; + const xRel = (x - eps) / (1.0 - eps); + const K = (1.0 - eps) / eps; + const num = Math.log(1.0 + xRel * K); + const denom = Math.log(1.0 + K); + return denom !== 0 ? num / denom : x; + } + } else if (scaleType === 'log(1+x)') { + const clampedX = Math.max(x, 0.0); + const num = Math.log(1.0 + clampedX * safeRange); + const denom = Math.log(1.0 + safeRange); + return denom !== 0 ? num / denom : x; + } else if (scaleType === 'log(x+c)') { + const safeC = Math.max(c, 0.00001); + const clampedX = Math.max(x, 0.0); + const num = Math.log(safeC + clampedX * safeRange) - Math.log(safeC); + const denom = Math.log(safeC + safeRange) - Math.log(safeC); + return denom !== 0 ? num / denom : x; + } else if (scaleType === 'sign(x)*sqrt(abs(x))') { + return Math.sign(x) * Math.sqrt(Math.abs(x)); + } else if (scaleType === 'exp(x)/100') { + const clampedX = Math.max(x, 0.0); + const num = Math.exp(clampedX * Math.min(safeRange, 10.0)) - 1.0; + const denom = Math.exp(Math.min(safeRange, 10.0)) - 1.0; + return denom !== 0 ? num / denom : x; + } else if (scaleType !== 'identity') { + const fn = evalCustomExprJS(scaleType); + if (fn) { + const v0 = fn(0); + const v1 = fn(1); + const vx = fn(x); + const denom = v1 - v0; + return denom !== 0 ? (vx - v0) / denom : vx; + } + } + return x; +} + +export function invertColorScale(t: number, scaleType: string, c = 1.0, logEps = 0.000001, dataRange = 100.0, minVal = 0.0): number { + const safeRange = Math.max(dataRange, 0.000001); + const clampedT = Math.max(0.0, Math.min(1.0, t)); + if (scaleType === 'log(x)') { + if (minVal > 0) { + const K = safeRange / minVal; + return (Math.pow(1.0 + K, clampedT) - 1.0) / K; + } else { + const eps = Math.max(logEps, 0.000001); + const K = (1.0 - eps) / eps; + const xRel = (Math.pow(1.0 + K, clampedT) - 1.0) / K; + return eps + xRel * (1.0 - eps); + } + } else if (scaleType === 'log(1+x)') { + return (Math.pow(1.0 + safeRange, clampedT) - 1.0) / safeRange; + } else if (scaleType === 'log(x+c)') { + const safeC = Math.max(c, 0.00001); + const ratio = 1.0 + safeRange / safeC; + return safeC * (Math.pow(ratio, clampedT) - 1.0) / safeRange; + } else if (scaleType === 'sign(x)*sqrt(abs(x))') { + return Math.sign(clampedT) * Math.pow(clampedT, 2); + } else if (scaleType === 'exp(x)/100') { + const expR = Math.min(safeRange, 10.0); + const num = Math.log(1.0 + clampedT * (Math.exp(expR) - 1.0)); + return num / expR; + } + return clampedT; +} + export const availableColorMapNames = Object.keys(colorschemes).sort(); export type ColormapEntry = { @@ -100,17 +235,49 @@ export function minMax(values: number[]): { min: number | undefined, max: number return { min, max }; } +export function detectUniqueCategories(data: ArrayLike, maxCategories = 50): number[] { + const set = new Set(); + for (let i = 0; i < data.length; i++) { + const val = data[i]; + if (!isNaN(val) && isFinite(val)) { + set.add(val); + if (set.size > maxCategories) break; + } + } + return Array.from(set).sort((a, b) => a - b); +} + export function GetColorMapTexture( texture: THREE.DataTexture | null = null, palette: string = "Spectral", alpha: number = 1, nan_color: string = "#000000", nan_alpha: number = 0, - reverse: boolean = false + reverse: boolean = false, + isCategorical: boolean = false, + numBins: number = 10 ): THREE.DataTexture { let unitInterval = Array.from({ length: 255 }, (_, index) => index / 254); - unitInterval = reverse ? unitInterval.reverse() : unitInterval - const rgbv = unitInterval.map(value => evaluateColorMap(value, palette, false)); + unitInterval = reverse ? unitInterval.reverse() : unitInterval; + + let rgbv: [number, number, number][]; + if (isCategorical) { + const bins = Math.max(2, Math.min(50, numBins)); + const schemeName = resolveColorSchemeName(palette); + const scheme = colorschemes[schemeName] || colorschemes['viridis']; + const resampled = resample(scheme, bins).colors; + const catColors: [number, number, number][] = resampled.map((c: any) => { + const rgb = c.toRgb255(); + return [rgb.r, rgb.g, rgb.b]; + }); + + rgbv = unitInterval.map(value => { + const idx = Math.min(bins - 1, Math.floor(value * bins)); + return catColors[idx]; + }); + } else { + rgbv = unitInterval.map(value => evaluateColorMap(value, palette, false)); + } const colData = new Uint8Array((rgbv.length + 1) * 4); for (let i = 0; i < rgbv.length; i++) { diff --git a/src/components/textures/index.ts b/src/components/textures/index.ts index 435212ac..cd386bee 100644 --- a/src/components/textures/index.ts +++ b/src/components/textures/index.ts @@ -1,13 +1,19 @@ -import { GetColorMapTexture, colormaps, evaluateColorMap, availableColorMapNames, getColormapGradientCss, colormapIndex } from './colormap'; -import {ArrayToTexture, CreateTexture}from './TextureMakers' - -export { - GetColorMapTexture, - colormaps, - evaluateColorMap, - availableColorMapNames, - getColormapGradientCss, - colormapIndex, - ArrayToTexture, - CreateTexture +import { GetColorMapTexture, colormaps, evaluateColorMap, availableColorMapNames, getColormapGradientCss, colormapIndex, COLOR_SCALE_OPTIONS, colorScaleToId, applyColorScale, invertColorScale, exprToGLSL, evalCustomExprJS } from './colormap'; +import { ArrayToTexture, CreateTexture } from './TextureMakers' + +export { + GetColorMapTexture, + colormaps, + evaluateColorMap, + availableColorMapNames, + getColormapGradientCss, + colormapIndex, + COLOR_SCALE_OPTIONS, + colorScaleToId, + applyColorScale, + invertColorScale, + exprToGLSL, + evalCustomExprJS, + ArrayToTexture, + CreateTexture } \ No newline at end of file diff --git a/src/components/textures/shaders/colorPipeline.glsl b/src/components/textures/shaders/colorPipeline.glsl new file mode 100644 index 00000000..39f4bced --- /dev/null +++ b/src/components/textures/shaders/colorPipeline.glsl @@ -0,0 +1,190 @@ +#ifndef CUSTOM_EXPR +#define CUSTOM_EXPR(val) (val) +#endif + +float evalCustomExpr(float val) { + return CUSTOM_EXPR(val); +} + +float applyColorScale(float x, int scaleType, float c, float eps, float range, float minV) { + float safeRange = max(range, 0.000001); + if (scaleType == 1) { + if (minV > 0.0) { + float K = safeRange / minV; + float clampedX = max(x, 0.0); + float num = log(1.0 + clampedX * K); + float denom = log(1.0 + K); + return denom != 0.0 ? num / denom : x; + } else { + float safeEps = max(eps, 0.000001); + if (x <= safeEps) return 0.0; + float xRel = (x - safeEps) / (1.0 - safeEps); + float K = (1.0 - safeEps) / safeEps; + float num = log(1.0 + xRel * K); + float denom = log(1.0 + K); + return denom != 0.0 ? num / denom : x; + } + } else if (scaleType == 2) { + float clampedX = max(x, 0.0); + float num = log(1.0 + clampedX * safeRange); + float denom = log(1.0 + safeRange); + return denom != 0.0 ? num / denom : x; + } else if (scaleType == 3) { + float safeC = max(c, 0.00001); + float clampedX = max(x, 0.0); + float num = log(safeC + clampedX * safeRange) - log(safeC); + float denom = log(safeC + safeRange) - log(safeC); + return denom != 0.0 ? num / denom : x; + } else if (scaleType == 4) { + return sign(x) * sqrt(abs(x)); + } else if (scaleType == 5) { + float clampedX = max(x, 0.0); + float expR = min(safeRange, 10.0); + float num = exp(clampedX * expR) - 1.0; + float denom = exp(expR) - 1.0; + return denom != 0.0 ? num / denom : x; + } else if (scaleType == 6) { + float v0 = evalCustomExpr(0.0); + float v1 = evalCustomExpr(1.0); + float vx = evalCustomExpr(x); + float denom = v1 - v0; + return denom != 0.0 ? (vx - v0) / denom : vx; + } + return x; +} + +bool isMasked(float maskSample, int maskVal) { + if (maskVal == 0) return false; + return maskVal == 1 ? (maskSample < 0.5) : (maskSample >= 0.5); +} + +vec4 getLowclipColor(bool useLow, vec4 lowclipVal, vec4 fallbackVal) { + return useLow ? lowclipVal : fallbackVal; +} + +vec4 getHighclipColor(bool useHigh, vec4 highclipVal, vec4 fallbackVal) { + return useHigh ? highclipVal : fallbackVal; +} + +void accumulateSample(inout vec4 accumColor, inout float alphaAcc, vec4 colorSample) { + if (colorSample.a > 0.0) { + accumColor.rgb += (1.0 - alphaAcc) * colorSample.a * colorSample.rgb; + alphaAcc += colorSample.a * (1.0 - alphaAcc); + } +} + +vec4 evaluateColorScale( + float val, + vec2 bounds, + float fillVal, + vec3 nanC, + float nanA, + sampler2D colormap, + float cScaleVal, + float cOffsetVal, + int scaleType, + float logC, + float eps, + float dataR, + float minV, + vec4 lowClipVal, + vec4 highClipVal, + bool useLow, + bool useHigh +) { + bool isNaN = (val == 1.0) || (abs(val - fillVal) < 0.005); + if (isNaN) { + return vec4(nanC, nanA); + } + if (val < bounds.x) { + return getLowclipColor(useLow, lowClipVal, vec4(0.0)); + } + if (val > bounds.y) { + return getHighclipColor(useHigh, highClipVal, vec4(0.0)); + } + + float range = max(bounds.y - bounds.x, 0.0001); + float normS = clamp((val - bounds.x) / range, 0.0, 1.0); + + vec4 cmapMinColor = vec4(texture(colormap, vec2(0.5 / 256.0, 0.5)).rgb, 1.0); + vec4 cmapMaxColor = vec4(texture(colormap, vec2(254.5 / 256.0, 0.5)).rgb, 1.0); + + if (scaleType == 1 && normS < eps) { + return getLowclipColor(useLow, lowClipVal, cmapMinColor); + } + + float scaledS = applyColorScale(normS, scaleType, logC, eps, dataR, minV); + float rawSampLoc = scaledS * cScaleVal + cOffsetVal; + + if (rawSampLoc < 0.0) { + return getLowclipColor(useLow, lowClipVal, cmapMinColor); + } + if (rawSampLoc > 1.0) { + return getHighclipColor(useHigh, highClipVal, cmapMaxColor); + } + + float sampU = (0.5 + clamp(rawSampLoc, 0.0, 1.0) * 254.0) / 256.0; + return vec4(texture(colormap, vec2(sampU, 0.5)).rgb, 1.0); +} + +vec4 evaluateVolumeColorScale( + float val, + vec2 bounds, + float fillVal, + vec3 nanC, + float nanA, + sampler2D colormap, + float cScaleVal, + float cOffsetVal, + int scaleType, + float logC, + float eps, + float dataR, + float minV, + vec4 lowClipVal, + vec4 highClipVal, + bool useLow, + bool useHigh, + bool useClipScale, + float transparency, + float opacityMag +) { + bool isNaN = (val == 1.0) || (abs(val - fillVal) < 0.005); + if (isNaN) { + return vec4(nanC, pow(nanA, 5.0)); + } + if (val < bounds.x) { + return useLow ? lowClipVal : vec4(0.0); + } + if (val > bounds.y) { + return useHigh ? highClipVal : vec4(0.0); + } + + float range = max(bounds.y - bounds.x, 0.0001); + float normS = clamp((val - bounds.x) / range, 0.0, 1.0); + + if (scaleType == 1 && normS < eps) { + return useLow ? lowClipVal : vec4(0.0); + } + + float scaledS = applyColorScale(normS, scaleType, logC, eps, dataR, minV); + float rawSampLoc = scaledS * cScaleVal + cOffsetVal; + + if (rawSampLoc < 0.0) { + return useLow ? lowClipVal : vec4(0.0); + } + if (rawSampLoc > 1.0) { + return useHigh ? highClipVal : vec4(0.0); + } + + float sampU = (0.5 + clamp(rawSampLoc, 0.0, 1.0) * 254.0) / 256.0; + vec4 col = texture(colormap, vec2(sampU, 0.5)); + float alpha; + if (useClipScale) { + float normalizedOpacity = clamp(scaledS, 0.0, 1.0); + alpha = pow(max(normalizedOpacity, 0.001), transparency * opacityMag); + } else { + alpha = pow(max(rawSampLoc, 0.001), transparency * opacityMag); + } + return vec4(col.rgb, alpha); +} diff --git a/src/components/textures/shaders/flatFrag.glsl b/src/components/textures/shaders/flatFrag.glsl index 1ac1b15e..f44484d9 100644 --- a/src/components/textures/shaders/flatFrag.glsl +++ b/src/components/textures/shaders/flatFrag.glsl @@ -1,118 +1,115 @@ -//This is for Flat Textures but with 3D textures to sample from i,e; animation - -#ifdef IS_FLAT - uniform sampler2D map[12]; -#else - uniform sampler3D map[12]; -#endif -uniform sampler2D maskTexture; -uniform sampler2D cmap; -uniform sampler2D remapTexture; -uniform vec3 textureDepths; - - -uniform float cOffset; -uniform float cScale; -uniform float animateProg; -uniform float nanAlpha; -uniform vec3 nanColor; -uniform vec2 latBounds; -uniform vec2 lonBounds; -uniform vec2 threshold; -uniform int maskValue; -uniform float fillValue; - -in vec2 vUv; -out vec4 Color; -#define epsilon 0.0001 -#define PI 3.14159265 - -vec2 realCoords(vec2 uv){ - vec2 normalizedLon = lonBounds/2./PI+0.5; - vec2 normalizedLat = latBounds/PI+0.5; - float lonScale = normalizedLon.y-normalizedLon.x; - float latScale = normalizedLat.y-normalizedLat.x; - - float u = uv.x * lonScale + normalizedLon.x; - float v = uv.y * latScale + normalizedLat.x; - - return vec2(u, v); -} - -float sample1( - #ifdef IS_FLAT - vec2 p, - #else - vec3 p, - #endif - int index - ) { // Shader doesn't support dynamic indexing so we gotta use switching - if (index == 0) return texture(map[0], p).r; - else if (index == 1) return texture(map[1], p).r; - else if (index == 2) return texture(map[2], p).r; - else if (index == 3) return texture(map[3], p).r; - else if (index == 4) return texture(map[4], p).r; - else if (index == 5) return texture(map[5], p).r; - else if (index == 6) return texture(map[6], p).r; - else if (index == 7) return texture(map[7], p).r; - else if (index == 8) return texture(map[8], p).r; - else if (index == 9) return texture(map[9], p).r; - else if (index == 10) return texture(map[10], p).r; - else if (index == 11) return texture(map[11], p).r; - // else if (index == 12) return texture(map[12], p).r; - // else if (index == 13) return texture(map[13], p).r; - else return 0.0; -} - -void main() { - if (maskValue != 0){ - vec2 maskUV = realCoords(vUv); - float mask = texture(maskTexture, maskUV).r; - bool cond = maskValue == 1 ? mask<0.5 : mask>=0.5; - if (cond){ - Color = vec4(nanColor, 1.); - Color.a = nanAlpha; - return; - } - } - int zStepSize = int(textureDepths.y) * int(textureDepths.x); - int yStepSize = int(textureDepths.x); - #ifdef IS_FLAT - vec2 texCoord = vUv; - #ifdef REPROJECT - vec3 remap = texture(remapTexture,texCoord.xy).rgb; - texCoord.xy = remap.rg; - if (remap.b < 0.5) discard; - #endif - texCoord.xy = clamp(texCoord.xy, vec2(0.0), 1. - vec2(epsilon)); // This prevent the very edges from looping around and causing line artifacts - ivec2 idx = clamp(ivec2(texCoord * textureDepths.xy), ivec2(0), ivec2(textureDepths.xy) - 1); - int textureIdx = idx.y * yStepSize + idx.x; - vec2 localCoord = texCoord * (textureDepths.xy); // Scale up - #else - vec3 texCoord = vec3(vUv, animateProg); - #ifdef REPROJECT - vec3 remap = texture(remapTexture,texCoord.xy).rgb; - texCoord.xy = remap.rg; - if (remap.b < 0.5) discard; - #endif - texCoord.xy = clamp(texCoord.xy, vec2(0.0), 1. - vec2(epsilon)); // This prevent the very edges from looping around and causing line artifacts - ivec3 idx = clamp(ivec3(texCoord * textureDepths), ivec3(0), ivec3(textureDepths) - 1); - int textureIdx = idx.z * zStepSize + idx.y * yStepSize + idx.x; - vec3 localCoord = texCoord * (textureDepths); // Scale up - #endif - localCoord = fract(localCoord); - - float strength = sample1(localCoord, textureIdx); - bool valid = (strength >= threshold.x) && (strength <= threshold.y); - if (!valid || abs(strength - fillValue) < 0.005){ - Color = vec4(0.); - return; - } - bool isNaN = strength == 1.; - float sampLoc = isNaN ? strength: (strength)*cScale; - sampLoc = isNaN ? strength : min(sampLoc+cOffset,0.995); - Color = isNaN ? vec4(nanColor, nanAlpha) : vec4(texture2D(cmap, vec2(sampLoc, 0.5)).rgb, 1.); - // float check = float(texture(remapTexture,texCoord.xy).g >= 0.); - // Color = vec4(check, 0., 0. , 1.); - // Color = vec4(texture(remapTexture,texCoord.xy).rg, 0. , 1.); +//This is for Flat Textures but with 3D textures to sample from i,e; animation + +#ifdef IS_FLAT + uniform sampler2D map[12]; +#else + uniform sampler3D map[12]; +#endif +uniform sampler2D maskTexture; +uniform sampler2D cmap; +uniform sampler2D remapTexture; +uniform vec3 textureDepths; + +uniform float cOffset; +uniform float cScale; +uniform float animateProg; +uniform float nanAlpha; +uniform vec3 nanColor; +uniform vec2 latBounds; +uniform vec2 lonBounds; +uniform vec2 threshold; +uniform int maskValue; +uniform float fillValue; +uniform int colorScale; +uniform float logConstant; +uniform float logEps; +uniform float dataRange; +uniform float minVal; +uniform vec4 lowclip; +uniform vec4 highclip; +uniform bool useLowclip; +uniform bool useHighclip; + +in vec2 vUv; +out vec4 Color; +#define epsilon 0.0001 +#define PI 3.14159265 + +// APPLY_COLOR_SCALE + +vec2 realCoords(vec2 uv){ + vec2 normalizedLon = lonBounds/2./PI+0.5; + vec2 normalizedLat = latBounds/PI+0.5; + float lonScale = normalizedLon.y-normalizedLon.x; + float latScale = normalizedLat.y-normalizedLat.x; + + float u = uv.x * lonScale + normalizedLon.x; + float v = uv.y * latScale + normalizedLat.x; + + return vec2(u, v); +} + +float sample1( + #ifdef IS_FLAT + vec2 p, + #else + vec3 p, + #endif + int index + ) { // Shader doesn't support dynamic indexing so we gotta use switching + if (index == 0) return texture(map[0], p).r; + else if (index == 1) return texture(map[1], p).r; + else if (index == 2) return texture(map[2], p).r; + else if (index == 3) return texture(map[3], p).r; + else if (index == 4) return texture(map[4], p).r; + else if (index == 5) return texture(map[5], p).r; + else if (index == 6) return texture(map[6], p).r; + else if (index == 7) return texture(map[7], p).r; + else if (index == 8) return texture(map[8], p).r; + else if (index == 9) return texture(map[9], p).r; + else if (index == 10) return texture(map[10], p).r; + else if (index == 11) return texture(map[11], p).r; + else return 0.0; +} + +void main() { + if (isMasked(texture(maskTexture, realCoords(vUv)).r, maskValue)) { + Color = vec4(nanColor, nanAlpha); + return; + } + + int zStepSize = int(textureDepths.y) * int(textureDepths.x); + int yStepSize = int(textureDepths.x); + #ifdef IS_FLAT + vec2 texCoord = vUv; + #ifdef REPROJECT + vec3 remap = texture(remapTexture,texCoord.xy).rgb; + texCoord.xy = remap.rg; + if (remap.b < 0.5) discard; + #endif + texCoord.xy = clamp(texCoord.xy, vec2(0.0), 1. - vec2(epsilon)); // This prevent the very edges from looping around and causing line artifacts + ivec2 idx = clamp(ivec2(texCoord * textureDepths.xy), ivec2(0), ivec2(textureDepths.xy) - 1); + int textureIdx = idx.y * yStepSize + idx.x; + vec2 localCoord = texCoord * (textureDepths.xy); // Scale up + #else + vec3 texCoord = vec3(vUv, animateProg); + #ifdef REPROJECT + vec3 remap = texture(remapTexture,texCoord.xy).rgb; + texCoord.xy = remap.rg; + if (remap.b < 0.5) discard; + #endif + texCoord.xy = clamp(texCoord.xy, vec2(0.0), 1. - vec2(epsilon)); // This prevent the very edges from looping around and causing line artifacts + ivec3 idx = clamp(ivec3(texCoord * textureDepths), ivec3(0), ivec3(textureDepths) - 1); + int textureIdx = idx.z * zStepSize + idx.y * yStepSize + idx.x; + vec3 localCoord = texCoord * (textureDepths); // Scale up + #endif + localCoord = fract(localCoord); + + float strength = sample1(localCoord, textureIdx); + + Color = evaluateColorScale( + strength, threshold, fillValue, nanColor, nanAlpha, + cmap, cScale, cOffset, colorScale, logConstant, logEps, + dataRange, minVal, lowclip, highclip, useLowclip, useHighclip + ); } \ No newline at end of file diff --git a/src/components/textures/shaders/fragmentOpt.glsl b/src/components/textures/shaders/fragmentOpt.glsl index da95a2a0..3c025f0a 100644 --- a/src/components/textures/shaders/fragmentOpt.glsl +++ b/src/components/textures/shaders/fragmentOpt.glsl @@ -30,10 +30,21 @@ uniform float fillValue; uniform int maskValue; uniform vec2 latBounds; uniform vec2 lonBounds; +uniform int colorScale; +uniform float logConstant; +uniform float logEps; +uniform float dataRange; +uniform float minVal; +uniform vec4 lowclip; +uniform vec4 highclip; +uniform bool useLowclip; +uniform bool useHighclip; #define epsilon 0.000001 #define pi 3.1415926535 +// APPLY_COLOR_SCALE + vec2 hitBox(vec3 orig, vec3 dir) { vec3 box_min = vec3(-(scale * 0.5)); @@ -117,15 +128,9 @@ void main() { continue; } vec3 texCoord = p / scale + 0.5; - if (maskValue != 0){ - vec2 newV = texCoord.xy; - vec2 realV = realCoords(newV); - float mask = texture(maskTexture, realV).r; - bool cond = maskValue == 1 ? mask<0.5 : mask>=0.5; - if (cond){ - t += useCoarseStep ? coarseDelta : fineDelta; - continue; - } + if (isMasked(texture(maskTexture, realCoords(texCoord.xy)).r, maskValue)) { + t += useCoarseStep ? coarseDelta : fineDelta; + continue; } texCoord.z = mod(texCoord.z + animateProg, 1.0001); texCoord = clamp(texCoord, vec3(0.0), 1. - vec3(epsilon)); // This prevents the the very end of the dimensions having floating point errors @@ -136,7 +141,7 @@ void main() { localCoord = fract(localCoord); float d = sample1(localCoord, textureIdx); - bool cond = (d > threshold.x) && (d < threshold.y+.01); //We skip over nans if the transparency is enabled + bool cond = (d >= threshold.x) && (d <= threshold.y + 0.01) || ((d < threshold.x && useLowclip) || (d > threshold.y + 0.01 && useHighclip)); if (cond) { // Hit something interesting - switch to fine stepping @@ -147,24 +152,13 @@ void main() { t -= coarseDelta; continue; } - if (d == 1. || abs(d - fillValue) < 0.005){ - accumColor.rgb += (1.0 - alphaAcc) * pow(nanAlpha, 5.) * nanColor.rgb; - alphaAcc += pow(nanAlpha, 5.); - } - else{ - float sampLoc = d*cScale; - sampLoc = min(sampLoc+cOffset,0.99); - vec4 col = texture(cmap, vec2(sampLoc, 0.5)); - float alpha; - if (useClipScale){ - float normalizedOpacity = clamp((sampLoc - threshold.x) / (threshold.y - threshold.x), 0.0, 1.0); - alpha = pow(max(normalizedOpacity, 0.001), transparency*opacityMag); - } else { - alpha = pow(max(sampLoc, 0.001), transparency*opacityMag); - } - accumColor.rgb += (1.0 - alphaAcc) * alpha * col.rgb; - alphaAcc += alpha * (1.0 - alphaAcc); - } + vec4 sampleCol = evaluateVolumeColorScale( + d, threshold, fillValue, nanColor, nanAlpha, + cmap, cScale, cOffset, colorScale, logConstant, logEps, + dataRange, minVal, lowclip, highclip, useLowclip, useHighclip, + useClipScale, transparency, opacityMag + ); + accumulateSample(accumColor, alphaAcc, sampleCol); if (alphaAcc >= 1.0) break; diff --git a/src/components/textures/shaders/index.ts b/src/components/textures/shaders/index.ts index 7f4d9f61..bd786a0e 100644 --- a/src/components/textures/shaders/index.ts +++ b/src/components/textures/shaders/index.ts @@ -1,16 +1,29 @@ -import pointFrag from './pointFrag.glsl'; +import colorPipelineChunk from './colorPipeline.glsl'; +import pointFragRaw from './pointFrag.glsl'; import pointVert from './pointVertex.glsl'; import vertexShader from './vertex.glsl'; -import fragmentShader from './volFragment.glsl'; -import sphereFrag from './sphereFrag.glsl'; +import fragmentShaderRaw from './volFragment.glsl'; +import sphereFragRaw from './sphereFrag.glsl'; import sphereVertex from './sphereVertex.glsl'; -import fragOpt from './fragmentOpt.glsl'; -import bordersFrag from './bordersFrag.glsl' -import flatFrag from './flatFrag.glsl' +import fragOptRaw from './fragmentOpt.glsl'; +import bordersFrag from './bordersFrag.glsl'; +import flatFragRaw from './flatFrag.glsl'; import sphereBlocksVert from './sphereBlocksVert.glsl'; -import sphereBlocksFrag from './sphereBlocksFrag.glsl'; +import sphereBlocksFragRaw from './sphereBlocksFrag.glsl'; import orthoVertex from './orthoVertex.glsl'; import flatBlocksVert from './flatBlocksVert.glsl'; + +function injectColorScale(shaderStr: string): string { + return shaderStr.replace('// APPLY_COLOR_SCALE', colorPipelineChunk); +} + +const pointFrag = injectColorScale(pointFragRaw); +const fragmentShader = injectColorScale(fragmentShaderRaw); +const sphereFrag = injectColorScale(sphereFragRaw); +const fragOpt = injectColorScale(fragOptRaw); +const flatFrag = injectColorScale(flatFragRaw); +const sphereBlocksFrag = injectColorScale(sphereBlocksFragRaw); + export { pointFrag, pointVert, diff --git a/src/components/textures/shaders/pointFrag.glsl b/src/components/textures/shaders/pointFrag.glsl index 75d25920..ba73175e 100644 --- a/src/components/textures/shaders/pointFrag.glsl +++ b/src/components/textures/shaders/pointFrag.glsl @@ -1,15 +1,30 @@ -out vec4 Color; - -in float vValue; - -uniform sampler2D cmap; -uniform float cScale; -uniform float cOffset; - -void main() { - float sampLoc = vValue == 1. ? vValue : (vValue - 0.5)*cScale + 0.5; - sampLoc = vValue == 1. ? vValue : min(sampLoc+cOffset,0.99); - vec4 color = texture(cmap, vec2(sampLoc, 0.5)); - color.a = 1.; - Color = color; -} +out vec4 Color; + +in float vValue; + +uniform sampler2D cmap; +uniform float cScale; +uniform float cOffset; +uniform vec2 valueRange; +uniform float fillValue; +uniform vec3 nanColor; +uniform float nanAlpha; +uniform int colorScale; +uniform float logConstant; +uniform float logEps; +uniform float dataRange; +uniform float minVal; +uniform vec4 lowclip; +uniform vec4 highclip; +uniform bool useLowclip; +uniform bool useHighclip; + +// APPLY_COLOR_SCALE + +void main() { + Color = evaluateColorScale( + vValue, valueRange, fillValue, nanColor, nanAlpha, + cmap, cScale, cOffset, colorScale, logConstant, logEps, + dataRange, minVal, lowclip, highclip, useLowclip, useHighclip + ); +} diff --git a/src/components/textures/shaders/sphereBlocksFrag.glsl b/src/components/textures/shaders/sphereBlocksFrag.glsl index a0c9f8ac..ed8677fe 100644 --- a/src/components/textures/shaders/sphereBlocksFrag.glsl +++ b/src/components/textures/shaders/sphereBlocksFrag.glsl @@ -1,20 +1,30 @@ -uniform sampler2D cmap; -uniform float cOffset; -uniform float cScale; - -in float vStrength; - -out vec4 Color; - - -void main() { - float strength = vStrength; - - strength *= cScale; - strength = min(strength+cOffset,0.996); - - vec3 sampColor = texture(cmap, vec2(strength, 0.5)).rgb; - - Color = vec4(sampColor, 1.0); - +uniform sampler2D cmap; +uniform float cOffset; +uniform float cScale; +uniform vec2 threshold; +uniform float fillValue; +uniform vec3 nanColor; +uniform float nanAlpha; +uniform int colorScale; +uniform float logConstant; +uniform float logEps; +uniform float dataRange; +uniform float minVal; +uniform vec4 lowclip; +uniform vec4 highclip; +uniform bool useLowclip; +uniform bool useHighclip; + +in float vStrength; + +out vec4 Color; + +// APPLY_COLOR_SCALE + +void main() { + Color = evaluateColorScale( + vStrength, threshold, fillValue, nanColor, nanAlpha, + cmap, cScale, cOffset, colorScale, logConstant, logEps, + dataRange, minVal, lowclip, highclip, useLowclip, useHighclip + ); } \ No newline at end of file diff --git a/src/components/textures/shaders/sphereFrag.glsl b/src/components/textures/shaders/sphereFrag.glsl index 7e66f1e1..13634610 100644 --- a/src/components/textures/shaders/sphereFrag.glsl +++ b/src/components/textures/shaders/sphereFrag.glsl @@ -24,10 +24,21 @@ uniform vec3 nanColor; uniform float nanAlpha; uniform float fillValue; uniform int maskValue; +uniform int colorScale; +uniform float logConstant; +uniform float logEps; +uniform float dataRange; +uniform float minVal; +uniform vec4 lowclip; +uniform vec4 highclip; +uniform bool useLowclip; +uniform bool useHighclip; #define pi 3.141592653 #define epsilon 0.0001 +// APPLY_COLOR_SCALE + vec2 giveUV(vec3 position){ vec3 n = normalize(position); float latitude = asin(n.y); @@ -113,30 +124,17 @@ void main(){ #endif localCoord = fract(localCoord); float strength = sample1(localCoord, textureIdx); - bool valid = (strength >= threshold.x) && (strength <= threshold.y); - if (!valid){ - color = vec4(0.); - return; - } - bool isNaN = strength == 1. || abs(strength - fillValue) < 0.005; - strength = isNaN ? strength : (strength)*cScale; - strength = isNaN ? strength : min(strength+cOffset,0.99); - color = isNaN ? vec4(nanColor, nanAlpha) : texture(cmap, vec2(strength, 0.5)); - if (!isNaN){ - color.a = 1.; - } - if (maskValue != 0){ - vec2 maskUV = giveMaskUV(aPosition); - float mask = texture(maskTexture, maskUV).r; - bool cond = maskValue == 1 ? mask<0.5 : mask>=0.5; - if (cond){ - color = vec4(nanColor, 1.); - color.a = nanAlpha; - } + + color = evaluateColorScale( + strength, threshold, fillValue, nanColor, nanAlpha, + cmap, cScale, cOffset, colorScale, logConstant, logEps, + dataRange, minVal, lowclip, highclip, useLowclip, useHighclip + ); + + if (isMasked(texture(maskTexture, giveMaskUV(aPosition)).r, maskValue)) { + color = vec4(nanColor, nanAlpha); } } else { - color = vec4(nanColor, 1.); - color.a = nanAlpha; + color = vec4(nanColor, nanAlpha); } - } \ No newline at end of file diff --git a/src/components/textures/shaders/volFragment.glsl b/src/components/textures/shaders/volFragment.glsl index 112489d6..d0e06e2e 100644 --- a/src/components/textures/shaders/volFragment.glsl +++ b/src/components/textures/shaders/volFragment.glsl @@ -30,10 +30,21 @@ uniform float fillValue; uniform int maskValue; uniform vec2 latBounds; uniform vec2 lonBounds; +uniform int colorScale; +uniform float logConstant; +uniform float logEps; +uniform float dataRange; +uniform float minVal; +uniform vec4 lowclip; +uniform vec4 highclip; +uniform bool useLowclip; +uniform bool useHighclip; #define epsilon 0.000001 #define pi 3.1415926535 +// APPLY_COLOR_SCALE + vec2 hitBox(vec3 orig, vec3 dir) { vec3 box_min = vec3(-(scale * 0.5)); vec3 box_max = vec3(scale * 0.5); @@ -117,14 +128,8 @@ void main() { if (remap.b < 0.5) {continue;} #endif - if (maskValue != 0){ - vec2 newV = texCoord.xy; - vec2 realV = realCoords(newV); - float mask = texture(maskTexture, realV).r; - bool cond = maskValue == 1 ? mask<0.5 : mask>=0.5; - if (cond){ - continue; - } + if (isMasked(texture(maskTexture, realCoords(texCoord.xy)).r, maskValue)) { + continue; } texCoord.z = mod(texCoord.z + animateProg, 1.0001); texCoord = clamp(texCoord, vec3(0.0), 1. - vec3(epsilon)); // This prevents the the very end of the dimensions having floating point errors @@ -135,30 +140,15 @@ void main() { localCoord = fract(localCoord); float d = sample1(localCoord, textureIdx); - bool cond = (d >= threshold.x) && (d <= threshold.y); - - if (cond) { - if (d == 1. || abs(d - fillValue) < 0.005){ - accumColor.rgb += (1.0 - alphaAcc) * pow(nanAlpha, 5.) * nanColor.rgb; - alphaAcc += pow(nanAlpha, 5.); - } - else{ - float sampLoc = d*cScale; - sampLoc = min(sampLoc+cOffset,0.99); - vec4 col = texture(cmap, vec2(sampLoc, 0.5)); - float alpha; - if (useClipScale){ - float normalizedOpacity = clamp((sampLoc - threshold.x) / (threshold.y - threshold.x), 0.0, 1.0); - alpha = pow(max(normalizedOpacity, 0.001), transparency*opacityMag); - } else { - alpha = pow(max(sampLoc, 0.001), transparency*opacityMag); - } - accumColor.rgb += (1.0 - alphaAcc) * alpha * col.rgb; - alphaAcc += alpha * (1.0 - alphaAcc); - } - - if (alphaAcc >= 1.0) break; - } + vec4 sampleCol = evaluateVolumeColorScale( + d, threshold, fillValue, nanColor, nanAlpha, + cmap, cScale, cOffset, colorScale, logConstant, logEps, + dataRange, minVal, lowclip, highclip, useLowclip, useHighclip, + useClipScale, transparency, opacityMag + ); + accumulateSample(accumColor, alphaAcc, sampleCol); + + if (alphaAcc >= 1.0) break; } accumColor.a = alphaAcc; // Set the final accumulated alpha color = accumColor; diff --git a/src/components/ui/Colorbar.tsx b/src/components/ui/Colorbar.tsx index 561423e3..10bf714b 100644 --- a/src/components/ui/Colorbar.tsx +++ b/src/components/ui/Colorbar.tsx @@ -9,9 +9,11 @@ import { useGlobalStore } from '@/GlobalStates/GlobalStore'; import { usePlotStore } from '@/GlobalStates/PlotStore'; import { useShallow } from 'zustand/shallow' import './css/Colorbar.css' -import { linspace } from '@/utils/HelperFuncs'; +import { linspace, getLogEps } from '@/utils/HelperFuncs'; import Metadata from "./MetaData"; +import { applyColorScale, invertColorScale } from "@/components/textures"; + const operationMap = { // Reductions Mean: "Mean", @@ -54,11 +56,27 @@ const Colorbar = ({units, metadata, valueScales} : {units: string, metadata: Rec variable: state.variable, scalingFactor: state.scalingFactor }))) - const {cScale, cOffset, setCScale, setCOffset} = usePlotStore(useShallow(state => ({ + const { + cScale, cOffset, setCScale, setCOffset, + colorScale, logConstant, lowclip, highclip, + useLowclip, useHighclip, isCategorical, categoricalMode, + numBins, setNumBins, uniqueCategories + } = usePlotStore(useShallow(state => ({ cScale: state.cScale, cOffset: state.cOffset, setCScale: state.setCScale, - setCOffset: state.setCOffset + setCOffset: state.setCOffset, + colorScale: state.colorScale, + logConstant: state.logConstant, + lowclip: state.lowclip, + highclip: state.highclip, + useLowclip: state.useLowclip, + useHighclip: state.useHighclip, + isCategorical: state.isCategorical, + categoricalMode: state.categoricalMode, + numBins: state.numBins, + setNumBins: state.setNumBins, + uniqueCategories: state.uniqueCategories, }))) const {variable2, analysisMode, operation, kernelOperation, execute} = useAnalysisStore(useShallow(state => ({ variable2: state.variable2, @@ -77,6 +95,8 @@ const Colorbar = ({units, metadata, valueScales} : {units: string, metadata: Rec }),[valueScales]) const range = origMax - origMin + const logEps = useMemo(() => getLogEps(origMin, origMax, (valueScales as any).minPosVal), [origMin, origMax, valueScales]); + const [tickCount, setTickCount] = useState(5) const [newMin, setNewMin] = useState(origMin) const [newMax, setNewMax] = useState(origMax) @@ -100,11 +120,25 @@ const Colorbar = ({units, metadata, valueScales} : {units: string, metadata: Rec return colors },[colormap]) + const effectiveMin = useMemo(() => { + if (colorScale === 'log(x)' && origMin <= 0) { + return origMin + logEps * range; + } + return newMin; + }, [colorScale, origMin, newMin, logEps, range]); + + const dataRange = useMemo(() => Math.max(range, 0.000001), [range]); + const [locs, vals] = useMemo(()=>{ - const locs = linspace(0, 100, tickCount) - const vals = linspace(newMin, newMax, tickCount) + const locs = linspace(0, 100, tickCount); + const startVal = (colorScale === 'log(x)' && origMin <= 0) ? effectiveMin : newMin; + const vals = locs.map(loc => { + const t = loc / 100; + const invT = invertColorScale(t, colorScale, logConstant, logEps, dataRange, newMin); + return startVal + (newMax - startVal) * invT; + }); return [locs, vals] - },[ tickCount, newMin, newMax]) + },[ tickCount, newMin, newMax, colorScale, logConstant, logEps, dataRange, effectiveMin, origMin]) // Mouse move handler const handleMouseMove = (e: MouseEvent) => { @@ -161,25 +195,83 @@ const Colorbar = ({units, metadata, valueScales} : {units: string, metadata: Rec setCScale(scale) },[newMin, newMax]) - useEffect(()=>{ // Update internal vals when global vals change - setDisplayMin(Num2String(origMin*Math.pow(10, scalingFactor??0))) + useEffect(()=>{ // Update internal vals when global dataset bounds change + const startMin = (colorScale === 'log(x)' && origMin <= 0) ? (origMin + logEps * range) : origMin; + setDisplayMin(Num2String(startMin*Math.pow(10, scalingFactor??0))) setDisplayMax(Num2String(origMax*Math.pow(10, scalingFactor??0))) setNewMin(origMin) setNewMax(origMax) - },[origMax, origMin, scalingFactor]) + },[origMax, origMin, scalingFactor, colorScale, logEps, range]) + + const catTicks = useMemo(() => { + if (!isCategorical) return []; + const factor = Math.pow(10, scalingFactor ?? 0); + if (categoricalMode === 'unique') { + const cats = uniqueCategories.length > 0 ? uniqueCategories : Array.from({ length: 10 }, (_, i) => origMin + (i + 0.5) * (origMax - origMin) / 10); + const n = cats.length; + return cats.map((catVal, i) => ({ + left: ((i + 0.5) / n) * 100, + label: Num2String(catVal * factor), + })); + } else { + const n = Math.max(2, numBins); + const binWidth = (newMax - newMin) / n; + return Array.from({ length: n }, (_, i) => { + const midVal = newMin + (i + 0.5) * binWidth; + return { + left: ((i + 0.5) / n) * 100, + label: Num2String(midVal * factor), + }; + }); + } + }, [isCategorical, categoricalMode, uniqueCategories, numBins, newMin, newMax, origMin, origMax, scalingFactor]); useEffect(() => { - if (canvasRef.current) { - const canvas = canvasRef.current; - const ctx = canvas.getContext("2d"); - if (ctx){ - colors.forEach((color, index) => { - ctx.fillStyle = color; - ctx.fillRect(index*2, 0, 2, 24); // Each color is 1px wide and 50px tall - }); + if (canvasRef.current && colors.length > 0) { + const canvas = canvasRef.current; + const ctx = canvas.getContext("2d"); + if (ctx) { + const width = canvas.width; + const height = canvas.height; + ctx.clearRect(0, 0, width, height); + + const numPaletteColors = colors.length > 1 ? colors.length - 1 : colors.length; + + if (isCategorical) { + const N = categoricalMode === 'unique' ? (uniqueCategories.length || 10) : numBins; + const blockWidth = width / N; + for (let i = 0; i < N; i++) { + const t = (i + 0.5) / N; + const colorIndex = Math.min(numPaletteColors - 1, Math.floor(t * numPaletteColors)); + ctx.fillStyle = colors[colorIndex] || '#000'; + ctx.fillRect(i * blockWidth, 0, blockWidth, height); + } + } else { + const clipWidth = 14; + const startX = useLowclip ? clipWidth : 0; + const endX = useHighclip ? width - clipWidth : width; + const mainWidth = endX - startX; + + if (useLowclip) { + ctx.fillStyle = lowclip; + ctx.fillRect(0, 0, clipWidth, height); + } + + for (let x = 0; x < mainWidth; x++) { + const normX = x / mainWidth; + const colorIndex = Math.min(numPaletteColors - 1, Math.floor(normX * numPaletteColors)); + ctx.fillStyle = colors[colorIndex] || '#000'; + ctx.fillRect(startX + x, 0, 1, height); + } + + if (useHighclip) { + ctx.fillStyle = highclip; + ctx.fillRect(endX, 0, clipWidth, height); + } + } } } - }, [colors]); + }, [colors, colorScale, logConstant, logEps, dataRange, newMin, lowclip, highclip, useLowclip, useHighclip, isCategorical, categoricalMode, numBins, uniqueCategories]); const analysisString = useMemo(()=>{ if (analysisMode){ const twoVar = variable2 != "Default"; @@ -195,54 +287,79 @@ const Colorbar = ({units, metadata, valueScales} : {units: string, metadata: Rec return ( <>
- {setDisplayMin(e.target.value); setNewMin(parseFloat(e.target.value)/Math.pow(10, scalingFactor??0))}} - onBlur={e=>setDisplayMin(Num2String(newMin*Math.pow(10, scalingFactor??0)))} - /> - {Array.from({length: tickCount}).map((_val,idx)=>{ - if (idx == 0 || idx == tickCount-1){ - return null - } - return (

{Num2String(vals[idx]*Math.pow(10,scalingFactor??0))} -

)} + {isCategorical ? ( + catTicks.map((tick, idx) => ( +

+ {tick.label} +

+ )) + ) : ( + <> + { + setDisplayMin(e.target.value); + const val = parseFloat(e.target.value); + if (!isNaN(val)) setNewMin(val / Math.pow(10, scalingFactor ?? 0)); + }} + onBlur={e=>setDisplayMin(Num2String(newMin*Math.pow(10, scalingFactor??0)))} + /> + {Array.from({length: tickCount}).map((_val,idx)=>{ + if (idx == 0 || idx == tickCount-1){ + return null + } + return (

{Num2String(vals[idx]*Math.pow(10,scalingFactor??0))} +

)} + )} + { + setDisplayMax(e.target.value); + const val = parseFloat(e.target.value); + if (!isNaN(val)) setNewMax(val / Math.pow(10, scalingFactor ?? 0)); + }} + onBlur={e=>setDisplayMax(Num2String(newMax*Math.pow(10, scalingFactor??0)))} + /> + )} - { - setDisplayMax(e.target.value); - setNewMax(parseFloat(e.target.value)/Math.pow(10, scalingFactor??0)) - }} - onBlur={e=>setDisplayMax(Num2String(newMax*Math.pow(10, scalingFactor??0)))} - />

{ const [hoveredCmap, setHoveredCmap] = useState(null); const [selectedCategory, setSelectedCategory] = useState(''); const [showNames, setShowNames] = useState(true); - const { colormap, setColormap, colormapName, flipColormap, setColormapName, setFlipColormap } = useGlobalStore( + const { colormap, setColormap, colormapName, flipColormap, setColormapName, setFlipColormap, valueScales } = useGlobalStore( useShallow((state) => ({ setColormap: state.setColormap, colormap: state.colormap, @@ -45,8 +47,52 @@ const Colormaps = () => { flipColormap: state.flipColormap, setColormapName: state.setColormapName, setFlipColormap: state.setFlipColormap, + valueScales: state.valueScales, })) ); + const { + colorScale, setColorScale, logConstant, setLogConstant, + lowclip, setLowclip, highclip, setHighclip, + useLowclip, setUseLowclip, useHighclip, setUseHighclip, + isCategorical, setIsCategorical, categoricalMode, setCategoricalMode, + numBins, setNumBins, uniqueCategories + } = usePlotStore( + useShallow((state) => ({ + colorScale: state.colorScale, + setColorScale: state.setColorScale, + logConstant: state.logConstant, + setLogConstant: state.setLogConstant, + lowclip: state.lowclip, + setLowclip: state.setLowclip, + highclip: state.highclip, + setHighclip: state.setHighclip, + useLowclip: state.useLowclip, + setUseLowclip: state.setUseLowclip, + useHighclip: state.useHighclip, + setUseHighclip: state.setUseHighclip, + isCategorical: state.isCategorical, + setIsCategorical: state.setIsCategorical, + categoricalMode: state.categoricalMode, + setCategoricalMode: state.setCategoricalMode, + numBins: state.numBins, + setNumBins: state.setNumBins, + uniqueCategories: state.uniqueCategories, + })) + ); + const isBuiltinScale = useMemo(() => { + return COLOR_SCALE_OPTIONS.some((opt) => opt.value === colorScale && opt.value !== 'custom'); + }, [colorScale]); + + const [customExprInput, setCustomExprInput] = useState(() => { + return !isBuiltinScale ? colorScale : 'x > 0 ? x/2 : x'; + }); + + useEffect(() => { + if (!isBuiltinScale) { + setCustomExprInput(colorScale); + } + }, [colorScale, isBuiltinScale]); + const [popoverSide, setPopoverSide] = useState<"left" | "top">("left"); const [prevColormapName, setPrevColormapName] = useState(colormapName || ''); @@ -109,6 +155,14 @@ const Colormaps = () => { previousTextureRef.current = colormap; }, [colormap]); + const activeBins = useMemo(() => { + if (!isCategorical) return 10; + if (categoricalMode === 'unique') { + return Math.max(2, uniqueCategories.length || 10); + } + return numBins; + }, [isCategorical, categoricalMode, uniqueCategories, numBins]); + useEffect(() => { if (hoveredCmap !== null) { // Show hovered colormap preview @@ -119,11 +173,13 @@ const Colormaps = () => { 1, "#000000", 0, - flipColormapRef.current + flipColormapRef.current, + isCategorical, + activeBins ) ); - } else if (lastHoveredCmap.current !== null) { - // Mouse left hover: revert to selected colormap + } else { + // Revert/update to active colormap setColormap( GetColorMapTexture( previousTextureRef.current, @@ -131,12 +187,14 @@ const Colormaps = () => { 1, "#000000", 0, - flipColormapRef.current + flipColormapRef.current, + isCategorical, + activeBins ) ); } lastHoveredCmap.current = hoveredCmap; - }, [hoveredCmap, setColormap]); + }, [hoveredCmap, setColormap, colormapName, flipColormap, isCategorical, activeBins]); useEffect(() => { const handleResize = () => { @@ -147,6 +205,11 @@ const Colormaps = () => { return () => window.removeEventListener("resize", handleResize); }, []); + const minVal = valueScales?.minVal ?? 0; + const maxVal = valueScales?.maxVal ?? 1; + const dataRange = useMemo(() => Math.max(maxVal - minVal, 0.000001), [maxVal, minVal]); + const logEps = useMemo(() => getLogEps(minVal, maxVal, (valueScales as any)?.minPosVal), [minVal, maxVal, valueScales]); + return (

@@ -159,7 +222,9 @@ const Colormaps = () => { size="icon" className='cursor-pointer hover:scale-90 transition-transform duration-100 ease-out rounded-full cmap-trigger' style={{ - backgroundImage: getColormapGradientCss((colormapName === 'Default' ? 'Spectral' : colormapName) || 'Spectral'), + backgroundImage: getColormapGradientCss( + (colormapName === 'Default' ? 'Spectral' : colormapName) || 'Spectral' + ), backgroundRepeat: 'no-repeat', backgroundPosition: 'center', backgroundSize: '100% 100%', @@ -278,6 +343,186 @@ const Colormaps = () => {
+
+ {/* Categorical Plotting Section */} +
+
+ +
+ + {isCategorical && ( +
+
+ Mode: + +
+ + {categoricalMode === 'unique' ? ( +
+ {uniqueCategories.length > 0 + ? `${uniqueCategories.length} categories auto-detected` + : 'Auto-detecting categories...'} +
+ ) : ( +
+ Bins (N): + setNumBins(Math.max(2, Math.min(50, parseInt(e.target.value) || 10)))} + className="w-16 h-7 px-2 text-xs rounded border border-neutral-300 dark:border-neutral-700 bg-transparent text-right font-mono" + /> +
+ )} +
+ )} +
+ + {/* Continuous Color Scale & Clipping Controls (Disabled in Categorical Mode) */} + {isCategorical ? ( +
+ Color scale transformations and low/high clips are disabled in categorical mode. +
+ ) : ( + <> +
+ Color Scale: + +
+ {(!isBuiltinScale || colorScale === 'custom') && ( +
+ Custom Expression f(x): +
+ setCustomExprInput(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter' && customExprInput.trim()) { + setColorScale(customExprInput.trim()); + } + }} + placeholder="e.g. x > 0 ? x/2 : x" + className="h-7 text-xs font-mono flex-1 px-2" + /> + +
+
+ )} + {colorScale === 'log(x+c)' && ( +
+ c = + setLogConstant(parseFloat(e.target.value) || 1.0)} + className="w-20 px-2 py-0.5 text-xs rounded border border-neutral-300 dark:border-neutral-700 bg-transparent" + /> +
+ )} +
+ + {useLowclip && ( + setLowclip(e.target.value)} + className="w-6 h-6 p-0 border-0 rounded cursor-pointer" + /> + )} +
+
+ + {useHighclip && ( + setHighclip(e.target.value)} + className="w-6 h-6 p-0 border-0 rounded cursor-pointer" + /> + )} +
+ + )} +
+ maxVal ? array[i] : maxVal + const v = array[i]; + if (!isNaN(v)) { + if (v < minVal) minVal = v; + if (v > maxVal) maxVal = v; + if (v > 0 && v < minPosVal) minPosVal = v; + } } - return [minVal,maxVal] + return [minVal, maxVal, minPosVal === Infinity ? undefined : minPosVal]; +} + +export function getLogEps(minVal: number, maxVal: number, minPosVal?: number): number { + if (minVal > 0) return 0.000001; + const range = maxVal - minVal; + if (range <= 0) return 0.0001; + const posMin = (minPosVal !== undefined && minPosVal > 0) + ? Math.max(minPosVal, maxVal * 1e-6) + : Math.max(maxVal * 1e-4, 1e-6); + const xPos = (posMin - minVal) / range; + return Math.max(Math.min(xPos, 0.5), 0.000001); } export async function getVariablesOptions(variablesPromise: Promise | undefined) { @@ -437,4 +453,15 @@ export function calculateStrides( return shape.reduce((a: number, b: number, i: number) => a * (i > idx ? b : 1), 1) }) return newStrides +} + +export function parseColorToVec4(hex: string, alpha = 1.0): THREE.Vector4 { + if (!hex) return new THREE.Vector4(0, 0, 0, alpha); + const cleanHex = hex.replace('#', ''); + const bigint = parseInt(cleanHex, 16); + if (isNaN(bigint)) return new THREE.Vector4(0, 0, 0, alpha); + const r = ((bigint >> 16) & 255) / 255; + const g = ((bigint >> 8) & 255) / 255; + const b = (bigint & 255) / 255; + return new THREE.Vector4(r, g, b, alpha); } \ No newline at end of file diff --git a/src/utils/plotUniforms.ts b/src/utils/plotUniforms.ts new file mode 100644 index 00000000..d4159589 --- /dev/null +++ b/src/utils/plotUniforms.ts @@ -0,0 +1,156 @@ +import * as THREE from 'three'; +import { useGlobalStore } from '@/GlobalStates/GlobalStore'; +import { usePlotStore } from '@/GlobalStates/PlotStore'; +import { useShallow } from 'zustand/shallow'; +import { useCoordBounds } from '@/hooks/useCoordBounds'; +import { deg2rad, getLogEps, parseColorToVec4 } from './HelperFuncs'; +import { colorScaleToId, exprToGLSL } from '@/components/textures'; + +export interface CommonUniformParams { + colormap: THREE.Texture; + cOffset: number; + cScale: number; + animProg: number; + nanColor: string; + nanTransparency: number; + colorScale: string; + logConstant: number; + valueScales: { minVal: number; maxVal: number; minPosVal?: number }; + lowclip: string; + highclip: string; + useLowclip: boolean; + useHighclip: boolean; + latBounds?: [number, number] | number[]; + lonBounds?: [number, number] | number[]; + valueRange?: [number, number] | number[]; + fillValue?: number; + maskValue?: number; + maskTexture?: THREE.Texture | null; + isCategorical?: boolean; +} + +export function useCommonPlotState() { + const globalState = useGlobalStore( + useShallow((state) => ({ + colormap: state.colormap, + isFlat: state.isFlat, + valueScales: state.valueScales, + flipY: state.flipY, + dataShape: state.dataShape, + textureArrayDepths: state.textureArrayDepths, + axisDimArrays: state.axisDimArrays, + remapTexture: state.remapTexture, + shape: state.shape, + })) + ); + + const plotState = usePlotStore( + useShallow((state) => ({ + cOffset: state.cOffset, + cScale: state.cScale, + animProg: state.animProg, + nanColor: state.nanColor, + nanTransparency: state.nanTransparency, + fillValue: state.fillValue, + maskValue: state.maskValue, + maskTexture: state.maskTexture, + valueRange: state.valueRange, + colorScale: state.colorScale, + logConstant: state.logConstant, + lowclip: state.lowclip, + highclip: state.highclip, + useLowclip: state.useLowclip, + useHighclip: state.useHighclip, + isCategorical: state.isCategorical, + })) + ); + + const { latBounds, lonBounds } = useCoordBounds(); + + return { + ...globalState, + ...plotState, + latBounds, + lonBounds, + }; +} + +export function createCommonUniforms(p: CommonUniformParams) { + const minV = p.valueScales?.minVal ?? 0; + const maxV = p.valueScales?.maxVal ?? 1; + const minPosV = (p.valueScales as any)?.minPosVal; + const latB = p.latBounds ?? [0, 0]; + const lonB = p.lonBounds ?? [0, 0]; + const valR = p.valueRange ?? [0, 1]; + + const scaleId = p.isCategorical ? 0 : colorScaleToId(p.colorScale); + + return { + cmap: { value: p.colormap }, + cOffset: { value: p.cOffset }, + cScale: { value: p.cScale }, + animateProg: { value: p.animProg }, + nanColor: { value: new THREE.Color(p.nanColor) }, + nanAlpha: { value: 1 - p.nanTransparency }, + fillValue: { value: p.fillValue ?? NaN }, + maskValue: { value: p.maskValue ?? 0 }, + maskTexture: { value: p.maskTexture ?? null }, + threshold: { value: new THREE.Vector2(valR[0], valR[1]) }, + valueRange: { value: new THREE.Vector2(valR[0], valR[1]) }, + latBounds: { value: new THREE.Vector2(deg2rad(latB[0]), deg2rad(latB[1])) }, + lonBounds: { value: new THREE.Vector2(deg2rad(lonB[0]), deg2rad(lonB[1])) }, + colorScale: { value: scaleId }, + logConstant: { value: p.logConstant }, + logEps: { value: getLogEps(minV, maxV, minPosV) }, + dataRange: { value: Math.max(maxV - minV, 0.000001) }, + minVal: { value: minV }, + lowclip: { value: parseColorToVec4(p.lowclip) }, + highclip: { value: parseColorToVec4(p.highclip) }, + useLowclip: { value: p.isCategorical ? false : p.useLowclip }, + useHighclip: { value: p.isCategorical ? false : p.useHighclip }, + }; +} + +export function updateCommonUniforms(material: THREE.ShaderMaterial, p: CommonUniformParams) { + if (!material) return; + const u = material.uniforms; + if (!u) return; + + const minV = p.valueScales?.minVal ?? 0; + const maxV = p.valueScales?.maxVal ?? 1; + const minPosV = (p.valueScales as any)?.minPosVal; + const latB = p.latBounds ?? [0, 0]; + const lonB = p.lonBounds ?? [0, 0]; + const valR = p.valueRange ?? [0, 1]; + + if (u.cmap) u.cmap.value = p.colormap; + if (u.cOffset) u.cOffset.value = p.cOffset; + if (u.cScale) u.cScale.value = p.cScale; + if (u.animateProg) u.animateProg.value = p.animProg; + if (u.nanColor) u.nanColor.value.set(p.nanColor); + if (u.nanAlpha) u.nanAlpha.value = 1 - p.nanTransparency; + if (u.fillValue) u.fillValue.value = p.fillValue ?? NaN; + if (u.maskValue && p.maskValue !== undefined) u.maskValue.value = p.maskValue; + if (u.maskTexture && p.maskTexture !== undefined) u.maskTexture.value = p.maskTexture; + if (u.threshold && valR) u.threshold.value.set(valR[0], valR[1]); + if (u.valueRange && valR) u.valueRange.value.set(valR[0], valR[1]); + if (u.latBounds && latB) u.latBounds.value.set(deg2rad(latB[0]), deg2rad(latB[1])); + if (u.lonBounds && lonB) u.lonBounds.value.set(deg2rad(lonB[0]), deg2rad(lonB[1])); + + const scaleId = p.isCategorical ? 0 : colorScaleToId(p.colorScale); + if (u.colorScale) u.colorScale.value = scaleId; + const customDef = scaleId === 6 ? exprToGLSL(p.colorScale) : '(val)'; + if (material.defines['CUSTOM_EXPR(val)'] !== customDef) { + material.defines['CUSTOM_EXPR(val)'] = customDef; + material.needsUpdate = true; + } + + if (u.logConstant) u.logConstant.value = p.logConstant; + if (u.logEps) u.logEps.value = getLogEps(minV, maxV, minPosV); + if (u.dataRange) u.dataRange.value = Math.max(maxV - minV, 0.000001); + if (u.minVal) u.minVal.value = minV; + if (u.lowclip) u.lowclip.value = parseColorToVec4(p.lowclip); + if (u.highclip) u.highclip.value = parseColorToVec4(p.highclip); + if (u.useLowclip) u.useLowclip.value = p.isCategorical ? false : p.useLowclip; + if (u.useHighclip) u.useHighclip.value = p.isCategorical ? false : p.useHighclip; +}