diff --git a/CHANGELOG.md b/CHANGELOG.md index 5cef38d6f9c31c589e0dd89fa3ba0abbb53f3c11..cbb48a69548cff0d1731947ff9c99e33a968f4e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Note that since we don't clearly distinguish between a public and private interf ## [Unreleased] - Make `PluginContext.initContainer` checkered canvas background optional +- `by-volume-value` theme (coloring or arbitrary geometries by user-selected volume) ## [v3.23.0] - 2022-10-19 diff --git a/src/mol-plugin-ui/controls/parameters.tsx b/src/mol-plugin-ui/controls/parameters.tsx index 4a59801972ee5f62b538f035d5bd5560cb676646..1d7cf96da53279b4e1a714ba289b82eb079ddd9d 100644 --- a/src/mol-plugin-ui/controls/parameters.tsx +++ b/src/mol-plugin-ui/controls/parameters.tsx @@ -18,7 +18,7 @@ import { getPrecision } from '../../mol-util/number'; import { ParamDefinition as PD } from '../../mol-util/param-definition'; import { ParamMapping } from '../../mol-util/param-mapping'; import { camelCaseToWords } from '../../mol-util/string'; -import { PluginUIComponent } from '../base'; +import { PluginReactContext, PluginUIComponent } from '../base'; import { PluginUIContext } from '../context'; import { ActionMenu } from './action-menu'; import { ColorOptions, ColorValueOption, CombinedColorControl } from './color'; @@ -505,10 +505,12 @@ export class ValueRefControl extends React.PureComponent<ParamProps<PD.ValueRef< toggle = () => this.setState({ showOptions: !this.state.showOptions }); - items = memoizeLatest((param: PD.ValueRef) => ActionMenu.createItemsFromSelectOptions(param.getOptions())); + private get items() { + return ActionMenu.createItemsFromSelectOptions(this.props.param.getOptions(this.context)); + } renderControl() { - const items = this.items(this.props.param); + const items = this.items; const current = this.props.value.ref ? ActionMenu.findItem(items, this.props.value.ref) : void 0; const label = current ? current.label @@ -521,7 +523,7 @@ export class ValueRefControl extends React.PureComponent<ParamProps<PD.ValueRef< renderAddOn() { if (!this.state.showOptions) return null; - const items = this.items(this.props.param); + const items = this.items; const current = ActionMenu.findItem(items, this.props.value.ref); return <ActionMenu items={items} current={current} onSelect={this.onSelect} />; @@ -539,6 +541,7 @@ export class ValueRefControl extends React.PureComponent<ParamProps<PD.ValueRef< }); } } +ValueRefControl.contextType = PluginReactContext; export class IntervalControl extends React.PureComponent<ParamProps<PD.Interval>, { isExpanded: boolean }> { state = { isExpanded: false }; diff --git a/src/mol-theme/color.ts b/src/mol-theme/color.ts index 84af7dad2d2a34f23cfcb4f270fc068dda79f949..05ed9a020c3a35f3aff308ea29b66d0c4dd1df86 100644 --- a/src/mol-theme/color.ts +++ b/src/mol-theme/color.ts @@ -40,6 +40,7 @@ import { VolumeValueColorThemeProvider } from './color/volume-value'; import { Vec3, Vec4 } from '../mol-math/linear-algebra'; import { ModelIndexColorThemeProvider } from './color/model-index'; import { StructureIndexColorThemeProvider } from './color/structure-index'; +import { ByVolumeValueColorThemeProvider } from './color/by-volume-value'; export type LocationColor = (location: Location, isSecondary: boolean) => Color @@ -152,6 +153,7 @@ namespace ColorTheme { 'unit-index': UnitIndexColorThemeProvider, 'uniform': UniformColorThemeProvider, 'volume-value': VolumeValueColorThemeProvider, + 'by-volume-value': ByVolumeValueColorThemeProvider, }; type _BuiltIn = typeof BuiltIn export type BuiltIn = keyof _BuiltIn diff --git a/src/mol-theme/color/by-volume-value.ts b/src/mol-theme/color/by-volume-value.ts new file mode 100644 index 0000000000000000000000000000000000000000..46567f33de8cf829a9e6c7075afcb9129cb4a993 --- /dev/null +++ b/src/mol-theme/color/by-volume-value.ts @@ -0,0 +1,126 @@ +/** + * Copyright (c) 2022 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import { Color, ColorScale } from '../../mol-util/color'; +import { Location } from '../../mol-model/location'; +import { ColorTheme } from '../color'; +import { ParamDefinition as PD } from '../../mol-util/param-definition'; +import { ThemeDataContext } from '../theme'; +import { Grid, Volume } from '../../mol-model/volume'; +import { type PluginContext } from '../../mol-plugin/context'; +import { isPositionLocation } from '../../mol-geo/util/location-iterator'; +import { Mat4, Vec3 } from '../../mol-math/linear-algebra'; +import { lerp } from '../../mol-math/interpolate'; + +const Description = `Assigns a color based volume value at a given vertex.`; + +export const ByVolumeValueColorThemeParams = { + volume: PD.ValueRef<Volume>( + (ctx: PluginContext) => { + const volumes = ctx.state.data.selectQ(q => q.root.subtree().filter(c => Volume.is(c.obj?.data))); + return volumes.map(v => [v.transform.ref, v.obj?.label ?? '<unknown>'] as [string, string]); + }, + (ref, getData) => getData(ref), + ), + domain: PD.MappedStatic('auto', { + custom: PD.Interval([-1, 1]), + auto: PD.EmptyGroup() + }), + list: PD.ColorList('red-white-blue', { presetKind: 'scale' }), + defaultColor: PD.Color(Color(0xcccccc)) +}; +export type ByVolumeValueColorThemeParams = typeof ByVolumeValueColorThemeParams + +export function ByVolumeValueColorTheme(ctx: ThemeDataContext, props: PD.Values<ByVolumeValueColorThemeParams>): ColorTheme<ByVolumeValueColorThemeParams> { + let volume: Volume | undefined; + try { + volume = props.volume.getValue(); + } catch { + // .getValue() is resolved during state reconciliation => would throw from UI + } + + const domain: [number, number] = props.domain.name === 'custom' ? props.domain.params : volume ? [volume.grid.stats.min, volume.grid.stats.max] : [-1, 1]; + const scale = ColorScale.create({ domain, listOrName: props.list.colors }); + + // NOTE: this will currently not work with GPU iso-surfaces since it requires vertex coloring + // TODO: create texture to be able to do the sampling on the GPU + + let color; + if (volume) { + const cartnToGrid = Grid.getGridToCartesianTransform(volume.grid); + Mat4.invert(cartnToGrid, cartnToGrid); + const gridCoords = Vec3(); + + const { dimensions, get } = volume.grid.cells.space; + const data = volume.grid.cells.data; + + const [mi, mj, mk] = dimensions; + + color = (location: Location): Color => { + if (!isPositionLocation(location)) { + return props.defaultColor; + } + + Vec3.copy(gridCoords, location.position); + Vec3.transformMat4(gridCoords, gridCoords, cartnToGrid); + + const i = Math.floor(gridCoords[0]); + const j = Math.floor(gridCoords[1]); + const k = Math.floor(gridCoords[2]); + + if (i < 0 || i >= mi || j < 0 || j >= mj || k < 0 || k >= mk) { + return props.defaultColor; + } + + const u = gridCoords[0] - i; + const v = gridCoords[1] - j; + const w = gridCoords[2] - k; + + // Tri-linear interpolation for the value + const ii = Math.min(i + 1, mi - 1); + const jj = Math.min(j + 1, mj - 1); + const kk = Math.min(k + 1, mk - 1); + + let a = get(data, i, j, k); + let b = get(data, ii, j, k); + let c = get(data, i, jj, k); + let d = get(data, ii, jj, k); + const x = lerp(lerp(a, b, u), lerp(c, d, u), v); + + a = get(data, i, j, kk); + b = get(data, ii, j, kk); + c = get(data, i, jj, kk); + d = get(data, ii, jj, kk); + const y = lerp(lerp(a, b, u), lerp(c, d, u), v); + + const value = lerp(x, y, w); + + return scale.color(value); + }; + } else { + color = () => props.defaultColor; + } + + return { + factory: ByVolumeValueColorTheme, + granularity: 'vertex', + preferSmoothing: true, + color, + props, + description: Description, + legend: scale ? scale.legend : undefined + }; +} + +export const ByVolumeValueColorThemeProvider: ColorTheme.Provider<ByVolumeValueColorThemeParams, 'by-volume-value'> = { + name: 'by-volume-value', + label: 'By Volume Value', + category: ColorTheme.Category.Misc, + factory: ByVolumeValueColorTheme, + getParams: () => ByVolumeValueColorThemeParams, + defaultValues: PD.getDefaultValues(ByVolumeValueColorThemeParams), + isApplicable: (ctx: ThemeDataContext) => true, // TODO +}; \ No newline at end of file diff --git a/src/mol-util/param-definition.ts b/src/mol-util/param-definition.ts index a9697f5f454d63753f657bba63ff2ab32771eb3c..5516f909767c683b15f454206eae413ea082d4c0 100644 --- a/src/mol-util/param-definition.ts +++ b/src/mol-util/param-definition.ts @@ -290,9 +290,9 @@ export namespace ParamDefinition { // getValue needs to be assigned by a runtime because it might not be serializable export interface ValueRef<T = any> extends Base<{ ref: string, getValue: () => T }> { type: 'value-ref', - resolveRef: (ref: string) => T, + resolveRef: (ref: string, getData: (ref: string) => any) => T, // a provider because the list changes over time - getOptions: () => Select<string>['options'], + getOptions: (ctx: any) => Select<string>['options'], } export function ValueRef<T>(getOptions: ValueRef['getOptions'], resolveRef: ValueRef<T>['resolveRef'], info?: Info & { defaultRef?: string }) { return setInfo<ValueRef<T>>({ type: 'value-ref', defaultValue: { ref: info?.defaultRef ?? '', getValue: unsetGetValue as any }, getOptions, resolveRef }, info); @@ -365,8 +365,8 @@ export namespace ParamDefinition { return d as Values<T>; } - function _resolveRef(resolve: (ref: string) => any, ref: string) { - return () => resolve(ref); + function _resolveRef(resolve: (ref: string, getData: (ref: string) => any) => any, ref: string, getData: (ref: string) => any) { + return () => resolve(ref, getData); } function resolveRefValue(p: Any, value: any, getData: (ref: string) => any) { @@ -375,11 +375,11 @@ export namespace ParamDefinition { if (p.type === 'value-ref') { const v = value as ValueRef['defaultValue']; if (!v.ref) v.getValue = () => { throw new Error('Unset ref in ValueRef value.'); }; - else v.getValue = _resolveRef(p.resolveRef, v.ref); + else v.getValue = _resolveRef(p.resolveRef, v.ref, getData); } else if (p.type === 'data-ref') { const v = value as ValueRef['defaultValue']; if (!v.ref) v.getValue = () => { throw new Error('Unset ref in ValueRef value.'); }; - else v.getValue = _resolveRef(getData, v.ref); + else v.getValue = _resolveRef(getData, v.ref, getData); } else if (p.type === 'group') { resolveRefs(p.params, value, getData); } else if (p.type === 'mapped') {