diff --git a/src/apps/rednatco/color-picker.tsx b/src/apps/rednatco/color-picker.tsx index d57b2037f97a020859800ad609f46e9f7a94d08b..77547251c349ec2d0f13979f958ff268eeda7698 100644 --- a/src/apps/rednatco/color-picker.tsx +++ b/src/apps/rednatco/color-picker.tsx @@ -11,7 +11,6 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Colors } from './colors'; import { PushButton, SpinBox } from './controls'; -import { parseInt as parseIntMS } from '../../mol-io/reader/common/text/number-parser'; import './assets/imgs/triangle-down.svg'; import './assets/imgs/triangle-up.svg'; @@ -43,10 +42,6 @@ function isSatValVal(v: number) { return v >= MIN_SATVAL && v <= MAX_SATVAL; } -function stoi(s: string) { - return parseIntMS(s, 0, s.length); -} - interface State { h: number; s: number; @@ -545,18 +540,13 @@ export class ColorPicker extends React.Component<ColorPicker.Props, State> { min={MIN_RGB} max={MAX_RGB} step={1} - value={this.state.rIn === '' ? null : Math.round(Colors.hsv2rgb(this.state.h, this.state.s, this.state.v).r)} + value={Math.round(Colors.hsv2rgb(this.state.h, this.state.s, this.state.v).r)} onChange={rIn => { - if (rIn === '') - this.setState({ ...this.state, rIn }); - else { - const r = stoi(rIn); - if (!isRgbVal(r)) - return; - - const { g, b } = Colors.hsv2rgb(this.state.h, this.state.s, this.state.v); - this.updateColorRgb({ r, g, b }); - } + if (!isRgbVal(rIn)) + return; + + const { g, b } = Colors.hsv2rgb(this.state.h, this.state.s, this.state.v); + this.updateColorRgb({ r: rIn, g, b }); }} pathPrefix={this.props.pathPrefix} /> @@ -565,18 +555,13 @@ export class ColorPicker extends React.Component<ColorPicker.Props, State> { min={MIN_RGB} max={MAX_RGB} step={1} - value={this.state.gIn === '' ? null : Math.round(Colors.hsv2rgb(this.state.h, this.state.s, this.state.v).g)} + value={Math.round(Colors.hsv2rgb(this.state.h, this.state.s, this.state.v).g)} onChange={gIn => { - if (gIn === '') - this.setState({ ...this.state, gIn }); - else { - const g = stoi(gIn); - if (!isRgbVal(g)) - return; - - const { r, b } = Colors.hsv2rgb(this.state.h, this.state.s, this.state.v); - this.updateColorRgb({ r, g, b }); - } + if (!isRgbVal(gIn)) + return; + + const { r, b } = Colors.hsv2rgb(this.state.h, this.state.s, this.state.v); + this.updateColorRgb({ r, g: gIn, b }); }} pathPrefix={this.props.pathPrefix} /> @@ -585,18 +570,13 @@ export class ColorPicker extends React.Component<ColorPicker.Props, State> { min={MIN_RGB} max={MAX_RGB} step={1} - value={this.state.bIn === '' ? null : Math.round(Colors.hsv2rgb(this.state.h, this.state.s, this.state.v).b)} + value={Math.round(Colors.hsv2rgb(this.state.h, this.state.s, this.state.v).b)} onChange={bIn => { - if (bIn === '') - this.setState({ ...this.state, bIn }); - else { - const b = stoi(bIn); - if (!isRgbVal(b)) - return; - - const { r, g } = Colors.hsv2rgb(this.state.h, this.state.s, this.state.v); - this.updateColorRgb({ r, g, b }); - } + if (!isRgbVal(bIn)) + return; + + const { r, g } = Colors.hsv2rgb(this.state.h, this.state.s, this.state.v); + this.updateColorRgb({ r, g, b: bIn }); }} pathPrefix={this.props.pathPrefix} /> @@ -614,17 +594,12 @@ export class ColorPicker extends React.Component<ColorPicker.Props, State> { min={MIN_HUE} max={MAX_HUE} step={1} - value={this.state.hIn === '' ? null : Math.round(this.state.h)} + value={Math.round(this.state.h)} onChange={hIn => { - if (hIn === '') - this.setState({ ...this.state, hIn }); - else { - const h = stoi(hIn); - if (!isHueVal(h)) - return; - - this.updateColorHsv({ h, s: this.state.s, v: this.state.v }); - } + if (!isHueVal(hIn)) + return; + + this.updateColorHsv({ h: hIn, s: this.state.s, v: this.state.v }); }} pathPrefix={this.props.pathPrefix} /> @@ -633,17 +608,12 @@ export class ColorPicker extends React.Component<ColorPicker.Props, State> { min={MIN_SATVAL} max={MAX_SATVAL} step={1} - value={this.state.sIn === '' ? null : Math.round(this.state.s * 100)} + value={Math.round(this.state.s * 100)} onChange={sIn => { - if (sIn === '') - this.setState({ ...this.state, sIn }); - else { - const s = stoi(sIn); - if (!isSatValVal(s)) - return; - - this.updateColorHsv({ h: this.state.h, s: s / 100, v: this.state.v }); - } + if (!isSatValVal(sIn)) + return; + + this.updateColorHsv({ h: this.state.h, s: sIn / 100, v: this.state.v }); }} pathPrefix={this.props.pathPrefix} /> @@ -652,17 +622,12 @@ export class ColorPicker extends React.Component<ColorPicker.Props, State> { min={MIN_SATVAL} max={MAX_SATVAL} step={1} - value={this.state.vIn === '' ? null : Math.round(this.state.v * 100)} + value={Math.round(this.state.v * 100)} onChange={vIn => { - if (vIn === '') - this.setState({ ...this.state, vIn }); - else { - const v = stoi(vIn); - if (!isSatValVal(v)) - return; - - this.updateColorHsv({ h: this.state.h, s: this.state.s, v: v / 100 }); - } + if (!isSatValVal(vIn)) + return; + + this.updateColorHsv({ h: this.state.h, s: this.state.s, v: vIn / 100 }); }} pathPrefix={this.props.pathPrefix} /> diff --git a/src/apps/rednatco/controls.tsx b/src/apps/rednatco/controls.tsx index 2c3c53896964c687c7ec6e10969c31fb2351dbda..d625c8cf67462cac105b06823058bf5867284410 100644 --- a/src/apps/rednatco/controls.tsx +++ b/src/apps/rednatco/controls.tsx @@ -1,4 +1,20 @@ import React from 'react'; +import { stof } from './util'; + +const Zero = '0'.charCodeAt(0); +const Nine = '9'.charCodeAt(0); +const Minus = '-'.charCodeAt(0); +const Period = '.'.charCodeAt(0); + +function maybeNumeric(s: string) { + for (let idx = 0; idx < s.length; idx++) { + const cc = s.charCodeAt(idx); + if (!((cc >= Zero && cc <= Nine) || cc === Minus || cc === Period)) + return false; + } + + return true; +} export class CollapsibleVertical extends React.Component<CollapsibleVertical.Props, { collapsed: boolean }> { constructor(props: CollapsibleVertical.Props) { @@ -67,8 +83,9 @@ export class RangeSlider extends React.Component<RangeSlider.Props> { max={this.props.max} step={this.props.step} onChange={evt => { - const n = parseFloat(evt.currentTarget.value); - this.props.onChange(isNaN(n) ? null : n); + const n = stof(evt.currentTarget.value); + if (n !== undefined) + this.props.onChange(n); }} /> ); @@ -84,7 +101,20 @@ export namespace RangeSlider { } } -export class SpinBox extends React.Component<SpinBox.Props> { +interface SpinBoxState { + displayedValue: string +} +export class SpinBox extends React.Component<SpinBox.Props, SpinBoxState> { + constructor(props: SpinBox.Props) { + super(props); + + this.state = { + displayedValue: this.props.formatter + ? this.props.formatter(this.props.value.toString()) + : this.props.value.toString(), + }; + } + private clsDisabled() { return this.props.classNameDisabled ?? 'rmsp-spinbox-input-disabled'; } @@ -94,19 +124,46 @@ export class SpinBox extends React.Component<SpinBox.Props> { } private decrease() { - if (this.props.value === null) + const n = stof(this.state.displayedValue); + if (n === undefined) return; - const nv = this.props.value - this.props.step; + const nv = n - this.props.step; if (nv >= this.props.min) - this.props.onChange(nv.toString()); + this.props.onChange(n); } private increase() { - if (this.props.value === null) + const n = stof(this.state.displayedValue); + if (n === undefined) return; - const nv = this.props.value + this.props.step; + const nv = n + this.props.step; if (nv >= this.props.min) - this.props.onChange(nv.toString()); + this.props.onChange(n); + } + + private handleChange(value: string) { + const n = stof(value); + + if ( + n !== undefined && + n !== this.props.value && + (this.props.min <= n && n <= this.props.max) && + value[value.length - 1] !== '.' + ) { + this.props.onChange(n); + } else { + if (maybeNumeric(value)) + this.setState({ ...this.state, displayedValue: value }); + } + } + + componentDidUpdate(prevProps: SpinBox.Props) { + if (this.props !== prevProps) { + const displayedValue = this.props.formatter + ? this.props.formatter(this.props.value.toString()) + : this.props.value.toString(); + this.setState({ ...this.state, displayedValue }); + } } render() { @@ -115,27 +172,21 @@ export class SpinBox extends React.Component<SpinBox.Props> { <input type='text' className={this.props.disabled ? this.clsDisabled() : this.clsEnabled()} - value={this.props.formatter ? this.props.formatter(this.props.value) : this.props.value?.toString() ?? ''} - onChange={evt =>{ - const v = evt.currentTarget.value; - const n = parseFloat(v); - if (!isNaN(n) && (n < this.props.min || n > this.props.max)) - return; - - this.props.onChange(evt.currentTarget.value); - }} + value={this.state.displayedValue} + onChange={evt => this.handleChange(evt.currentTarget.value)} onWheel={evt => { evt.stopPropagation(); - if (this.props.value === null) + const n = stof(this.state.displayedValue); + if (n === undefined) return; if (evt.deltaY < 0) { - const nv = this.props.value + this.props.step; + const nv = n + this.props.step; if (nv <= this.props.max) - this.props.onChange(nv.toString()); + this.props.onChange(nv); } else if (evt.deltaY > 0) { - const nv = this.props.value - this.props.step; + const nv = n - this.props.step; if (nv >= this.props.min) - this.props.onChange(nv.toString()); + this.props.onChange(nv); } }} /> @@ -155,15 +206,15 @@ export class SpinBox extends React.Component<SpinBox.Props> { } export namespace SpinBox { export interface Formatter { - (v: number|null): string; + (v: string): string; } export interface OnChange { - (newValue: string): void; + (newValue: number): void; } export interface Props { - value: number|null; + value: number; onChange: OnChange; min: number; max: number; diff --git a/src/apps/rednatco/density-map-controls.tsx b/src/apps/rednatco/density-map-controls.tsx index b922006a04cc4f67f5bbbd9dae4cfe27e4d7315e..293e719c196667cd6607041627f39e7d8ff83ced 100644 --- a/src/apps/rednatco/density-map-controls.tsx +++ b/src/apps/rednatco/density-map-controls.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { CollapsibleVertical, RangeSlider, SpinBox, ToggleButton } from './controls'; -import { isoToFixed } from './util'; +import { isoToFixed, numDecimals, stof } from './util'; export class DensityMapControls extends React.Component<DensityMapControls.Props> { render() { @@ -49,7 +49,7 @@ export class DensityMapControls extends React.Component<DensityMapControls.Props max={this.props.isoMax} step={this.props.isoStep} value={isoToFixed(this.props.iso, this.props.isoStep)} - onChange={(v) => this.props.changeIso(parseFloat(v))} + onChange={(n) => this.props.changeIso(n)} pathPrefix='' /> <div /> @@ -76,9 +76,15 @@ export class DensityMapControls extends React.Component<DensityMapControls.Props min={0} max={1} step={0.1} - value={parseFloat((1.0 - this.props.alpha).toFixed(1))} - onChange={(v) => this.props.changeAlpha(1.0 - parseFloat(v))} + value={(1.0 - this.props.alpha)} + onChange={(n) => this.props.changeAlpha(1.0 - n)} pathPrefix='' + formatter={v => { + const n = stof(v); + if (n !== undefined && numDecimals(v) > 1) + return n.toFixed(1); + return v; + }} /> </div> </div> diff --git a/src/apps/rednatco/index.tsx b/src/apps/rednatco/index.tsx index f6dda49d199c4f7006086bb396b767ac926d6b84..68a2625cf759d7a7481f3834ae999d2406f87af8 100644 --- a/src/apps/rednatco/index.tsx +++ b/src/apps/rednatco/index.tsx @@ -138,7 +138,10 @@ export class ReDNATCOMsp extends React.Component<ReDNATCOMsp.Props, State> { : this.classColorToConformers(changes.cls, Color(changes.color))) }; - const display = { ...this.state.display, classColors, conformerColors }; + const display = { ...this.state.display }; + display.structures.classColors = classColors; + display.structures.conformerColors = conformerColors; + this.viewer!.changeNtCColors(display); this.setState({ ...this.state, display }); } @@ -150,7 +153,9 @@ export class ReDNATCOMsp extends React.Component<ReDNATCOMsp.Props, State> { else conformerColors[changes.conformer] = Color(changes.color); - const display = { ...this.state.display, conformerColors }; + const display = { ...this.state.display }; + display.structures.conformerColors = conformerColors; + this.viewer!.changeNtCColors(display); this.setState({ ...this.state, display }); } diff --git a/src/apps/rednatco/util.ts b/src/apps/rednatco/util.ts index 165709e39b5f8bb96f1018ca2146b8c5f0ef4215..c943be480fc90fd766fae94bcfd8d4ff16d0d2e5 100644 --- a/src/apps/rednatco/util.ts +++ b/src/apps/rednatco/util.ts @@ -1,4 +1,5 @@ import { Color } from '../../mol-util/color'; +import { parseInt as parseIntMS, parseFloat as parseFloatMS } from '../../mol-io/reader/common/text/number-parser'; export function isoBounds(min: number, max: number): { min: number, max: number, step: number } { let diff = max - min; @@ -31,10 +32,33 @@ export function luminance(color: Color) { return Math.sqrt(0.299 * r * r + 0.587 * g * g + 0.114 * b * b); } +export function numDecimals(s: string) { + const idx = s.lastIndexOf('.'); + return idx >= 0 ? s.length - idx - 1 : 0; +} + export function prettyIso(iso: number, step: number) { return Math.floor((iso - step) / step) * step + step; } +export function stof(s: string) { + if (s.length === 0) + return void 0; + if (s === '-') + return void 0; + const n = parseFloatMS(s, 0, s.length); + return isNaN(n) ? undefined : n; +} + +export function stoi(s: string) { + if (s.length === 0) + return void 0; + if (s === '-') + return void 0; + const n = parseIntMS(s, 0, s.length); + return isNaN(n) ? undefined : n; +} + export function toggleArray<T>(array: T[], elem: T) { if (array.includes(elem)) return array.filter((x) => x !== elem);