From 607284585c5f159ad47080d12d0e11c9c96d4048 Mon Sep 17 00:00:00 2001 From: Alexander Rose <alex.rose@rcsb.org> Date: Thu, 12 Mar 2020 17:46:08 -0700 Subject: [PATCH] intersect modifier for ui selections --- .../structure/structure/element/loci.ts | 16 +++++++++++++++ src/mol-plugin-state/manager/interactivity.ts | 13 ++++++++++-- .../manager/structure/selection.ts | 20 ++++++++++++++++++- src/mol-plugin-ui/controls/common.tsx | 4 ++-- src/mol-plugin-ui/structure/measurements.tsx | 3 +-- src/mol-plugin-ui/structure/selection.tsx | 15 ++++++++------ 6 files changed, 58 insertions(+), 13 deletions(-) diff --git a/src/mol-model/structure/structure/element/loci.ts b/src/mol-model/structure/structure/element/loci.ts index 3e9c13faa..cf3ab7569 100644 --- a/src/mol-model/structure/structure/element/loci.ts +++ b/src/mol-model/structure/structure/element/loci.ts @@ -188,6 +188,22 @@ export namespace Loci { return Loci(xs.structure, elements); } + /** Intersect `xs` and `ys` */ + export function intersect(xs: Loci, ys: Loci): Loci { + const map = new Map<number, OrderedSet<UnitIndex>>(); + for (const e of xs.elements) map.set(e.unit.id, e.indices); + + const elements: Loci['elements'][0][] = []; + for (const e of ys.elements) { + if (!map.has(e.unit.id)) continue; + const indices = OrderedSet.intersect(map.get(e.unit.id)!, e.indices); + if (OrderedSet.size(indices) === 0) continue; + elements[elements.length] = { unit: e.unit, indices }; + } + + return Loci(xs.structure, elements); + } + export function areIntersecting(xs: Loci, ys: Loci): boolean { if (xs.elements.length > ys.elements.length) return areIntersecting(ys, xs); if (Loci.isEmpty(xs)) return Loci.isEmpty(ys); diff --git a/src/mol-plugin-state/manager/interactivity.ts b/src/mol-plugin-state/manager/interactivity.ts index 1253bcbad..459571b43 100644 --- a/src/mol-plugin-state/manager/interactivity.ts +++ b/src/mol-plugin-state/manager/interactivity.ts @@ -196,6 +196,14 @@ namespace InteractivityManager { this.mark(normalized, MarkerAction.Select); } + selectJoin(current: Loci<ModelLoci>, applyGranularity = true) { + const normalized = this.normalizedLoci(current, applyGranularity) + if (StructureElement.Loci.is(normalized.loci)) { + this.sel.modify('intersect', normalized.loci); + } + this.mark(normalized, MarkerAction.Select); + } + selectOnly(current: Loci<ModelLoci>, applyGranularity = true) { this.deselectAll() const normalized = this.normalizedLoci(current, applyGranularity) @@ -225,8 +233,9 @@ namespace InteractivityManager { protected mark(current: Loci<ModelLoci>, action: MarkerAction.Select | MarkerAction.Deselect) { const { loci } = current if (StructureElement.Loci.is(loci)) { - // do a full deselect/select for the current structure so visuals - // that are marked with granularity unequal to 'element' are handled properly + // do a full deselect/select for the current structure so visuals that are + // marked with granularity unequal to 'element' and join/intersect operations + // are handled properly super.mark({ loci: Structure.Loci(loci.structure) }, MarkerAction.Deselect) super.mark({ loci: this.sel.getLoci(loci.structure) }, MarkerAction.Select) } else { diff --git a/src/mol-plugin-state/manager/structure/selection.ts b/src/mol-plugin-state/manager/structure/selection.ts index 4d735418f..de1866da3 100644 --- a/src/mol-plugin-state/manager/structure/selection.ts +++ b/src/mol-plugin-state/manager/structure/selection.ts @@ -30,7 +30,7 @@ interface StructureSelectionManagerState { const boundaryHelper = new BoundaryHelper('98'); const HISTORY_CAPACITY = 8; -export type StructureSelectionModifier = 'add' | 'remove' | 'set' +export type StructureSelectionModifier = 'add' | 'remove' | 'intersect' | 'set' export class StructureSelectionManager extends PluginComponent<StructureSelectionManagerState> { readonly events = { @@ -107,6 +107,19 @@ export class StructureSelectionManager extends PluginComponent<StructureSelectio return !StructureElement.Loci.areEqual(sel, entry.selection); } + private intersect(loci: Loci): boolean { + if (!StructureElement.Loci.is(loci)) return false; + + const entry = this.getEntry(loci.structure); + if (!entry) return false; + + const sel = entry.selection; + entry.selection = StructureElement.Loci.intersect(entry.selection, loci); + this.addHistory(loci); + this.referenceLoci = loci + return !StructureElement.Loci.areEqual(sel, entry.selection); + } + private set(loci: Loci) { if (!StructureElement.Loci.is(loci)) return false; @@ -115,6 +128,7 @@ export class StructureSelectionManager extends PluginComponent<StructureSelectio const sel = entry.selection; entry.selection = loci; + this.addHistory(loci); this.referenceLoci = undefined; return !StructureElement.Loci.areEqual(sel, entry.selection); } @@ -329,6 +343,7 @@ export class StructureSelectionManager extends PluginComponent<StructureSelectio switch (modifier) { case 'add': changed = this.add(loci); break; case 'remove': changed = this.remove(loci); break; + case 'intersect': changed = this.intersect(loci); break; case 'set': changed = this.set(loci); break; } @@ -352,6 +367,9 @@ export class StructureSelectionManager extends PluginComponent<StructureSelectio case 'remove': this.plugin.managers.interactivity.lociSelects.deselect({ loci }, applyGranularity) break + case 'intersect': + this.plugin.managers.interactivity.lociSelects.selectJoin({ loci }, applyGranularity) + break case 'set': this.plugin.managers.interactivity.lociSelects.selectOnly({ loci }, applyGranularity) break diff --git a/src/mol-plugin-ui/controls/common.tsx b/src/mol-plugin-ui/controls/common.tsx index ce264d9e6..8dfd97e97 100644 --- a/src/mol-plugin-ui/controls/common.tsx +++ b/src/mol-plugin-ui/controls/common.tsx @@ -308,7 +308,7 @@ export type ToggleButtonProps = { style?: React.CSSProperties, className?: string, disabled?: boolean, - label: string | JSX.Element, + label?: string | JSX.Element, title?: string, icon?: IconName, isSelected?: boolean, @@ -327,7 +327,7 @@ export class ToggleButton extends React.PureComponent<ToggleButtonProps> { return <button onClick={this.onClick} title={this.props.title} disabled={props.disabled} style={props.style} className={props.className}> <Icon name={this.props.icon} /> - {this.props.isSelected ? <b>{label}</b> : label} + {label && this.props.isSelected ? <b>{label}</b> : label} </button>; } } diff --git a/src/mol-plugin-ui/structure/measurements.tsx b/src/mol-plugin-ui/structure/measurements.tsx index 703be8a3e..2927255ef 100644 --- a/src/mol-plugin-ui/structure/measurements.tsx +++ b/src/mol-plugin-ui/structure/measurements.tsx @@ -129,7 +129,7 @@ export class MeasurementControls extends PurePluginUIComponent<{}, { isBusy: boo ]; return ret; } - + selectAction: ActionMenu.OnSelect = item => { this.toggleAdd(); if (!item) return; @@ -256,7 +256,6 @@ class MeasurementEntry extends PurePluginUIComponent<{ cell: StructureMeasuremen <IconButton small={true} customClass='msp-form-control' onClick={this.toggleVisibility} icon='eye' style={{ width: '52px' }} title={cell.state.isHidden ? 'Show' : 'Hide'} toggleState={!cell.state.isHidden} /> </div> } - } function toLociBundle(data: FiniteArray<{ loci: Loci }, any>): { loci: FiniteArray<Loci, any> } { diff --git a/src/mol-plugin-ui/structure/selection.tsx b/src/mol-plugin-ui/structure/selection.tsx index 7b0d60500..45620ac84 100644 --- a/src/mol-plugin-ui/structure/selection.tsx +++ b/src/mol-plugin-ui/structure/selection.tsx @@ -130,16 +130,19 @@ export class StructureSelectionControls<P, S extends StructureSelectionControlsS toggleAdd = this.showAction('add') toggleRemove = this.showAction('remove') - toggleOnly = this.showAction('set') + toggleIntersect = this.showAction('intersect') + toggleSet = this.showAction('set') toggleColor = this.showAction('color') + // TODO better icons get controls() { return <div> <div className='msp-control-row msp-select-row'> - <ToggleButton icon='plus' label='Add' toggle={this.toggleAdd} isSelected={this.state.action === 'add'} disabled={this.isDisabled} /> - <ToggleButton icon='minus' label='Rem' toggle={this.toggleRemove} isSelected={this.state.action === 'remove'} disabled={this.isDisabled} /> - <ToggleButton icon='flash' label='Set' toggle={this.toggleOnly} isSelected={this.state.action === 'set'} disabled={this.isDisabled} /> - <ToggleButton icon='brush' label='Color' toggle={this.toggleColor} isSelected={this.state.action === 'color'} disabled={this.isDisabled} /> + <ToggleButton icon='plus' title='Add' toggle={this.toggleAdd} isSelected={this.state.action === 'add'} disabled={this.isDisabled} /> + <ToggleButton icon='minus' title='Remove' toggle={this.toggleRemove} isSelected={this.state.action === 'remove'} disabled={this.isDisabled} /> + <ToggleButton icon='star' title='Intersect' toggle={this.toggleIntersect} isSelected={this.state.action === 'intersect'} disabled={this.isDisabled} /> + <ToggleButton icon='flash' title='Set' toggle={this.toggleSet} isSelected={this.state.action === 'set'} disabled={this.isDisabled} /> + <ToggleButton icon='brush' title='Color' toggle={this.toggleColor} isSelected={this.state.action === 'color'} disabled={this.isDisabled} /> </div> {(this.state.action && this.state.action !== 'color') && <ActionMenu items={this.queries} onSelect={this.selectQuery} />} {this.state.action === 'color' && <div className='msp-control-offset'><ApplyColorControls /></div>} @@ -172,7 +175,7 @@ export class StructureSelectionControls<P, S extends StructureSelectionControlsS for (let i = 0, _i = Math.min(4, mng.history.length); i < _i; i++) { const e = mng.history[i]; history.push(<li key={e!.label}> - <button className='msp-btn msp-btn-block msp-form-control' style={{ borderRight: '6px solid transparent', overflow: 'hidden' }} + <button className='msp-btn msp-btn-block msp-form-control' style={{ overflow: 'hidden' }} title='Click to focus.' onClick={this.focusLoci(e.loci)}> <span dangerouslySetInnerHTML={{ __html: e.label.split('|').reverse().join(' | ') }} /> </button> -- GitLab