UNPKG

molstar

Version:

A comprehensive macromolecular library.

997 lines 68.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ScriptControl = exports.ConvertedControl = exports.ConditionedControl = exports.ObjectListControl = exports.MappedControl = exports.GroupControl = exports.MultiSelectControl = exports.FileListControl = exports.FileControl = exports.UrlControl = exports.Mat4Control = exports.Vec3Control = exports.OffsetColorListControl = exports.ColorListControl = exports.ColorControl = exports.BoundedIntervalControl = exports.IntervalControl = exports.ValueRefControl = exports.SelectControl = exports.PureSelectControl = exports.TextControl = exports.NumberRangeControl = exports.NumberInputControl = exports.LineGraphControl = exports.BoolControl = exports.SimpleParam = exports.ParamHelp = exports.ParameterMappingControl = exports.ParameterControls = void 0; exports.ToggleParamHelpButton = ToggleParamHelpButton; const tslib_1 = require("tslib"); const jsx_runtime_1 = require("react/jsx-runtime"); /** * Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author David Sehnal <david.sehnal@gmail.com> * @author Alexander Rose <alexander.rose@weirdbyte.de> * @author Lukáš Polák <admin@lukaspolak.cz> */ const React = tslib_1.__importStar(require("react")); const linear_algebra_1 = require("../../mol-math/linear-algebra"); const script_1 = require("../../mol-script/script"); const assets_1 = require("../../mol-util/assets"); const color_1 = require("../../mol-util/color"); const lists_1 = require("../../mol-util/color/lists"); const memoize_1 = require("../../mol-util/memoize"); const number_1 = require("../../mol-util/number"); const param_definition_1 = require("../../mol-util/param-definition"); const string_1 = require("../../mol-util/string"); const base_1 = require("../base"); const action_menu_1 = require("./action-menu"); const color_2 = require("./color"); const common_1 = require("./common"); const icons_1 = require("./icons"); const legend_1 = require("./legend"); const line_graph_component_1 = require("./line-graph/line-graph-component"); const slider_1 = require("./slider"); class ParameterControls extends React.PureComponent { constructor() { super(...arguments); this.onChange = (params) => { var _a, _b; (_b = (_a = this.props).onChange) === null || _b === void 0 ? void 0 : _b.call(_a, params, this.props.values); if (this.props.onChangeValues) { const values = { ...this.props.values, [params.name]: params.value }; this.props.onChangeValues(values, this.props.values); } }; this.paramGroups = (0, memoize_1.memoizeLatest)((params) => classifyParams(params)); } renderGroup(group) { var _a; if (group.length === 0) return null; const values = this.props.values; let ctrls = null; let category = void 0; for (const [key, p, Control] of group) { if ((_a = p.hideIf) === null || _a === void 0 ? void 0 : _a.call(p, values)) continue; if (!ctrls) ctrls = []; category = p.category; ctrls.push((0, jsx_runtime_1.jsx)(Control, { param: p, onChange: this.onChange, onEnter: this.props.onEnter, isDisabled: this.props.isDisabled, name: key, value: values[key] }, key)); } if (!ctrls) return null; if (category) { return [(0, jsx_runtime_1.jsx)(common_1.ExpandGroup, { header: category, children: ctrls }, category)]; } return ctrls; } renderPart(groups) { let parts = null; for (const g of groups) { const ctrls = this.renderGroup(g); if (!ctrls) continue; if (!parts) parts = []; for (const c of ctrls) parts.push(c); } return parts; } render() { const groups = this.paramGroups(this.props.params); const essentials = this.renderPart(groups.essentials); const advanced = this.renderPart(groups.advanced); if (essentials && advanced) { return (0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [essentials, (0, jsx_runtime_1.jsx)(common_1.ExpandGroup, { header: 'Advanced Options', children: advanced })] }); } else if (essentials) { return essentials; } else { return advanced; } } } exports.ParameterControls = ParameterControls; class ParameterMappingControl extends base_1.PluginUIComponent { constructor() { super(...arguments); this.state = { isDisabled: false, }; this.setSettings = (p, old) => { const values = { ...old, [p.name]: p.value }; const t = this.props.mapping.update(values, this.plugin); this.props.mapping.apply(t, this.plugin); }; } componentDidMount() { this.subscribe(this.plugin.state.data.behaviors.isUpdating, v => { this.setState({ isDisabled: v }); }); } render() { const t = this.props.mapping.getTarget(this.plugin); const values = this.props.mapping.getValues(t, this.plugin); const params = this.props.mapping.params(this.plugin); return (0, jsx_runtime_1.jsx)(ParameterControls, { params: params, values: values, onChange: this.setSettings, isDisabled: this.state.isDisabled }); } } exports.ParameterMappingControl = ParameterMappingControl; function classifyParams(params) { function addParam(k, p, group) { const ctrl = controlFor(p); if (!ctrl) return; if (!p.category) group.params[0].push([k, p, ctrl]); else { if (!group.map) group.map = new Map(); let c = group.map.get(p.category); if (!c) { c = []; group.map.set(p.category, c); group.params.push(c); } c.push([k, p, ctrl]); } } function sortGroups(x, y) { const a = x[0], b = y[0]; if (!a || !a[1].category) return -1; if (!b || !b[1].category) return 1; return a[1].category < b[1].category ? -1 : 1; } const keys = Object.keys(params); const essentials = { params: [[]], map: void 0 }; const advanced = { params: [[]], map: void 0 }; for (const k of keys) { const p = params[k]; if (p.isHidden) continue; if (p.isEssential) addParam(k, p, essentials); else addParam(k, p, advanced); } essentials.params.sort(sortGroups); advanced.params.sort(sortGroups); return { essentials: essentials.params, advanced: advanced.params }; } function controlFor(param) { switch (param.type) { case 'value': return void 0; case 'boolean': return BoolControl; case 'number': return typeof param.min !== 'undefined' && typeof param.max !== 'undefined' ? NumberRangeControl : NumberInputControl; case 'converted': return ConvertedControl; case 'conditioned': return ConditionedControl; case 'multi-select': return MultiSelectControl; case 'color': return color_2.CombinedColorControl; case 'color-list': return param.offsets ? OffsetColorListControl : ColorListControl; case 'vec3': return Vec3Control; case 'mat4': return Mat4Control; case 'url': return UrlControl; case 'file': return FileControl; case 'file-list': return FileListControl; case 'select': return SelectControl; case 'value-ref': return ValueRefControl; case 'data-ref': return void 0; case 'text': return TextControl; case 'interval': return typeof param.min !== 'undefined' && typeof param.max !== 'undefined' ? BoundedIntervalControl : IntervalControl; case 'group': return GroupControl; case 'mapped': return MappedControl; case 'line-graph': return LineGraphControl; case 'script': return ScriptControl; case 'object-list': return ObjectListControl; default: const _ = param; console.warn(`${_} has no associated UI component`); return void 0; } } class ParamHelp extends React.PureComponent { render() { const { legend, description } = this.props; const Legend = legend && (0, legend_1.legendFor)(legend); return (0, jsx_runtime_1.jsx)("div", { className: 'msp-help-text', children: (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsxs)("div", { className: 'msp-help-description', children: [(0, jsx_runtime_1.jsx)(icons_1.Icon, { svg: icons_1.HelpOutlineSvg, inline: true }), description] }), Legend && (0, jsx_runtime_1.jsx)("div", { className: 'msp-help-legend', children: (0, jsx_runtime_1.jsx)(Legend, { legend: legend }) })] }) }); } } exports.ParamHelp = ParamHelp; function renderSimple(options) { const { props, state, control, toggleHelp, addOn } = options; const _className = []; if (props.param.shortLabel) _className.push('msp-control-label-short'); if (props.param.twoColumns) _className.push('msp-control-col-2'); if (props.param.multiline) _className.push('msp-control-twoline'); const className = _className.join(' '); const label = props.param.label || (0, string_1.camelCaseToWords)(props.name); const help = props.param.help ? props.param.help(props.value) : { description: props.param.description, legend: props.param.legend }; const hasHelp = help.description || help.legend; const desc = label + (hasHelp ? '. Click for help.' : ''); return (0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(common_1.ControlRow, { className: className, title: desc, label: (0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [label, hasHelp && (0, jsx_runtime_1.jsx)(ToggleParamHelpButton, { show: state.showHelp, toggle: toggleHelp, title: desc })] }), control: control }), hasHelp && state.showHelp && (0, jsx_runtime_1.jsx)("div", { className: 'msp-control-offset', children: (0, jsx_runtime_1.jsx)(ParamHelp, { legend: help.legend, description: help.description }) }), addOn] }); } function ToggleParamHelpButton({ show, toggle, title }) { return (0, jsx_runtime_1.jsx)("button", { className: 'msp-help msp-btn-link msp-btn-icon msp-control-group-expander', onClick: toggle, title: title || `${show ? 'Hide' : 'Show'} help`, style: { background: 'transparent', textAlign: 'left', padding: '0' }, children: (0, jsx_runtime_1.jsx)(icons_1.Icon, { svg: icons_1.HelpOutlineSvg }) }); } class SimpleParam extends React.PureComponent { constructor() { super(...arguments); this.state = { showHelp: false }; this.toggleHelp = () => this.setState({ showHelp: !this.state.showHelp }); } update(value) { this.props.onChange({ param: this.props.param, name: this.props.name, value }); } renderAddOn() { return null; } render() { return renderSimple({ props: this.props, state: this.state, control: this.renderControl(), toggleHelp: this.toggleHelp, addOn: this.renderAddOn() }); } } exports.SimpleParam = SimpleParam; class BoolControl extends SimpleParam { constructor() { super(...arguments); this.onClick = (e) => { this.update(!this.props.value); e.currentTarget.blur(); }; } renderControl() { return (0, jsx_runtime_1.jsxs)("button", { onClick: this.onClick, disabled: this.props.isDisabled, children: [(0, jsx_runtime_1.jsx)(icons_1.Icon, { svg: this.props.value ? icons_1.CheckSvg : icons_1.ClearSvg }), this.props.value ? 'On' : 'Off'] }); } } exports.BoolControl = BoolControl; class LineGraphControl extends React.PureComponent { constructor() { super(...arguments); this.state = { isExpanded: false, isOverPoint: false, message: `${this.props.param.defaultValue.length} points`, }; this.onHover = (point) => { this.setState({ isOverPoint: !this.state.isOverPoint }); if (point) { this.setState({ message: this.pointToLabel(point) }); } else { this.setState({ message: `${this.props.value.length} points` }); } }; this.onDrag = (point) => { this.setState({ message: this.pointToLabel(point) }); }; this.onChange = (value) => { this.props.onChange({ name: this.props.name, param: this.props.param, value: value }); }; this.toggleExpanded = (e) => { this.setState({ isExpanded: !this.state.isExpanded }); e.currentTarget.blur(); }; } pointToLabel(point) { var _a, _b; if (!point) return ''; const volume = (_b = (_a = this.props.param).getVolume) === null || _b === void 0 ? void 0 : _b.call(_a); if (volume) { const { min, max, mean, sigma } = volume.grid.stats; const v = min + (max - min) * point[0]; const s = (v - mean) / sigma; return `(${v.toFixed(2)} | ${s.toFixed(2)}σ, ${point[1].toFixed(2)})`; } else { return `(${point[0].toFixed(2)}, ${point[1].toFixed(2)})`; } } render() { var _a, _b; const label = this.props.param.label || (0, string_1.camelCaseToWords)(this.props.name); return (0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(common_1.ControlRow, { label: label, control: (0, jsx_runtime_1.jsx)("button", { onClick: this.toggleExpanded, disabled: this.props.isDisabled, children: `${this.state.message}` }) }), (0, jsx_runtime_1.jsx)("div", { className: 'msp-control-offset', style: { display: this.state.isExpanded ? 'block' : 'none', marginTop: 1 }, children: (0, jsx_runtime_1.jsx)(line_graph_component_1.LineGraphComponent, { data: this.props.value, volume: (_b = (_a = this.props.param).getVolume) === null || _b === void 0 ? void 0 : _b.call(_a), onChange: this.onChange, onHover: this.onHover, onDrag: this.onDrag }) })] }); } } exports.LineGraphControl = LineGraphControl; class NumberInputControl extends React.PureComponent { constructor() { super(...arguments); this.state = { value: '0' }; this.update = (value) => { const p = (0, number_1.getPrecision)(this.props.param.step || 0.01); value = parseFloat(value.toFixed(p)); this.props.onChange({ param: this.props.param, name: this.props.name, value }); }; } render() { const placeholder = this.props.param.label || (0, string_1.camelCaseToWords)(this.props.name); const label = this.props.param.label || (0, string_1.camelCaseToWords)(this.props.name); const p = (0, number_1.getPrecision)(this.props.param.step || 0.01); return (0, jsx_runtime_1.jsx)(common_1.ControlRow, { title: this.props.param.description, label: label, control: (0, jsx_runtime_1.jsx)(common_1.TextInput, { numeric: true, value: parseFloat(this.props.value.toFixed(p)), onEnter: this.props.onEnter, placeholder: placeholder, isDisabled: this.props.isDisabled, onChange: this.update }) }); } } exports.NumberInputControl = NumberInputControl; class NumberRangeControl extends SimpleParam { constructor() { super(...arguments); this.onChange = (v) => { this.update(v); }; } renderControl() { const value = typeof this.props.value === 'undefined' ? this.props.param.defaultValue : this.props.value; return (0, jsx_runtime_1.jsx)(slider_1.Slider, { value: value, min: this.props.param.min, max: this.props.param.max, step: this.props.param.step, onChange: this.onChange, onChangeImmediate: this.props.param.immediateUpdate ? this.onChange : void 0, disabled: this.props.isDisabled, onEnter: this.props.onEnter }); } } exports.NumberRangeControl = NumberRangeControl; class TextControl extends SimpleParam { constructor() { super(...arguments); this.updateValue = (value) => { if (value !== this.props.value) { this.update(value); } }; } renderControl() { const placeholder = this.props.param.placeholder || this.props.param.label || (0, string_1.camelCaseToWords)(this.props.name); return (0, jsx_runtime_1.jsx)(TextCtrl, { props: this.props, placeholder: placeholder, update: this.updateValue }); } } exports.TextControl = TextControl; function TextCtrl({ props, placeholder, update }) { const [value, setValue] = React.useState(props.value); React.useEffect(() => setValue(props.value), [props.value]); if (props.param.multiline) { return (0, jsx_runtime_1.jsx)("div", { className: 'msp-control-text-area-wrapper', children: (0, jsx_runtime_1.jsx)("textarea", { value: props.param.disableInteractiveUpdates ? (value || '') : props.value, placeholder: placeholder, onChange: e => { if (props.param.disableInteractiveUpdates) setValue(e.target.value); else update(e.target.value); }, onBlur: e => { if (props.param.disableInteractiveUpdates) update(e.target.value); }, onKeyDown: e => { if (e.key === 'Enter' && (e.shiftKey || e.ctrlKey || e.metaKey)) { e.currentTarget.blur(); } }, disabled: props.isDisabled }) }); } return (0, jsx_runtime_1.jsx)("input", { type: 'text', value: props.param.disableInteractiveUpdates ? (value || '') : props.value, placeholder: placeholder, onChange: e => { if (props.param.disableInteractiveUpdates) setValue(e.target.value); else update(e.target.value); }, onBlur: e => { if (props.param.disableInteractiveUpdates) update(e.target.value); }, disabled: props.isDisabled, onKeyDown: e => { if (e.key !== 'Enter') return; if (props.onEnter) { e.stopPropagation(); props.onEnter(); } else if (e.key === 'Enter' && (e.shiftKey || e.ctrlKey || e.metaKey)) { e.currentTarget.blur(); } else if (props.param.disableInteractiveUpdates) { update(value); } } }); } class PureSelectControl extends React.PureComponent { constructor() { super(...arguments); this.onChange = (e) => { if (typeof this.props.param.defaultValue === 'number') { this.update(parseInt(e.target.value, 10)); } else { this.update(e.target.value); } }; } update(value) { this.props.onChange({ param: this.props.param, name: this.props.name, value }); } render() { const isInvalid = this.props.value !== void 0 && !this.props.param.options.some(e => e[0] === this.props.value); return (0, jsx_runtime_1.jsxs)("select", { className: 'msp-form-control', title: this.props.title, value: this.props.value !== void 0 ? this.props.value : this.props.param.defaultValue, onChange: this.onChange, disabled: this.props.isDisabled, children: [isInvalid && (0, jsx_runtime_1.jsx)("option", { value: this.props.value, children: `[Invalid] ${this.props.value}` }, this.props.value), this.props.param.options.map(([value, label]) => (0, jsx_runtime_1.jsx)("option", { value: value, children: label }, value))] }); } } exports.PureSelectControl = PureSelectControl; class SelectControl extends React.PureComponent { constructor() { super(...arguments); this.state = { showHelp: false, showOptions: false }; this.onSelect = item => { if (!item || item.value === this.props.value) { this.setState({ showOptions: false }); } else { this.setState({ showOptions: false }, () => { this.props.onChange({ param: this.props.param, name: this.props.name, value: item.value }); }); } }; this.toggle = () => this.setState({ showOptions: !this.state.showOptions }); this.cycle = () => { const { options } = this.props.param; const current = options.findIndex(o => o[0] === this.props.value); const next = current === options.length - 1 ? 0 : current + 1; this.props.onChange({ param: this.props.param, name: this.props.name, value: options[next][0] }); }; this.items = (0, memoize_1.memoizeLatest)((param) => action_menu_1.ActionMenu.createItemsFromSelectOptions(param.options)); this.toggleHelp = () => this.setState({ showHelp: !this.state.showHelp }); } renderControl() { var _a; const items = this.items(this.props.param); const current = this.props.value !== undefined ? action_menu_1.ActionMenu.findItem(items, this.props.value) : void 0; const label = current ? current.label : typeof this.props.value === 'undefined' ? `${((_a = action_menu_1.ActionMenu.getFirstItem(items)) === null || _a === void 0 ? void 0 : _a.label) || ''} [Default]` : `[Invalid] ${this.props.value}`; const toggle = this.props.param.cycle ? this.cycle : this.toggle; const textAlign = this.props.param.cycle ? 'center' : 'left'; const icon = this.props.param.cycle ? (this.props.value === 'on' ? icons_1.CheckSvg : this.props.value === 'off' ? icons_1.ClearSvg : void 0) : void 0; return (0, jsx_runtime_1.jsx)(common_1.ToggleButton, { disabled: this.props.isDisabled, style: { textAlign, overflow: 'hidden', textOverflow: 'ellipsis' }, label: label, title: label, icon: icon, toggle: toggle, isSelected: this.state.showOptions }); } renderAddOn() { if (!this.state.showOptions) return null; const items = this.items(this.props.param); const current = action_menu_1.ActionMenu.findItem(items, this.props.value); return (0, jsx_runtime_1.jsx)(action_menu_1.ActionMenu, { items: items, current: current, onSelect: this.onSelect }); } render() { return renderSimple({ props: this.props, state: this.state, control: this.renderControl(), toggleHelp: this.toggleHelp, addOn: this.renderAddOn() }); } } exports.SelectControl = SelectControl; class ValueRefControl extends React.PureComponent { constructor() { super(...arguments); this.state = { showHelp: false, showOptions: false }; this.onSelect = item => { if (!item || item.value === this.props.value) { this.setState({ showOptions: false }); } else { this.setState({ showOptions: false }, () => { this.props.onChange({ param: this.props.param, name: this.props.name, value: { ref: item.value } }); }); } }; this.toggle = () => this.setState({ showOptions: !this.state.showOptions }); this.toggleHelp = () => this.setState({ showHelp: !this.state.showHelp }); } get items() { return action_menu_1.ActionMenu.createItemsFromSelectOptions(this.props.param.getOptions(this.context)); } renderControl() { var _a; const items = this.items; const current = this.props.value.ref ? action_menu_1.ActionMenu.findItem(items, this.props.value.ref) : void 0; const label = current ? current.label : `[Ref] ${(_a = this.props.value.ref) !== null && _a !== void 0 ? _a : ''}`; return (0, jsx_runtime_1.jsx)(common_1.ToggleButton, { disabled: this.props.isDisabled, style: { textAlign: 'left', overflow: 'hidden', textOverflow: 'ellipsis' }, label: label, title: label, toggle: this.toggle, isSelected: this.state.showOptions }); } renderAddOn() { if (!this.state.showOptions) return null; const items = this.items; const current = action_menu_1.ActionMenu.findItem(items, this.props.value.ref); return (0, jsx_runtime_1.jsx)(action_menu_1.ActionMenu, { items: items, current: current, onSelect: this.onSelect }); } render() { return renderSimple({ props: this.props, state: this.state, control: this.renderControl(), toggleHelp: this.toggleHelp, addOn: this.renderAddOn() }); } } exports.ValueRefControl = ValueRefControl; ValueRefControl.contextType = base_1.PluginReactContext; class IntervalControl extends React.PureComponent { constructor() { super(...arguments); this.state = { isExpanded: false }; this.components = { 0: param_definition_1.ParamDefinition.Numeric(0, { step: this.props.param.step }, { label: 'Min' }), 1: param_definition_1.ParamDefinition.Numeric(0, { step: this.props.param.step }, { label: 'Max' }) }; this.componentChange = ({ name, value }) => { const v = [...this.props.value]; v[+name] = value; this.change(v); }; this.toggleExpanded = (e) => { this.setState({ isExpanded: !this.state.isExpanded }); e.currentTarget.blur(); }; } change(value) { this.props.onChange({ name: this.props.name, param: this.props.param, value }); } render() { const v = this.props.value; const label = this.props.param.label || (0, string_1.camelCaseToWords)(this.props.name); const p = (0, number_1.getPrecision)(this.props.param.step || 0.01); const value = `[${v[0].toFixed(p)}, ${v[1].toFixed(p)}]`; return (0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(common_1.ControlRow, { label: label, control: (0, jsx_runtime_1.jsx)("button", { onClick: this.toggleExpanded, disabled: this.props.isDisabled, children: value }) }), this.state.isExpanded && (0, jsx_runtime_1.jsx)("div", { className: 'msp-control-offset', children: (0, jsx_runtime_1.jsx)(ParameterControls, { params: this.components, values: v, onChange: this.componentChange, onEnter: this.props.onEnter }) })] }); } } exports.IntervalControl = IntervalControl; class BoundedIntervalControl extends SimpleParam { constructor() { super(...arguments); this.onChange = (v) => { this.update(v); }; } renderControl() { return (0, jsx_runtime_1.jsx)(slider_1.Slider2, { value: this.props.value, min: this.props.param.min, max: this.props.param.max, step: this.props.param.step, onChange: this.onChange, disabled: this.props.isDisabled, onEnter: this.props.onEnter }); } } exports.BoundedIntervalControl = BoundedIntervalControl; class ColorControl extends SimpleParam { constructor() { super(...arguments); this.onChange = (e) => { this.update((0, color_1.Color)(parseInt(e.target.value))); }; } stripStyle() { return { background: color_1.Color.toStyle(this.props.value), position: 'absolute', bottom: '0', height: '4px', right: '0', left: '0' }; } renderControl() { return (0, jsx_runtime_1.jsxs)("div", { style: { position: 'relative' }, children: [(0, jsx_runtime_1.jsxs)("select", { value: this.props.value, onChange: this.onChange, children: [(0, color_2.ColorValueOption)(this.props.value), (0, color_2.ColorOptions)()] }), (0, jsx_runtime_1.jsx)("div", { style: this.stripStyle() })] }); } } exports.ColorControl = ColorControl; function colorEntryToStyle(e, includeOffset = false) { if (Array.isArray(e)) { if (includeOffset) return `${color_1.Color.toStyle(e[0])} ${(100 * e[1]).toFixed(2)}%`; return color_1.Color.toStyle(e[0]); } return color_1.Color.toStyle(e); } const colorGradientInterpolated = (0, memoize_1.memoize1)((colors) => { if (colors.length === 0) return 'linear-gradient(to right, #000 0%, #000 100%)'; const hasOffsets = colors.every(c => Array.isArray(c)); let styles; if (hasOffsets) { const off = [...colors]; off.sort((a, b) => a[1] - b[1]); styles = off.map(c => colorEntryToStyle(c, true)); } else { styles = colors.map(c => colorEntryToStyle(c)); } return `linear-gradient(to right, ${styles.join(', ')})`; }); const colorGradientBanded = (0, memoize_1.memoize1)((colors) => { const n = colors.length; const styles = []; const hasOffsets = colors.every(c => Array.isArray(c)); if (hasOffsets) { const off = [...colors]; // 0 colors present if (!off[0]) { return 'linear-gradient(to right, #000 0%, #000 100%)'; } off.sort((a, b) => a[1] - b[1]); styles.push(`${color_1.Color.toStyle(off[0][0])} ${(100 * off[0][1]).toFixed(2)}%`); for (let i = 0, il = off.length - 1; i < il; ++i) { const [c0, o0] = off[i]; const [c1, o1] = off[i + 1]; const o = o0 + (o1 - o0) / 2; styles.push(`${color_1.Color.toStyle(c0)} ${(100 * o).toFixed(2)}%`, `${color_1.Color.toStyle(c1)} ${(100 * o).toFixed(2)}%`); } styles.push(`${color_1.Color.toStyle(off[off.length - 1][0])} ${(100 * off[off.length - 1][1]).toFixed(2)}%`); } else { styles.push(`${colorEntryToStyle(colors[0])} ${100 * (1 / n)}%`); for (let i = 1, il = n - 1; i < il; ++i) { styles.push(`${colorEntryToStyle(colors[i])} ${100 * (i / n)}%`, `${colorEntryToStyle(colors[i])} ${100 * ((i + 1) / n)}%`); } styles.push(`${colorEntryToStyle(colors[n - 1])} ${100 * ((n - 1) / n)}%`); } return `linear-gradient(to right, ${styles.join(', ')})`; }); function colorStripStyle(list, right = '0') { return { background: colorGradient(list.colors, list.kind === 'set'), position: 'absolute', bottom: '0', height: '4px', right, left: '0' }; } function colorGradient(colors, banded) { return banded ? colorGradientBanded(colors) : colorGradientInterpolated(colors); } function createColorListHelpers() { const addOn = (l) => { const preset = (0, lists_1.getColorListFromName)(l[0]); return (0, jsx_runtime_1.jsx)("div", { style: colorStripStyle({ kind: preset.type !== 'qualitative' ? 'interpolate' : 'set', colors: preset.list }) }); }; return { ColorPresets: { all: action_menu_1.ActionMenu.createItemsFromSelectOptions(lists_1.ColorListOptions, { addOn }), scale: action_menu_1.ActionMenu.createItemsFromSelectOptions(lists_1.ColorListOptionsScale, { addOn }), set: action_menu_1.ActionMenu.createItemsFromSelectOptions(lists_1.ColorListOptionsSet, { addOn }) }, ColorsParam: param_definition_1.ParamDefinition.ObjectList({ color: param_definition_1.ParamDefinition.Color(0x0) }, ({ color }) => color_1.Color.toHexString(color).toUpperCase()), OffsetColorsParam: param_definition_1.ParamDefinition.ObjectList({ color: param_definition_1.ParamDefinition.Color(0x0), offset: param_definition_1.ParamDefinition.Numeric(0, { min: 0, max: 1, step: 0.01 }) }, ({ color, offset }) => `${color_1.Color.toHexString(color).toUpperCase()} [${offset.toFixed(2)}]`), IsInterpolatedParam: param_definition_1.ParamDefinition.Boolean(false, { label: 'Interpolated' }) }; } let _colorListHelpers; function ColorListHelpers() { if (_colorListHelpers) return _colorListHelpers; _colorListHelpers = createColorListHelpers(); return _colorListHelpers; } class ColorListControl extends React.PureComponent { constructor() { super(...arguments); this.state = { showHelp: false, show: void 0 }; this.toggleEdit = () => this.setState({ show: this.state.show === 'edit' ? void 0 : 'edit' }); this.togglePresets = () => this.setState({ show: this.state.show === 'presets' ? void 0 : 'presets' }); this.selectPreset = item => { if (!item) return; this.setState({ show: void 0 }); const preset = (0, lists_1.getColorListFromName)(item.value); this.update({ kind: preset.type !== 'qualitative' ? 'interpolate' : 'set', colors: preset.list }); }; this.colorsChanged = ({ value }) => { this.update({ kind: this.props.value.kind, colors: value.map(c => c.color) }); }; this.isInterpolatedChanged = ({ value }) => { this.update({ kind: value ? 'interpolate' : 'set', colors: this.props.value.colors }); }; this.toggleHelp = () => this.setState({ showHelp: !this.state.showHelp }); } update(value) { this.props.onChange({ param: this.props.param, name: this.props.name, value }); } renderControl() { const { value } = this.props; // TODO: fix the button right offset return (0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("button", { onClick: this.toggleEdit, style: { position: 'relative', paddingRight: '33px' }, children: [value.colors.length === 1 ? '1 color' : `${value.colors.length} colors`, (0, jsx_runtime_1.jsx)("div", { style: colorStripStyle(value, '33px') })] }), (0, jsx_runtime_1.jsx)(common_1.IconButton, { svg: icons_1.BookmarksOutlinedSvg, onClick: this.togglePresets, toggleState: this.state.show === 'presets', title: 'Color Presets', style: { padding: 0, position: 'absolute', right: 0, top: 0, width: '32px' } })] }); } renderColors() { if (!this.state.show) return null; const { ColorPresets, ColorsParam, IsInterpolatedParam } = ColorListHelpers(); const preset = ColorPresets[this.props.param.presetKind]; if (this.state.show === 'presets') return (0, jsx_runtime_1.jsx)(action_menu_1.ActionMenu, { items: preset, onSelect: this.selectPreset }); // It might happen that the colors are either in the form of [Color, number] or just Color, show them as just Color const values = this.props.value.colors.map(color => ({ color: Array.isArray(color) ? color[0] : color })); return (0, jsx_runtime_1.jsxs)("div", { className: 'msp-control-offset', children: [(0, jsx_runtime_1.jsx)(ObjectListControl, { name: 'colors', param: ColorsParam, value: values, onChange: this.colorsChanged, isDisabled: this.props.isDisabled, onEnter: this.props.onEnter }), (0, jsx_runtime_1.jsx)(BoolControl, { name: 'isInterpolated', param: IsInterpolatedParam, value: this.props.value.kind === 'interpolate', onChange: this.isInterpolatedChanged, isDisabled: this.props.isDisabled, onEnter: this.props.onEnter })] }); } render() { return renderSimple({ props: this.props, state: this.state, control: this.renderControl(), toggleHelp: this.toggleHelp, addOn: this.renderColors() }); } } exports.ColorListControl = ColorListControl; class OffsetColorListControl extends React.PureComponent { constructor() { super(...arguments); this.state = { showHelp: false, show: void 0 }; this.toggleEdit = () => this.setState({ show: this.state.show === 'edit' ? void 0 : 'edit' }); this.togglePresets = () => this.setState({ show: this.state.show === 'presets' ? void 0 : 'presets' }); this.selectPreset = item => { if (!item) return; this.setState({ show: void 0 }); const preset = (0, lists_1.getColorListFromName)(item.value); this.update({ kind: preset.type !== 'qualitative' ? 'interpolate' : 'set', colors: preset.list }); }; this.colorsChanged = ({ value }) => { const colors = value.map(c => [c.color, c.offset]); colors.sort((a, b) => a[1] - b[1]); this.update({ kind: this.props.value.kind, colors }); }; this.isInterpolatedChanged = ({ value }) => { this.update({ kind: value ? 'interpolate' : 'set', colors: this.props.value.colors }); }; this.toggleHelp = () => this.setState({ showHelp: !this.state.showHelp }); } update(value) { this.props.onChange({ param: this.props.param, name: this.props.name, value }); } renderControl() { const { value } = this.props; // TODO: fix the button right offset return (0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("button", { onClick: this.toggleEdit, style: { position: 'relative', paddingRight: '33px' }, children: [value.colors.length === 1 ? '1 color' : `${value.colors.length} colors`, (0, jsx_runtime_1.jsx)("div", { style: colorStripStyle(value, '33px') })] }), (0, jsx_runtime_1.jsx)(common_1.IconButton, { svg: icons_1.BookmarksOutlinedSvg, onClick: this.togglePresets, toggleState: this.state.show === 'presets', title: 'Color Presets', style: { padding: 0, position: 'absolute', right: 0, top: 0, width: '32px' } })] }); } renderColors() { if (!this.state.show) return null; const { ColorPresets, OffsetColorsParam, IsInterpolatedParam } = ColorListHelpers(); const preset = ColorPresets[this.props.param.presetKind]; if (this.state.show === 'presets') return (0, jsx_runtime_1.jsx)(action_menu_1.ActionMenu, { items: preset, onSelect: this.selectPreset }); const colors = this.props.value.colors; const values = colors.map((color, i) => { if (Array.isArray(color)) return { color: color[0], offset: color[1] }; return { color, offset: i / colors.length }; }); values.sort((a, b) => a.offset - b.offset); return (0, jsx_runtime_1.jsxs)("div", { className: 'msp-control-offset', children: [(0, jsx_runtime_1.jsx)(ObjectListControl, { name: 'colors', param: OffsetColorsParam, value: values, onChange: this.colorsChanged, isDisabled: this.props.isDisabled, onEnter: this.props.onEnter }), (0, jsx_runtime_1.jsx)(BoolControl, { name: 'isInterpolated', param: IsInterpolatedParam, value: this.props.value.kind === 'interpolate', onChange: this.isInterpolatedChanged, isDisabled: this.props.isDisabled, onEnter: this.props.onEnter })] }); } render() { return renderSimple({ props: this.props, state: this.state, control: this.renderControl(), toggleHelp: this.toggleHelp, addOn: this.renderColors() }); } } exports.OffsetColorListControl = OffsetColorListControl; class Vec3Control extends React.PureComponent { constructor() { super(...arguments); this.state = { isExpanded: false }; this.components = { 0: param_definition_1.ParamDefinition.Numeric(0, { step: this.props.param.step }, { label: (this.props.param.fieldLabels && this.props.param.fieldLabels.x) || 'X' }), 1: param_definition_1.ParamDefinition.Numeric(0, { step: this.props.param.step }, { label: (this.props.param.fieldLabels && this.props.param.fieldLabels.y) || 'Y' }), 2: param_definition_1.ParamDefinition.Numeric(0, { step: this.props.param.step }, { label: (this.props.param.fieldLabels && this.props.param.fieldLabels.z) || 'Z' }) }; this.componentChange = ({ name, value }) => { const v = linear_algebra_1.Vec3.copy(linear_algebra_1.Vec3.zero(), this.props.value); v[+name] = value; this.change(v); }; this.toggleExpanded = (e) => { this.setState({ isExpanded: !this.state.isExpanded }); e.currentTarget.blur(); }; } change(value) { this.props.onChange({ name: this.props.name, param: this.props.param, value }); } render() { const v = this.props.value; const label = this.props.param.label || (0, string_1.camelCaseToWords)(this.props.name); const p = (0, number_1.getPrecision)(this.props.param.step || 0.01); const value = `[${v[0].toFixed(p)}, ${v[1].toFixed(p)}, ${v[2].toFixed(p)}]`; return (0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(common_1.ControlRow, { label: label, control: (0, jsx_runtime_1.jsx)("button", { onClick: this.toggleExpanded, disabled: this.props.isDisabled, children: value }) }), this.state.isExpanded && (0, jsx_runtime_1.jsx)("div", { className: 'msp-control-offset', children: (0, jsx_runtime_1.jsx)(ParameterControls, { params: this.components, values: v, onChange: this.componentChange, onEnter: this.props.onEnter }) })] }); } } exports.Vec3Control = Vec3Control; class Mat4Control extends React.PureComponent { constructor() { super(...arguments); this.state = { isExpanded: false }; this.components = { json: param_definition_1.ParamDefinition.Text(JSON.stringify((0, linear_algebra_1.Mat4)()), { description: 'JSON array with 4x4 matrix in a column major (j * 4 + i indexing) format' }) }; this.componentChange = ({ name, value }) => { const v = linear_algebra_1.Mat4.copy((0, linear_algebra_1.Mat4)(), this.props.value); if (name === 'json') { linear_algebra_1.Mat4.copy(v, JSON.parse(value)); } else { v[+name] = value; } this.change(v); }; this.toggleExpanded = (e) => { this.setState({ isExpanded: !this.state.isExpanded }); e.currentTarget.blur(); }; } change(value) { this.props.onChange({ name: this.props.name, param: this.props.param, value }); } changeValue(idx) { return (v) => { const m = linear_algebra_1.Mat4.copy((0, linear_algebra_1.Mat4)(), this.props.value); m[idx] = v; this.change(m); }; } get grid() { const v = this.props.value; const rows = []; for (let i = 0; i < 4; i++) { const row = []; for (let j = 0; j < 4; j++) { row.push((0, jsx_runtime_1.jsx)(common_1.TextInput, { numeric: true, delayMs: 50, value: linear_algebra_1.Mat4.getValue(v, i, j), onChange: this.changeValue(4 * j + i), className: 'msp-form-control', blurOnEnter: true, isDisabled: this.props.isDisabled }, j)); } rows.push((0, jsx_runtime_1.jsx)("div", { className: 'msp-flex-row', children: row }, i)); } return (0, jsx_runtime_1.jsx)("div", { className: 'msp-parameter-matrix', children: rows }); } render() { const v = { json: JSON.stringify(this.props.value) }; const label = this.props.param.label || (0, string_1.camelCaseToWords)(this.props.name); return (0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(common_1.ControlRow, { label: label, control: (0, jsx_runtime_1.jsx)("button", { onClick: this.toggleExpanded, disabled: this.props.isDisabled, children: '4\u00D74 Matrix' }) }), this.state.isExpanded && (0, jsx_runtime_1.jsxs)("div", { className: 'msp-control-offset', children: [this.grid, (0, jsx_runtime_1.jsx)(ParameterControls, { params: this.components, values: v, onChange: this.componentChange, onEnter: this.props.onEnter })] })] }); } } exports.Mat4Control = Mat4Control; class UrlControl extends SimpleParam { constructor() { super(...arguments); this.onChange = (e) => { const value = e.target.value; if (value !== assets_1.Asset.getUrl(this.props.value || '')) { this.update(assets_1.Asset.Url(value)); } }; this.onKeyPress = (e) => { if ((e.keyCode === 13 || e.charCode === 13 || e.key === 'Enter')) { if (this.props.onEnter) this.props.onEnter(); } e.stopPropagation(); }; } renderControl() { const placeholder = this.props.param.label || (0, string_1.camelCaseToWords)(this.props.name); return (0, jsx_runtime_1.jsx)("input", { type: 'text', value: assets_1.Asset.getUrl(this.props.value || ''), placeholder: placeholder, onChange: this.onChange, onKeyPress: this.props.onEnter ? this.onKeyPress : void 0, disabled: this.props.isDisabled }); } } exports.UrlControl = UrlControl; class FileControl extends React.PureComponent { constructor() { super(...arguments); this.state = { showHelp: false }; this.onChangeFile = (e) => { this.change(e.target.files[0]); }; this.toggleHelp = () => this.setState({ showHelp: !this.state.showHelp }); } change(value) { this.props.onChange({ name: this.props.name, param: this.props.param, value: assets_1.Asset.File(value) }); } renderControl() { const value = this.props.value; return (0, jsx_runtime_1.jsxs)("div", { className: 'msp-btn msp-btn-block msp-btn-action msp-loader-msp-btn-file', style: { marginTop: '1px' }, children: [value ? value.name : 'Select a file...', " ", (0, jsx_runtime_1.jsx)("input", { disabled: this.props.isDisabled, onChange: this.onChangeFile, type: 'file', multiple: false, accept: this.props.param.accept })] }); } render() { if (this.props.param.label) { return renderSimple({ props: this.props, state: this.state, control: this.renderControl(), toggleHelp: this.toggleHelp, addOn: null }); } else { return this.renderControl(); } } } exports.FileControl = FileControl; class FileListControl extends React.PureComponent { constructor() { super(...arguments); this.state = { showHelp: false }; this.onChangeFileList = (e) => { this.change(e.target.files); }; this.toggleHelp = () => this.setState({ showHelp: !this.state.showHelp }); } change(value) { const files = []; if (value) { for (let i = 0, il = value.length; i < il; ++i) { files.push(assets_1.Asset.File(value[i])); } } this.props.onChange({ name: this.props.name, param: this.props.param, value: files }); } renderControl() { const value = this.props.value; const names = []; if (value) { for (const file of value) { names.push(file.name); } } const label = names.length === 0 ? 'Select files...' : names.length === 1 ? names[0] : `${names.length} files selected`; return (0, jsx_runtime_1.jsxs)("div", { className: 'msp-btn msp-btn-block msp-btn-action msp-loader-msp-btn-file', style: { marginTop: '1px' }, children: [label, " ", (0, jsx_runtime_1.jsx)("input", { disabled: this.props.isDisabled, onChange: this.onChangeFileList, type: 'file', multiple: true, accept: this.props.param.accept })] }); } render() { if (this.props.param.label) { return renderSimple({ props: this.props, state: this.state, control: this.renderControl(), toggleHelp: this.toggleHelp, addOn: null }); } else { return this.renderControl(); } } } exports.FileListControl = FileListControl; class MultiSelectControl extends React.PureComponent { constructor() { super(...arguments); this.state = { isExpanded: false }; this.toggleExpanded = (e) => { this.setState({ isExpanded: !this.state.isExpanded }); e.currentTarget.blur(); }; } change(value) { this.props.onChange({ name: this.props.name, param: this.props.param, value }); } toggle(key) { return (e) => { if (this.props.value.indexOf(key) < 0) this.change(this.props.value.concat(key)); else this.change(this.props.value.filter(v => v !== key)); e.currentTarget.blur(); }; } render() { const current = this.props.value; const emptyLabel = this.props.param.emptyValue; const label = this.props.param.label || (0, string_1.camelCaseToWords)(this.props.name); return (0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(common_1.ControlRow, { label: label, control: (0, jsx_runtime_1.jsx)("button", { onClick: this.toggleExpanded, disabled: this.props.isDisabled, children: current.length === 0 && emptyLabel ? emptyLabel : `${current.length} of ${this.props.param.options.length}` }) }), this.state.isExpanded && (0, jsx_runtime_1.jsx)("div", { className: 'msp-control-offset', children: this.props.param.options.map(([value, label]) => { const sel = current.indexOf(value) >= 0; return (0, jsx_runtime_1.jsx)(common_1.Button, { onClick: this.toggle(value), disabled: this.props.isDisabled, s