diff --git a/CHANGELOG.md b/CHANGELOG.md index ae6f04b132e0e7ba17ac8ec817ffa3394352e292..e2778a2013e89d6ffe54dff326bacf1010bf1611 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] - `meshes` extension: Fixed a bug in mesh visualization (show backfaces when opacity < 1) +- Add color quick select control to Volume controls ## [v3.28.0] - 2022-12-20 diff --git a/src/mol-plugin-state/builder/structure.ts b/src/mol-plugin-state/builder/structure.ts index 1c0d9db00b47815798d7386c60992b5b7aa9953f..5049e3d84d8cc5d8d31c9d4d478bf4f181f2f45d 100644 --- a/src/mol-plugin-state/builder/structure.ts +++ b/src/mol-plugin-state/builder/structure.ts @@ -83,7 +83,7 @@ export class StructureBuilder { return unitcell.commit({ revertOnError: true }); } - createStructure(modelRef: StateObjectRef<SO.Molecule.Model>, params?: RootStructureDefinition.Params, initialState?: Partial<StateTransform.State>) { + createStructure(modelRef: StateObjectRef<SO.Molecule.Model>, params?: RootStructureDefinition.Params, initialState?: Partial<StateTransform.State>, tags?: string | string[]) { const state = this.dataState; if (!params) { @@ -95,7 +95,7 @@ export class StructureBuilder { } const structure = state.build().to(modelRef) - .apply(StateTransforms.Model.StructureFromModel, { type: params || { name: 'assembly', params: { } } }, { state: initialState }); + .apply(StateTransforms.Model.StructureFromModel, { type: params || { name: 'assembly', params: { } } }, { state: initialState, tags }); return structure.commit({ revertOnError: true }); } diff --git a/src/mol-plugin-ui/controls/color.tsx b/src/mol-plugin-ui/controls/color.tsx index 5e5cc779216bde9d534de02bc405a48dfc5bc6a0..d537c631be74c701a97bb79aeb11e8f34315141a 100644 --- a/src/mol-plugin-ui/controls/color.tsx +++ b/src/mol-plugin-ui/controls/color.tsx @@ -14,9 +14,9 @@ import { ParamProps } from './parameters'; import { TextInput, Button, ControlRow } from './common'; import { DefaultColorSwatch } from '../../mol-util/color/swatches'; -export class CombinedColorControl extends React.PureComponent<ParamProps<PD.Color>, { isExpanded: boolean, lightness: number }> { +export class CombinedColorControl extends React.PureComponent<ParamProps<PD.Color> & { hideNameRow?: boolean }, { isExpanded: boolean, lightness: number }> { state = { - isExpanded: !!this.props.param.isExpanded, + isExpanded: !!this.props.param.isExpanded || !!this.props.hideNameRow, lightness: 0 }; @@ -72,21 +72,30 @@ export class CombinedColorControl extends React.PureComponent<ParamProps<PD.Colo render() { const label = this.props.param.label || camelCaseToWords(this.props.name); const [r, g, b] = Color.toRgb(this.props.value); + + const inner = <> + {this.swatch()} + <ControlRow label='RGB' className='msp-control-label-short' control={<div style={{ display: 'flex', textAlignLast: 'center', left: '80px' }}> + <TextInput onChange={this.onR} numeric value={r} delayMs={250} style={{ order: 1, flex: '1 1 auto', minWidth: 0 }} className='msp-form-control' onEnter={this.props.onEnter} blurOnEnter={true} blurOnEscape={true} /> + <TextInput onChange={this.onG} numeric value={g} delayMs={250} style={{ order: 2, flex: '1 1 auto', minWidth: 0 }} className='msp-form-control' onEnter={this.props.onEnter} blurOnEnter={true} blurOnEscape={true} /> + <TextInput onChange={this.onB} numeric value={b} delayMs={250} style={{ order: 3, flex: '1 1 auto', minWidth: 0 }} className='msp-form-control' onEnter={this.props.onEnter} blurOnEnter={true} blurOnEscape={true} /> + </div>} /> + <div style={{ display: 'flex', textAlignLast: 'center' }}> + <Button onClick={this.onLighten} style={{ order: 1, flex: '1 1 auto', minWidth: 0 }} className='msp-form-control'>Lighten</Button> + <Button onClick={this.onDarken} style={{ order: 1, flex: '1 1 auto', minWidth: 0 }} className='msp-form-control'>Darken</Button> + </div> + </>; + + if (this.props.hideNameRow) { + return inner; + } + return <> <ControlRow title={this.props.param.description} label={label} control={<Button onClick={this.toggleExpanded} inline className='msp-combined-color-button' style={{ background: Color.toStyle(this.props.value) }} />} /> {this.state.isExpanded && <div className='msp-control-offset'> - {this.swatch()} - <ControlRow label='RGB' className='msp-control-label-short' control={<div style={{ display: 'flex', textAlignLast: 'center', left: '80px' }}> - <TextInput onChange={this.onR} numeric value={r} delayMs={250} style={{ order: 1, flex: '1 1 auto', minWidth: 0 }} className='msp-form-control' onEnter={this.props.onEnter} blurOnEnter={true} blurOnEscape={true} /> - <TextInput onChange={this.onG} numeric value={g} delayMs={250} style={{ order: 2, flex: '1 1 auto', minWidth: 0 }} className='msp-form-control' onEnter={this.props.onEnter} blurOnEnter={true} blurOnEscape={true} /> - <TextInput onChange={this.onB} numeric value={b} delayMs={250} style={{ order: 3, flex: '1 1 auto', minWidth: 0 }} className='msp-form-control' onEnter={this.props.onEnter} blurOnEnter={true} blurOnEscape={true} /> - </div>}/> - <div style={{ display: 'flex', textAlignLast: 'center' }}> - <Button onClick={this.onLighten} style={{ order: 1, flex: '1 1 auto', minWidth: 0 }} className='msp-form-control'>Lighten</Button> - <Button onClick={this.onDarken} style={{ order: 1, flex: '1 1 auto', minWidth: 0 }} className='msp-form-control'>Darken</Button> - </div> + {inner} </div>} </>; } diff --git a/src/mol-plugin-ui/structure/volume.tsx b/src/mol-plugin-ui/structure/volume.tsx index 9c5666147f195ea764b3bd3e72671e201a7a4b91..8c8aaea6d99c9eaf89583a32b65616be7b692109 100644 --- a/src/mol-plugin-ui/structure/volume.tsx +++ b/src/mol-plugin-ui/structure/volume.tsx @@ -15,15 +15,19 @@ import { InitVolumeStreaming } from '../../mol-plugin/behavior/dynamic/volume-st import { State, StateObjectCell, StateObjectSelector, StateSelection, StateTransform } from '../../mol-state'; import { CollapsableControls, CollapsableState, PurePluginUIComponent } from '../base'; import { ActionMenu } from '../controls/action-menu'; -import { Button, ExpandGroup, IconButton } from '../controls/common'; +import { Button, ControlGroup, ExpandGroup, IconButton } from '../controls/common'; import { ApplyActionControl } from '../state/apply-action'; import { UpdateTransformControl } from '../state/update-transform'; import { BindingsHelp } from '../viewport/help'; import { PluginCommands } from '../../mol-plugin/commands'; -import { BlurOnSvg, ErrorSvg, CheckSvg, AddSvg, VisibilityOffOutlinedSvg, VisibilityOutlinedSvg, DeleteOutlinedSvg, MoreHorizSvg } from '../controls/icons'; +import { BlurOnSvg, ErrorSvg, CheckSvg, AddSvg, VisibilityOffOutlinedSvg, VisibilityOutlinedSvg, DeleteOutlinedSvg, MoreHorizSvg, CloseSvg } from '../controls/icons'; import { PluginStateObject } from '../../mol-plugin-state/objects'; import { StateTransforms } from '../../mol-plugin-state/transforms'; import { createVolumeRepresentationParams } from '../../mol-plugin-state/helpers/volume-representation-params'; +import { Color } from '../../mol-util/color'; +import { ParamDefinition } from '../../mol-util/param-definition'; +import { CombinedColorControl } from '../controls/color'; +import { ParamOnChange } from '../controls/parameters'; interface VolumeStreamingControlState extends CollapsableState { isBusy: boolean @@ -260,7 +264,7 @@ export class VolumeSourceControls extends CollapsableControls<{}, VolumeSourceCo } } -type VolumeRepresentationEntryActions = 'update' +type VolumeRepresentationEntryActions = 'update' | 'select-color' class VolumeRepresentationControls extends PurePluginUIComponent<{ representation: VolumeRepresentationRef }, { action?: VolumeRepresentationEntryActions }> { state = { action: void 0 as VolumeRepresentationEntryActions | undefined }; @@ -279,6 +283,10 @@ class VolumeRepresentationControls extends PurePluginUIComponent<{ representatio this.plugin.managers.volume.hierarchy.toggleVisibility([this.props.representation]); }; + toggleColor = () => { + this.setState({ action: this.state.action === 'select-color' ? undefined : 'select-color' }); + }; + toggleUpdate = () => this.setState({ action: this.state.action === 'update' ? void 0 : 'update' }); highlight = (e: React.MouseEvent<HTMLElement>) => { @@ -299,10 +307,30 @@ class VolumeRepresentationControls extends PurePluginUIComponent<{ representatio if (lociList) this.plugin.managers.camera.focusLoci(lociList, { extraRadius: 1 }); }; + private get color() { + const repr = this.props.representation.cell; + const isUniform = repr.transform.params?.colorTheme.name === 'uniform'; + if (!isUniform) return void 0; + return repr.transform.params?.colorTheme.params.value; + } + + updateColor: ParamOnChange = ({ value }) => { + const t = this.props.representation.cell.transform; + return this.plugin.build().to(t.ref).update({ + ...t.params, + colorTheme: { + name: 'uniform', + params: { value } + }, + }).commit(); + }; + render() { const repr = this.props.representation.cell; + const color = this.color; return <> <div className='msp-flex-row'> + {color !== void 0 && <Button style={{ backgroundColor: Color.toStyle(color), minWidth: 32, width: 32 }} onClick={this.toggleColor} />} <Button noOverflow className='msp-control-button-label' title={`${repr.obj?.label}. Click to focus.`} onClick={this.focus} onMouseEnter={this.highlight} onMouseLeave={this.clearHighlight} style={{ textAlign: 'left' }}> {repr.obj?.label} <small className='msp-25-lower-contrast-text' style={{ float: 'right' }}>{repr.obj?.description}</small> @@ -314,6 +342,14 @@ class VolumeRepresentationControls extends PurePluginUIComponent<{ representatio {this.state.action === 'update' && !!repr.parent && <div style={{ marginBottom: '6px' }} className='msp-accent-offset'> <UpdateTransformControl state={repr.parent} transform={repr.transform} customHeader='none' noMargin /> </div>} + {this.state.action === 'select-color' && color !== void 0 && <div style={{ marginBottom: '6px', marginTop: 1 }} className='msp-accent-offset'> + <ControlGroup header='Select Color' initialExpanded={true} hideExpander={true} hideOffset={true} onHeaderClick={this.toggleColor} + topRightIcon={CloseSvg} noTopMargin childrenClassName='msp-viewport-controls-panel-controls'> + <CombinedColorControl param={VolumeColorParam} value={this.color} onChange={this.updateColor} name='color' hideNameRow /> + </ControlGroup> + </div>} </>; } -} \ No newline at end of file +} + +const VolumeColorParam = ParamDefinition.Color(Color(0x121212)); diff --git a/src/mol-script/transpilers/helper.ts b/src/mol-script/transpilers/helper.ts index 03b3d583aedf184e59595d37f5361b9b72c4167b..6a245626b05c14ff6dfe65cbdfb0019aa071e822 100644 --- a/src/mol-script/transpilers/helper.ts +++ b/src/mol-script/transpilers/helper.ts @@ -13,10 +13,9 @@ import { MolScriptBuilder } from '../../mol-script/language/builder'; const B = MolScriptBuilder; import { Expression } from '../language/expression'; import { KeywordDict, PropertyDict, FunctionDict, OperatorList } from './types'; +import { escapeRegExp } from '../../mol-util/string'; -export function escapeRegExp(s: String) { - return String(s).replace(/[\\^$*+?.()|[\]{}]/g, '\\$&'); -} +export { escapeRegExp }; // Takes a parser for the prefix operator, and a parser for the base thing being // parsed, and parses as many occurrences as possible of the prefix operator. diff --git a/src/mol-util/string.ts b/src/mol-util/string.ts index 73604b6933872173944d72344de79d46031a6368..1db2675730b0e3ee81bc18983ba0be8ef9265b44 100644 --- a/src/mol-util/string.ts +++ b/src/mol-util/string.ts @@ -93,4 +93,13 @@ export function trimCharEnd(str: string, char: string) { /** Simple function to strip tags from a string */ export function stripTags(str: string) { return str.replace(/<\/?[^>]+>/g, ''); +} + +/** + * Escape string for use in Javascript regex + * + * From https://stackoverflow.com/questions/3446170/escape-string-for-use-in-javascript-regex/6969486#6969486 + */ +export function escapeRegExp(str: string) { + return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string } \ No newline at end of file