@datagrok/helm 2.13.8 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/css/helm.css +40 -0
- package/dist/137.js +2 -0
- package/dist/137.js.map +1 -0
- package/dist/323.js +2 -0
- package/dist/323.js.map +1 -0
- package/dist/657.js +2 -0
- package/dist/657.js.map +1 -0
- package/dist/782.js +2 -0
- package/dist/782.js.map +1 -0
- package/dist/884.js +2 -0
- package/dist/884.js.map +1 -0
- package/dist/94.js +2 -0
- package/dist/94.js.map +1 -0
- package/dist/libraries_bio_src_monomer-works_seq-to-molfile-worker_ts.js +16420 -0
- package/dist/libraries_bio_src_monomer-works_seq-to-molfile-worker_ts.js.map +1 -0
- package/dist/package-test.js +1 -1
- package/dist/package-test.js.map +1 -1
- package/dist/package.js +1 -1
- package/dist/package.js.map +1 -1
- package/dist/vendors-HELM_node_modules_lru-cache_dist_esm_index_js.js +1560 -0
- package/dist/vendors-HELM_node_modules_lru-cache_dist_esm_index_js.js.map +1 -0
- package/files/samples/helm-showcase.csv +56 -0
- package/package.json +8 -12
- package/src/helm-helper.ts +134 -410
- package/src/package-api.ts +14 -11
- package/src/package-test.ts +0 -2
- package/src/package-utils.ts +8 -298
- package/src/package.g.ts +2 -8
- package/src/package.ts +11 -31
- package/src/tests/helm-activity-cliffs.ts +2 -2
- package/src/tests/helm-input-tests.ts +83 -0
- package/src/tests/parse-helm-tests.ts +7 -57
- package/src/tests/properties-widget-tests.ts +21 -11
- package/src/utils/helm-grid-cell-renderer.ts +141 -151
- package/src/utils/index.ts +3 -7
- package/src/widgets/helm-input.ts +42 -12
- package/src/widgets/properties-widget.ts +18 -11
- package/webpack.config.js +1 -12
- package/helm/JSDraw/Pistoia.HELM-uncompressed.js +0 -9694
- package/helm/JSDraw/ReadMe.txt +0 -8
- package/helm/JSDraw/Scilligence.JSDraw2.Lite-uncompressed.js +0 -31126
- package/helm/dojo/package.ts +0 -41
- package/helm/dojo/types.ts +0 -12
- package/src/helm-web-editor.ts +0 -68
- package/src/tests/get-monomer-tests.ts +0 -214
- package/src/tests/helm-service-tests.ts +0 -98
- package/src/types/dojo.ts +0 -3
- package/src/types/index.ts +0 -5
- package/src/utils/get-monomer.ts +0 -90
- package/src/utils/helm-service.ts +0 -166
- package/test-console-output-1.log +0 -919
- package/test-record-1.mp4 +0 -0
- package/vendor/dojo-1.10.10/dijit/BackgroundIframe.js +0 -63
- package/vendor/dojo-1.10.10/dijit/BackgroundIframe.js.uncompressed.js +0 -116
- package/vendor/dojo-1.10.10/dijit/Destroyable.js +0 -35
- package/vendor/dojo-1.10.10/dijit/Destroyable.js.uncompressed.js +0 -83
- package/vendor/dojo-1.10.10/dijit/Tooltip.js +0 -240
- package/vendor/dojo-1.10.10/dijit/Tooltip.js.uncompressed.js +0 -612
- package/vendor/dojo-1.10.10/dijit/Viewport.js +0 -44
- package/vendor/dojo-1.10.10/dijit/Viewport.js.uncompressed.js +0 -87
- package/vendor/dojo-1.10.10/dijit/WidgetSet.js +0 -76
- package/vendor/dojo-1.10.10/dijit/WidgetSet.js.uncompressed.js +0 -247
- package/vendor/dojo-1.10.10/dijit/_AttachMixin.js +0 -94
- package/vendor/dojo-1.10.10/dijit/_AttachMixin.js.uncompressed.js +0 -238
- package/vendor/dojo-1.10.10/dijit/_Contained.js +0 -17
- package/vendor/dojo-1.10.10/dijit/_Contained.js.uncompressed.js +0 -56
- package/vendor/dojo-1.10.10/dijit/_Container.js +0 -47
- package/vendor/dojo-1.10.10/dijit/_Container.js.uncompressed.js +0 -108
- package/vendor/dojo-1.10.10/dijit/_CssStateMixin.js +0 -198
- package/vendor/dojo-1.10.10/dijit/_CssStateMixin.js.uncompressed.js +0 -372
- package/vendor/dojo-1.10.10/dijit/_FocusMixin.js +0 -11
- package/vendor/dojo-1.10.10/dijit/_FocusMixin.js.uncompressed.js +0 -66
- package/vendor/dojo-1.10.10/dijit/_HasDropDown.js +0 -188
- package/vendor/dojo-1.10.10/dijit/_HasDropDown.js.uncompressed.js +0 -472
- package/vendor/dojo-1.10.10/dijit/_OnDijitClickMixin.js +0 -8
- package/vendor/dojo-1.10.10/dijit/_OnDijitClickMixin.js.uncompressed.js +0 -31
- package/vendor/dojo-1.10.10/dijit/_TemplatedMixin.js +0 -90
- package/vendor/dojo-1.10.10/dijit/_TemplatedMixin.js.uncompressed.js +0 -205
- package/vendor/dojo-1.10.10/dijit/_Widget.js +0 -66
- package/vendor/dojo-1.10.10/dijit/_Widget.js.uncompressed.js +0 -352
- package/vendor/dojo-1.10.10/dijit/_WidgetBase.js +0 -353
- package/vendor/dojo-1.10.10/dijit/_WidgetBase.js.uncompressed.js +0 -1200
- package/vendor/dojo-1.10.10/dijit/_base/focus.js +0 -68
- package/vendor/dojo-1.10.10/dijit/_base/focus.js.uncompressed.js +0 -207
- package/vendor/dojo-1.10.10/dijit/_base/manager.js +0 -10
- package/vendor/dojo-1.10.10/dijit/_base/manager.js.uncompressed.js +0 -35
- package/vendor/dojo-1.10.10/dijit/_base/place.js +0 -52
- package/vendor/dojo-1.10.10/dijit/_base/place.js.uncompressed.js +0 -131
- package/vendor/dojo-1.10.10/dijit/_base/popup.js +0 -23
- package/vendor/dojo-1.10.10/dijit/_base/popup.js.uncompressed.js +0 -58
- package/vendor/dojo-1.10.10/dijit/_base/scroll.js +0 -6
- package/vendor/dojo-1.10.10/dijit/_base/scroll.js.uncompressed.js +0 -22
- package/vendor/dojo-1.10.10/dijit/_base/sniff.js +0 -3
- package/vendor/dojo-1.10.10/dijit/_base/sniff.js.uncompressed.js +0 -12
- package/vendor/dojo-1.10.10/dijit/_base/typematic.js +0 -3
- package/vendor/dojo-1.10.10/dijit/_base/typematic.js.uncompressed.js +0 -10
- package/vendor/dojo-1.10.10/dijit/_base/wai.js +0 -32
- package/vendor/dojo-1.10.10/dijit/_base/wai.js.uncompressed.js +0 -109
- package/vendor/dojo-1.10.10/dijit/_base/window.js +0 -6
- package/vendor/dojo-1.10.10/dijit/_base/window.js.uncompressed.js +0 -18
- package/vendor/dojo-1.10.10/dijit/_base.js +0 -4
- package/vendor/dojo-1.10.10/dijit/_base.js.uncompressed.js +0 -27
- package/vendor/dojo-1.10.10/dijit/a11y.js +0 -109
- package/vendor/dojo-1.10.10/dijit/a11y.js.uncompressed.js +0 -191
- package/vendor/dojo-1.10.10/dijit/a11yclick.js +0 -66
- package/vendor/dojo-1.10.10/dijit/a11yclick.js.uncompressed.js +0 -139
- package/vendor/dojo-1.10.10/dijit/focus.js +0 -188
- package/vendor/dojo-1.10.10/dijit/focus.js.uncompressed.js +0 -371
- package/vendor/dojo-1.10.10/dijit/form/Button.js +0 -49
- package/vendor/dojo-1.10.10/dijit/form/Button.js.uncompressed.js +0 -130
- package/vendor/dojo-1.10.10/dijit/form/ComboBox.js +0 -4
- package/vendor/dojo-1.10.10/dijit/form/ComboBox.js.uncompressed.js +0 -27
- package/vendor/dojo-1.10.10/dijit/form/ComboButton.js +0 -21
- package/vendor/dojo-1.10.10/dijit/form/ComboButton.js.uncompressed.js +0 -86
- package/vendor/dojo-1.10.10/dijit/form/DropDownButton.js +0 -38
- package/vendor/dojo-1.10.10/dijit/form/DropDownButton.js.uncompressed.js +0 -102
- package/vendor/dojo-1.10.10/dijit/form/ToggleButton.js +0 -7
- package/vendor/dojo-1.10.10/dijit/form/ToggleButton.js.uncompressed.js +0 -26
- package/vendor/dojo-1.10.10/dijit/form/_ButtonMixin.js +0 -50
- package/vendor/dojo-1.10.10/dijit/form/_ButtonMixin.js.uncompressed.js +0 -116
- package/vendor/dojo-1.10.10/dijit/form/_FormValueMixin.js +0 -26
- package/vendor/dojo-1.10.10/dijit/form/_FormValueMixin.js.uncompressed.js +0 -81
- package/vendor/dojo-1.10.10/dijit/form/_FormValueWidget.js +0 -24
- package/vendor/dojo-1.10.10/dijit/form/_FormValueWidget.js.uncompressed.js +0 -51
- package/vendor/dojo-1.10.10/dijit/form/_FormWidget.js +0 -22
- package/vendor/dojo-1.10.10/dijit/form/_FormWidget.js.uncompressed.js +0 -67
- package/vendor/dojo-1.10.10/dijit/form/_FormWidgetMixin.js +0 -111
- package/vendor/dojo-1.10.10/dijit/form/_FormWidgetMixin.js.uncompressed.js +0 -254
- package/vendor/dojo-1.10.10/dijit/form/_ToggleButtonMixin.js +0 -32
- package/vendor/dojo-1.10.10/dijit/form/_ToggleButtonMixin.js.uncompressed.js +0 -69
- package/vendor/dojo-1.10.10/dijit/hccss.js +0 -9
- package/vendor/dojo-1.10.10/dijit/hccss.js.uncompressed.js +0 -21
- package/vendor/dojo-1.10.10/dijit/layout/AccordionContainer.js +0 -215
- package/vendor/dojo-1.10.10/dijit/layout/AccordionContainer.js.uncompressed.js +0 -550
- package/vendor/dojo-1.10.10/dijit/layout/AccordionPane.js +0 -7
- package/vendor/dojo-1.10.10/dijit/layout/AccordionPane.js.uncompressed.js +0 -25
- package/vendor/dojo-1.10.10/dijit/layout/ContentPane.js +0 -235
- package/vendor/dojo-1.10.10/dijit/layout/ContentPane.js.uncompressed.js +0 -653
- package/vendor/dojo-1.10.10/dijit/layout/StackContainer.js +0 -171
- package/vendor/dojo-1.10.10/dijit/layout/StackContainer.js.uncompressed.js +0 -411
- package/vendor/dojo-1.10.10/dijit/layout/StackController.js +0 -194
- package/vendor/dojo-1.10.10/dijit/layout/StackController.js.uncompressed.js +0 -410
- package/vendor/dojo-1.10.10/dijit/layout/_ContentPaneResizeMixin.js +0 -99
- package/vendor/dojo-1.10.10/dijit/layout/_ContentPaneResizeMixin.js.uncompressed.js +0 -236
- package/vendor/dojo-1.10.10/dijit/layout/_LayoutWidget.js +0 -47
- package/vendor/dojo-1.10.10/dijit/layout/_LayoutWidget.js.uncompressed.js +0 -190
- package/vendor/dojo-1.10.10/dijit/layout/utils.js +0 -77
- package/vendor/dojo-1.10.10/dijit/layout/utils.js.uncompressed.js +0 -151
- package/vendor/dojo-1.10.10/dijit/main.js +0 -4
- package/vendor/dojo-1.10.10/dijit/main.js.uncompressed.js +0 -16
- package/vendor/dojo-1.10.10/dijit/nls/dijit-all_en-us.js +0 -2
- package/vendor/dojo-1.10.10/dijit/nls/dijit-all_en-us.js.uncompressed.js +0 -21
- package/vendor/dojo-1.10.10/dijit/nls/loading.js +0 -2
- package/vendor/dojo-1.10.10/dijit/nls/loading.js.uncompressed.js +0 -47
- package/vendor/dojo-1.10.10/dijit/place.js +0 -142
- package/vendor/dojo-1.10.10/dijit/place.js.uncompressed.js +0 -407
- package/vendor/dojo-1.10.10/dijit/popup.js +0 -162
- package/vendor/dojo-1.10.10/dijit/popup.js.uncompressed.js +0 -441
- package/vendor/dojo-1.10.10/dijit/registry.js +0 -79
- package/vendor/dojo-1.10.10/dijit/registry.js.uncompressed.js +0 -159
- package/vendor/dojo-1.10.10/dijit/selection.js +0 -364
- package/vendor/dojo-1.10.10/dijit/selection.js.uncompressed.js +0 -525
- package/vendor/dojo-1.10.10/dijit/typematic.js +0 -96
- package/vendor/dojo-1.10.10/dijit/typematic.js.uncompressed.js +0 -211
- package/vendor/dojo-1.10.10/dojo/NodeList-manipulate.js +0 -195
- package/vendor/dojo-1.10.10/dojo/NodeList-manipulate.js.uncompressed.js +0 -761
- package/vendor/dojo-1.10.10/dojo/Stateful.js +0 -90
- package/vendor/dojo-1.10.10/dojo/Stateful.js.uncompressed.js +0 -218
- package/vendor/dojo-1.10.10/dojo/_base/url.js +0 -87
- package/vendor/dojo-1.10.10/dojo/_base/url.js.uncompressed.js +0 -109
- package/vendor/dojo-1.10.10/dojo/cache.js +0 -9
- package/vendor/dojo-1.10.10/dojo/cache.js.uncompressed.js +0 -7
- package/vendor/dojo-1.10.10/dojo/colors.js +0 -74
- package/vendor/dojo-1.10.10/dojo/colors.js.uncompressed.js +0 -232
- package/vendor/dojo-1.10.10/dojo/cookie.js +0 -48
- package/vendor/dojo-1.10.10/dojo/cookie.js.uncompressed.js +0 -98
- package/vendor/dojo-1.10.10/dojo/date/stamp.js +0 -82
- package/vendor/dojo-1.10.10/dojo/date/stamp.js.uncompressed.js +0 -144
- package/vendor/dojo-1.10.10/dojo/dojo.js +0 -7
- package/vendor/dojo-1.10.10/dojo/dojo.js.uncompressed.js +0 -18680
- package/vendor/dojo-1.10.10/dojo/fx/Toggler.js +0 -27
- package/vendor/dojo-1.10.10/dojo/fx/Toggler.js.uncompressed.js +0 -101
- package/vendor/dojo-1.10.10/dojo/fx/easing.js +0 -165
- package/vendor/dojo-1.10.10/dojo/fx/easing.js.uncompressed.js +0 -276
- package/vendor/dojo-1.10.10/dojo/fx.js +0 -271
- package/vendor/dojo-1.10.10/dojo/fx.js.uncompressed.js +0 -443
- package/vendor/dojo-1.10.10/dojo/hccss.js +0 -26
- package/vendor/dojo-1.10.10/dojo/hccss.js.uncompressed.js +0 -52
- package/vendor/dojo-1.10.10/dojo/html.js +0 -161
- package/vendor/dojo-1.10.10/dojo/html.js.uncompressed.js +0 -370
- package/vendor/dojo-1.10.10/dojo/io/iframe.js +0 -71
- package/vendor/dojo-1.10.10/dojo/io/iframe.js.uncompressed.js +0 -190
- package/vendor/dojo-1.10.10/dojo/io/script.js +0 -112
- package/vendor/dojo-1.10.10/dojo/io/script.js.uncompressed.js +0 -280
- package/vendor/dojo-1.10.10/dojo/parser.js +0 -437
- package/vendor/dojo-1.10.10/dojo/parser.js.uncompressed.js +0 -915
- package/vendor/dojo-1.10.10/dojo/promise/all.js +0 -59
- package/vendor/dojo-1.10.10/dojo/promise/all.js.uncompressed.js +0 -77
- package/vendor/dojo-1.10.10/dojo/regexp.js +0 -32
- package/vendor/dojo-1.10.10/dojo/regexp.js.uncompressed.js +0 -70
- package/vendor/dojo-1.10.10/dojo/request/iframe.js +0 -291
- package/vendor/dojo-1.10.10/dojo/request/iframe.js.uncompressed.js +0 -438
- package/vendor/dojo-1.10.10/dojo/request/script.js +0 -129
- package/vendor/dojo-1.10.10/dojo/request/script.js.uncompressed.js +0 -237
- package/vendor/dojo-1.10.10/dojo/require.js +0 -11
- package/vendor/dojo-1.10.10/dojo/require.js.uncompressed.js +0 -7
- package/vendor/dojo-1.10.10/dojo/resources/blank.gif +0 -0
- package/vendor/dojo-1.10.10/dojo/string.js +0 -67
- package/vendor/dojo-1.10.10/dojo/string.js.uncompressed.js +0 -181
- package/vendor/dojo-1.10.10/dojo/touch.js +0 -191
- package/vendor/dojo-1.10.10/dojo/touch.js.uncompressed.js +0 -468
- package/vendor/dojo-1.10.10/dojo/uacss.js +0 -36
- package/vendor/dojo-1.10.10/dojo/uacss.js.uncompressed.js +0 -85
- package/vendor/dojo-1.10.10/dojo/window.js +0 -141
- package/vendor/dojo-1.10.10/dojo/window.js.uncompressed.js +0 -241
- package/vendor/dojo-1.10.10/dojox/charting/Chart.js +0 -626
- package/vendor/dojo-1.10.10/dojox/charting/Chart.js.uncompressed.js +0 -1220
- package/vendor/dojo-1.10.10/dojox/charting/Chart2D.js +0 -5
- package/vendor/dojo-1.10.10/dojox/charting/Chart2D.js.uncompressed.js +0 -16
- package/vendor/dojo-1.10.10/dojox/charting/Element.js +0 -247
- package/vendor/dojo-1.10.10/dojox/charting/Element.js.uncompressed.js +0 -402
- package/vendor/dojo-1.10.10/dojox/charting/Series.js +0 -24
- package/vendor/dojo-1.10.10/dojox/charting/Series.js.uncompressed.js +0 -58
- package/vendor/dojo-1.10.10/dojox/charting/SimpleTheme.js +0 -191
- package/vendor/dojo-1.10.10/dojox/charting/SimpleTheme.js.uncompressed.js +0 -577
- package/vendor/dojo-1.10.10/dojox/charting/Theme.js +0 -42
- package/vendor/dojo-1.10.10/dojox/charting/Theme.js.uncompressed.js +0 -120
- package/vendor/dojo-1.10.10/dojox/charting/action2d/Base.js +0 -11
- package/vendor/dojo-1.10.10/dojox/charting/action2d/Base.js.uncompressed.js +0 -36
- package/vendor/dojo-1.10.10/dojox/charting/action2d/Highlight.js +0 -77
- package/vendor/dojo-1.10.10/dojox/charting/action2d/Highlight.js.uncompressed.js +0 -144
- package/vendor/dojo-1.10.10/dojox/charting/action2d/Magnify.js +0 -58
- package/vendor/dojo-1.10.10/dojox/charting/action2d/Magnify.js.uncompressed.js +0 -123
- package/vendor/dojo-1.10.10/dojox/charting/action2d/MoveSlice.js +0 -54
- package/vendor/dojo-1.10.10/dojox/charting/action2d/MoveSlice.js.uncompressed.js +0 -124
- package/vendor/dojo-1.10.10/dojox/charting/action2d/PlotAction.js +0 -28
- package/vendor/dojo-1.10.10/dojox/charting/action2d/PlotAction.js.uncompressed.js +0 -74
- package/vendor/dojo-1.10.10/dojox/charting/action2d/Tooltip.js +0 -116
- package/vendor/dojo-1.10.10/dojox/charting/action2d/Tooltip.js.uncompressed.js +0 -178
- package/vendor/dojo-1.10.10/dojox/charting/axis2d/Base.js +0 -24
- package/vendor/dojo-1.10.10/dojox/charting/axis2d/Base.js.uncompressed.js +0 -83
- package/vendor/dojo-1.10.10/dojox/charting/axis2d/Default.js +0 -557
- package/vendor/dojo-1.10.10/dojox/charting/axis2d/Default.js.uncompressed.js +0 -980
- package/vendor/dojo-1.10.10/dojox/charting/axis2d/Invisible.js +0 -65
- package/vendor/dojo-1.10.10/dojox/charting/axis2d/Invisible.js.uncompressed.js +0 -224
- package/vendor/dojo-1.10.10/dojox/charting/axis2d/common.js +0 -91
- package/vendor/dojo-1.10.10/dojox/charting/axis2d/common.js.uncompressed.js +0 -164
- package/vendor/dojo-1.10.10/dojox/charting/plot2d/Areas.js +0 -7
- package/vendor/dojo-1.10.10/dojox/charting/plot2d/Areas.js.uncompressed.js +0 -14
- package/vendor/dojo-1.10.10/dojox/charting/plot2d/Bars.js +0 -168
- package/vendor/dojo-1.10.10/dojox/charting/plot2d/Bars.js.uncompressed.js +0 -311
- package/vendor/dojo-1.10.10/dojox/charting/plot2d/Base.js +0 -61
- package/vendor/dojo-1.10.10/dojox/charting/plot2d/Base.js.uncompressed.js +0 -161
- package/vendor/dojo-1.10.10/dojox/charting/plot2d/Bubble.js +0 -141
- package/vendor/dojo-1.10.10/dojox/charting/plot2d/Bubble.js.uncompressed.js +0 -242
- package/vendor/dojo-1.10.10/dojox/charting/plot2d/Candlesticks.js +0 -126
- package/vendor/dojo-1.10.10/dojox/charting/plot2d/Candlesticks.js.uncompressed.js +0 -243
- package/vendor/dojo-1.10.10/dojox/charting/plot2d/CartesianBase.js +0 -108
- package/vendor/dojo-1.10.10/dojox/charting/plot2d/CartesianBase.js.uncompressed.js +0 -290
- package/vendor/dojo-1.10.10/dojox/charting/plot2d/ClusteredBars.js +0 -13
- package/vendor/dojo-1.10.10/dojox/charting/plot2d/ClusteredBars.js.uncompressed.js +0 -14
- package/vendor/dojo-1.10.10/dojox/charting/plot2d/ClusteredColumns.js +0 -13
- package/vendor/dojo-1.10.10/dojox/charting/plot2d/ClusteredColumns.js.uncompressed.js +0 -14
- package/vendor/dojo-1.10.10/dojox/charting/plot2d/Columns.js +0 -153
- package/vendor/dojo-1.10.10/dojox/charting/plot2d/Columns.js.uncompressed.js +0 -249
- package/vendor/dojo-1.10.10/dojox/charting/plot2d/Default.js +0 -253
- package/vendor/dojo-1.10.10/dojox/charting/plot2d/Default.js.uncompressed.js +0 -472
- package/vendor/dojo-1.10.10/dojox/charting/plot2d/Grid.js +0 -182
- package/vendor/dojo-1.10.10/dojox/charting/plot2d/Grid.js.uncompressed.js +0 -356
- package/vendor/dojo-1.10.10/dojox/charting/plot2d/Lines.js +0 -6
- package/vendor/dojo-1.10.10/dojox/charting/plot2d/Lines.js.uncompressed.js +0 -12
- package/vendor/dojo-1.10.10/dojox/charting/plot2d/Markers.js +0 -6
- package/vendor/dojo-1.10.10/dojox/charting/plot2d/Markers.js.uncompressed.js +0 -12
- package/vendor/dojo-1.10.10/dojox/charting/plot2d/MarkersOnly.js +0 -7
- package/vendor/dojo-1.10.10/dojox/charting/plot2d/MarkersOnly.js.uncompressed.js +0 -13
- package/vendor/dojo-1.10.10/dojox/charting/plot2d/OHLC.js +0 -110
- package/vendor/dojo-1.10.10/dojox/charting/plot2d/OHLC.js.uncompressed.js +0 -208
- package/vendor/dojo-1.10.10/dojox/charting/plot2d/Pie.js +0 -303
- package/vendor/dojo-1.10.10/dojox/charting/plot2d/Pie.js.uncompressed.js +0 -547
- package/vendor/dojo-1.10.10/dojox/charting/plot2d/Scatter.js +0 -134
- package/vendor/dojo-1.10.10/dojox/charting/plot2d/Scatter.js.uncompressed.js +0 -207
- package/vendor/dojo-1.10.10/dojox/charting/plot2d/Spider.js +0 -299
- package/vendor/dojo-1.10.10/dojox/charting/plot2d/Spider.js.uncompressed.js +0 -537
- package/vendor/dojo-1.10.10/dojox/charting/plot2d/Stacked.js +0 -24
- package/vendor/dojo-1.10.10/dojox/charting/plot2d/Stacked.js.uncompressed.js +0 -44
- package/vendor/dojo-1.10.10/dojox/charting/plot2d/StackedAreas.js +0 -7
- package/vendor/dojo-1.10.10/dojox/charting/plot2d/StackedAreas.js.uncompressed.js +0 -14
- package/vendor/dojo-1.10.10/dojox/charting/plot2d/StackedBars.js +0 -22
- package/vendor/dojo-1.10.10/dojox/charting/plot2d/StackedBars.js.uncompressed.js +0 -33
- package/vendor/dojo-1.10.10/dojox/charting/plot2d/StackedColumns.js +0 -20
- package/vendor/dojo-1.10.10/dojox/charting/plot2d/StackedColumns.js.uncompressed.js +0 -31
- package/vendor/dojo-1.10.10/dojox/charting/plot2d/StackedLines.js +0 -6
- package/vendor/dojo-1.10.10/dojox/charting/plot2d/StackedLines.js.uncompressed.js +0 -12
- package/vendor/dojo-1.10.10/dojox/charting/plot2d/_PlotEvents.js +0 -68
- package/vendor/dojo-1.10.10/dojox/charting/plot2d/_PlotEvents.js.uncompressed.js +0 -120
- package/vendor/dojo-1.10.10/dojox/charting/plot2d/common.js +0 -209
- package/vendor/dojo-1.10.10/dojox/charting/plot2d/common.js.uncompressed.js +0 -214
- package/vendor/dojo-1.10.10/dojox/charting/plot2d/commonStacked.js +0 -76
- package/vendor/dojo-1.10.10/dojox/charting/plot2d/commonStacked.js.uncompressed.js +0 -77
- package/vendor/dojo-1.10.10/dojox/charting/scaler/common.js +0 -59
- package/vendor/dojo-1.10.10/dojox/charting/scaler/common.js.uncompressed.js +0 -70
- package/vendor/dojo-1.10.10/dojox/charting/scaler/linear.js +0 -220
- package/vendor/dojo-1.10.10/dojox/charting/scaler/linear.js.uncompressed.js +0 -262
- package/vendor/dojo-1.10.10/dojox/charting/scaler/primitive.js +0 -23
- package/vendor/dojo-1.10.10/dojox/charting/scaler/primitive.js.uncompressed.js +0 -36
- package/vendor/dojo-1.10.10/dojox/charting/themes/Claro.js +0 -39
- package/vendor/dojo-1.10.10/dojox/charting/themes/Claro.js.uncompressed.js +0 -106
- package/vendor/dojo-1.10.10/dojox/charting/themes/Wetland.js +0 -5
- package/vendor/dojo-1.10.10/dojox/charting/themes/Wetland.js.uncompressed.js +0 -12
- package/vendor/dojo-1.10.10/dojox/charting/themes/common.js +0 -4
- package/vendor/dojo-1.10.10/dojox/charting/themes/common.js.uncompressed.js +0 -3
- package/vendor/dojo-1.10.10/dojox/charting/widget/Legend.js +0 -102
- package/vendor/dojo-1.10.10/dojox/charting/widget/Legend.js.uncompressed.js +0 -165
- package/vendor/dojo-1.10.10/dojox/color/Palette.js +0 -163
- package/vendor/dojo-1.10.10/dojox/color/Palette.js.uncompressed.js +0 -460
- package/vendor/dojo-1.10.10/dojox/color/_base.js +0 -174
- package/vendor/dojo-1.10.10/dojox/color/_base.js.uncompressed.js +0 -196
- package/vendor/dojo-1.10.10/dojox/gfx/_base.js +0 -253
- package/vendor/dojo-1.10.10/dojox/gfx/_base.js.uncompressed.js +0 -1024
- package/vendor/dojo-1.10.10/dojox/gfx/fx.js +0 -297
- package/vendor/dojo-1.10.10/dojox/gfx/fx.js.uncompressed.js +0 -350
- package/vendor/dojo-1.10.10/dojox/gfx/gradutils.js +0 -63
- package/vendor/dojo-1.10.10/dojox/gfx/gradutils.js.uncompressed.js +0 -91
- package/vendor/dojo-1.10.10/dojox/gfx/matrix.js +0 -170
- package/vendor/dojo-1.10.10/dojox/gfx/matrix.js.uncompressed.js +0 -501
- package/vendor/dojo-1.10.10/dojox/gfx/path.js +0 -319
- package/vendor/dojo-1.10.10/dojox/gfx/path.js.uncompressed.js +0 -478
- package/vendor/dojo-1.10.10/dojox/gfx/shape.js +0 -459
- package/vendor/dojo-1.10.10/dojox/gfx/shape.js.uncompressed.js +0 -1091
- package/vendor/dojo-1.10.10/dojox/gfx/svg.js +0 -662
- package/vendor/dojo-1.10.10/dojox/gfx/svg.js.uncompressed.js +0 -1023
- package/vendor/dojo-1.10.10/dojox/gfx/utils.js +0 -199
- package/vendor/dojo-1.10.10/dojox/gfx/utils.js.uncompressed.js +0 -322
- package/vendor/dojo-1.10.10/dojox/gfx.js +0 -2
- package/vendor/dojo-1.10.10/dojox/gfx.js.uncompressed.js +0 -1136
- package/vendor/dojo-1.10.10/dojox/lang/functional/array.js +0 -151
- package/vendor/dojo-1.10.10/dojox/lang/functional/array.js.uncompressed.js +0 -169
- package/vendor/dojo-1.10.10/dojox/lang/functional/fold.js +0 -93
- package/vendor/dojo-1.10.10/dojox/lang/functional/fold.js.uncompressed.js +0 -125
- package/vendor/dojo-1.10.10/dojox/lang/functional/lambda.js +0 -81
- package/vendor/dojo-1.10.10/dojox/lang/functional/lambda.js.uncompressed.js +0 -132
- package/vendor/dojo-1.10.10/dojox/lang/functional/object.js +0 -54
- package/vendor/dojo-1.10.10/dojox/lang/functional/object.js.uncompressed.js +0 -78
- package/vendor/dojo-1.10.10/dojox/lang/functional/reversed.js +0 -61
- package/vendor/dojo-1.10.10/dojox/lang/functional/reversed.js.uncompressed.js +0 -77
- package/vendor/dojo-1.10.10/dojox/lang/functional/scan.js +0 -87
- package/vendor/dojo-1.10.10/dojox/lang/functional/scan.js.uncompressed.js +0 -107
- package/vendor/dojo-1.10.10/dojox/lang/functional.js +0 -4
- package/vendor/dojo-1.10.10/dojox/lang/functional.js.uncompressed.js +0 -3
- package/vendor/dojo-1.10.10/dojox/lang/utils.js +0 -86
- package/vendor/dojo-1.10.10/dojox/lang/utils.js.uncompressed.js +0 -121
- package/vendor/dojo-1.10.10/dojox/main.js +0 -4
- package/vendor/dojo-1.10.10/dojox/main.js.uncompressed.js +0 -14
- package/vendor/dojo-1.10.10/dojox/storage/LocalStorageProvider.js +0 -112
- package/vendor/dojo-1.10.10/dojox/storage/LocalStorageProvider.js.uncompressed.js +0 -208
- package/vendor/dojo-1.10.10/dojox/storage/Provider.js +0 -62
- package/vendor/dojo-1.10.10/dojox/storage/Provider.js.uncompressed.js +0 -344
- package/vendor/dojo-1.10.10/dojox/storage/manager.js +0 -110
- package/vendor/dojo-1.10.10/dojox/storage/manager.js.uncompressed.js +0 -263
package/dist/884.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"884.js","mappings":"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","sources":["webpack://helm/webpack/runtime/chunk loaded","webpack://helm/webpack/runtime/startup chunk dependencies","webpack://helm/../../../HELM/src/core/errors.ts","webpack://helm/../../../HELM/src/core/constants.ts","webpack://helm/../../../HELM/src/util/observable.ts","webpack://helm/../../../HELM/src/monomer-lib/color-rules.ts","webpack://helm/../../../HELM/src/monomer-lib/library.ts","webpack://helm/../../../HELM/src/layout/line.ts","webpack://helm/../../../HELM/src/layout/circle.ts","webpack://helm/../../../HELM/src/util/types.ts","webpack://helm/../../../HELM/src/render/shapes.ts","webpack://helm/../../../HELM/src/render/theme.ts","webpack://helm/../../../HELM/src/render/atom-shape.ts","webpack://helm/../../../HELM/src/render/svg-builder.ts","webpack://helm/../../../HELM/src/render/svg-renderer.ts","webpack://helm/../../../HELM/src/service/helm-service.ts","webpack://helm/../../../HELM/src/core/selection.ts","webpack://helm/../../../HELM/src/editor/view/dom-utils.ts","webpack://helm/../../../HELM/src/editor/view/tooltip-rdkit.ts","webpack://helm/../../../HELM/src/editor/view/view.ts","webpack://helm/../../../HELM/src/render/selection-overlay.ts","webpack://helm/../../../HELM/src/editor/dialogs/modal.ts","webpack://helm/../../../HELM/src/editor/editor.ts","webpack://helm/../../../HELM/src/editor/palette/rna-builder.ts","webpack://helm/../../../HELM/src/editor/palette/palette.ts","webpack://helm/../../../HELM/src/editor/palette/tile.ts","webpack://helm/../../../HELM/src/datagrok-adapter/monomer-lib-bridge.ts","webpack://helm/../../libraries/bio/src/helm/consts.ts","webpack://helm/../../libraries/bio/src/monomer-works/consts.ts","webpack://helm/../../libraries/bio/src/monomer-works/types.ts","webpack://helm/../../libraries/bio/src/utils/const.ts","webpack://helm/../../libraries/bio/src/utils/macromolecule/types.ts","webpack://helm/../../libraries/bio/src/utils/macromolecule/consts.ts","webpack://helm/../../libraries/bio/src/monomer-works/monomer-works.ts","webpack://helm/../../libraries/bio/src/monomer-works/to-atomic-level-utils.ts","webpack://helm/../../libraries/bio/src/monomer-works/seq-to-molfile-worker.ts","webpack://helm/webpack/bootstrap","webpack://helm/webpack/runtime/define property getters","webpack://helm/webpack/runtime/ensure chunk","webpack://helm/webpack/runtime/get javascript chunk filename","webpack://helm/webpack/runtime/global","webpack://helm/webpack/runtime/hasOwnProperty shorthand","webpack://helm/webpack/runtime/publicPath","webpack://helm/webpack/runtime/importScripts chunk loading","webpack://helm/webpack/startup"],"sourcesContent":["var deferred = [];\n__webpack_require__.O = (result, chunkIds, fn, priority) => {\n\tif(chunkIds) {\n\t\tpriority = priority || 0;\n\t\tfor(var i = deferred.length; i > 0 && deferred[i - 1][2] > priority; i--) deferred[i] = deferred[i - 1];\n\t\tdeferred[i] = [chunkIds, fn, priority];\n\t\treturn;\n\t}\n\tvar notFulfilled = Infinity;\n\tfor (var i = 0; i < deferred.length; i++) {\n\t\tvar [chunkIds, fn, priority] = deferred[i];\n\t\tvar fulfilled = true;\n\t\tfor (var j = 0; j < chunkIds.length; j++) {\n\t\t\tif ((priority & 1 === 0 || notFulfilled >= priority) && Object.keys(__webpack_require__.O).every((key) => (__webpack_require__.O[key](chunkIds[j])))) {\n\t\t\t\tchunkIds.splice(j--, 1);\n\t\t\t} else {\n\t\t\t\tfulfilled = false;\n\t\t\t\tif(priority < notFulfilled) notFulfilled = priority;\n\t\t\t}\n\t\t}\n\t\tif(fulfilled) {\n\t\t\tdeferred.splice(i--, 1)\n\t\t\tvar r = fn();\n\t\t\tif (r !== undefined) result = r;\n\t\t}\n\t}\n\treturn result;\n};","var next = __webpack_require__.x;\n__webpack_require__.x = () => {\n\treturn __webpack_require__.e(782).then(next);\n};","// Typed error hierarchy for the public API. Every method that can fail\n// throws (or returns a Result wrapping) one of these classes — never a bare\n// `Error`. See API.md §9 and ARCHITECTURE.md §8.1.\n\nimport type { PolymerType } from './constants';\n\n/**\n * Validation diagnostic shape produced by the parser/validator and carried by\n * `HelmValidationError`. Mirrors the surface contract documented in\n * VALIDATION.md §1; we re-export the type here so error consumers can `import`\n * a single place.\n */\nexport interface ValidationDiagnostic {\n severity: 'error' | 'warning' | 'info';\n /** Stable, machine-readable code (e.g. 'unknown-monomer'). */\n code: string;\n /** Human-readable message. */\n message: string;\n /** Source span into the offending HELM string, when known. */\n span?: { start: number; end: number };\n /** Free-form structured context (chain id, expected/found values, ...). */\n context?: {\n chainId?: string;\n atomId?: number;\n bondId?: number;\n polymerType?: PolymerType;\n expected?: unknown;\n found?: unknown;\n [key: string]: unknown;\n };\n}\n\nexport type { PolymerType };\n\n/**\n * Base error for everything thrown by the library. Carries a stable `code`\n * and an opaque `context` bag. Concrete subclasses add structured fields\n * (`span` for parse errors, `diagnostics` for validation, etc.).\n *\n * Subclasses restore the prototype chain so `instanceof` checks work across\n * transpilation targets that down-level `extends Error` (ES5 etc.).\n */\nexport class HwError extends Error {\n readonly code: string;\n readonly context?: Readonly<Record<string, unknown>>;\n\n constructor(code: string, message: string, context?: Record<string, unknown>) {\n super(message);\n this.name = 'HwError';\n this.code = code;\n if (context !== undefined) this.context = Object.freeze({ ...context });\n Object.setPrototypeOf(this, new.target.prototype);\n if (typeof Error.captureStackTrace === 'function')\n Error.captureStackTrace(this, new.target);\n }\n}\n\n/**\n * Thrown when a HELM string cannot be parsed. Carries the offending source\n * span (`start`/`end` byte offsets, half-open) when the parser can pinpoint\n * one.\n */\nexport class HelmParseError extends HwError {\n readonly span?: { start: number; end: number };\n\n constructor(\n code: string,\n message: string,\n options?: { span?: { start: number; end: number }; context?: Record<string, unknown> },\n ) {\n super(code, message, options?.context);\n this.name = 'HelmParseError';\n if (options?.span !== undefined) this.span = { ...options.span };\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/**\n * Thrown when a `Mol` (or its source HELM) fails semantic validation in\n * strict mode. Carries every diagnostic produced by the validator. `message`\n * defaults to a short summary including the first diagnostic's text.\n */\nexport class HelmValidationError extends HwError {\n readonly diagnostics: readonly ValidationDiagnostic[];\n\n constructor(diagnostics: ValidationDiagnostic[], message?: string) {\n const frozen = Object.freeze([...diagnostics]);\n const msg = message ?? defaultValidationMessage(frozen);\n super('helm-validation-failed', msg);\n this.name = 'HelmValidationError';\n this.diagnostics = frozen;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\nfunction defaultValidationMessage(diagnostics: readonly ValidationDiagnostic[]): string {\n if (diagnostics.length === 0) return 'HELM validation failed';\n const first = diagnostics[0];\n // `first` is defined because we just checked length; the cast is to satisfy\n // `noUncheckedIndexedAccess`.\n const head = first as ValidationDiagnostic;\n const more = diagnostics.length > 1 ? ` (+${diagnostics.length - 1} more)` : '';\n return `HELM validation failed: ${head.code} — ${head.message}${more}`;\n}\n\n/**\n * Thrown when the library is asked to do something a host should support but\n * we deliberately do not (e.g. atomic-molfile expansion without a plug-in).\n * Differs from `HelmParseError` in that the input is well-formed — we are\n * just not equipped to act on it.\n */\nexport class HelmNotSupportedError extends HwError {\n constructor(code: string, message?: string, context?: Record<string, unknown>) {\n super(code, message ?? `HELM operation not supported: ${code}`, context);\n this.name = 'HelmNotSupportedError';\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/**\n * Thrown by strict monomer-library lookups when a `(polymerType, symbol)`\n * pair is not present. Carries the missing identity so callers can compose a\n * recovery path (e.g. register an inline-SMILES monomer).\n */\nexport class MonomerNotFoundError extends HwError {\n readonly polymerType: PolymerType;\n readonly symbol: string;\n\n constructor(polymerType: PolymerType, symbol: string) {\n super(\n 'monomer-not-found',\n `Monomer not found in library: ${polymerType}/${symbol}`,\n { polymerType, symbol },\n );\n this.name = 'MonomerNotFoundError';\n this.polymerType = polymerType;\n this.symbol = symbol;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n","// Stable string constants for HELM types, polymer types, and monomer types.\n//\n// We use `as const` records (not TS `enum`) so the values tree-shake away when\n// unused and remain plain string literals at runtime. See ARCHITECTURE.md §3.3.\n//\n// Member names use the UPPERCASE Pistoia/HELM convention (`HelmTypes.SUGAR`,\n// `PolymerTypes.PEPTIDE`, `MonomerTypes.BACKBONE`) so that the Datagrok `bio`\n// library can re-export these as the single source of truth without a\n// casing-remap shim. The string VALUES match the canonical Pistoia HELM Web\n// Editor — except `NUCLEOTIDE`, which fixes the long-standing legacy typo\n// (`HELM_NUCLETIDE` → `HELM_NUCLEOTIDE`); Datagrok consumers fix-forward to the\n// corrected value.\n\n/**\n * HELM type tags — the biotype of an atom in a HELM Mol.\n */\nexport const HelmTypes = {\n AA: 'HELM_AA',\n SUGAR: 'HELM_SUGAR',\n BASE: 'HELM_BASE',\n LINKER: 'HELM_LINKER',\n CHEM: 'HELM_CHEM',\n BLOB: 'HELM_BLOB',\n NUCLEOTIDE: 'HELM_NUCLEOTIDE',\n} as const;\n\n/** Union of all HELM type strings. */\nexport type HelmType = (typeof HelmTypes)[keyof typeof HelmTypes];\n\n/**\n * Polymer-type tags as they appear in HELM strings (e.g. `PEPTIDE1{...}`).\n * `G` is the HELM \"generic/group\" polymer type used by some Datagrok flows.\n */\nexport const PolymerTypes = {\n PEPTIDE: 'PEPTIDE',\n RNA: 'RNA',\n CHEM: 'CHEM',\n BLOB: 'BLOB',\n G: 'G',\n} as const;\n\n/** Union of all polymer-type strings. */\nexport type PolymerType = (typeof PolymerTypes)[keyof typeof PolymerTypes];\n\n/**\n * Monomer role within a chain — determines how the monomer participates\n * in backbone vs. branch connectivity.\n */\nexport const MonomerTypes = {\n BACKBONE: 'Backbone',\n BRANCH: 'Branch',\n TERMINAL: 'Terminal',\n UNDEFINED: 'Undefined',\n} as const;\n\n/** Union of all monomer-type strings. */\nexport type MonomerType = (typeof MonomerTypes)[keyof typeof MonomerTypes];\n","// Tiny observable primitive. The rest of the library speaks this small, stable\n// surface instead of importing rxjs directly. Phase 1 (Datagrok migration)\n// removed the rxjs runtime dependency: the public surface we actually use\n// (`subscribe` / `next` / `complete` / `observerCount`) is a few lines of\n// multicast bookkeeping, and depending on rxjs forced a version (7.x) that\n// conflicts with Datagrok's platform-wide rxjs 6.x. Keeping `hwe` zero-runtime-\n// dependency also keeps it a clean leaf library. See ARCHITECTURE.md §5.\n\nimport type { Unsubscribe } from './types';\n\n/**\n * Minimal multicast subject. Subscribers receive every value emitted after\n * they subscribe; previously emitted values are not replayed. Once\n * `complete()` is called the subject is sealed: no further emissions and new\n * subscribers receive nothing.\n *\n * The public surface is deliberately tiny — `subscribe`, `next`, `complete`,\n * and an `observerCount` introspection field. If a consumer needs rxjs\n * operators they can construct an `Observable` from `subscribe` themselves.\n */\nexport class Subject<T> {\n private readonly _listeners: Set<(value: T) => void> = new Set();\n private _completed: boolean = false;\n\n /**\n * Register a handler. Returns an `Unsubscribe` function; calling it tears\n * down this single subscription. Idempotent. Subscribing after `complete()`\n * is a no-op that returns a no-op disposer.\n */\n subscribe(fn: (value: T) => void): Unsubscribe {\n if (this._completed) return () => { /* sealed — nothing to dispose */ };\n this._listeners.add(fn);\n return () => {\n this._listeners.delete(fn);\n };\n }\n\n /**\n * Emit a value to every current subscriber. No-op after `complete()`.\n *\n * We iterate a snapshot so a handler that subscribes or unsubscribes during\n * delivery does not perturb the in-flight emission (matches rxjs `Subject`).\n */\n next(value: T): void {\n if (this._completed) return;\n for (const fn of [...this._listeners]) fn(value);\n }\n\n /** Seal the subject. Further `next()` calls are no-ops; drops all listeners. */\n complete(): void {\n this._completed = true;\n this._listeners.clear();\n }\n\n /** Number of currently subscribed observers. */\n get observerCount(): number {\n return this._listeners.size;\n }\n}\n","// Default color rules for monomer rendering. The renderer (Phase 4) reads\n// these tables; this file has no dependency on the renderer (one-way arrow).\n//\n// Resolution order — MONOMER-LIBRARY.md §6 — is:\n// 1. monomer.meta.colors[themeName] (per-theme override)\n// 2. monomer.meta.colors.default (theme-agnostic override)\n// 3. theme.palette[biotype][monomer.naturalAnalog] (palette default)\n// 4. theme.fallback (last-resort triple)\n//\n// For unresolved monomers (`monomer === null`) we paint `theme.errorTriple`\n// so users see immediately \"something is wrong here\" (RENDERING.md §3).\n//\n// Legacy reference: HELMWebEditor/helm/MonomerColors.js — palette letters keyed\n// on natural-analog letters. We preserve that mapping verbatim for `light` and\n// derive `dark` from it.\n\nimport { HelmTypes } from '../core/constants';\nimport type { HelmType } from '../core/constants';\nimport type { ColorTriple, Monomer, MonomerColors } from './types';\n\n/**\n * A self-contained color theme. The `palette` is keyed first on `HelmType`\n * (the atom's biotype) and then on a single-letter `naturalAnalog`. A missing\n * key falls through to `fallback`; `errorTriple` is reserved for atoms whose\n * monomer could not be resolved.\n */\nexport interface ColorTheme {\n name: string;\n fallback: ColorTriple;\n errorTriple: ColorTriple;\n palette: Partial<Record<HelmType, Record<string, ColorTriple>>>;\n}\n\n// ---------- Helpers ----------\n\n/**\n * Build a `ColorTriple` whose `line` is the legacy black-on-white stroke and\n * whose `text` is black, leaving `background` configurable. The legacy editor\n * draws every shape's outline in `#202020` (effectively black) and the symbol\n * label in `#000000`.\n */\nfunction lightTriple(background: string): ColorTriple {\n return { line: '#202020', text: '#000000', background };\n}\n\n/**\n * Build a dark-theme `ColorTriple`. The legacy editor didn't have a dark\n * theme; we derive ours by:\n * - using `#ffffff` for the symbol label (text on dark backgrounds),\n * - using `#e0e0e0` for the stroke,\n * - keeping the legacy hue but lowering its lightness by ~25% so the\n * symbol stays legible on a dark canvas.\n *\n * The actual `background` is supplied per-monomer. Saturation tweaks are not\n * applied here — we trust the palette author and prefer determinism over\n * cleverness.\n */\nfunction darkTriple(background: string): ColorTriple {\n return { line: '#e0e0e0', text: '#ffffff', background: darken(background, 0.25) };\n}\n\n/**\n * Parse `#rrggbb` or `#rgb` into [r,g,b] integers. Returns null on a\n * non-hex input (typical when the palette was extended with `rgb(...)`\n * literals).\n */\nfunction parseHex(hex: string): [number, number, number] | null {\n const m6 = /^#([0-9a-fA-F]{6})$/.exec(hex);\n const m3 = /^#([0-9a-fA-F]{3})$/.exec(hex);\n if (m6 !== null) {\n const s = m6[1] as string;\n return [\n parseInt(s.slice(0, 2), 16),\n parseInt(s.slice(2, 4), 16),\n parseInt(s.slice(4, 6), 16),\n ];\n }\n if (m3 !== null) {\n const s = m3[1] as string;\n return [\n parseInt(s[0] as string + (s[0] as string), 16),\n parseInt(s[1] as string + (s[1] as string), 16),\n parseInt(s[2] as string + (s[2] as string), 16),\n ];\n }\n return null;\n}\n\nfunction hexByte(n: number): string { return n.toString(16).padStart(2, '0'); }\n\nfunction toHex(r: number, g: number, b: number): string {\n return `#${hexByte(r)}${hexByte(g)}${hexByte(b)}`;\n}\n\n/**\n * Convert [r,g,b] (0..255) to HSL where h is in [0, 360), s and l are in\n * [0, 1]. Standard formula — kept inline so we don't depend on an external\n * colour library for one routine.\n */\nfunction rgbToHsl(r: number, g: number, b: number): [number, number, number] {\n const rn = r / 255, gn = g / 255, bn = b / 255;\n const max = Math.max(rn, gn, bn);\n const min = Math.min(rn, gn, bn);\n const l = (max + min) / 2;\n if (max === min) return [0, 0, l];\n const d = max - min;\n const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);\n let h: number;\n if (max === rn) h = ((gn - bn) / d + (gn < bn ? 6 : 0)) * 60;\n else if (max === gn) h = ((bn - rn) / d + 2) * 60;\n else h = ((rn - gn) / d + 4) * 60;\n return [h, s, l];\n}\n\nfunction hue2rgb(p: number, q: number, t: number): number {\n let tt = t;\n if (tt < 0) tt += 1;\n if (tt > 1) tt -= 1;\n if (tt < 1 / 6) return p + (q - p) * 6 * tt;\n if (tt < 1 / 2) return q;\n if (tt < 2 / 3) return p + (q - p) * (2 / 3 - tt) * 6;\n return p;\n}\n\nfunction hslToRgb(h: number, s: number, l: number): [number, number, number] {\n if (s === 0) {\n const v = Math.round(l * 255);\n return [v, v, v];\n }\n const q = l < 0.5 ? l * (1 + s) : l + s - l * s;\n const p = 2 * l - q;\n const hn = h / 360;\n return [\n Math.round(hue2rgb(p, q, hn + 1 / 3) * 255),\n Math.round(hue2rgb(p, q, hn) * 255),\n Math.round(hue2rgb(p, q, hn - 1 / 3) * 255),\n ];\n}\n\n/**\n * \"Soften\" a palette colour — desaturate by ~30%, lighten by ~10% — into a\n * pastel suitable for palette tiles and (optionally) the canvas. The\n * algorithm:\n * 1. Convert hex to HSL.\n * 2. Multiply saturation by `(1 - sAmt)` (default 0.30 = -30%).\n * 3. Add `lAmt * (1 - L)` to lightness (default 0.10 = +10%), clamped at\n * 0.95 so the colour never becomes pure white.\n * 4. Convert back to hex.\n *\n * Inputs that don't parse as hex are returned unchanged.\n *\n * The result is deterministic and round-trips to the same value for any\n * given input, so tests can compare against the soften() output rather\n * than hand-tuning a soft palette table.\n *\n * Sample (sanity check):\n * soften('#E60A0A') → ~'#f0563f' (red → coral pastel)\n * soften('#145AFF') → ~'#588fff' (royal blue → sky pastel)\n */\nexport function soften(\n color: string,\n sAmt: number = 0.30,\n lAmt: number = 0.10,\n): string {\n const rgb = parseHex(color);\n if (rgb === null) return color;\n const [h, s, l] = rgbToHsl(rgb[0], rgb[1], rgb[2]);\n const newS = Math.max(0, Math.min(1, s * (1 - sAmt)));\n const newL = Math.max(0, Math.min(0.95, l + lAmt * (1 - l)));\n const [r2, g2, b2] = hslToRgb(h, newS, newL);\n return toHex(r2, g2, b2);\n}\n\n/**\n * Compute the WCAG 2.x relative-luminance value for a hex colour. Returns 0\n * for non-hex inputs.\n */\nfunction relativeLuminance(hex: string): number {\n const rgb = parseHex(hex);\n if (rgb === null) return 0;\n const channel = (c: number): number => {\n const cs = c / 255;\n return cs <= 0.03928 ? cs / 12.92 : Math.pow((cs + 0.055) / 1.055, 2.4);\n };\n return 0.2126 * channel(rgb[0]) + 0.7152 * channel(rgb[1]) + 0.0722 * channel(rgb[2]);\n}\n\n/**\n * WCAG contrast ratio between two hex colours. Result ≥ 4.5 passes the\n * \"normal text\" AA threshold; ≥ 5 is the threshold the editor uses for\n * palette tiles (small symbol labels on a pastel background).\n */\nexport function contrastRatio(fg: string, bg: string): number {\n const l1 = relativeLuminance(fg);\n const l2 = relativeLuminance(bg);\n const hi = Math.max(l1, l2);\n const lo = Math.min(l1, l2);\n return (hi + 0.05) / (lo + 0.05);\n}\n\n/**\n * Phase 18 (T-1804) — Convert a hex colour string to its HSL components.\n *\n * - `h` ∈ [0, 360) — hue in degrees. Greys (R = G = B) return `h = 0`.\n * - `s` ∈ [0, 1] — saturation. Greys return `s = 0`.\n * - `l` ∈ [0, 1] — lightness.\n *\n * Inputs that do not parse as hex return `{h: 0, s: 0, l: 0}` so callers\n * can safely sort an array of mixed-validity colours without checking for\n * `null` at every comparison.\n *\n * Used by the palette to sort tiles within a subsection by colour, so\n * visually similar monomers cluster instead of producing a random rainbow.\n */\nexport function hexToHsl(hex: string): { h: number; s: number; l: number } {\n const rgb = parseHex(hex);\n if (rgb === null) return { h: 0, s: 0, l: 0 };\n const [h, s, l] = rgbToHsl(rgb[0], rgb[1], rgb[2]);\n return { h, s, l };\n}\n\n/**\n * Phase 23 (T-2301) — Convert HSL back to hex. Inverse of `hexToHsl`. Values\n * outside the canonical ranges are clamped (h wraps mod 360, s/l clamp to\n * [0,1]). Used by `deriveLineColor` to compute a \"slightly darker\" version\n * of a monomer's background fill.\n */\nexport function hslToHex(hsl: { h: number; s: number; l: number }): string {\n const h = ((hsl.h % 360) + 360) % 360;\n const s = Math.max(0, Math.min(1, hsl.s));\n const l = Math.max(0, Math.min(1, hsl.l));\n const [r, g, b] = hslToRgb(h, s, l);\n return toHex(r, g, b);\n}\n\n/**\n * Phase 23 (T-2301) — Derive a monomer outline (\"line\") colour from the\n * resolved background fill. The user reported that the legacy `#202020`\n * black outline on every monomer was visually heavy, and asked for the\n * outline to be a slightly darker version of the fill — except for white\n * or very-light fills, where a darker neutral falls back so the shape\n * stays visible against the white canvas.\n *\n * Algorithm:\n * 1. Convert `background` to HSL.\n * 2. If lightness > 0.85, OR (saturation < 0.15 AND lightness > 0.7),\n * treat as \"near-white\" and return `#1f2937` (the editor's dark\n * text colour — matches the tooltip body colour).\n * 3. Otherwise, reduce lightness by 0.30 with a floor of 0.08\n * (already-dark fills like `#0F820F` would otherwise hit pure\n * black, defeating the \"not black\" rule), and slightly boost\n * saturation (+0.10, clamped at 1) so the darkened colour reads\n * as a \"stronger\" tint of the same hue rather than a muddy\n * grey-shift.\n *\n * Inputs that don't parse as hex return `#1f2937` (the dark fallback).\n *\n * Sample (sanity):\n * deriveLineColor('#FF0000') → '#660000' (pure red → dark red)\n * deriveLineColor('#FFFFFF') → '#1f2937' (white → dark slate)\n * deriveLineColor('#F5F5F5') → '#1f2937' (near-white → dark slate)\n * deriveLineColor('#C8C8C8') → '#1f2937' (light grey, low saturation → dark slate)\n * deriveLineColor('#A0A0FF') → darker blue (saturated → ~half-lightness)\n * deriveLineColor('#0F820F') → very dark green (NOT '#000000')\n */\nexport function deriveLineColor(background: string): string {\n const rgb = parseHex(background);\n if (rgb === null) return '#1f2937';\n const [h, s, l] = rgbToHsl(rgb[0], rgb[1], rgb[2]);\n if (l > 0.85 || (s < 0.15 && l > 0.7))\n return '#1f2937';\n // Floor at 0.08 so already-dark, saturated fills (Leucine green,\n // Tryptophan purple, etc.) don't collapse to pure black -- pure\n // black is the explicit \"I don't like this\" colour the user\n // banned at the top of T-2301.\n const darkerL = Math.max(0.08, l - 0.30);\n const boostedS = Math.min(1, s + 0.10);\n return hslToHex({ h, s: boostedS, l: darkerL });\n}\n\n/**\n * Phase 23.2 (T-2310) — Parse `#rrggbb` (or `#rgb`) into `{r, g, b}` with\n * each channel in `[0, 1]`. Returns `null` on a non-hex input. Used by\n * `bestContrastTextColor` so the WCAG relative-luminance formula can apply\n * directly without intermediate 0..255 -> 0..1 scaling at every call site.\n *\n * Implemented as a thin wrapper around the existing `parseHex` (which\n * normalises 3-digit hex like `#abc` -> `#aabbcc` already).\n */\nfunction hexToRgb01(hex: string): { r: number; g: number; b: number } | null {\n const rgb = parseHex(hex);\n if (rgb === null) return null;\n return { r: rgb[0] / 255, g: rgb[1] / 255, b: rgb[2] / 255 };\n}\n\n/**\n * Phase 23.2 (T-2310) — Pick the best-contrast text colour (dark slate vs.\n * pure white) for a given background. The user reported that the monomer\n * symbol label was always painted black, which is illegible on dark-saturated\n * backgrounds like Arginine's `#145AFF` royal blue or Tryptophan's purple.\n * Phase 23 fixed the *outline* via `deriveLineColor`; this completes the\n * picture by making the *text* contrast with the fill too.\n *\n * Algorithm (WCAG 2.1 §1.4.6 relative luminance, §1.4.3 contrast ratio):\n * 1. Convert the background hex to RGB in `[0, 1]`.\n * 2. Linearise each channel (sRGB -> linear).\n * 3. Compute relative luminance L = 0.2126 R + 0.7152 G + 0.0722 B.\n * 4. Compute contrast ratios against pure black (L=0) and pure white (L=1)\n * via `(L_lighter + 0.05) / (L_darker + 0.05)`.\n * 5. Return `'#ffffff'` if white-text contrast is higher, otherwise\n * `'#1f2937'` (the editor's dark-slate text colour, matching the\n * Phase-23 `deriveLineColor` near-white fallback).\n *\n * Non-hex inputs return `'#1f2937'` (the dark fallback) so callers feeding\n * `rgb(...)` literals don't silently get pure white on a light background.\n *\n * Citations:\n * - WCAG 2.1 §1.4.3 — contrast ratio definition.\n * - WCAG 2.1 §1.4.6 — relative luminance formula.\n *\n * Sample (sanity check, hand-traced):\n * bestContrastTextColor('#ffffff') -> '#1f2937' (dark on white)\n * bestContrastTextColor('#000000') -> '#ffffff' (white on black)\n * bestContrastTextColor('#145AFF') -> '#ffffff' (white on royal blue)\n * bestContrastTextColor('#C8C8C8') -> '#1f2937' (dark on light grey)\n * bestContrastTextColor('#FFFF00') -> '#1f2937' (dark on yellow)\n * bestContrastTextColor('#A4E2DC') -> '#1f2937' (dark on light tiffany)\n */\nexport function bestContrastTextColor(backgroundHex: string): string {\n const rgb = hexToRgb01(backgroundHex);\n if (rgb === null) return '#1f2937';\n const toLinear = (c: number): number =>\n c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);\n const L = 0.2126 * toLinear(rgb.r) + 0.7152 * toLinear(rgb.g) + 0.0722 * toLinear(rgb.b);\n const contrastWithBlack = (L + 0.05) / (0 + 0.05);\n const contrastWithWhite = (1 + 0.05) / (L + 0.05);\n return contrastWithWhite > contrastWithBlack ? '#ffffff' : '#1f2937';\n}\n\n/**\n * Reduce a hex color's lightness by the given fraction. We parse `#rrggbb`\n * (or `#rgb`) into [r,g,b], multiply by `1 - amt`, and emit `#rrggbb`. Inputs\n * that don't match the hex pattern are returned unchanged (rare; only safety\n * for caller-supplied non-hex CSS strings like `rgb(...)`).\n */\nfunction darken(hex: string, amt: number): string {\n const rgb = parseHex(hex);\n if (rgb === null) return hex;\n const f = 1 - amt;\n const nr = Math.max(0, Math.min(255, Math.round(rgb[0] * f)));\n const ng = Math.max(0, Math.min(255, Math.round(rgb[1] * f)));\n const nb = Math.max(0, Math.min(255, Math.round(rgb[2] * f)));\n return toHex(nr, ng, nb);\n}\n\n// ---------- Legacy palette (light theme) ----------\n\nconst LIGHT_BASES: Record<string, ColorTriple> = {\n A: lightTriple('#A0A0FF'),\n G: lightTriple('#FF7070'),\n T: lightTriple('#A0FFA0'),\n C: lightTriple('#FF8C4B'),\n U: lightTriple('#FF8080'),\n};\n\nconst LIGHT_SUGARS: Record<string, ColorTriple> = {\n R: lightTriple('#7a85c1'),\n r: lightTriple('#7a85c1'),\n};\n\nconst LIGHT_LINKERS: Record<string, ColorTriple> = {\n P: lightTriple('#9aa5e1'),\n p: lightTriple('#9aa5e1'),\n};\n\nconst LIGHT_AAS: Record<string, ColorTriple> = {\n A: lightTriple('#C8C8C8'),\n R: lightTriple('#145AFF'),\n N: lightTriple('#00DCDC'),\n D: lightTriple('#E60A0A'),\n C: lightTriple('#E6E600'),\n E: lightTriple('#00DCDC'),\n Q: lightTriple('#E60A0A'),\n G: lightTriple('#EBEBEB'),\n H: lightTriple('#8282D2'),\n I: lightTriple('#0F820F'),\n L: lightTriple('#0F820F'),\n K: lightTriple('#145AFF'),\n M: lightTriple('#E6E600'),\n F: lightTriple('#3232AA'),\n P: lightTriple('#DC9682'),\n S: lightTriple('#FA9600'),\n T: lightTriple('#FA9600'),\n W: lightTriple('#B45AB4'),\n Y: lightTriple('#3232AA'),\n V: lightTriple('#0F820F'),\n};\n\nconst LIGHT_CHEMS: Record<string, ColorTriple> = {\n R: lightTriple('#eeeeee'),\n};\n\nconst LIGHT_BLOBS: Record<string, ColorTriple> = {\n B: lightTriple('#999999'),\n G: lightTriple('#e2e2e2'),\n};\n\nconst LIGHT_NUCLEOTIDES: Record<string, ColorTriple> = {\n // Pseudo-biotype used for the gap base-pair marker; mirror the linker palette.\n P: lightTriple('#9aa5e1'),\n};\n\n/** Built-in light theme — matches the legacy `MonomerColors.js` palette. */\nexport const lightPalette: ColorTheme = {\n name: 'light',\n fallback: { line: '#202020', text: '#000000', background: '#eeeeee' },\n errorTriple: { line: '#cc0000', text: '#000000', background: '#FFFF00' },\n palette: {\n [HelmTypes.BASE]: LIGHT_BASES,\n [HelmTypes.SUGAR]: LIGHT_SUGARS,\n [HelmTypes.LINKER]: LIGHT_LINKERS,\n [HelmTypes.AA]: LIGHT_AAS,\n [HelmTypes.CHEM]: LIGHT_CHEMS,\n [HelmTypes.BLOB]: LIGHT_BLOBS,\n [HelmTypes.NUCLEOTIDE]: LIGHT_NUCLEOTIDES,\n },\n};\n\n// ---------- Dark theme derivation ----------\n\nfunction darkenMap(src: Record<string, ColorTriple>): Record<string, ColorTriple> {\n const out: Record<string, ColorTriple> = {};\n for (const [k, v] of Object.entries(src))\n out[k] = darkTriple(v.background);\n return out;\n}\n\n/** Dark theme — derived from `lightPalette` by lightening text & darkening fills. */\nexport const darkPalette: ColorTheme = {\n name: 'dark',\n fallback: { line: '#e0e0e0', text: '#ffffff', background: darken('#eeeeee', 0.5) },\n errorTriple: { line: '#ff5050', text: '#ffffff', background: darken('#FFFF00', 0.4) },\n palette: {\n [HelmTypes.BASE]: darkenMap(LIGHT_BASES),\n [HelmTypes.SUGAR]: darkenMap(LIGHT_SUGARS),\n [HelmTypes.LINKER]: darkenMap(LIGHT_LINKERS),\n [HelmTypes.AA]: darkenMap(LIGHT_AAS),\n [HelmTypes.CHEM]: darkenMap(LIGHT_CHEMS),\n [HelmTypes.BLOB]: darkenMap(LIGHT_BLOBS),\n [HelmTypes.NUCLEOTIDE]: darkenMap(LIGHT_NUCLEOTIDES),\n },\n};\n\n// ---------- Public resolution function ----------\n\n/**\n * Resolve the final `MonomerColors` triple for the given (monomer, biotype)\n * pair under the given theme. Follows the order in MONOMER-LIBRARY.md §6.\n *\n * - `monomer === null` paints `theme.errorTriple` (the \"missing monomer\"\n * cue).\n * - `monomer.meta.colors[themeName]` wins over `default`.\n * - `monomer.meta.colors.default` wins over the palette.\n * - Palette lookup is keyed on `biotype` first, then `monomer.naturalAnalog`\n * (case-sensitive — matches the legacy table).\n * - Final fallback is `theme.fallback`.\n *\n * Phase 23 (T-2301) — Outline colour policy. The user reported that the\n * legacy `#202020` black outline on every monomer was visually heavy and\n * asked for the outline to be a slightly darker version of the monomer's\n * fill, falling back to a dark neutral when the fill is white-ish. We\n * implement that by post-processing the resolved triple: the `line`\n * field is ALWAYS replaced by `deriveLineColor(background)`. Palette-\n * supplied `line` fields (the legacy `#202020` defaults) are overridden,\n * and Phase 23.3 (T-2311) extended this to per-monomer `meta.colors.*.line`\n * overrides too — the HELMCoreLibrary ships every monomer with an\n * explicit `meta.colors.default.line: \"#202020\"`, which is the same\n * legacy near-black value Phase 23 was meant to abolish. Respecting it\n * would re-introduce the exact \"outline is still black\" complaint the\n * user filed on 2026-05-18. We treat outline as a derived presentational\n * property, NOT user data; the background and text fields from\n * `meta.colors` are still respected (only the line is derived).\n *\n * Phase 23.2 (T-2310) — Text colour policy. The user reported a similar\n * complaint about the symbol label: the legacy palette always painted\n * the text `#000000` (pure black), which is illegible on dark-saturated\n * backgrounds like Arginine's `#145AFF` royal blue. We mirror the Phase 23\n * outline approach: unless the monomer's `meta.colors` explicitly supplied\n * a `text` field, the `text` is replaced by\n * `bestContrastTextColor(background)` (WCAG 2.1 relative-luminance pick\n * between `'#1f2937'` dark-slate and `'#ffffff'` pure white). The\n * missing-monomer error path keeps its `errorTriple.text` so the\n * \"something is wrong here\" cue stays intact. Unlike the outline path,\n * an explicit `meta.colors.*.text` is still respected — text is treated\n * as content the library author may deliberately want (e.g. a custom\n * branded glyph colour), while outline is treated as a derived chrome\n * property the renderer owns.\n *\n * The returned `MonomerColors` is always fully populated (no undefined\n * fields).\n */\nexport function resolveMonomerColors(\n monomer: Monomer | null,\n biotype: HelmType,\n theme: ColorTheme = lightPalette,\n): MonomerColors {\n if (monomer === null)\n return { ...theme.errorTriple };\n\n const metaColors = monomer.meta?.colors;\n if (metaColors !== undefined) {\n // `themeName === 'light' | 'dark' | custom` — only `light` and `dark`\n // get auto-resolved from the meta block; any other theme name still\n // checks `default` first.\n //\n // Phase 23.3 (T-2311) — the resolved `line` is ALWAYS derived from\n // the background. The HELMCoreLibrary ships every monomer with\n // `meta.colors.default.line: \"#202020\"` (the same legacy near-black\n // value Phase 23 set out to abolish from the palette branch). The\n // earlier Phase-23 code respected explicit meta `line` fields,\n // which re-introduced exactly the \"outline is still black\"\n // complaint the user filed on 2026-05-18 for the ~30% of monomers\n // that carry colour metadata. Treat outline as derived chrome,\n // NOT user data; the background and text fields stay sourced from\n // the meta block.\n const themed =\n theme.name === 'light' ? metaColors.light :\n theme.name === 'dark' ? metaColors.dark :\n undefined;\n if (themed !== undefined)\n return { ...themed, line: deriveLineColor(themed.background) };\n if (metaColors.default !== undefined)\n return { ...metaColors.default, line: deriveLineColor(metaColors.default.background) };\n }\n\n const na = monomer.naturalAnalog;\n if (na !== undefined && na !== '') {\n const perType = theme.palette[biotype];\n if (perType !== undefined) {\n const hit = perType[na];\n if (hit !== undefined)\n // Phase 23 (T-2301): palette `line` is the legacy `#202020`\n // default. The user wants the outline derived from the\n // background; override.\n // Phase 23.2 (T-2310): palette `text` is the legacy `#000000`\n // (or `#ffffff` for dark theme). The user wants the text to\n // contrast with the resolved background; override via WCAG\n // relative-luminance pick.\n return {\n ...hit,\n line: deriveLineColor(hit.background),\n text: bestContrastTextColor(hit.background),\n };\n }\n }\n\n return {\n ...theme.fallback,\n line: deriveLineColor(theme.fallback.background),\n text: bestContrastTextColor(theme.fallback.background),\n };\n}\n","// Concrete `MonomerLib` class — implements `IMonomerLib`. Indexes monomers by\n// `(polymerType, symbol)` for O(1) lookup, mirrors the index by HELM biotype,\n// and pre-computes `ResolvedMonomer` data attached out-of-band so the public\n// `Monomer` type stays clean. See API.md §5 and MONOMER-LIBRARY.md §3.\n//\n// Layering: this file is monomer-lib only — no core/io/render/etc. imports\n// beyond `HelmTypes` (a constant) and the typed error class (`HwError`).\n//\n// Pistoia HELM Web Editor parity references:\n// - `helm2Type` — Monomers.ts:518 (legacy)\n// - case sensitivity — `kCaseSensitive = true` in `packages/Helm/src/utils/package-utils.ts:255`\n// - inline SMILES `#N` — Monomers.ts:548\n\nimport { HelmTypes, PolymerTypes } from '../core/constants';\nimport type { HelmType, PolymerType } from '../core/constants';\nimport { HwError } from '../core/errors';\nimport { Subject } from '../util/observable';\nimport { resolveMonomerColors, lightPalette, darkPalette } from './color-rules';\nimport type { ColorTheme } from './color-rules';\nimport { detectSmilesRGroups } from './smiles-monomer';\nimport type {\n IMonomerLib,\n Monomer,\n MonomerColors,\n MonomerLibChangeEvent,\n MonomerLibSchema,\n MonomerMeta,\n RGroupSpec,\n ResolvedMonomer,\n} from './types';\nimport {\n validateMonomerLibSchema,\n type SchemaError,\n} from './validate-schema';\nimport { normalizeCapGroupSMILES } from './rgroup-fields';\n\n/** Optional diagnostics sink for `fromJSON` — used to surface skipped entries. */\nexport interface MonomerLibLoadDiagnostics {\n /** Errors reported by the schema validator for skipped entries. */\n skipped: { index: number; errors: SchemaError[] }[];\n}\n\nconst RLABEL_RE = /^R([1-9][0-9]*)$/;\n\n// ---------- biotype mapping (`helm2Type`) ----------\n\n/**\n * Compute the HELM biotype for a monomer record. Mirrors `helm2Type` from\n * the legacy editor (Monomers.ts:518) — the only HELM-specific business\n * rule that lives in the library layer.\n *\n * Rules:\n * - `PEPTIDE` → `HELM_AA`\n * - `CHEM` → `HELM_CHEM`\n * - `BLOB` → `HELM_BLOB`\n * - `RNA` Branch → `HELM_BASE`\n * - `RNA` Backbone + naturalAnalog in {'P','p'} → `HELM_LINKER`\n * - `RNA` Backbone otherwise → `HELM_SUGAR`\n * - anything else → `null` (caller can fall back to a default)\n */\nexport function helm2Type(monomer: Monomer): HelmType | null {\n switch (monomer.polymerType) {\n case PolymerTypes.PEPTIDE:\n return HelmTypes.AA;\n case PolymerTypes.CHEM:\n return HelmTypes.CHEM;\n case PolymerTypes.BLOB:\n return HelmTypes.BLOB;\n case PolymerTypes.RNA: {\n if (monomer.monomerType === 'Branch') return HelmTypes.BASE;\n if (monomer.monomerType === 'Backbone') {\n const na = monomer.naturalAnalog;\n if (na === 'P' || na === 'p') return HelmTypes.LINKER;\n return HelmTypes.SUGAR;\n }\n return null;\n }\n default:\n return null;\n }\n}\n\n// ---------- resolved-monomer construction ----------\n\nfunction buildResolved(m: Monomer): ResolvedMonomer {\n const rgroupMap = new Map<number, RGroupSpec>();\n for (const rg of m.rgroups) {\n const match = RLABEL_RE.exec(rg.label);\n if (match !== null)\n rgroupMap.set(parseInt(match[1] as string, 10), rg);\n }\n return Object.assign({}, m, {\n rs: m.rgroups.length,\n rgroupMap: rgroupMap as ReadonlyMap<number, RGroupSpec>,\n });\n}\n\n// ---------- the class ----------\n\n/**\n * In-process monomer library. Loads JSON in the Pistoia shape, indexes by\n * polymer type / biotype / natural analog, and provides reactive mutations\n * via `onChanged`.\n *\n * Lookups are **case-sensitive** to match `kCaseSensitive = true` in the\n * Datagrok Helm package. `version` starts at `0` and is bumped on every\n * mutation; consumers cache derived values keyed on `${source}@${version}`.\n *\n * The class implements `IMonomerLib`; the `getTooltipDom` method is omitted\n * (it's optional in the interface — DOM construction lives in the editor\n * layer).\n */\nexport class MonomerLib implements IMonomerLib {\n readonly source: string;\n readonly onChanged: Subject<MonomerLibChangeEvent>;\n\n private _version: number = 0;\n private readonly _byPolymerSymbol: Map<PolymerType, Map<string, Monomer>> = new Map();\n private readonly _byBiotypeSymbol: Map<HelmType, Map<string, Monomer>> = new Map();\n private readonly _byNaturalAnalog: Map<PolymerType, Map<string, Monomer[]>> = new Map();\n private readonly _resolved: WeakMap<Monomer, ResolvedMonomer> = new WeakMap();\n private _inlineSmilesCounter: number = 0;\n\n /**\n * Private — use `MonomerLib.fromJSON`, `fromText`, `fromUrl`, or `empty`\n * to construct. Direct construction would force every caller to learn the\n * indexing rules.\n */\n private constructor(source: string) {\n this.source = source;\n this.onChanged = new Subject<MonomerLibChangeEvent>();\n }\n\n // ---------- factories ----------\n\n /** Construct an empty library. */\n static empty(source: string = 'empty'): MonomerLib {\n return new MonomerLib(source);\n }\n\n /**\n * Construct from an already-parsed schema array. Invalid entries are\n * SKIPPED (with a console warning, not thrown) so a corrupt library\n * doesn't poison every editor on the page. If you want strict validation,\n * call `validateMonomerLibSchema` first.\n *\n * @param schema the array of `Monomer` records.\n * @param source identity string used in cache keys (default `'inline'`).\n * @param diagnostics optional sink for the skipped-entry report.\n */\n static fromJSON(\n schema: MonomerLibSchema,\n source: string = 'inline',\n diagnostics?: MonomerLibLoadDiagnostics,\n ): MonomerLib {\n const lib = new MonomerLib(source);\n for (let i = 0; i < schema.length; i++) {\n const entry = schema[i] as Monomer;\n const validation = validateMonomerLibSchema([entry]);\n if (!validation.ok) {\n if (diagnostics !== undefined)\n diagnostics.skipped.push({ index: i, errors: validation.errors });\n else\n console.warn(`MonomerLib.fromJSON: skipping invalid entry at index ${i}`, validation.errors);\n continue;\n }\n lib._addNoFire(entry);\n }\n return lib;\n }\n\n /**\n * Parse JSON text and construct. Validation failures throw `HwError`\n * (`'invalid-monomer-lib'`) because at this stage we have no idea what\n * shape was attempted.\n */\n static fromText(jsonText: string, source: string = 'inline'): MonomerLib {\n let parsed: unknown;\n try {\n parsed = JSON.parse(jsonText);\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n throw new HwError('invalid-monomer-lib', `MonomerLib.fromText: JSON.parse failed — ${msg}`);\n }\n const validation = validateMonomerLibSchema(parsed);\n if (!validation.ok) {\n throw new HwError(\n 'invalid-monomer-lib',\n `MonomerLib.fromText: schema validation failed (${validation.errors.length} errors)`,\n { errors: validation.errors },\n );\n }\n return MonomerLib.fromJSON(validation.data, source);\n }\n\n /**\n * Load a library over the network. `fetchImpl` defaults to `globalThis.fetch`;\n * in Node 16 (no built-in fetch) the caller must supply one or the function\n * throws.\n */\n static async fromUrl(url: string, fetchImpl?: typeof fetch): Promise<MonomerLib> {\n const f = fetchImpl ?? (globalThis as { fetch?: typeof fetch }).fetch;\n if (f === undefined)\n throw new HwError(\n 'fetch-unavailable',\n 'MonomerLib.fromUrl: no fetch implementation available — pass `fetchImpl` or run in a fetch-capable environment.',\n { url },\n );\n const res = await f(url);\n if (!res.ok)\n throw new HwError(\n 'monomer-lib-fetch-failed',\n `MonomerLib.fromUrl: HTTP ${res.status} fetching ${url}`,\n { url, status: res.status },\n );\n const text = await res.text();\n return MonomerLib.fromText(text, url);\n }\n\n /**\n * Merge any number of libraries into a new `MonomerLib`. Later arguments\n * win on `(polymerType, symbol)` collisions, matching the convention in\n * MONOMER-LIBRARY.md §3.\n */\n static merge(...libs: IMonomerLib[]): MonomerLib {\n const source = libs.map((l) => l.source).join('+') || 'empty';\n const merged = new MonomerLib(source);\n for (const lib of libs) {\n for (const polymerType of lib.getPolymerTypes()) {\n for (const sym of lib.getMonomerSymbolsByType(polymerType)) {\n const m = lib.getMonomer(polymerType, sym);\n if (m !== null) merged._addNoFire(m);\n }\n }\n }\n return merged;\n }\n\n // ---------- identity ----------\n\n get isEmpty(): boolean {\n for (const inner of this._byPolymerSymbol.values())\n if (inner.size > 0) return false;\n return true;\n }\n\n get version(): number {\n return this._version;\n }\n\n // ---------- reads ----------\n\n getMonomer(polymerType: PolymerType, symbol: string): Monomer | null {\n const inner = this._byPolymerSymbol.get(polymerType);\n if (inner === undefined) return null;\n return inner.get(symbol) ?? null;\n }\n\n getMonomerSet(biotype: HelmType): Map<string, Monomer> {\n const inner = this._byBiotypeSymbol.get(biotype);\n // We return a copy so callers can't mutate our index.\n return inner === undefined ? new Map() : new Map(inner);\n }\n\n getMonomerSymbolsByType(polymerType: PolymerType): string[] {\n const inner = this._byPolymerSymbol.get(polymerType);\n return inner === undefined ? [] : Array.from(inner.keys());\n }\n\n getByNaturalAnalog(polymerType: PolymerType, na: string): Monomer[] {\n const outer = this._byNaturalAnalog.get(polymerType);\n if (outer === undefined) return [];\n const arr = outer.get(na);\n return arr === undefined ? [] : arr.slice();\n }\n\n getPolymerTypes(): PolymerType[] {\n const out: PolymerType[] = [];\n for (const [pt, inner] of this._byPolymerSymbol.entries())\n if (inner.size > 0) out.push(pt);\n return out;\n }\n\n /**\n * Resolve the monomer's color triple. `themeName` is `'light'` (default),\n * `'dark'`, or a custom theme name. Unknown theme names fall back to the\n * light palette so colors are always produced.\n */\n getColors(monomer: Monomer | null, themeName?: string): MonomerColors {\n const theme = pickTheme(themeName);\n const biotype = monomer === null\n ? HelmTypes.AA // unused by resolveMonomerColors when monomer===null\n : helm2Type(monomer) ?? HelmTypes.AA;\n return resolveMonomerColors(monomer, biotype, theme);\n }\n\n /** Lookup the precomputed `ResolvedMonomer` for a `Monomer` from this lib. */\n getResolved(monomer: Monomer): ResolvedMonomer | undefined {\n return this._resolved.get(monomer);\n }\n\n // ---------- mutations ----------\n\n /** Register or replace a monomer. Fires `onChanged({ type: 'add'|'update' })`. */\n addMonomer(monomer: Monomer): void {\n const validation = validateMonomerLibSchema([monomer]);\n if (!validation.ok)\n throw new HwError(\n 'invalid-monomer',\n `MonomerLib.addMonomer: schema validation failed`,\n { errors: validation.errors },\n );\n const existed = this._addNoFire(monomer);\n this._bumpVersion();\n this.onChanged.next({\n type: existed ? 'update' : 'add',\n affectedSymbols: [monomer.symbol],\n });\n }\n\n /** Bulk add. Fires a single `onChanged` after all entries are inserted. */\n addMonomers(monomers: Iterable<Monomer>): void {\n const affected: string[] = [];\n let anyExisted = false;\n for (const m of monomers) {\n const existed = this._addNoFire(m);\n if (existed) anyExisted = true;\n affected.push(m.symbol);\n }\n if (affected.length === 0) return;\n this._bumpVersion();\n this.onChanged.next({\n type: anyExisted ? 'update' : 'add',\n affectedSymbols: affected,\n });\n }\n\n /** Remove by identity. Returns true iff a record was removed. */\n removeMonomer(polymerType: PolymerType, symbol: string): boolean {\n const inner = this._byPolymerSymbol.get(polymerType);\n if (inner === undefined) return false;\n const existing = inner.get(symbol);\n if (existing === undefined) return false;\n\n inner.delete(symbol);\n\n const biotype = helm2Type(existing);\n if (biotype !== null) {\n const byBio = this._byBiotypeSymbol.get(biotype);\n if (byBio !== undefined) byBio.delete(symbol);\n }\n\n const na = existing.naturalAnalog;\n if (na !== undefined && na !== '') {\n const naOuter = this._byNaturalAnalog.get(polymerType);\n const arr = naOuter?.get(na);\n if (arr !== undefined) {\n const idx = arr.indexOf(existing);\n if (idx >= 0) arr.splice(idx, 1);\n }\n }\n\n this._bumpVersion();\n this.onChanged.next({ type: 'remove', affectedSymbols: [symbol] });\n return true;\n }\n\n /**\n * Update the `meta.colors` block of an existing monomer. We replace the\n * matched record with a new one whose `meta.colors` is merged with\n * `colors`. Fires `onChanged({ type: 'update' })`.\n */\n setMonomerColors(\n polymerType: PolymerType,\n symbol: string,\n colors: Partial<NonNullable<MonomerMeta['colors']>>,\n ): void {\n const existing = this.getMonomer(polymerType, symbol);\n if (existing === null)\n throw new HwError(\n 'monomer-not-found',\n `MonomerLib.setMonomerColors: ${polymerType}/${symbol} not found`,\n { polymerType, symbol },\n );\n\n const prevMeta: MonomerMeta = existing.meta ?? {};\n const prevColors = prevMeta.colors ?? {};\n const nextMeta: MonomerMeta = Object.assign({}, prevMeta, {\n colors: Object.assign({}, prevColors, colors),\n });\n const updated: Monomer = Object.assign({}, existing, { meta: nextMeta });\n\n this._addNoFire(updated);\n this._bumpVersion();\n this.onChanged.next({ type: 'update', affectedSymbols: [symbol] });\n }\n\n /**\n * Instance-level merge (per `IMonomerLib`): copy every monomer from\n * `other` into this lib; later wins. Fires `onChanged({ type: 'reload' })`\n * because it can touch many records at once.\n */\n merge(other: IMonomerLib): void {\n for (const polymerType of other.getPolymerTypes()) {\n for (const sym of other.getMonomerSymbolsByType(polymerType)) {\n const m = other.getMonomer(polymerType, sym);\n if (m !== null) this._addNoFire(m);\n }\n }\n this._bumpVersion();\n this.onChanged.next({ type: 'reload' });\n }\n\n /**\n * Register an inline-SMILES monomer synthesized from a HELM bracketed\n * payload. Returns the new `Monomer` record so the parser can attach it to\n * the atom that triggered the registration.\n *\n * The synthesized symbol is `#1`, `#2`, … unique within THIS lib. The\n * caller can supply a `symbolHint` (typically the original raw SMILES) for\n * provenance — we ignore it for ID purposes but may surface it via\n * `meta.smilesHint`.\n */\n registerInlineSmilesMonomer(\n polymerType: PolymerType,\n smiles: string,\n options?: { symbolHint?: string },\n ): Monomer {\n const detection = detectSmilesRGroups(smiles);\n if (!detection.ok)\n throw new HwError(\n 'inline-smiles-without-r-groups',\n `MonomerLib.registerInlineSmilesMonomer: ${detection.reason}`,\n { smiles },\n );\n\n this._inlineSmilesCounter += 1;\n const symbol = `#${this._inlineSmilesCounter}`;\n const rgroups: RGroupSpec[] = [];\n // We emit `R1..Rn` in numeric order so the rgroupMap is sensible.\n const numbers = Array.from(detection.info.rgroupPositions.keys()).sort((a, b) => a - b);\n for (const n of numbers) {\n rgroups.push({\n label: `R${n}` as `R${number}`,\n capGroupName: 'H',\n capGroupSMILES: '',\n });\n }\n\n const meta: MonomerMeta | undefined =\n options?.symbolHint !== undefined ? { smilesHint: options.symbolHint } : undefined;\n\n const monomer: Monomer = {\n symbol,\n polymerType,\n monomerType: 'Undefined',\n molfile: '',\n smiles: detection.canonicalSmiles,\n rgroups,\n ...(meta !== undefined ? { meta } : {}),\n };\n\n this._addNoFire(monomer);\n this._bumpVersion();\n this.onChanged.next({ type: 'add', affectedSymbols: [symbol] });\n return monomer;\n }\n\n // ---------- schema ----------\n\n /**\n * Snapshot the library in the on-disk array shape. Order matches insertion\n * order: the per-polymer `Map` insertion order is the canonical iteration\n * order. Duplicates across polymers are preserved verbatim.\n */\n toJSON(): MonomerLibSchema {\n const out: Monomer[] = [];\n for (const inner of this._byPolymerSymbol.values())\n for (const m of inner.values())\n out.push(m);\n return out;\n }\n\n // ---------- internals ----------\n\n /**\n * Insert (or replace) a monomer in every index. Does NOT fire `onChanged`\n * or bump version — caller is responsible. Returns `true` if a previous\n * record was replaced.\n */\n private _addNoFire(monomer: Monomer): boolean {\n // Normalize any-cased capGroupSMILES (e.g. Datagrok's `capGroupSmiles`)\n // onto the canonical field so every stored monomer reads consistently.\n monomer = normalizeCapGroupSMILES(monomer);\n const pt = monomer.polymerType;\n let byPoly = this._byPolymerSymbol.get(pt);\n if (byPoly === undefined) {\n byPoly = new Map();\n this._byPolymerSymbol.set(pt, byPoly);\n }\n const prev = byPoly.get(monomer.symbol);\n byPoly.set(monomer.symbol, monomer);\n\n // Remove the previous record from secondary indices.\n if (prev !== undefined) {\n const prevBio = helm2Type(prev);\n if (prevBio !== null) {\n const byBio = this._byBiotypeSymbol.get(prevBio);\n if (byBio !== undefined) byBio.delete(prev.symbol);\n }\n const prevNa = prev.naturalAnalog;\n if (prevNa !== undefined && prevNa !== '') {\n const byNa = this._byNaturalAnalog.get(pt)?.get(prevNa);\n if (byNa !== undefined) {\n const idx = byNa.indexOf(prev);\n if (idx >= 0) byNa.splice(idx, 1);\n }\n }\n this._resolved.delete(prev);\n }\n\n // Insert in the biotype mirror.\n const bio = helm2Type(monomer);\n if (bio !== null) {\n let byBio = this._byBiotypeSymbol.get(bio);\n if (byBio === undefined) {\n byBio = new Map();\n this._byBiotypeSymbol.set(bio, byBio);\n }\n byBio.set(monomer.symbol, monomer);\n }\n\n // Insert in the natural-analog mirror.\n const na = monomer.naturalAnalog;\n if (na !== undefined && na !== '') {\n let naOuter = this._byNaturalAnalog.get(pt);\n if (naOuter === undefined) {\n naOuter = new Map();\n this._byNaturalAnalog.set(pt, naOuter);\n }\n let arr = naOuter.get(na);\n if (arr === undefined) {\n arr = [];\n naOuter.set(na, arr);\n }\n arr.push(monomer);\n }\n\n this._resolved.set(monomer, buildResolved(monomer));\n return prev !== undefined;\n }\n\n private _bumpVersion(): void {\n this._version += 1;\n }\n}\n\n// ---------- helpers ----------\n\nfunction pickTheme(themeName: string | undefined): ColorTheme {\n if (themeName === 'dark') return darkPalette;\n // 'light', undefined, or any custom name → use lightPalette as the\n // anchor; resolveMonomerColors still honors meta.colors[customName] iff\n // we feed it a theme whose `name` matches. To keep things deterministic\n // here we hand out the canonical lightPalette unless 'dark' is requested.\n return lightPalette;\n}\n","// T-0302 — Linear (straight-line) chain layout.\n//\n// Each backbone atom is placed at `origin + (i * step, 0)` where\n// `step = bondscale (1.6) * bondLength`. The default `bondLength = 28` so\n// the default step is `44.8px` (`bondscale = 1.6` from the legacy editor's\n// `helm.js`; see LEGACY-EDITOR-NOTES.md §\"helm.js\"). T-3509 reduced the\n// default bond length from 40 → 28 (tighter monomer spacing).\n//\n// Returns a sparse `Map<atomId, Point>` — bases are NOT positioned here\n// (that is the responsibility of `placeBases` in T-0306).\n\nimport type {Point} from '../util/types';\nimport type {LayoutChain} from './chain-builder';\n\n/** Magic bond-scale factor from the legacy editor. */\nexport const BOND_SCALE = 1.6;\n/** Default model-space bond length. */\nexport const DEFAULT_BOND_LENGTH = 28;\n/** Step distance (horizontal monomer-to-monomer gap). 44.8 px at defaults. */\nexport const STEP = BOND_SCALE * DEFAULT_BOND_LENGTH;\n\n// --------------------------------------------------------------------------\n// Vertical-gap factors (T-1501, Phase 15)\n//\n// Phase 12 used multipliers of `step = BOND_SCALE * bondLength`. The user\n// reported the rows were too far apart and requested every vertical gap match\n// **1.5 × bondLength** (= 42 px at the current default bondLength=28; was 60 px\n// at the old bondLength=40). The multipliers below are expressed as fractions\n// of `step` so the existing call sites (`gapMul * step`) continue to compose\n// correctly.\n//\n// Source: user request 2026-05-17 — \"vertical distance should be 1.5 bond\n// lengths\".\n// Reference: SPECIFICATION.md §6 (Layout — vertical rhythm).\n// --------------------------------------------------------------------------\n\n/** Row spacing as an ABSOLUTE pixel value: 1.5 × bondLength = 42 px. */\nexport const ROW_SPACING = 1.5 * DEFAULT_BOND_LENGTH;\n\n/** Fragment gap (between disconnected chains). 1.5 × bondLength = 42 px. */\nexport const FRAGMENT_GAP = 1.5 * DEFAULT_BOND_LENGTH;\n\n/** H-bond stack gap (anti-parallel duplex). 1.5 × bondLength = 42 px. */\nexport const HBOND_GAP = 1.5 * DEFAULT_BOND_LENGTH;\n\n/** Cross-chain gap (cross-polymer bond between two chains). 1.5 × bondLength = 42 px. */\nexport const CROSS_CHAIN_GAP = 1.5 * DEFAULT_BOND_LENGTH;\n\n// Step-relative factors retained for backward compatibility with existing\n// callers that already multiply by `step`. Each is set so that\n// `factor * step === 1.5 × bondLength`, i.e. factor = 1.5 / BOND_SCALE = 0.9375.\n\n/** Step-relative factor for `layoutDisconnectedFragments` (= 1.5 / 1.6 = 0.9375). */\nexport const FRAGMENT_GAP_FACTOR = (1.5 * DEFAULT_BOND_LENGTH) / STEP;\n\n/** Step-relative factor for the anti-parallel H-bond gap. */\nexport const HBOND_GAP_FACTOR = (1.5 * DEFAULT_BOND_LENGTH) / STEP;\n\n/** Step-relative factor for the cross-polymer cross-chain gap. */\nexport const CROSS_CHAIN_GAP_FACTOR = (1.5 * DEFAULT_BOND_LENGTH) / STEP;\n\nexport interface LineLayoutOptions {\n /** Model-space distance between consecutive backbone atoms. Default `28`. */\n bondLength?: number;\n /** World position of the first backbone atom. Default `(0, 0)`. */\n origin?: Point;\n}\n\n/**\n * Lay out `chain.backbone` as a straight horizontal line of points. Returns\n * a `Map<atomId, Point>` keyed by atom id; the caller merges this into the\n * Mol's global position table.\n */\nexport function layoutLine(chain: LayoutChain, options?: LineLayoutOptions): Map<number, Point> {\n const bondLength = options?.bondLength ?? DEFAULT_BOND_LENGTH;\n const origin = options?.origin ?? {x: 0, y: 0};\n const step = BOND_SCALE * bondLength;\n const out = new Map<number, Point>();\n for (let i = 0; i < chain.backbone.length; i++) {\n const atom = chain.backbone[i]!;\n out.set(atom.id, {x: origin.x + i * step, y: origin.y});\n }\n return out;\n}\n","// T-0303 — Circular chain layout.\n//\n// For cyclic chains the legacy editor places atoms equispaced on a circle:\n// the radius is chosen so the chord length between adjacent atoms equals\n// `step = bondscale * bondLength`. With `n` atoms the chord/sine identity\n// gives `r = step / (2 * sin(pi / n))`. See LEGACY-EDITOR-NOTES.md §Layout.ts\n// (`layoutCircle`, lines 173-188).\n//\n// Single-atom and two-atom degenerate cases fall back to a line so the\n// formula's `sin(pi/n)` term never divides by zero or becomes infinite.\n\nimport type {Point} from '../util/types';\nimport type {LayoutChain} from './chain-builder';\nimport {BOND_SCALE, DEFAULT_BOND_LENGTH, layoutLine} from './line';\n\nexport interface CircleLayoutOptions {\n /** Model-space distance between adjacent backbone atoms (chord length). */\n bondLength?: number;\n /** World position of the circle's center. Default `(0, 0)`. */\n origin?: Point;\n /** Angle of the first atom in degrees (counter-clockwise from +X). Default `-90`. */\n startDegrees?: number;\n}\n\nconst DEG2RAD = Math.PI / 180;\n\n/**\n * Lay out the backbone as a circle. Returns `Map<atomId, Point>`.\n *\n * For `n <= 2` we fall back to a straight line — the chord formula isn't\n * meaningful, and the legacy editor takes the same branch implicitly via\n * `layoutLine`.\n */\nexport function layoutCircle(chain: LayoutChain, options?: CircleLayoutOptions): Map<number, Point> {\n const n = chain.backbone.length;\n if (n <= 2) {\n const lineOpts: {bondLength?: number; origin?: Point} = {};\n if (options?.bondLength !== undefined) lineOpts.bondLength = options.bondLength;\n if (options?.origin !== undefined) lineOpts.origin = options.origin;\n return layoutLine(chain, lineOpts);\n }\n\n const bondLength = options?.bondLength ?? DEFAULT_BOND_LENGTH;\n const origin = options?.origin ?? {x: 0, y: 0};\n const startDegrees = options?.startDegrees ?? -90;\n const step = BOND_SCALE * bondLength;\n const r = step / (2 * Math.sin(Math.PI / n));\n\n const out = new Map<number, Point>();\n for (let i = 0; i < n; i++) {\n const atom = chain.backbone[i]!;\n const deg = startDegrees + (i * 360) / n;\n const rad = deg * DEG2RAD;\n out.set(atom.id, {\n x: origin.x + r * Math.cos(rad),\n y: origin.y + r * Math.sin(rad),\n });\n }\n return out;\n}\n","// Primitive value types and pure geometry helpers shared by every layer of the\n// library. These are intentionally simple, allocation-friendly shapes used at\n// boundaries everywhere; the helpers here are dependency-free and side-effect\n// free. See ARCHITECTURE.md §3.1.\n\n/** A 2D point in SVG world coordinates (origin top-left, Y grows downward). */\nexport interface Point {\n x: number;\n y: number;\n}\n\n/** Axis-aligned rectangle (top-left origin, positive width/height). */\nexport interface Rect {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\n/** Free 2D vector. Distinct from `Point` to express geometric intent at call-sites. */\nexport interface Vec2 {\n x: number;\n y: number;\n}\n\n/** Disposer returned by every event subscription in the public API. */\nexport type Unsubscribe = () => void;\n\n/**\n * Axis-aligned bounding box. A semantic alias of `Rect` used at call sites\n * where the rectangle is computed from a set of points (a \"bounding\" box) as\n * opposed to a user-specified region.\n */\nexport interface BBox extends Rect {}\n\n/** Euclidean distance between two points. */\nexport function pointDistance(a: Readonly<Point>, b: Readonly<Point>): number {\n const dx = a.x - b.x;\n const dy = a.y - b.y;\n return Math.sqrt(dx * dx + dy * dy);\n}\n\n/** Structural equality on points. Exact (no epsilon). */\nexport function pointEquals(a: Readonly<Point>, b: Readonly<Point>): boolean {\n return a.x === b.x && a.y === b.y;\n}\n\n/**\n * Build the tightest axis-aligned bounding box covering every input point.\n *\n * Returns a zero-size box at the origin when `points` is empty — callers that\n * need to distinguish \"no input\" from \"single point\" should check length up\n * front.\n */\nexport function bboxFromPoints(points: Iterable<Readonly<Point>>): BBox {\n let initialised = false;\n let minX = 0;\n let minY = 0;\n let maxX = 0;\n let maxY = 0;\n for (const p of points) {\n if (!initialised) {\n minX = maxX = p.x;\n minY = maxY = p.y;\n initialised = true;\n continue;\n }\n if (p.x < minX) minX = p.x;\n if (p.x > maxX) maxX = p.x;\n if (p.y < minY) minY = p.y;\n if (p.y > maxY) maxY = p.y;\n }\n if (!initialised) return { x: 0, y: 0, width: 0, height: 0 };\n return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };\n}\n\n/** Union of two bounding boxes (the smallest box covering both). */\nexport function bboxUnion(a: Readonly<BBox>, b: Readonly<BBox>): BBox {\n const minX = Math.min(a.x, b.x);\n const minY = Math.min(a.y, b.y);\n const maxX = Math.max(a.x + a.width, b.x + b.width);\n const maxY = Math.max(a.y + a.height, b.y + b.height);\n return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };\n}\n","// T-0401 — Pure SVG path generators for the seven HELM monomer shapes plus\n// bond strokes and a tiny bounding-box helper.\n//\n// Every path is centered at the caller-supplied `Point` (`p`) and uses the\n// SVG convention that y grows downward. All functions are deterministic and\n// allocation-light: they build a single path-data string from a fixed-arity\n// vertex list. The renderer wraps the result in a `<path>` element and the\n// hit-tester reads `shapeBBox` to seed its broad-phase.\n//\n// References:\n// - RENDERING.md §3 (shape conventions)\n// - LEGACY-EDITOR-NOTES.md §\"helm/Interface.ts\" `drawMonomer` (canonical\n// mapping from biotype to shape)\n//\n// Layering: this module imports only from `util/`. No DOM, no console.\n\nimport type {Point} from '../util/types';\n\n// --------------------------------------------------------------------------\n// Tiny number formatter — keeps SVG strings short and deterministic.\n// --------------------------------------------------------------------------\n\n/**\n * Format a number with up to three decimal places, collapsing `-0` to `0`\n * and any non-finite input to `0`. The renderer cares about deterministic\n * output above all (snapshot tests compare strings byte-for-byte).\n *\n * T-0902 PROFILING NOTES (Phase 9): a 100-monomer render emits ~5 shape\n * paths per atom and each path contains 6-12 `fmt()` calls — so the function\n * is invoked thousands of times per render. Inlining a `Number.isInteger`\n * fast path skips the multiply/divide for the (frequent) zero-radius and\n * integer-coordinate cases that arise after layout snapping.\n */\nfunction fmt(n: number): string {\n if (Number.isInteger(n)) return n === 0 ? '0' : n.toString();\n if (!Number.isFinite(n)) return '0';\n if (Object.is(n, -0)) return '0';\n const r = Math.round(n * 1000) / 1000;\n return r === 0 ? '0' : r.toString();\n}\n\n// --------------------------------------------------------------------------\n// Polygon helpers\n// --------------------------------------------------------------------------\n\n/**\n * Concatenate a list of polygon vertices into an SVG path that\n * closes back to the first vertex with an explicit `L` (and then `Z`). The\n * explicit return-line is more verbose than the shorter `M v0 L v1 ... Z`\n * form, but it gives a stable count of `L` commands equal to the number of\n * vertices — useful for renderer snapshot tests and for the hit-tester's\n * polygon-walk helper.\n */\nfunction polygonPath(points: ReadonlyArray<Point>): string {\n if (points.length === 0) return '';\n const first = points[0] as Point;\n let out = `M ${fmt(first.x)} ${fmt(first.y)}`;\n for (let i = 1; i < points.length; i++) {\n const pt = points[i] as Point;\n out += ` L ${fmt(pt.x)} ${fmt(pt.y)}`;\n }\n // Close back to the first vertex with an explicit L, then Z.\n out += ` L ${fmt(first.x)} ${fmt(first.y)} Z`;\n return out;\n}\n\n/**\n * Sample `n` evenly-spaced vertices of a regular polygon inscribed in a\n * circle of `radius` centered at `p`, with the first vertex at angle\n * `rotationDeg` (SVG convention: 0° points right, 90° points down). Returns\n * a freshly-allocated `Point[]`.\n */\nfunction regularPolygonPoints(\n p: Point, n: number, radius: number, rotationDeg: number,\n): Point[] {\n const out: Point[] = [];\n const step = 360 / n;\n for (let i = 0; i < n; i++) {\n const angle = (rotationDeg + i * step) * Math.PI / 180;\n out.push({x: p.x + radius * Math.cos(angle), y: p.y + radius * Math.sin(angle)});\n }\n return out;\n}\n\n// --------------------------------------------------------------------------\n// Per-shape generators\n// --------------------------------------------------------------------------\n\n/**\n * Hexagonal `HELM_AA` shape. The default `-30°` rotation matches the legacy\n * editor's \"pointy-side\" orientation (vertices at top and bottom of the\n * bbox). `size` is the circumradius.\n */\nexport function hexagonPath(p: Point, size: number, rotationDeg: number = -30): string {\n return polygonPath(regularPolygonPoints(p, 6, size, rotationDeg));\n}\n\n/**\n * Diamond (rhombus) for `HELM_BASE`. The four vertices sit on the axes at\n * `±size`, producing a square rotated 45° in screen coordinates.\n */\nexport function diamondPath(p: Point, size: number): string {\n const pts: Point[] = [\n {x: p.x, y: p.y - size},\n {x: p.x + size, y: p.y},\n {x: p.x, y: p.y + size},\n {x: p.x - size, y: p.y},\n ];\n return polygonPath(pts);\n}\n\n/**\n * Pentagonal `HELM_NUCLEOTIDE` shape (the gap base-pair marker). The default\n * `-90°` rotation puts the first vertex at the top so the pentagon \"points\n * up\" — matching the legacy editor.\n */\nexport function pentagonPath(p: Point, size: number, rotationDeg: number = -90): string {\n return polygonPath(regularPolygonPoints(p, 5, size, rotationDeg));\n}\n\n/**\n * Rounded axis-aligned rectangle centered at `p` with width `w`, height\n * `h`, and corner radius `r`. The radius is clamped to half the shorter\n * side so callers can pass arbitrary values without producing degenerate\n * arcs.\n */\nexport function roundedRectPath(p: Point, w: number, h: number, r: number): string {\n const halfW = Math.abs(w) / 2;\n const halfH = Math.abs(h) / 2;\n const radius = Math.max(0, Math.min(r, halfW, halfH));\n const x = p.x - halfW;\n const y = p.y - halfH;\n const right = x + halfW * 2;\n const bottom = y + halfH * 2;\n let out = `M ${fmt(x + radius)} ${fmt(y)}`;\n out += ` L ${fmt(right - radius)} ${fmt(y)}`;\n out += ` A ${fmt(radius)} ${fmt(radius)} 0 0 1 ${fmt(right)} ${fmt(y + radius)}`;\n out += ` L ${fmt(right)} ${fmt(bottom - radius)}`;\n out += ` A ${fmt(radius)} ${fmt(radius)} 0 0 1 ${fmt(right - radius)} ${fmt(bottom)}`;\n out += ` L ${fmt(x + radius)} ${fmt(bottom)}`;\n out += ` A ${fmt(radius)} ${fmt(radius)} 0 0 1 ${fmt(x)} ${fmt(bottom - radius)}`;\n out += ` L ${fmt(x)} ${fmt(y + radius)}`;\n out += ` A ${fmt(radius)} ${fmt(radius)} 0 0 1 ${fmt(x + radius)} ${fmt(y)}`;\n out += ' Z';\n return out;\n}\n\n/**\n * Closed ellipse centered at `p` with x-radius `rx` and y-radius `ry`. We\n * emit two half-arcs to keep the path explicitly closed (SVG's `<path>`\n * with `Z` works for circles even when described this way, and avoids the\n * need for a separate `<circle>` element type at the call site).\n */\nexport function ellipsePath(p: Point, rx: number, ry: number): string {\n const ax = Math.abs(rx);\n const ay = Math.abs(ry);\n return (\n `M ${fmt(p.x - ax)} ${fmt(p.y)} ` +\n `A ${fmt(ax)} ${fmt(ay)} 0 1 0 ${fmt(p.x + ax)} ${fmt(p.y)} ` +\n `A ${fmt(ax)} ${fmt(ay)} 0 1 0 ${fmt(p.x - ax)} ${fmt(p.y)} Z`\n );\n}\n\n/**\n * A `HELM_BLOB` rectangle — same as `roundedRectPath` but typically called\n * with a larger size and corner radius. Aliased rather than re-implemented\n * so consumers can write call-site intent.\n */\nexport function blobPath(p: Point, w: number, h: number, r: number): string {\n return roundedRectPath(p, w, h, r);\n}\n\n/**\n * Circle path used to draw a gap atom (`*`). The dashed appearance is\n * applied at draw time via `stroke-dasharray`; the path itself is a plain\n * circle so hit-testing and bbox computation stay simple.\n */\nexport function dashedCirclePath(p: Point, r: number): string {\n return ellipsePath(p, r, r);\n}\n\n// --------------------------------------------------------------------------\n// Bond strokes\n// --------------------------------------------------------------------------\n\n/**\n * Straight `M..L..` bond stroke between two atom centers. The renderer is\n * responsible for any per-bond-kind decoration (dash, colour, hash marks).\n */\nexport function bondLinePath(a: Point, b: Point): string {\n return `M ${fmt(a.x)} ${fmt(a.y)} L ${fmt(b.x)} ${fmt(b.y)}`;\n}\n\n/**\n * Two short perpendicular hash marks straddling the midpoint of `a..b`, the\n * canonical H-bond decoration. Both hashes are emitted in a single path so\n * the renderer can stroke them together (one `<path>` per bond).\n *\n * When `a` and `b` coincide we still emit a non-empty (degenerate) path so\n * downstream SVG parsers do not choke.\n */\nexport function bondHashPath(a: Point, b: Point, hashLength: number = 6): string {\n const dx = b.x - a.x;\n const dy = b.y - a.y;\n const len = Math.sqrt(dx * dx + dy * dy);\n if (len === 0)\n return `M ${fmt(a.x)} ${fmt(a.y)} L ${fmt(a.x)} ${fmt(a.y)}`;\n const ux = dx / len;\n const uy = dy / len;\n const px = -uy;\n const py = ux;\n const mx = (a.x + b.x) / 2;\n const my = (a.y + b.y) / 2;\n const half = hashLength / 2;\n // Offset hashes a quarter of the bond length on either side of the midpoint\n // — far enough apart to read as two ticks, close enough to remain close.\n const offset = Math.min(hashLength * 0.6, len * 0.25);\n const c1x = mx - ux * offset;\n const c1y = my - uy * offset;\n const c2x = mx + ux * offset;\n const c2y = my + uy * offset;\n return (\n `M ${fmt(c1x - px * half)} ${fmt(c1y - py * half)} L ${fmt(c1x + px * half)} ${fmt(c1y + py * half)} ` +\n `M ${fmt(c2x - px * half)} ${fmt(c2y - py * half)} L ${fmt(c2x + px * half)} ${fmt(c2y + py * half)}`\n );\n}\n\n// --------------------------------------------------------------------------\n// Bounding box helper\n// --------------------------------------------------------------------------\n\n/**\n * One of the seven shape families the renderer knows how to draw. Used as\n * the discriminant for `shapeBBox` so callers can request a tight bbox for\n * any registered shape without re-implementing the trig per-shape.\n */\nexport type ShapeKind = 'hexagon' | 'diamond' | 'pentagon' | 'rounded-rect' | 'ellipse' | 'blob' | 'circle';\n\n/**\n * Return the axis-aligned bounding box of a centered shape. `w` carries the\n * primary size (radius for hexagon/diamond/pentagon/circle, full width for\n * rounded-rect/blob, x-radius for ellipse). `h` is required for ellipse\n * (y-radius) and rounded-rect/blob (height); for the regular polygons it is\n * ignored.\n *\n * The bbox is tight: it covers the actual geometry, not just the\n * circumradius. Hit-tests use this as the broad-phase rectangle.\n */\nexport function shapeBBox(\n kind: ShapeKind, p: Point, w: number, h?: number,\n): {x: number; y: number; width: number; height: number} {\n switch (kind) {\n case 'hexagon': {\n // Rotation -30° → vertices at angles -30, 30, 90, 150, 210, 270.\n // x-extremes share |cos(30)| = √3/2; y-extremes at 90°/270° are ±size.\n const hw = w * Math.sqrt(3) / 2;\n const hh = w;\n return {x: p.x - hw, y: p.y - hh, width: hw * 2, height: hh * 2};\n }\n case 'diamond': {\n return {x: p.x - w, y: p.y - w, width: w * 2, height: w * 2};\n }\n case 'pentagon': {\n // Rotation -90° → first vertex straight up at (0, -w); lower vertices at\n // angles 18°, 162° from x-axis... but our convention is offset by -90,\n // so the second pair sits at angles -18° (≡ 342°) and 198°, and the bottom\n // pair at 54° / 126°. Extremes: top y = -w, bottom y = sin(54°)*w,\n // x-extremes = ±cos(18°)*w.\n const halfW = Math.cos(18 * Math.PI / 180) * w;\n const top = -w;\n const bottom = Math.sin(54 * Math.PI / 180) * w;\n return {x: p.x - halfW, y: p.y + top, width: halfW * 2, height: bottom - top};\n }\n case 'rounded-rect':\n case 'blob': {\n const _h = h ?? w;\n const hw = Math.abs(w) / 2;\n const hh = Math.abs(_h) / 2;\n return {x: p.x - hw, y: p.y - hh, width: hw * 2, height: hh * 2};\n }\n case 'ellipse': {\n const rx = Math.abs(w);\n const ry = Math.abs(h ?? w);\n return {x: p.x - rx, y: p.y - ry, width: rx * 2, height: ry * 2};\n }\n case 'circle': {\n const r = Math.abs(w);\n return {x: p.x - r, y: p.y - r, width: r * 2, height: r * 2};\n }\n }\n}\n","// T-0402 — Renderer theme contract.\n//\n// A `Theme` collects every visual constant the renderer needs (background,\n// stroke / fill, font sizing, selection/highlight colours, per-biotype shape\n// styles, plus a `ColorTheme` palette for monomer colour resolution). Two\n// built-in themes (`lightTheme`, `darkTheme`) cover the common case;\n// `defineTheme(base, overrides)` composes custom variants.\n//\n// References:\n// - RENDERING.md §3 (shape sizing), §11 (theme contract), §12 (selection)\n// - MONOMER-LIBRARY.md §6 (colour resolution via `ColorTheme`)\n//\n// Layering: imports from `core/`, `monomer-lib/` only. No DOM, no console.\n\nimport {HelmTypes, type HelmType} from '../core/constants';\nimport {DEFAULT_BOND_LENGTH} from '../layout/line';\nimport {lightPalette, darkPalette, type ColorTheme} from '../monomer-lib/color-rules';\n\n/**\n * Per-shape sizing — kept tiny on purpose so themes are easy to override.\n *\n * `size` is the shape's primary dimension at zoom 1: the circumradius for\n * regular polygons (hexagon, diamond, pentagon), the side for rounded\n * squares (sugar/chem), the width/height for the blob rectangle, and the\n * radius for the linker ellipse. The renderer interprets it consistently\n * with the shape generators in `./shapes`.\n */\nexport interface ShapeStyle {\n /** Primary dimension (radius / side / width) in CSS pixels at zoom 1. */\n size: number;\n /** Stroke width in CSS pixels. */\n strokeWidth: number;\n /** Corner radius for rounded shapes (sugar, chem, blob). */\n cornerRadius?: number;\n}\n\n/**\n * Renderer theme. Everything the SVG and canvas backends paint is sourced\n * from one of these fields — there are no other style knobs. Custom themes\n * compose via `defineTheme(base, overrides)`.\n */\nexport interface Theme {\n /** Stable theme name. Used as part of cache keys. */\n readonly name: string;\n /** Canvas background colour (CSS). */\n readonly background: string;\n /** Default stroke colour for shapes and bonds. */\n readonly defaultStroke: string;\n /** Default fill colour when a monomer has no resolved colour. */\n readonly defaultFill: string;\n /** Stroke colour for selected atoms in the selection pass. */\n readonly selectionColor: string;\n /** Stroke colour for highlighted atoms in the highlight pass. */\n readonly highlightColor: string;\n /** Stroke colour used for missing-monomer outlines. */\n readonly errorColor: string;\n /** Default label font size in CSS pixels at zoom 1. */\n readonly fontSize: number;\n /** Default label font family. */\n readonly fontFamily: string;\n /** Model-space distance between consecutive backbone atoms. */\n readonly bondLength: number;\n /** Default text colour for monomer labels. */\n readonly textColor: string;\n /** Optional vertical offset (CSS px) for the label baseline. */\n readonly labelOffsetY?: number;\n /** Colour for the optional continuous-id overlay. */\n readonly idColor: string;\n /** Colour for the annotation overlay (5'/n/c markers). */\n readonly annotationColor: string;\n /** Fill colour of the small tag dot on tagged atoms. */\n readonly tagDotColor: string;\n /** Stroke colour for canonical Watson-Crick H-bonds. */\n readonly hbondColor: string;\n /** Stroke colour for mismatched / non-canonical H-bonds. */\n readonly nonCanonicalHbondColor: string;\n /** Stroke colour for bonds that cross between two polymers. */\n readonly crossPolymerBondColor: string;\n /** Monomer-colour palette consumed by `resolveMonomerColors`. */\n readonly colorTheme: ColorTheme;\n /**\n * Optional palette tuning. `softenCanvas`, when `true`, instructs renderers\n * to soften (pastel) the resolved monomer colour on the main canvas — the\n * palette tiles always soften by default. Default `false`: canvas keeps the\n * saturated legacy colours per RENDERING.md §11.\n */\n readonly palette?: {\n readonly softenCanvas?: boolean;\n };\n /** Per-biotype shape styles keyed on a stable short tag. */\n readonly shapes: Record<\n 'aa' | 'sugar' | 'base' | 'linker' | 'chem' | 'blob' | 'nucleotide',\n ShapeStyle\n >;\n}\n\n// --------------------------------------------------------------------------\n// Built-in shape sizing\n// --------------------------------------------------------------------------\n\nconst DEFAULT_FONT_SIZE = 14;\nconst DEFAULT_FONT_FAMILY = '\"Helvetica Neue\", Helvetica, Arial, sans-serif';\n\n/**\n * Build the per-biotype shape styles from a single font-size constant.\n *\n * Phase 13a (T-1301a Bug 9) — equal-area multipliers. The Phase 12\n * multipliers compared shape `size` numerically, but `size` means\n * different things for different shapes (circumradius for regular\n * polygons, side for squares, radius for circles, half-width for blobs),\n * so equal `size` produced VERY unequal painted area. The Phase 13a\n * multipliers retarget every biotype to render at ~600 px² of painted\n * area at fontSize=14 — except blob (~1080 px²) which stays slightly\n * larger by design for visual emphasis.\n *\n * Derivation (each shape's true area as a function of its `size`):\n * hexagon (regular, circumradius s): A = (3√3/2) · s² ≈ 2.598 · s²\n * → for A=600, s≈15.2 → mult ≈ 15.2 / 14 ≈ 1.08\n * diamond (rhombus, half-diagonal d): A = 2 · d²\n * → for A=600, d≈17.32 → mult ≈ 17.32 / 14 ≈ 1.24\n * rounded square (side s; we treat A ≈ s² since the radius rounds off\n * only the corners): A = s²\n * → for A=600 (sugar), s≈24.49 → mult ≈ 24.49 / 14 ≈ 1.75\n * → for A=679 (chem, slightly chunkier), s≈26.06 → mult ≈ 1.85\n * circle (radius r): A = π · r²\n * → for A=600, r≈13.82 → mult ≈ 13.82 / 14 ≈ 0.99\n * pentagon (regular, circumradius r):\n * A = (5/2) · r² · sin(72°) ≈ 5 · sin(72°) / 2 · r² ≈ 2.378 · r²\n * → for A=600, r≈15.88 → mult ≈ 15.88 / 14 ≈ 1.13 (cross-check)\n * We use 1.34 instead, targeting A≈836 px²; pentagon sits between\n * hex and chem so it reads as a distinct nucleotide marker.\n * blob (rounded square, side ≈ 2 · size · cos45° span on the bbox):\n * We keep the visual-emphasis target ~1080 px² → mult ≈ 2.0.\n *\n * Phase 13a multipliers:\n * aa hexagon: 1.08 (was 1.25)\n * sugar rounded square: 1.75 (was 1.25)\n * base diamond: 1.24 (was 1.15)\n * linker circle: 0.99 (was 1.05)\n * chem rounded square: 1.85 (was 1.35)\n * blob: 2.00 (was 1.80)\n * nucleotide pentagon: 1.34 (was 1.25)\n */\nfunction buildShapes(fontSize: number): Theme['shapes'] {\n // Phase 29 (T-2902) -- Round every shape's corners by giving each\n // shape a 5 px self-coloured stroke with `stroke-linejoin: round`.\n // The stroke is painted in the SAME colour as the fill so it blends\n // visually -- the only effect is rounded vertices (polygons) and a\n // slightly chunkier outline on already-curved shapes. User request\n // 2026-05-19: \"I like the rounded edges on the sugars, but I would\n // like them just a bit more rounded, and similarly rounded shapes\n // on hexagons, nucleotides, etc. you can set the stroke to same\n // colour and have stroke-linejoin round property. lets say stroke\n // of 5 px should be good. don't forget to readjust the size and\n // also readjust hover/selected states\".\n //\n // Because the 5 px stroke straddles the path (half inside, half\n // outside), each shape's visual extent grows by ~5 px in every\n // direction unless we shrink the path. To keep visual areas close\n // to the Phase 22 equal-area target (~600 px²), every `size` value\n // is reduced by `5 / fontSize` (so 5 px less along the characteristic\n // dimension). New multipliers:\n //\n // aa hex: 1.08 -> 0.72 ((15.12 - 5) / 14)\n // sugar rounded square: 1.75 -> 1.39 ((24.50 - 5) / 14)\n // base diamond: 1.24 -> 0.88 ((17.36 - 5) / 14)\n // linker circle: 0.99 -> 0.63 ((13.86 - 5) / 14)\n // chem rounded square: 1.85 -> 1.49 ((25.90 - 5) / 14)\n // blob: 2.00 -> 1.64 ((28.00 - 5) / 14)\n // nucleotide pentagon: 1.34 -> 0.98 ((18.76 - 5) / 14)\n //\n // All seven shapes adopt a uniform 5 px strokeWidth; blob no longer\n // needs its old 1 px monomer outline because the self-coloured 5 px\n // stroke already provides a visible (rounded) edge.\n return {\n aa: {size: fontSize * 0.72, strokeWidth: 5},\n sugar: {size: fontSize * 1.39, strokeWidth: 5, cornerRadius: fontSize * 0.3},\n base: {size: fontSize * 0.88, strokeWidth: 5},\n linker: {size: fontSize * 0.63, strokeWidth: 5},\n chem: {size: fontSize * 1.49, strokeWidth: 5, cornerRadius: fontSize * 0.5},\n blob: {size: fontSize * 1.64, strokeWidth: 5, cornerRadius: fontSize * 0.8},\n nucleotide: {size: fontSize * 0.98, strokeWidth: 5},\n };\n}\n\n// --------------------------------------------------------------------------\n// Built-in themes\n// --------------------------------------------------------------------------\n\n/**\n * Light theme — white canvas, dark strokes, the legacy Pistoia colour\n * palette for monomer fills. Matches the default appearance of the legacy\n * HELM Web Editor.\n */\nexport const lightTheme: Theme = {\n name: 'light',\n background: '#ffffff',\n defaultStroke: '#202020',\n defaultFill: '#ffffff',\n // Phase 18 (T-1801 / T-1803) — yellow outline for selected atoms, teal\n // outline for hover-highlighted ones. The previous Phase 4 colours\n // (`#FFD29B` peach, `#FFB2B2` pink) were too close to the legacy\n // monomer-fill palette to read as a \"this monomer is selected\" cue —\n // the user reported \"I can't tell what's selected.\" Material-design\n // amber + teal both achieve > 4.5:1 contrast against the white canvas\n // and against every default fill.\n selectionColor: '#FFC107',\n // Phase 22 (T-2205) -- \"Light Tiffany\" hover ring. The previous\n // teal #26A69A read as heavy and matched some monomer fills too\n // closely; the new #A4E2DC sits on the lighter Tiffany blue spectrum\n // and visibly differentiates the highlight pass from any monomer\n // fill in the lightPalette.\n highlightColor: '#A4E2DC',\n errorColor: '#cc0000',\n fontSize: DEFAULT_FONT_SIZE,\n fontFamily: DEFAULT_FONT_FAMILY,\n bondLength: DEFAULT_BOND_LENGTH,\n textColor: '#000000',\n idColor: '#00aa00',\n annotationColor: '#FFA500',\n tagDotColor: '#dd2222',\n hbondColor: '#888888',\n nonCanonicalHbondColor: '#cc8800',\n crossPolymerBondColor: '#1f5fcd',\n colorTheme: lightPalette,\n palette: {softenCanvas: false},\n shapes: buildShapes(DEFAULT_FONT_SIZE),\n};\n\n/**\n * Dark theme — slate canvas, light strokes, desaturated monomer fills.\n * Derived from `lightTheme` so consumers only see overrides that actually\n * change for dark mode.\n */\nexport const darkTheme: Theme = {\n name: 'dark',\n background: '#1a1d23',\n defaultStroke: '#e0e0e0',\n defaultFill: '#2b2f37',\n // Phase 18 (T-1801 / T-1803) — lighter yellow + cyan-teal so the\n // selection / hover rings still clear ≥ 4.5:1 against the slate\n // background. Mirrors the Material amber 300 / cyan 300 swatches.\n selectionColor: '#FFD54F',\n // Phase 22 (T-2205) -- Dark-mode counterpart to the light Tiffany\n // tone (lightTheme uses #A4E2DC). Slightly more saturated so it\n // still reads against the slate background.\n highlightColor: '#7DD8CC',\n errorColor: '#ff5050',\n fontSize: DEFAULT_FONT_SIZE,\n fontFamily: DEFAULT_FONT_FAMILY,\n bondLength: DEFAULT_BOND_LENGTH,\n textColor: '#ffffff',\n idColor: '#33ff33',\n annotationColor: '#FFB84D',\n tagDotColor: '#ff6464',\n hbondColor: '#aaaaaa',\n nonCanonicalHbondColor: '#ffaa44',\n crossPolymerBondColor: '#6699ff',\n colorTheme: darkPalette,\n palette: {softenCanvas: false},\n shapes: buildShapes(DEFAULT_FONT_SIZE),\n};\n\n// --------------------------------------------------------------------------\n// Composition helper\n// --------------------------------------------------------------------------\n\n/**\n * Compose a new theme by shallow-merging `overrides` on top of `base`. The\n * `shapes` map is merged shallowly too so callers can override just one\n * biotype without having to restate the others. Other nested objects\n * (`colorTheme`) are taken from the override outright when present.\n */\nexport function defineTheme(base: Theme, overrides: Partial<Theme>): Theme {\n const shapes =\n overrides.shapes === undefined\n ? base.shapes\n : {...base.shapes, ...overrides.shapes};\n return {...base, ...overrides, shapes};\n}\n\n/**\n * Pick the appropriate `ShapeStyle` for an atom's `biotype`. Every concrete\n * `HelmType` maps to exactly one entry in `theme.shapes`; the switch is\n * exhaustive so TypeScript guarantees no missing cases.\n */\nexport function shapeStyleFor(theme: Theme, biotype: HelmType): ShapeStyle {\n switch (biotype) {\n case HelmTypes.AA:\n return theme.shapes.aa;\n case HelmTypes.SUGAR:\n return theme.shapes.sugar;\n case HelmTypes.BASE:\n return theme.shapes.base;\n case HelmTypes.LINKER:\n return theme.shapes.linker;\n case HelmTypes.CHEM:\n return theme.shapes.chem;\n case HelmTypes.BLOB:\n return theme.shapes.blob;\n case HelmTypes.NUCLEOTIDE:\n return theme.shapes.nucleotide;\n }\n}\n","// Shared helpers used by `svg-renderer.ts`, `selection-overlay.ts`, and\n// `hit-test.ts` so each of them computes per-atom geometry the same way.\n//\n// These functions encapsulate two concerns:\n// 1. The biotype → shape-kind mapping (which generator function to call).\n// 2. The shape-style + atom position → path data + bbox composition.\n//\n// Layering: imports from `core/`, `util/`, plus the local `./shapes` and\n// `./theme` modules. No DOM, no console.\n\nimport type {Atom} from '../core/model';\nimport {HelmTypes, type HelmType} from '../core/constants';\nimport type {BBox} from '../util/types';\nimport {\n blobPath,\n dashedCirclePath,\n diamondPath,\n ellipsePath,\n hexagonPath,\n pentagonPath,\n roundedRectPath,\n shapeBBox,\n type ShapeKind,\n} from './shapes';\nimport {shapeStyleFor, type ShapeStyle, type Theme} from './theme';\n\n/** Sentinel symbol used by the parser/resolver for gap atoms (HELM `*`). */\nconst GAP_SYMBOL = '*';\n\n/**\n * Subset of `ShapeKind`s that are reachable from an atom's `biotype` — the\n * shape lookup never produces a generic 'ellipse' (linker uses a circle of\n * equal radii). Narrowing the return type lets callers' switches stay\n * exhaustive without dead branches.\n */\nexport type AtomShapeKind = Exclude<ShapeKind, 'ellipse'>;\n\n/**\n * Map an atom's `biotype` to one of the registered `ShapeKind`s in\n * `./shapes`. CHEM and SUGAR both use rounded rectangles — they differ\n * only in size / corner radius, which the caller pulls from the\n * `ShapeStyle` separately.\n */\nexport function shapeKindFor(biotype: HelmType): AtomShapeKind {\n switch (biotype) {\n case HelmTypes.AA:\n return 'hexagon';\n case HelmTypes.SUGAR:\n return 'rounded-rect';\n case HelmTypes.BASE:\n return 'diamond';\n case HelmTypes.LINKER:\n return 'circle';\n case HelmTypes.CHEM:\n return 'rounded-rect';\n case HelmTypes.BLOB:\n return 'blob';\n case HelmTypes.NUCLEOTIDE:\n return 'pentagon';\n }\n}\n\n/**\n * Build the SVG `d` attribute for an atom's shape under the given theme.\n * Gap atoms (`atom.symbol === '*'`) always render as a circle; the dashed\n * stroke is applied by the caller via `stroke-dasharray`.\n *\n * Phase 31 (T-3002) — `outset` (px) shifts every edge of the shape\n * outward by that perpendicular distance. The highlight / selection\n * ring builders pass a positive `outset` so the ring's inner edge sits\n * flush against the atom's outer edge instead of overlapping it.\n *\n * The per-shape math derives from the apothem formula for a regular\n * n-gon: the minimum-distance edge (apothem) is `radius * cos(π / n)`.\n * To move the edge outward by `d` perpendicular pixels, the\n * circumradius grows by `d / cos(π / n)`. For a circle (the\n * degenerate ∞-gon) `cos(π / n) → 1` so the radius grows by `d`. For\n * a rounded rectangle whose `size` is the **full** width/height, each\n * side moves outward by `d` so width and height grow by `2d`; the\n * corner radius gains `d` to keep the corners concentric.\n */\nexport function atomShapePath(atom: Atom, theme: Theme, outset: number = 0): string {\n const style = shapeStyleFor(theme, atom.biotype);\n if (atom.symbol === GAP_SYMBOL)\n return dashedCirclePath(atom.p, style.size + outset);\n const kind = shapeKindFor(atom.biotype);\n switch (kind) {\n case 'hexagon': {\n // -30° rotation → apothem at angle 0; cos(π/6) ≈ 0.866.\n const grow = outset === 0 ? 0 : outset / Math.cos(Math.PI / 6);\n return hexagonPath(atom.p, style.size + grow);\n }\n case 'diamond': {\n // n = 4; cos(π/4) = √2/2 ≈ 0.707.\n const grow = outset === 0 ? 0 : outset / Math.cos(Math.PI / 4);\n return diamondPath(atom.p, style.size + grow);\n }\n case 'pentagon': {\n // n = 5; cos(π/5) ≈ 0.809.\n const grow = outset === 0 ? 0 : outset / Math.cos(Math.PI / 5);\n return pentagonPath(atom.p, style.size + grow);\n }\n case 'rounded-rect':\n return roundedRectPath(\n atom.p,\n style.size + 2 * outset,\n style.size + 2 * outset,\n (style.cornerRadius ?? 0) + outset,\n );\n case 'blob':\n return blobPath(\n atom.p,\n style.size + 2 * outset,\n style.size + 2 * outset,\n (style.cornerRadius ?? 0) + outset,\n );\n case 'circle':\n return ellipsePath(atom.p, style.size + outset, style.size + outset);\n }\n}\n\n/**\n * Tight axis-aligned bbox for an atom's shape, including stroke half-width\n * padding so the bbox covers the visible ink. Used by the renderer to size\n * the canvas and by the hit-tester as a broad-phase rect.\n */\nexport function atomBBox(atom: Atom, theme: Theme): BBox {\n const style = shapeStyleFor(theme, atom.biotype);\n const kind: AtomShapeKind = atom.symbol === GAP_SYMBOL ? 'circle' : shapeKindFor(atom.biotype);\n let bbox: BBox;\n switch (kind) {\n case 'hexagon':\n case 'diamond':\n case 'pentagon':\n case 'circle':\n bbox = shapeBBox(kind, atom.p, style.size);\n break;\n case 'rounded-rect':\n case 'blob':\n bbox = shapeBBox(kind, atom.p, style.size, style.size);\n break;\n }\n const pad = style.strokeWidth / 2;\n return {\n x: bbox.x - pad,\n y: bbox.y - pad,\n width: bbox.width + 2 * pad,\n height: bbox.height + 2 * pad,\n };\n}\n\n/**\n * Hit-test radius used by the broad-phase / nearest-atom logic. We adopt\n * the legacy editor's convention of `atomscale * fontSize / 2` (the inner\n * circumradius of the atom's bounding circle), giving a single scalar per\n * atom that's cheap to compare against an Euclidean distance.\n */\nexport function atomHitRadius(atom: Atom, theme: Theme): number {\n const style = shapeStyleFor(theme, atom.biotype);\n return style.size;\n}\n\n/**\n * Convenience: re-export the per-atom `ShapeStyle` resolution so render\n * call sites can grab style + path with a single import.\n */\nexport function atomStyle(atom: Atom, theme: Theme): ShapeStyle {\n return shapeStyleFor(theme, atom.biotype);\n}\n","// T-0403 — Tiny string-based SVG element builder.\n//\n// The renderer produces an SVG document as a single string (no DOM\n// traversal, no `innerHTML` parsing). This builder exists so attribute\n// escaping happens in exactly one place and number formatting is\n// consistent with `./shapes` (3-decimal rounding, `-0` collapsed).\n//\n// The API is deliberately minimal: a single `tag(name, attrs?, children?)`\n// helper plus the two escape primitives. Higher-level call sites assemble\n// elements by concatenation.\n//\n// Layering: imports nothing. No DOM, no console.\n//\n// T-0902 PROFILING NOTES (Phase 9): a 100-monomer cold render performs\n// ~5-7 `tag(...)` calls per atom (path, label, optional ambiguity / id / tag\n// dot, outer `<g>`), and almost every attribute value passes through\n// `escapeAttr` or `formatNumber`. The bench results showed both functions in\n// the top 5 self-time frames. Optimizations applied below — all purely\n// algorithmic, no API change:\n// 1. `escapeAttr` / `escapeText` short-circuit when no escapable char is\n// present (the common case for atom labels like \"A\", \"C\", \"meI\" and for\n// generated class names). `String#test`-style ASCII scans dwarf five\n// chained `.replace(/g, ...)` calls, the latter of which always allocate\n// a result string even when nothing matches.\n// 2. `formatNumber` adds a small-integer fast path. Most coordinates pass\n// through `Math.round`'d already-integer values; `Number.isInteger` is\n// a microsecond check that skips the multiply/divide + `Object.is(-0)`\n// branch when applicable.\n// 3. `tag(...)` checks `attrs === undefined` and short-circuits the loop\n// entirely; the rendered output is unchanged because the keys-loop was\n// already a no-op in that case but the `Object.keys()` allocation was\n// not.\n\n/**\n * Allowed attribute value types: strings (HTML-escaped on emit), numbers\n * (formatted with 3-decimal precision), booleans (presence/absence flags),\n * and `undefined` (skipped). Other types are rejected at the call site by\n * the type system.\n */\nexport type SvgAttrValue = string | number | boolean | undefined;\n\n/** Attribute map for the `tag` builder. */\nexport type SvgAttrs = Record<string, SvgAttrValue>;\n\n// Module-level scan regexes — kept as constants so they aren't recompiled\n// per call. The `test()` returns false for the common monomer-symbol /\n// generated-class-name case, in which case we can return the input as-is.\nconst ATTR_NEEDS_ESCAPE = /[&\"'<>]/;\nconst TEXT_NEEDS_ESCAPE = /[&<>]/;\n\n/**\n * Escape a string for use as an XML attribute value. Quotes are doubled\n * (`\"`) so callers can wrap attribute values in `\"…\"` without worrying\n * about embedded characters.\n *\n * Fast path: if the string contains none of `&\"'<>`, the input is returned\n * unchanged. This dominates the hot path because attribute values are\n * generated from monomer symbols and class names — neither contains XML\n * metacharacters in practice.\n */\nexport function escapeAttr(s: string): string {\n if (!ATTR_NEEDS_ESCAPE.test(s)) return s;\n return s\n .replace(/&/g, '&')\n .replace(/\"/g, '"')\n .replace(/'/g, ''')\n .replace(/</g, '<')\n .replace(/>/g, '>');\n}\n\n/**\n * Escape a string for use as XML text content. Only `&`, `<`, and `>`\n * matter inside text nodes — quotes pass through.\n */\nexport function escapeText(s: string): string {\n if (!TEXT_NEEDS_ESCAPE.test(s)) return s;\n return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');\n}\n\n/**\n * Format a number with up to three decimal places. Mirrors the formatting\n * used by `./shapes` so attribute values and path data round identically.\n *\n * Integer fast path: every value passed through the renderer's surface\n * transform ends up close to an integer (pixel grid); when the input is\n * already an exact integer we can skip the multiply/divide entirely and\n * fall straight to `Number#toString`.\n */\nexport function formatNumber(n: number): string {\n if (Number.isInteger(n)) return n === 0 ? '0' : n.toString();\n if (!Number.isFinite(n)) return '0';\n if (Object.is(n, -0)) return '0';\n const r = Math.round(n * 1000) / 1000;\n return r === 0 ? '0' : r.toString();\n}\n\n/**\n * Build a single SVG element as a string. If `children` is undefined the\n * tag is emitted self-closing (`<g/>`); otherwise the children are\n * concatenated and wrapped in matching open/close tags.\n *\n * Attribute encoding rules:\n * - `undefined` → attribute omitted\n * - `false` → attribute omitted\n * - `true` → bare attribute name (no value)\n * - `number` → formatted via `formatNumber`\n * - `string` → escaped via `escapeAttr`\n */\nexport function tag(name: string, attrs?: SvgAttrs, children?: string | ReadonlyArray<string>): string {\n let out = `<${name}`;\n if (attrs !== undefined) {\n // Inline the attribute loop. We iterate `for...in` rather than\n // `Object.keys(...).forEach` so we don't allocate a temporary key array\n // per tag call (called O(atoms * 5) per render).\n for (const k in attrs) {\n const v = attrs[k];\n if (v === undefined || v === false) continue;\n if (v === true) {\n out += ` ${k}`;\n continue;\n }\n if (typeof v === 'number') {\n out += ` ${k}=\"${formatNumber(v)}\"`;\n } else {\n out += ` ${k}=\"${escapeAttr(v)}\"`;\n }\n }\n }\n if (children === undefined) return `${out}/>`;\n if (typeof children === 'string') return `${out}>${children}</${name}>`;\n return `${out}>${children.join('')}</${name}>`;\n}\n","// T-0403 — SVG renderer.\n//\n// Builds a single self-contained `<svg>` document for a `Mol`. The structure\n// follows RENDERING.md §8 (rect background → highlight layer → selection\n// layer → bonds → atoms → annotations). All output is concatenated as a\n// string via `./svg-builder`; we never touch `innerHTML`.\n//\n// The renderer is deterministic — given the same `Mol`, theme, and options\n// it produces the same string byte-for-byte. Snapshot tests rely on that.\n//\n// Layering: imports from `core/`, `util/`, `monomer-lib/`, and `./` only.\n// No DOM, no console. `document` is consulted only via `options.documentImpl`\n// when the host wants a `Document` round-trip; the default path stays string-\n// based.\n//\n// References:\n// - RENDERING.md §3 (shapes), §4 (bonds), §5 (labels), §6 (passes),\n// §8 (SVG structure), §11 (theme), §12 (selection)\n\nimport type {Atom, Bond, Mol} from '../core/model';\nimport {HelmTypes} from '../core/constants';\nimport type {BBox} from '../util/types';\nimport {bboxUnion} from '../util/types';\nimport {resolveMonomerColors} from '../monomer-lib/color-rules';\n\nimport {atomBBox, atomShapePath, atomStyle} from './atom-shape';\nimport {bondHashPath, bondLinePath} from './shapes';\nimport {escapeText, formatNumber, tag} from './svg-builder';\nimport {lightTheme, type Theme} from './theme';\n\n// --------------------------------------------------------------------------\n// Public API\n// --------------------------------------------------------------------------\n\n/** Options accepted by `renderSvg`. See RENDERING.md §8 for semantics. */\nexport interface RenderOptions {\n /** Output canvas width in CSS pixels. Default `400`. */\n width?: number;\n /** Output canvas height in CSS pixels. Default `100`. */\n height?: number;\n /** Background colour. Defaults to `theme.background`. */\n backColor?: string;\n /** Theme used for colours and shape sizing. Default `lightTheme`. */\n theme?: Theme;\n /** Padding around the drawn content in surface pixels. Default `16`. */\n padding?: number;\n /** Show the continuous-id overlay on each atom. Default `false`. */\n showIds?: boolean;\n /** Show per-atom / per-bond annotation text. Default `true`. */\n showAnnotations?: boolean;\n /** Show R-group labels on bond endpoints. Default `false`. */\n showRGroupLabels?: boolean;\n /** Override the theme's `bondLength`. Default `theme.bondLength`. */\n bondLength?: number;\n /** Override the theme's `fontSize`. Default `theme.fontSize`. */\n fontSize?: number;\n /**\n * Maximum up-scale factor when fitting the model into the surface. Default\n * `1` — the renderer never enlarges past native monomer size, so a small\n * drawing leaves empty space (the historical behaviour every snapshot\n * golden assumes). Datagrok consumers pass `Infinity` (or a large number)\n * so a single cell / editor pane is filled by scaling the drawing UP to\n * the available width/height. The fit still preserves aspect ratio and\n * never shrinks below what is needed to fit (down-scaling is always on).\n */\n maxScale?: number;\n /** Atom ids that should render in the selection pass. */\n selectedAtomIds?: ReadonlySet<number>;\n /** Atom ids that should render in the highlight pass. */\n highlightedAtomIds?: ReadonlySet<number>;\n /**\n * Optional `Document` implementation for headless callers that want to\n * parse the resulting SVG. Defaults to `globalThis.document` when\n * available. The renderer never *requires* a Document — output is a\n * string — but exposes the hook so callers can pin behaviour.\n */\n documentImpl?: Document;\n /**\n * T-1704 — Override the auto-fit. When supplied, the renderer skips\n * `computeModelBbox(mol)` and uses this bbox for the transform math.\n * The interactive editor uses this to keep the canvas transform stable\n * across mutations: after each `mol:changed` the view re-renders with\n * the bbox captured at the most recent layout (`setHelm` /\n * `cleanLayout`), so adding / removing / moving atoms does NOT cause a\n * visible re-fit. Headless callers can omit it for the auto-fit\n * behaviour.\n */\n fixedModelBbox?: { x: number; y: number; width: number; height: number };\n}\n\n/** Result returned by `renderSvg`. */\nexport interface RenderResult {\n /** The full `<svg>…</svg>` document as a string. */\n readonly svg: string;\n /** Tight bbox of the drawn content in model space. */\n readonly bbox: {x: number; y: number; width: number; height: number};\n /**\n * Where that bbox sits inside the surface in surface-space pixels. `scale`\n * is the uniform model→surface scale factor applied by the fit (so a model\n * point `(mx, my)` maps to surface `(mx·scale + tx, my·scale + ty)` where\n * `tx = drawBox.x − bbox.x·scale`, `ty = drawBox.y − bbox.y·scale`).\n * Carrying `scale` here lets consumers invert the transform even for\n * degenerate (zero-width or zero-height) bboxes, where it can't be derived\n * from `drawBox.width / bbox.width`.\n */\n readonly drawBox: {x: number; y: number; width: number; height: number; scale: number};\n}\n\n// --------------------------------------------------------------------------\n// Renderer entry point\n// --------------------------------------------------------------------------\n\nconst SVG_NS = 'http://www.w3.org/2000/svg';\n\n/**\n * Continuous-id (monomer index) overlay geometry, shared by the svg + canvas\n * renderers so both place the number identically. The offset is a UNIFORM\n * value per drawing (independent of each shape's `size`) so every monomer's\n * number sits at the same height — a sugar (size 1.39×fs) and a phosphate\n * linker (0.63×fs) align.\n *\n * T-3509 — The offset now scales with the layout's `bondLength` rather than\n * the font size: `idOff = bondLength × ID_OFFSET_PER_BONDLENGTH`. Monomer\n * spacing is a function of `bondLength`, so a font-size-pinned offset looked\n * fine at the old default (40) but floated far from the monomers once the\n * default dropped to 28 (user report 2026-06-15, image). The factor `0.525`\n * reproduces the previous look at `bondLength = 40` (40 × 0.525 ≈ 21 = the old\n * `1.5 × fontSize`) and pulls the numbers proportionally closer as the bond\n * length shrinks.\n */\nexport const ID_OFFSET_PER_BONDLENGTH = 0.525;\n/** Id-label font size as a multiple of the base font size (was 0.6). */\nexport const ID_FONT_FACTOR = 0.8;\n\n/**\n * Uniform bond stroke width in CSS pixels, shared by the svg + canvas\n * renderers. T-3509 — bumped to 1.5× the previous 0.75 px (user request\n * 2026-06-15) so bonds read a little heavier against the monomer shapes.\n */\nexport const BOND_STROKE_WIDTH = 1.125;\n\nconst EMBEDDED_CSS =\n '.hw-atom{transition:stroke-width 120ms,filter 120ms;}' +\n '.hw-atom:focus{outline:none;}' +\n '.hw-atom__shape{shape-rendering:geometricPrecision;}' +\n '.hw-bond__stroke{stroke-linecap:round;}';\n\nconst CANONICAL_PAIRS: ReadonlyArray<[string, string]> = [\n ['A', 'T'],\n ['A', 'U'],\n ['G', 'C'],\n];\n\n/**\n * Render a `Mol` to a self-contained SVG document. The result includes the\n * model bbox (in model coordinates) and a draw box (in surface coordinates)\n * so consumers can remap hit-test points without re-running the renderer.\n *\n * When `options.documentImpl` is supplied AND no global `document` is\n * available, the renderer still produces a string but skips any future\n * DOM-aware enhancements (no current call site requires DOM access; the\n * option exists for forward-compatibility).\n */\nexport function renderSvg(mol: Mol, options: RenderOptions = {}): RenderResult {\n const theme = options.theme ?? lightTheme;\n const width = options.width ?? 400;\n const height = options.height ?? 100;\n const padding = options.padding ?? 16;\n const backColor = options.backColor ?? theme.background;\n const showIds = options.showIds ?? false;\n const showAnnotations = options.showAnnotations ?? true;\n const fontSize = options.fontSize ?? theme.fontSize;\n const selected = options.selectedAtomIds ?? new Set<number>();\n const highlighted = options.highlightedAtomIds ?? new Set<number>();\n // `documentImpl` is accepted for API symmetry; the string path doesn't\n // need it but a future enhancement (DOM-returning variant) will. We only\n // touch globals lazily — never at module init — and only when the caller\n // didn't supply one. Reference it here so tests can confirm the option\n // is at least inspected.\n const docImpl = options.documentImpl ?? (typeof globalThis !== 'undefined' && typeof (globalThis as {document?: Document}).document !== 'undefined'\n ? (globalThis as {document: Document}).document\n : undefined);\n void docImpl;\n\n // -----------------------------------------------------------------------\n // Compute model-space bbox + surface transform\n // T-1704 — Honour `fixedModelBbox` (if supplied) so the auto-fit\n // transform stays stable across interactive mutations. The interactive\n // editor pins this to the bbox captured at the most recent layout so\n // adding / removing / moving atoms doesn't visibly rescale the canvas.\n // -----------------------------------------------------------------------\n const modelBbox = options.fixedModelBbox ?? computeModelBbox(mol, theme);\n const drawBox = fitToSurface(modelBbox, width, height, padding, options.maxScale ?? 1);\n const tx = drawBox.x - modelBbox.x * drawBox.scale;\n const ty = drawBox.y - modelBbox.y * drawBox.scale;\n\n // -----------------------------------------------------------------------\n // Layers\n //\n // Phase 19 (T-1901) — Paint order changed: highlights/selections now\n // come AFTER atoms so the rings actually paint ON TOP of the atom\n // shape (painter's algorithm). Previously the rings were drawn\n // beneath atoms and were occluded by the atom's fill, which made\n // selection invisible and hover indistinguishable from the CSS\n // `:hover` outline. The new order is: bonds → atoms → annotations →\n // highlights → selections. Selections are LAST so a selected+hovered\n // atom shows the yellow ring (selection wins over highlight when\n // both are present).\n // -----------------------------------------------------------------------\n const highlightsLayer = buildHighlightLayer(mol, theme, highlighted);\n const selectionsLayer = buildSelectionLayer(mol, theme, selected);\n const bondsLayer = buildBondsLayer(mol, theme);\n const atomsLayer = buildAtomsLayer(mol, theme, fontSize, showIds, options.bondLength ?? theme.bondLength);\n const annotationsLayer = showAnnotations ? buildAnnotationsLayer(mol, theme, fontSize) : '';\n\n const transformValue =\n `translate(${formatNumber(tx)} ${formatNumber(ty)}) scale(${formatNumber(drawBox.scale)})`;\n\n const inner = tag(\n 'g',\n {class: 'hw-content', transform: transformValue},\n [bondsLayer, atomsLayer, annotationsLayer, highlightsLayer, selectionsLayer].join(''),\n );\n\n const defs = tag('defs', undefined, `<style>${EMBEDDED_CSS}</style>`);\n const bg = tag('rect', {class: 'hw-bg', x: 0, y: 0, width, height, fill: backColor});\n\n const svg = tag(\n 'svg',\n {\n xmlns: SVG_NS,\n viewBox: `0 0 ${formatNumber(width)} ${formatNumber(height)}`,\n width,\n height,\n 'data-hw-theme': theme.name,\n },\n [defs, bg, inner].join(''),\n );\n\n return {\n svg,\n bbox: {x: modelBbox.x, y: modelBbox.y, width: modelBbox.width, height: modelBbox.height},\n drawBox: {x: drawBox.x, y: drawBox.y, width: drawBox.width, height: drawBox.height, scale: drawBox.scale},\n };\n}\n\n// --------------------------------------------------------------------------\n// Geometry helpers\n// --------------------------------------------------------------------------\n\n/**\n * Tight bbox covering every atom's shape (with stroke half-width). Empty\n * `Mol` yields a zero-size bbox at the origin.\n */\n/**\n * Compute the tight model-space bbox for `mol` (the union of every\n * atom's drawn shape). Exposed so the interactive editor can capture\n * the bbox of one rendered state and pin subsequent renders to it\n * (T-1704 — keeps the canvas transform stable across mutations).\n */\nexport function computeModelBbox(mol: Mol, theme: Theme = lightTheme): BBox {\n if (mol.atoms.length === 0) return {x: 0, y: 0, width: 0, height: 0};\n let acc = atomBBox(mol.atoms[0] as Atom, theme);\n for (let i = 1; i < mol.atoms.length; i++)\n acc = bboxUnion(acc, atomBBox(mol.atoms[i] as Atom, theme));\n return acc;\n}\n\ninterface FitResult {\n x: number;\n y: number;\n width: number;\n height: number;\n scale: number;\n}\n\n/**\n * Compute the scale + offset that places the model bbox inside the surface,\n * centred and with `padding` on every side. `maxScale` caps up-scaling\n * (default `1` = never enlarge past native size). Down-scaling to fit is\n * always applied regardless of `maxScale`.\n */\nfunction fitToSurface(\n model: BBox, width: number, height: number, padding: number, maxScale: number = 1,\n): FitResult {\n const innerW = Math.max(0, width - 2 * padding);\n const innerH = Math.max(0, height - 2 * padding);\n let scale = 1;\n if (model.width > 0 || model.height > 0) {\n const sx = model.width > 0 ? innerW / model.width : Number.POSITIVE_INFINITY;\n const sy = model.height > 0 ? innerH / model.height : Number.POSITIVE_INFINITY;\n // Fit on the tighter axis, but never enlarge beyond `maxScale`.\n scale = Math.min(sx, sy, maxScale);\n if (!Number.isFinite(scale) || scale <= 0) scale = 1;\n }\n const contentW = model.width * scale;\n const contentH = model.height * scale;\n return {\n x: (width - contentW) / 2,\n y: (height - contentH) / 2,\n width: contentW,\n height: contentH,\n scale,\n };\n}\n\n// --------------------------------------------------------------------------\n// Layer builders\n// --------------------------------------------------------------------------\n\n/**\n * Phase 29 / Phase 31 — Width of the highlight / selection ring stroke\n * in CSS pixels. The same value drives:\n *\n * 1. The ring path's `stroke-width` attribute (so the ring appears\n * as a chunky halo, not a hairline).\n * 2. The OUTSET applied to the ring's path geometry (Phase 31\n * T-3002). Outset = `(atomStrokeWidth + ringStrokeWidth) / 2` —\n * the half-stroke of the atom plus the half-stroke of the ring,\n * so the ring's INNER edge sits flush against the atom's OUTER\n * edge with no gap and no overlap.\n *\n * Atom stroke is 5 px (theme.ts), ring stroke 3 px → outset 4 px.\n *\n * History:\n * Phase 29 initial: 9 px (T-2902). User reported \"way too thick\".\n * T-2911: 9 → 3. Visible gap from path+1.5 to path+2.5 (atom stroke\n * extended further than ring), so T-2912 bumped to 5.\n * T-2912: 3 → 5. Ring exactly replaced atom's outer stroke colour\n * AT the path location — no halo \"outside\" the atom.\n * T-3002 (Phase 31): ring path moved OUTSIDE the atom path\n * (`outset` parameter on `atomShapePath`). The gap argument\n * that drove the T-2912 bump no longer applies, so the\n * stroke trims back to 3 px (\"a bit thinner\") per the\n * user's Phase 31 follow-up.\n */\nconst RING_STROKE_WIDTH = 3;\n\n/** Highlight pass — heavy stroke ring beneath each highlighted atom. */\nexport function buildHighlightLayer(\n mol: Mol, theme: Theme, highlighted: ReadonlySet<number>,\n): string {\n if (highlighted.size === 0)\n return tag('g', {class: 'hw-layer hw-highlights'}, '');\n const children: string[] = [];\n for (const atom of mol.atoms) {\n if (!highlighted.has(atom.id)) continue;\n const style = atomStyle(atom, theme);\n // Phase 31 (T-3002) — Ring sits OUTSIDE the atom shape. See the\n // commentary on `RING_STROKE_WIDTH` for the derivation.\n const outset = (style.strokeWidth + RING_STROKE_WIDTH) / 2;\n const d = atomShapePath(atom, theme, outset);\n children.push(tag('path', {\n d,\n class: 'hw-highlight',\n fill: 'none',\n stroke: theme.highlightColor,\n // Phase 29 (T-2902) — Ring is a chunky halo. The atom shape\n // paints with a 5 px self-coloured stroke (rounded-corner\n // trick); the ring's stroke matches so the halo reads as a\n // peer of the atom outline rather than a hairline addition.\n //\n // Phase 31 (T-3002) — The PATH is now outset by `outset` px so\n // the ring's inner edge sits flush against the atom's visual\n // outer edge instead of being painted directly on top of it.\n //\n // History:\n // Phase 29 initial: 9 px ring, same path as atom (T-2902).\n // T-2911: 9 → 3 (user reported \"way too thick\").\n // T-2912: 3 → 5 (3 left a visible gap from path+1.5 to\n // path+2.5 since atom stroke extends 2.5 outward;\n // 5 px ring cleanly replaced the atom's outer rim).\n // T-3002 (Phase 31): 5 px ring kept, but path outset by 5 px\n // so the ring reads as a halo OUTSIDE the atom shape.\n 'stroke-width': RING_STROKE_WIDTH,\n 'stroke-linejoin': 'round',\n 'stroke-linecap': 'round',\n // Phase 18 (T-1803): NO `data-atom-id` on rings. The atom's\n // identifier lives on its `<g class=\"hw-atom\">` element only —\n // duplicating it onto the ring made `[data-atom-id=\"X\"]`\n // selectors ambiguous (the ring is rendered FIRST in source\n // order, so the broad selector picked up the ring instead of\n // the atom). The `data-testid` below encodes the same id but\n // with the `highlight-ring-` prefix, keeping the two element\n // kinds addressable by disjoint selectors.\n 'data-symbol': atom.symbol,\n 'data-ring-atom-id': atom.id,\n 'data-testid': `highlight-ring-${atom.id}`,\n }));\n }\n return tag('g', {class: 'hw-layer hw-highlights'}, children.join(''));\n}\n\n/** Selection pass — even heavier stroke ring, sits on top of highlight. */\nexport function buildSelectionLayer(\n mol: Mol, theme: Theme, selected: ReadonlySet<number>,\n): string {\n if (selected.size === 0)\n return tag('g', {class: 'hw-layer hw-selections'}, '');\n const children: string[] = [];\n for (const atom of mol.atoms) {\n if (!selected.has(atom.id)) continue;\n const style = atomStyle(atom, theme);\n // Phase 31 (T-3002) — See `buildHighlightLayer` for the outset\n // derivation. The selection ring sits at the same outset as the\n // highlight ring; only the colour and z-order differ.\n const outset = (style.strokeWidth + RING_STROKE_WIDTH) / 2;\n const d = atomShapePath(atom, theme, outset);\n children.push(tag('path', {\n d,\n class: 'hw-selection',\n fill: 'none',\n stroke: theme.selectionColor,\n // Phase 29 / Phase 31 — See `buildHighlightLayer` for the\n // history.\n 'stroke-width': RING_STROKE_WIDTH,\n 'stroke-linejoin': 'round',\n 'stroke-linecap': 'round',\n // Phase 18 (T-1801): NO `data-atom-id` on rings — see the\n // commentary on `buildHighlightLayer` for why. The\n // `data-ring-atom-id` mirrors the id under a different\n // attribute name so consumers that want the correlation can\n // still get it, without colliding with `[data-atom-id=\"X\"]`\n // selectors that target the atom group.\n 'data-symbol': atom.symbol,\n 'data-ring-atom-id': atom.id,\n 'data-testid': `selection-ring-${atom.id}`,\n }));\n }\n return tag('g', {class: 'hw-layer hw-selections'}, children.join(''));\n}\n\n/** Bond pass — one `<g class=\"hw-bond\">` per bond, addressable by id. */\nexport function buildBondsLayer(mol: Mol, theme: Theme): string {\n const children: string[] = [];\n for (const bond of mol.bonds)\n children.push(buildBondElement(bond, theme));\n return tag('g', {class: 'hw-layer hw-bonds'}, children.join(''));\n}\n\n/**\n * Build the `<g class=\"hw-bond\">` for one bond. Picks colour, dash pattern\n * and optional decoration based on `bond.kind` and the participating atoms.\n */\nfunction buildBondElement(bond: Bond, theme: Theme): string {\n const a = bond.a1.p;\n const b = bond.a2.p;\n const stroke = bondStrokeColor(bond, theme);\n const dash = bondDashArray(bond);\n const sw = bondStrokeWidth(bond);\n const linePath = bondLinePath(a, b);\n\n const attrs: Parameters<typeof tag>[1] = {\n d: linePath,\n class: 'hw-bond__stroke',\n fill: 'none',\n stroke,\n 'stroke-width': sw,\n };\n if (dash !== undefined) attrs['stroke-dasharray'] = dash;\n\n const inner: string[] = [tag('path', attrs)];\n\n // H-bonds get an extra \"hash\" decoration at the bond midpoint.\n if (bond.kind === 'hbond') {\n inner.push(tag('path', {\n d: bondHashPath(a, b, theme.fontSize * 0.6),\n class: 'hw-bond__hash',\n fill: 'none',\n stroke,\n 'stroke-width': sw,\n }));\n }\n\n return tag('g', {\n class: `hw-bond hw-bond--${bond.kind}`,\n 'data-bond-id': bond.id,\n // Phase 13c (UX-test request) — Expose the bond kind on the DOM so\n // Playwright specs can verify hbond / cross-polymer / backbone\n // membership without going through the bridge.\n 'data-kind': bond.kind,\n 'data-testid': `canvas-bond-${bond.id}`,\n }, inner.join(''));\n}\n\nfunction bondStrokeColor(bond: Bond, theme: Theme): string {\n switch (bond.kind) {\n case 'backbone':\n case 'branch':\n return theme.defaultStroke;\n case 'cross-polymer':\n return theme.crossPolymerBondColor;\n case 'hbond':\n return isCanonicalHbond(bond) ? theme.hbondColor : theme.nonCanonicalHbondColor;\n case 'group':\n return theme.defaultStroke;\n }\n}\n\nfunction bondDashArray(bond: Bond): string | undefined {\n if (bond.kind === 'hbond') return '4 3';\n return undefined;\n}\n\nfunction bondStrokeWidth(_bond: Bond): number {\n // Phase 23 (T-2302) -- A uniform bond stroke (was 0.75 px) keeps every kind\n // subtly visible without overpowering the small monomer shapes; bond *kind*\n // is still legible via the per-kind stroke colour (`bondStrokeColor`) and the\n // hbond `stroke-dasharray`. T-3509 bumped it 1.5× to `BOND_STROKE_WIDTH`.\n return BOND_STROKE_WIDTH;\n}\n\n/**\n * Hbonds with bases that aren't a canonical Watson-Crick pair (A-T, A-U,\n * G-C) get rendered in the \"non-canonical\" colour. Symbols are normalised\n * to upper-case so `a`/`A` count as the same base.\n */\nfunction isCanonicalHbond(bond: Bond): boolean {\n if (bond.a1.biotype !== HelmTypes.BASE || bond.a2.biotype !== HelmTypes.BASE)\n return false;\n const x = baseLetter(bond.a1);\n const y = baseLetter(bond.a2);\n for (const [u, v] of CANONICAL_PAIRS) {\n if ((x === u && y === v) || (x === v && y === u)) return true;\n }\n return false;\n}\n\nfunction baseLetter(atom: Atom): string {\n const na = atom.monomer?.naturalAnalog;\n const s = (na !== undefined && na !== '' ? na : atom.symbol);\n return s.length === 0 ? '' : (s.charAt(0).toUpperCase());\n}\n\n/** Atom pass — one focusable `<g class=\"hw-atom\">` per atom. */\nexport function buildAtomsLayer(\n mol: Mol, theme: Theme, fontSize: number, showIds: boolean, bondLength: number = theme.bondLength,\n): string {\n const children: string[] = [];\n for (const atom of mol.atoms) {\n if (atom.hidden) continue;\n children.push(buildAtomElement(atom, theme, fontSize, showIds, bondLength));\n }\n return tag('g', {class: 'hw-layer hw-atoms'}, children.join(''));\n}\n\nfunction buildAtomElement(\n atom: Atom, theme: Theme, fontSize: number, showIds: boolean, bondLength: number = theme.bondLength,\n): string {\n const style = atomStyle(atom, theme);\n const d = atomShapePath(atom, theme);\n const colors = resolveMonomerColors(atom.monomer, atom.biotype, theme.colorTheme);\n const fill = colors.background;\n const textColor = colors.text;\n const dash = atom.symbol === '*' ? '3 3' : undefined;\n\n // Phase 29 (T-2902) -- The shape carries a 5 px self-coloured stroke\n // with `stroke-linejoin: round` to give polygons rounded vertices\n // and chunkier round edges on rounded-rect / circle shapes. The\n // stroke is painted in the SAME colour as the fill so it doesn't\n // read as an outline -- the only visual effect is rounded corners.\n // The hover (`.hw-highlight`) and selection (`.hw-selection`)\n // rings sit in their own layers ABOVE the shape and paint at a\n // 3 px stroke (T-2911): they overpaint the atom's outer 1.5 px on\n // each side and extend a further 1.5 px outward, giving a subtle\n // halo of ring colour just outside the atom edge.\n //\n // The `stroke-linecap: round` is defensive -- closed polygon paths\n // never hit it, but if a future shape uses an open path the cap\n // rounding keeps the look consistent.\n //\n // History:\n // Phase 22 (T-2204): stroke-width 0.5 px, dark outline.\n // Phase 23 (T-2301) / 23.3 (T-2311): derived darker fill colour.\n // Phase 23.4 (T-2312): stroke=\"none\" entirely.\n // Phase 29 (T-2902): stroke=fill, 5 px, round joins.\n const shapeAttrs: Parameters<typeof tag>[1] = {\n d,\n class: 'hw-atom__shape',\n fill,\n stroke: fill,\n 'stroke-width': style.strokeWidth,\n 'stroke-linejoin': 'round',\n 'stroke-linecap': 'round',\n };\n if (dash !== undefined) shapeAttrs['stroke-dasharray'] = dash;\n\n const elements: string[] = [tag('path', shapeAttrs)];\n\n // Label — multi-char symbols scale down (legacy `min(1, 2/len)`). Prefer the\n // RESOLVED monomer's symbol so an inline/explicit SMILES monomer shows its\n // registered short symbol (`#P1`) instead of the raw SMILES string.\n const label = atom.monomer?.symbol ?? atom.symbol;\n if (label !== '') {\n const scale = label.length <= 1 ? 1 : Math.min(1, 2 / label.length);\n const px = fontSize * scale;\n elements.push(tag('text', {\n class: 'hw-atom__label',\n x: atom.p.x,\n y: atom.p.y + px * 0.35,\n 'text-anchor': 'middle',\n 'font-size': px,\n 'font-family': theme.fontFamily,\n fill: textColor,\n }, escapeText(label)));\n }\n\n // Ambiguity string (small text below).\n if (atom.bio.ambiguity !== undefined && atom.bio.ambiguity !== '') {\n elements.push(tag('text', {\n class: 'hw-atom__ambiguity',\n x: atom.p.x,\n y: atom.p.y + style.size + fontSize * 0.7,\n 'text-anchor': 'middle',\n 'font-size': fontSize * 0.7,\n 'font-family': theme.fontFamily,\n fill: textColor,\n }, escapeText(atom.bio.ambiguity)));\n }\n\n // Continuous-id overlay (above-left). The offset is UNIFORM per drawing —\n // NOT `style.size` — so every monomer's number sits at the same height\n // regardless of shape size (previously a linker circle, `size` 0.63×fontSize,\n // parked its number much lower than a sugar at 1.39×fontSize; user report\n // 2026-06-15 image 1). T-3509: it scales with `bondLength` so the numbers\n // track the monomer spacing instead of floating far away at small bond\n // lengths. Font bumped 0.6→0.8 for legibility.\n if (showIds) {\n const idOff = bondLength * ID_OFFSET_PER_BONDLENGTH;\n elements.push(tag('text', {\n class: 'hw-atom__id',\n x: atom.p.x - idOff,\n y: atom.p.y - idOff,\n 'font-size': fontSize * ID_FONT_FACTOR,\n 'font-family': theme.fontFamily,\n fill: theme.idColor,\n }, escapeText(String(atom.bio.continuousId))));\n }\n\n // Tag dot — small filled circle at upper right.\n if (atom.tag !== undefined && atom.tag !== '') {\n elements.push(tag('circle', {\n class: 'hw-atom__tag-dot',\n cx: atom.p.x + style.size * 0.7,\n cy: atom.p.y - style.size * 0.7,\n r: Math.max(2, fontSize * 0.18),\n fill: theme.tagDotColor,\n }));\n }\n\n const aria = atomAriaLabel(atom);\n return tag(\n 'g',\n {\n class: 'hw-atom',\n 'data-atom-id': atom.id,\n // Phase 13c (Bug 8 + 10 + UX-test request) — Expose the symbol and\n // chainId on the atom group so Playwright specs can address atoms\n // by symbol without re-deriving from the mol bridge. The\n // `data-testid` makes the atom a first-class fixture for\n // user-facing tests.\n 'data-symbol': atom.symbol,\n 'data-chain-id': atom.bio.chainId,\n 'data-biotype': atom.biotype,\n 'data-testid': `canvas-atom-${atom.id}`,\n tabindex: 0,\n role: 'button',\n 'aria-label': aria,\n },\n elements.join(''),\n );\n}\n\nfunction atomAriaLabel(atom: Atom): string {\n const sym = atom.symbol === '' ? '?' : atom.symbol;\n const chain = atom.bio.chainId === '' ? 'unknown chain' : atom.bio.chainId;\n const pos = atom.bio.positionInChain;\n return `${sym} at position ${pos} of ${chain}`;\n}\n\n/** Annotation pass — bond ratios, polymer labels, etc. */\nexport function buildAnnotationsLayer(mol: Mol, theme: Theme, fontSize: number): string {\n const children: string[] = [];\n // Per-bond ratio labels.\n for (const bond of mol.bonds) {\n if (bond.ratio === undefined) continue;\n const ax = (bond.a1.p.x + bond.a2.p.x) / 2;\n const ay = (bond.a1.p.y + bond.a2.p.y) / 2;\n const txt = bond.ratio.denominator !== undefined\n ? `${bond.ratio.numerator}/${bond.ratio.denominator}`\n : `${bond.ratio.numerator}`;\n children.push(tag('text', {\n class: 'hw-annotation hw-annotation--bond-ratio',\n x: ax,\n y: ay - fontSize * 0.4,\n 'text-anchor': 'middle',\n 'font-size': fontSize * 0.7,\n 'font-family': theme.fontFamily,\n fill: theme.annotationColor,\n }, escapeText(txt)));\n }\n // Per-atom annotations (5'/n/c markers).\n for (const atom of mol.atoms) {\n const ann = atom.bio.annotation;\n if (ann === undefined || ann === '') continue;\n const style = atomStyle(atom, theme);\n children.push(tag('text', {\n class: 'hw-annotation hw-annotation--atom',\n x: atom.p.x + style.size + 2,\n y: atom.p.y - style.size,\n 'font-size': fontSize * 0.7,\n 'font-family': theme.fontFamily,\n fill: theme.annotationColor,\n }, escapeText(ann)));\n }\n // Per-polymer label at the chain bbox upper-left.\n for (const chain of mol.chains) {\n if (chain.annotation === undefined || chain.annotation === '') continue;\n const first = chain.backboneAtoms[0];\n if (first === undefined) continue;\n children.push(tag('text', {\n class: 'hw-annotation hw-annotation--chain',\n x: first.p.x,\n y: first.p.y - theme.fontSize * 1.4,\n 'font-size': fontSize * 0.8,\n 'font-family': theme.fontFamily,\n fill: theme.annotationColor,\n }, escapeText(chain.annotation)));\n }\n // Phase 33 (T-3301) — 5'/3' direction markers for nucleic-acid\n // chains. Position the marker just OUTSIDE the chain in the local\n // backbone-tangent direction at each end, so after the antisense\n // horizontal+vertical mirror the markers fall on the visually\n // correct sides (5' on the right of the bottom strand, 3' on the\n // left, matching the legacy Pistoia editor). Strand-type ('ss' /\n // 'as') is suffixed when the parser resolved it from section 3.\n for (const chain of mol.chains) {\n // HELM models DNA as RNA with deoxy sugars, so RNA polymer-type\n // is the only nucleic-acid bucket. PEPTIDE / CHEM / BLOB chains\n // don't carry directional 5'/3' semantics.\n if (chain.polymerType !== 'RNA') continue;\n const atoms = chain.backboneAtoms;\n // T-3518 — a single-monomer chain has its 5' and 3' ends at the SAME\n // atom, so both markers would stack on top of each other. With only one\n // monomer there is no meaningful strand direction, so draw neither.\n if (atoms.length < 2) continue;\n const first = atoms[0]!;\n const last = atoms[atoms.length - 1]!;\n const second = atoms.length >= 2 ? atoms[1]! : null;\n const penultimate = atoms.length >= 2 ? atoms[atoms.length - 2]! : null;\n const offset = theme.fontSize * 1.4;\n const fivePos = chainEndMarkerPos(first.p, second?.p ?? null, offset);\n const threePos = chainEndMarkerPos(last.p, penultimate?.p ?? null, offset);\n const fiveLabel = chain.strandType !== undefined ? `5'${chain.strandType}` : \"5'\";\n const threeLabel = chain.strandType !== undefined ? `3'${chain.strandType}` : \"3'\";\n children.push(tag('text', {\n class: 'hw-annotation hw-annotation--prime hw-annotation--prime-5',\n 'data-testid': `prime-5-${chain.id}`,\n x: fivePos.x,\n y: fivePos.y,\n 'text-anchor': 'middle',\n 'dominant-baseline': 'central',\n 'font-size': fontSize * 0.85,\n 'font-family': theme.fontFamily,\n 'font-weight': 'bold',\n fill: theme.annotationColor,\n }, escapeText(fiveLabel)));\n children.push(tag('text', {\n class: 'hw-annotation hw-annotation--prime hw-annotation--prime-3',\n 'data-testid': `prime-3-${chain.id}`,\n x: threePos.x,\n y: threePos.y,\n 'text-anchor': 'middle',\n 'dominant-baseline': 'central',\n 'font-size': fontSize * 0.85,\n 'font-family': theme.fontFamily,\n 'font-weight': 'bold',\n fill: theme.annotationColor,\n }, escapeText(threeLabel)));\n }\n return tag('g', {class: 'hw-layer hw-annotations'}, children.join(''));\n}\n\n/**\n * Phase 33 (T-3301) — Compute the model coords of a 5'/3' chain-end\n * marker. The marker sits `offset` model-pixels OUTSIDE the chain\n * along the local backbone tangent at that end (so it leaves the\n * chain in the opposite direction from the second atom). Falls back\n * to \"directly to the left\" when only one backbone atom exists.\n */\nfunction chainEndMarkerPos(end: {x: number; y: number}, neighbour: {x: number; y: number} | null, offset: number): {x: number; y: number} {\n if (neighbour === null) return {x: end.x - offset, y: end.y};\n const dx = end.x - neighbour.x;\n const dy = end.y - neighbour.y;\n const len = Math.hypot(dx, dy);\n if (len === 0) return {x: end.x - offset, y: end.y};\n return {x: end.x + offset * (dx / len), y: end.y + offset * (dy / len)};\n}\n\n","// T-0407 / T-0408 / T-0410 — `HelmService` class.\n//\n// Single-instance entry point for every headless operation. Constructed once\n// with a monomer library and reused; **no method takes a lib argument** — the\n// lib is bound for the service's lifetime. See API.md §4.\n//\n// Internally the service composes:\n// - parse / serialize / validate / canonicalize / removeGaps from `../io/`\n// - layoutMol from `../layout/`\n// - renderSvg from `../render/svg-renderer`\n// - renderToPng from `../render/canvas-renderer` (browser-only)\n// - computeFormula, molWeight, extinctionCoefficient, pseudoMolfile from\n// `../calc/`\n//\n// Three LRU caches sit in front of the hot paths (parse, layout, render),\n// keyed on stable strings so the same input returns the same object reference\n// across calls. Subscribing to `monomerLib.onChanged` clears every cache on\n// any mutation.\n//\n// Layering: imports from every layer below; nothing above (no editor).\n\nimport type { Mol } from '../core/model';\nimport type { ValidationDiagnostic } from '../core/errors';\nimport { HelmNotSupportedError } from '../core/errors';\nimport type { IMonomerLib } from '../monomer-lib/types';\nimport type { Unsubscribe, BBox } from '../util/types';\nimport { LruCache } from '../util/lru-cache';\nimport type { Logger } from '../util/logger';\nimport { SilentLogger } from '../util/logger';\n\nimport {\n parseHelm as _parseHelm,\n serialize as _serialize,\n validate as _validate,\n canonicalize as _canonicalize,\n removeGaps as _removeGaps,\n} from '../io';\nimport type {\n ParseOptions as IoParseOptions,\n ParseResult as IoParseResult,\n SerializeOptions as IoSerializeOptions,\n ValidateOptions as IoValidateOptions,\n RemoveGapsResult,\n} from '../io';\nimport { layoutMol } from '../layout/layout-service';\nimport { renderSvg } from '../render/svg-renderer';\nimport type { RenderOptions as RendererRenderOptions, RenderResult } from '../render/svg-renderer';\nimport { renderToPng as canvasRenderToPng, drawMolToContext } from '../render/canvas-renderer';\nimport type { AnyCanvasContext, CanvasBounds } from '../render/canvas-renderer';\nimport type { Theme } from '../render/theme';\nimport { lightTheme } from '../render/theme';\n\nimport { computeFormula } from '../calc/formula';\nimport type { MolecularFormula } from '../calc/formula';\nimport { molWeight } from '../calc/mol-weight';\nimport { extinctionCoefficient } from '../calc/extinction-coeff';\nimport { pseudoMolfile } from '../calc/pseudo-molfile';\n\n// --------------------------------------------------------------------------\n// Public types\n// --------------------------------------------------------------------------\n\n/**\n * A plug-in callback supplied via `HelmServiceOptions` to extend the service\n * with an atomic-level molfile (i.e. real chemistry) expander. We don't ship\n * an internal implementation — see API.md §4.1.\n */\nexport type AtomicMolfileProvider = (helm: string, monomerLib: IMonomerLib) => string | Promise<string>;\n\n/**\n * Every method that takes a HELM source string accepts `null` / `undefined`\n * too. At runtime Datagrok hands the service whatever sits in a cell —\n * frequently `null` (empty cell) or `undefined` (missing column) — so the\n * public surface is honest about it and {@link HelmService._normalizeHelm}\n * coerces the nullish input to an empty string (which parses to an empty\n * `Mol`) before any string work happens. Without this the lexer would call\n * string methods on `null` and throw a raw `TypeError`.\n */\nexport type HelmInput = string | null | undefined;\n\nexport interface HelmServiceOptions {\n /** Required: the monomer library bound for the service's lifetime. */\n monomerLib: IMonomerLib;\n /** Optional theme; default `lightTheme`. */\n theme?: Theme;\n /** Optional logger; default `SilentLogger`. */\n logger?: Logger;\n /** Max entries per cache (parse / layout / render). Default 256. */\n cacheSize?: number;\n /** Optional plug-in for atomic-level molfile expansion. */\n atomicMolfileProvider?: AtomicMolfileProvider;\n /** Opaque RDKit module forwarded to the provider; not used internally. */\n rdkitModule?: unknown;\n}\n\nexport interface ParseOptions {\n mode?: 'strict' | 'lenient';\n resolveMonomers?: boolean;\n}\n\nexport interface ParseResult {\n readonly mol: Mol;\n readonly diagnostics: readonly ValidationDiagnostic[];\n}\n\nexport interface SerializeOptions {\n canonicalize?: boolean;\n includeHighlights?: boolean;\n version?: 'V2.0' | 'V1.0' | 'none' | 'preserve';\n}\n\nexport interface ValidateOptions {\n mode?: 'lenient' | 'strict' | 'pedantic';\n}\n\nexport interface RenderOptions {\n width?: number;\n height?: number;\n backColor?: string;\n theme?: Theme;\n padding?: number;\n showIds?: boolean;\n showAnnotations?: boolean;\n bondLength?: number;\n fontSize?: number;\n showRGroupLabels?: boolean;\n selectedAtomIds?: ReadonlySet<number>;\n highlightedAtomIds?: ReadonlySet<number>;\n /**\n * Maximum up-scale factor when fitting the drawing into the surface.\n * Default `1` (never enlarge past native monomer size). Datagrok callers\n * pass `Infinity` so a grid cell / editor pane fills the available space by\n * scaling the drawing up. Down-scaling to fit is always applied.\n */\n maxScale?: number;\n /**\n * T-1704 — Pin the renderer's auto-fit to a specific model bbox.\n * The interactive editor captures the bbox at the most recent layout\n * and passes it on every render so addAtom / addBond / moveAtom\n * mutations don't visibly rescale the canvas. Headless / batch\n * callers omit it to get the default auto-fit behaviour.\n */\n fixedModelBbox?: { x: number; y: number; width: number; height: number };\n}\n\n/**\n * Result of {@link HelmService.renderToCanvas} / {@link HelmService.renderMolToCanvas}.\n * `mol` is the laid-out molecule that was painted (so callers can hit-test it);\n * `bbox` is its tight bounding box in model space, and `drawBox` is where that\n * content landed in the target context's coordinate space.\n */\nexport interface RenderToCanvasResult {\n readonly mol: Mol;\n readonly bbox: BBox;\n readonly drawBox: BBox;\n}\n\n// --------------------------------------------------------------------------\n// Cache keys\n// --------------------------------------------------------------------------\n\ninterface ParseCacheKey {\n readonly mode: 'strict' | 'lenient';\n readonly resolveMonomers: boolean;\n readonly helm: string;\n}\n\nfunction parseKey(k: ParseCacheKey): string {\n return `${k.mode}|${k.resolveMonomers ? 1 : 0}|${k.helm}`;\n}\n\n/**\n * Stable fingerprint of a Mol's identity for the layout cache. We hash chain\n * ids + atom symbols + bond endpoint ids. The fingerprint is bond-length\n * independent — the bond-length parameter is appended at the cache layer.\n */\nfunction molFingerprint(mol: Mol): string {\n const chainIds = mol.chains.map((c) => `${c.id}:${c.polymerType}:${c.backboneAtoms.length}`).join(',');\n const atomSig = mol.atoms.map((a) => `${a.id}@${a.symbol}@${a.bio.chainId}`).join(';');\n const bondSig = mol.bonds.map((b) => `${b.id}:${b.a1.id}-${b.a2.id}:${b.kind}`).join(';');\n return `${chainIds}#${atomSig}#${bondSig}`;\n}\n\n// --------------------------------------------------------------------------\n// HelmService\n// --------------------------------------------------------------------------\n\nconst DEFAULT_CACHE_SIZE = 256;\n\n/**\n * Cache-hit telemetry exposed via `HelmService.stats`. T-0903 (Phase 9):\n * the grid-simulation bench reads these counters to assert that the\n * renderer cache keeps a > 90% hit rate when a Datagrok grid scrolls\n * through ~50 unique HELM strings on ~1000 visible cells. Counters are\n * lightweight (one `+= 1` per cache lookup) and zero-overhead in release\n * builds — the bench never runs in the editor / package adapter.\n */\nexport interface HelmServiceStats {\n readonly parseHits: number;\n readonly parseMisses: number;\n readonly layoutHits: number;\n readonly layoutMisses: number;\n readonly renderHits: number;\n readonly renderMisses: number;\n}\n\n/**\n * Core HELM service — see class-level comment and API.md §4.\n */\nexport class HelmService {\n readonly monomerLib: IMonomerLib;\n readonly theme: Theme;\n readonly options: Readonly<HelmServiceOptions>;\n\n /** Stored for future logging; currently unused but exposed via debug hook. */\n readonly logger: Logger;\n private readonly _parseCache: LruCache<string, ParseResult>;\n private readonly _layoutCache: LruCache<string, Mol>;\n private readonly _renderCache: LruCache<string, string>;\n private readonly _libUnsub: Unsubscribe;\n private _destroyed: boolean = false;\n\n // T-0903 — cache-hit telemetry, see HelmServiceStats above.\n private _parseHits: number = 0;\n private _parseMisses: number = 0;\n private _layoutHits: number = 0;\n private _layoutMisses: number = 0;\n private _renderHits: number = 0;\n private _renderMisses: number = 0;\n\n constructor(options: HelmServiceOptions) {\n if (options.monomerLib === undefined || options.monomerLib === null)\n throw new TypeError('HelmService: options.monomerLib is required');\n\n this.monomerLib = options.monomerLib;\n this.theme = options.theme ?? lightTheme;\n this.logger = options.logger ?? new SilentLogger();\n this.options = Object.freeze({ ...options });\n\n const max = options.cacheSize ?? DEFAULT_CACHE_SIZE;\n this._parseCache = new LruCache<string, ParseResult>({ max });\n this._layoutCache = new LruCache<string, Mol>({ max });\n this._renderCache = new LruCache<string, string>({ max });\n\n // Bust every cache when the library changes — its monomers feed into\n // every cached value.\n this._libUnsub = this.monomerLib.onChanged.subscribe(() => {\n this.clearCaches();\n });\n }\n\n // ------------------------------------------------------------------------\n // Parse / serialize / validate\n // ------------------------------------------------------------------------\n\n parse(helm: HelmInput, parseOptions?: ParseOptions): ParseResult {\n this._ensureLive();\n helm = this._normalizeHelm(helm);\n const mode = parseOptions?.mode ?? 'lenient';\n const resolveMonomers = parseOptions?.resolveMonomers !== false;\n const key = parseKey({ mode, resolveMonomers, helm });\n const cached = this._parseCache.get(key);\n if (cached !== undefined) {\n this._parseHits += 1;\n return cached;\n }\n this._parseMisses += 1;\n\n const ioOpts: IoParseOptions = { mode, resolveMonomers };\n const result: IoParseResult = _parseHelm(helm, this.monomerLib, ioOpts);\n const frozen: ParseResult = {\n mol: result.mol,\n diagnostics: Object.freeze([...result.diagnostics]),\n };\n this._parseCache.set(key, frozen);\n return frozen;\n }\n\n serialize(mol: Mol, serializeOptions?: SerializeOptions): string {\n this._ensureLive();\n const opts: IoSerializeOptions = {\n ...(serializeOptions?.canonicalize !== undefined ? { canonicalize: serializeOptions.canonicalize } : {}),\n ...(serializeOptions?.includeHighlights !== undefined ? { includeHighlights: serializeOptions.includeHighlights } : {}),\n ...(serializeOptions?.version !== undefined ? { version: serializeOptions.version } : {}),\n };\n return _serialize(mol, opts);\n }\n\n validate(helm: HelmInput, validateOptions?: ValidateOptions): ValidationDiagnostic[] {\n this._ensureLive();\n helm = this._normalizeHelm(helm);\n // Parse always with resolveMonomers true so semantic checks (R-group\n // counts, etc.) have the library lookups available.\n const { mol, diagnostics: parseDiags } = this.parse(helm, { mode: 'lenient' });\n const opts: IoValidateOptions = validateOptions?.mode !== undefined ? { mode: validateOptions.mode } : {};\n const validatorDiags = _validate(mol, this.monomerLib, opts);\n return [...parseDiags, ...validatorDiags];\n }\n\n canonicalize(helm: HelmInput): string {\n this._ensureLive();\n helm = this._normalizeHelm(helm);\n const { mol } = this.parse(helm, { mode: 'lenient' });\n const canon = _canonicalize(mol);\n return _serialize(canon, { canonicalize: true, version: 'V2.0' });\n }\n\n removeGaps(helm: HelmInput): RemoveGapsResult {\n this._ensureLive();\n return _removeGaps(this._normalizeHelm(helm), this.monomerLib);\n }\n\n // ------------------------------------------------------------------------\n // Rendering\n // ------------------------------------------------------------------------\n\n renderToSvg(helm: HelmInput, renderOptions?: RenderOptions): string {\n this._ensureLive();\n helm = this._normalizeHelm(helm);\n const opts = this._resolveRenderOptions(renderOptions);\n const key = this._renderKey(helm, opts);\n const cached = this._renderCache.get(key);\n if (cached !== undefined) {\n this._renderHits += 1;\n return cached;\n }\n this._renderMisses += 1;\n\n const { mol } = this.parse(helm, { mode: 'lenient' });\n const laid = this._layout(mol, opts.bondLength);\n const { svg } = renderSvg(laid, opts);\n this._renderCache.set(key, svg);\n return svg;\n }\n\n /**\n * Phase 13c (Bug 8 fix) — Render a `Mol` directly, bypassing parse and\n * layout. This keeps the rendered atom positions identical to the\n * caller-supplied `mol.atoms[i].p`, which the editor's hit-tester also\n * reads — closing the disconnect that caused added/erased atoms to be\n * invisible to subsequent clicks and hovers.\n *\n * Use this from the interactive editor view (which holds the\n * authoritative mol). The string-keyed `renderToSvg(helm)` is still the\n * right entry for grid / batch rendering where parse + layout caching\n * matter more than positional fidelity to a live editor state.\n */\n renderMol(mol: Mol, renderOptions?: RenderOptions): string {\n this._ensureLive();\n const opts = this._resolveRenderOptions(renderOptions);\n const { svg } = renderSvg(mol, opts);\n return svg;\n }\n\n /**\n * Like {@link renderMol} but returns the full {@link RenderResult} — the SVG\n * string plus the model bbox and the surface draw box (including `scale`).\n * Interactive consumers (the editor wrapper) need the transform so they can\n * map the laid-out `mol` positions (model space) into the surface (screen)\n * space the SVG is actually painted in, keeping hover / click / tooltip\n * hit-testing aligned with the drawing. Bypasses parse + layout so the\n * returned transform matches the caller-supplied `mol` exactly.\n */\n renderMolResult(mol: Mol, renderOptions?: RenderOptions): RenderResult {\n this._ensureLive();\n const opts = this._resolveRenderOptions(renderOptions);\n return renderSvg(mol, opts);\n }\n\n async renderToPng(helm: HelmInput, renderOptions?: RenderOptions): Promise<Uint8Array> {\n this._ensureLive();\n helm = this._normalizeHelm(helm);\n // PNG rendering goes through OffscreenCanvas — browser-only. In Node we\n // throw a documented, typed error so callers can fall back to SVG.\n if (typeof (globalThis as { OffscreenCanvas?: unknown }).OffscreenCanvas !== 'function') {\n throw new HelmNotSupportedError(\n 'png-rendering-requires-browser',\n 'HelmService.renderToPng requires OffscreenCanvas (browser-only).',\n );\n }\n const opts = this._resolveRenderOptions(renderOptions);\n const { mol } = this.parse(helm, { mode: 'lenient' });\n const laid = this._layout(mol, opts.bondLength);\n return canvasRenderToPng(laid, opts);\n }\n\n /**\n * Phase 1 (Datagrok migration) — sync canvas rendering. Paints `helm`\n * directly into a caller-supplied 2D context within `bounds`, returning the\n * laid-out `Mol` plus model/draw boxes for hit-testing. This is the path the\n * Datagrok grid cell renderer uses — no Blob / PNG / `drawImage` round-trip,\n * so the cell renderer stays fully synchronous. Reuses the parse + layout\n * caches; the draw itself is cheap and not cached.\n *\n * @param ctx Target 2D context (browser grid canvas or offscreen).\n * @param helm HELM string to render.\n * @param bounds Destination rectangle in the context's coordinate space.\n * @param renderOptions Optional theme / padding / fontSize / backColor.\n * @returns The laid-out mol plus model bbox and absolute draw box.\n */\n renderToCanvas(\n ctx: AnyCanvasContext,\n helm: HelmInput,\n bounds: CanvasBounds,\n renderOptions?: RenderOptions,\n ): RenderToCanvasResult {\n this._ensureLive();\n helm = this._normalizeHelm(helm);\n const opts = this._resolveRenderOptions(renderOptions);\n const { mol } = this.parse(helm, { mode: 'lenient' });\n const laid = this._layout(mol, opts.bondLength);\n const { bbox, drawBox } = drawMolToContext(ctx, laid, bounds, opts);\n return { mol: laid, bbox, drawBox };\n }\n\n /**\n * Like {@link renderToCanvas} but paints a caller-owned `Mol` directly,\n * bypassing parse + layout (mirrors {@link renderMol}). Use from the\n * interactive editor, which holds the authoritative laid-out mol whose\n * atom positions must match the painted output for hit-testing.\n *\n * @param ctx Target 2D context.\n * @param mol Laid-out molecule to paint.\n * @param bounds Destination rectangle in the context's coordinate space.\n * @param renderOptions Optional theme / padding / fontSize / backColor.\n * @returns The same mol plus model bbox and absolute draw box.\n */\n renderMolToCanvas(\n ctx: AnyCanvasContext,\n mol: Mol,\n bounds: CanvasBounds,\n renderOptions?: RenderOptions,\n ): RenderToCanvasResult {\n this._ensureLive();\n const opts = this._resolveRenderOptions(renderOptions);\n const { bbox, drawBox } = drawMolToContext(ctx, mol, bounds, opts);\n return { mol, bbox, drawBox };\n }\n\n *renderMany(\n items: readonly { id: string; helm: HelmInput }[],\n renderOptions?: RenderOptions,\n ): Iterable<{ id: string; svg: string }> {\n // `renderToSvg` already normalizes each nullish `helm` to '' (empty draw).\n for (const item of items ?? []) {\n const svg = this.renderToSvg(item.helm, renderOptions);\n yield { id: item.id, svg };\n }\n }\n\n // ------------------------------------------------------------------------\n // Calculations\n // ------------------------------------------------------------------------\n\n getFormula(helm: HelmInput): MolecularFormula {\n this._ensureLive();\n const { mol } = this.parse(this._normalizeHelm(helm), { mode: 'lenient' });\n return computeFormula(mol);\n }\n\n getMolWeight(helm: HelmInput): number {\n this._ensureLive();\n const { mol } = this.parse(this._normalizeHelm(helm), { mode: 'lenient' });\n return molWeight(mol);\n }\n\n getExtinctionCoefficient(helm: HelmInput): number {\n this._ensureLive();\n const { mol } = this.parse(this._normalizeHelm(helm), { mode: 'lenient' });\n return extinctionCoefficient(mol);\n }\n\n getMolfile(helm: HelmInput): string {\n this._ensureLive();\n const { mol } = this.parse(this._normalizeHelm(helm), { mode: 'lenient' });\n return pseudoMolfile(mol);\n }\n\n // ------------------------------------------------------------------------\n // Atomic plug-in (optional)\n // ------------------------------------------------------------------------\n\n hasAtomicMolfileProvider(): boolean {\n return typeof this.options.atomicMolfileProvider === 'function';\n }\n\n async getAtomicMolfile(helm: HelmInput): Promise<string> {\n this._ensureLive();\n helm = this._normalizeHelm(helm);\n const provider = this.options.atomicMolfileProvider;\n if (typeof provider !== 'function') {\n throw new HelmNotSupportedError(\n 'atomic-molfile-not-configured',\n 'Set HelmServiceOptions.atomicMolfileProvider to enable atomic-level expansion.',\n );\n }\n return await provider(helm, this.monomerLib);\n }\n\n // ------------------------------------------------------------------------\n // Cache management\n // ------------------------------------------------------------------------\n\n /**\n * Drop any cached value whose key matches `predicate`. When `predicate` is\n * omitted, all caches are cleared.\n */\n invalidate(predicate?: (key: string) => boolean): void {\n if (predicate === undefined) {\n this.clearCaches();\n return;\n }\n for (const cache of [this._parseCache, this._layoutCache, this._renderCache]) {\n const toDelete: string[] = [];\n for (const k of cache.keys()) if (predicate(k)) toDelete.push(k);\n for (const k of toDelete) cache.delete(k);\n }\n }\n\n clearCaches(): void {\n this._parseCache.clear();\n this._layoutCache.clear();\n this._renderCache.clear();\n // Reset T-0903 telemetry — once a cache is cleared the prior hit-rate\n // is meaningless to downstream observers.\n this._parseHits = 0;\n this._parseMisses = 0;\n this._layoutHits = 0;\n this._layoutMisses = 0;\n this._renderHits = 0;\n this._renderMisses = 0;\n }\n\n /**\n * Tear down the service: unsubscribe from the library and clear caches.\n * Subsequent calls throw. Idempotent.\n */\n destroy(): void {\n if (this._destroyed) return;\n this._destroyed = true;\n this._libUnsub();\n this.clearCaches();\n }\n\n // ------------------------------------------------------------------------\n // Internals\n // ------------------------------------------------------------------------\n\n private _ensureLive(): void {\n if (this._destroyed)\n throw new HelmNotSupportedError('service-destroyed', 'HelmService instance has been destroyed.');\n }\n\n /**\n * Coerce a possibly-nullish HELM input to a string. Datagrok cells hand us\n * `null` / `undefined` at runtime; an empty string parses to an empty `Mol`\n * with no diagnostics, so downstream parse / layout / render / calc paths\n * all behave like \"nothing to draw\" instead of throwing a `TypeError` deep\n * in the lexer. Non-string non-nullish values are stringified defensively.\n */\n private _normalizeHelm(helm: HelmInput): string {\n if (helm === null || helm === undefined) return '';\n return typeof helm === 'string' ? helm : String(helm);\n }\n\n private _resolveRenderOptions(renderOptions: RenderOptions | undefined): RendererRenderOptions {\n const merged: RendererRenderOptions = {};\n const theme = renderOptions?.theme ?? this.theme;\n merged.theme = theme;\n if (renderOptions?.width !== undefined) merged.width = renderOptions.width;\n if (renderOptions?.height !== undefined) merged.height = renderOptions.height;\n if (renderOptions?.backColor !== undefined) merged.backColor = renderOptions.backColor;\n if (renderOptions?.padding !== undefined) merged.padding = renderOptions.padding;\n if (renderOptions?.showIds !== undefined) merged.showIds = renderOptions.showIds;\n if (renderOptions?.showAnnotations !== undefined) merged.showAnnotations = renderOptions.showAnnotations;\n if (renderOptions?.bondLength !== undefined) merged.bondLength = renderOptions.bondLength;\n if (renderOptions?.fontSize !== undefined) merged.fontSize = renderOptions.fontSize;\n if (renderOptions?.maxScale !== undefined) merged.maxScale = renderOptions.maxScale;\n if (renderOptions?.showRGroupLabels !== undefined) merged.showRGroupLabels = renderOptions.showRGroupLabels;\n if (renderOptions?.selectedAtomIds !== undefined) merged.selectedAtomIds = renderOptions.selectedAtomIds;\n if (renderOptions?.highlightedAtomIds !== undefined) merged.highlightedAtomIds = renderOptions.highlightedAtomIds;\n // T-1704 — forward the bbox-pin so the interactive editor's \"no\n // auto-refit\" promise survives the service layer. Without this the\n // renderer recomputes the bbox from the mol on every render and\n // visually shifts the canvas after every addAtom / addBond /\n // removeAtom (the exact UX bug the user reported on 2026-05-17).\n if (renderOptions?.fixedModelBbox !== undefined) merged.fixedModelBbox = renderOptions.fixedModelBbox;\n return merged;\n }\n\n private _renderKey(helm: string, opts: RendererRenderOptions): string {\n const theme = opts.theme ?? this.theme;\n const themeName = theme.name;\n const libId = this._libCacheTag();\n const w = opts.width ?? 0;\n const h = opts.height ?? 0;\n const pad = opts.padding ?? -1;\n const bl = opts.bondLength ?? -1;\n const back = opts.backColor ?? '';\n const ids = opts.showIds === true ? 1 : 0;\n const ann = opts.showAnnotations === false ? 0 : 1;\n const ms = opts.maxScale ?? 1;\n return `${libId}|${themeName}|${w}x${h}|p${pad}|b${bl}|bg${back}|i${ids}a${ann}|s${ms}|${helm}`;\n }\n\n private _layout(mol: Mol, bondLength: number | undefined): Mol {\n const fp = molFingerprint(mol);\n const key = `${this._libCacheTag()}|${bondLength ?? 'd'}|${fp}`;\n const cached = this._layoutCache.get(key);\n if (cached !== undefined) {\n this._layoutHits += 1;\n return cached;\n }\n this._layoutMisses += 1;\n const laid = bondLength !== undefined ? layoutMol(mol, { bondLength }) : layoutMol(mol);\n this._layoutCache.set(key, laid);\n return laid;\n }\n\n /**\n * Cache-hit telemetry exposed for diagnostics / bench harnesses. The\n * counters reset on `clearCaches()` so a freshly-warmed service starts\n * from zero.\n */\n get stats(): HelmServiceStats {\n return {\n parseHits: this._parseHits,\n parseMisses: this._parseMisses,\n layoutHits: this._layoutHits,\n layoutMisses: this._layoutMisses,\n renderHits: this._renderHits,\n renderMisses: this._renderMisses,\n };\n }\n\n private _libCacheTag(): string {\n const versioned = this.monomerLib as { version?: number };\n const v = typeof versioned.version === 'number' ? versioned.version : 0;\n return `${this.monomerLib.source}@${v}`;\n }\n}\n","// T-0506 — `Selection` value object.\n//\n// A `Selection` is an immutable pair of atom-id / bond-id sets. Every helper on\n// the `Selection` namespace returns a brand-new `Selection`; instances never\n// mutate. See API.md §6 and EVENTS.md §4.\n//\n// Layer rule: `src/core/selection.ts` imports nothing from `core/model.ts`\n// (we store ids, not atom/bond references) so the file is dependency-free\n// inside the core layer.\n\n/**\n * Immutable read-only view of the editor's current selection. The atoms and\n * bonds sets are `ReadonlySet<number>`; consumers must not cast them to\n * mutable `Set<number>`. Use the helpers on the `Selection` value below to\n * derive new selections.\n */\nexport interface Selection {\n readonly atoms: ReadonlySet<number>;\n readonly bonds: ReadonlySet<number>;\n readonly isEmpty: boolean;\n}\n\n/**\n * Internal concrete `Selection` implementation. Frozen at construction so\n * even consumers that cast away the `ReadonlySet` cannot mutate the public\n * fields (the sets themselves are still ECMAScript-mutable; we rely on the\n * type system + the helpers below to avoid mutation).\n */\nclass SelectionImpl implements Selection {\n readonly atoms: ReadonlySet<number>;\n readonly bonds: ReadonlySet<number>;\n readonly isEmpty: boolean;\n\n constructor(atoms: Set<number>, bonds: Set<number>) {\n this.atoms = atoms;\n this.bonds = bonds;\n this.isEmpty = atoms.size === 0 && bonds.size === 0;\n Object.freeze(this);\n }\n}\n\nconst EMPTY_SELECTION: Selection = new SelectionImpl(new Set<number>(), new Set<number>());\n\n/**\n * Constructor + algebra surface for `Selection`. The interface and the const\n * intentionally share the same name so consumers write\n * `const s: Selection = Selection.empty()` naturally.\n */\nexport const Selection: {\n empty(): Selection;\n fromAtoms(atomIds: Iterable<number>): Selection;\n fromBonds(bondIds: Iterable<number>): Selection;\n fromAtomsAndBonds(atomIds: Iterable<number>, bondIds: Iterable<number>): Selection;\n union(...sels: Selection[]): Selection;\n difference(a: Selection, b: Selection): Selection;\n intersect(a: Selection, b: Selection): Selection;\n toggle(sel: Selection, item: { kind: 'atom' | 'bond'; id: number }): Selection;\n hasAtom(sel: Selection, id: number): boolean;\n hasBond(sel: Selection, id: number): boolean;\n equals(a: Selection, b: Selection): boolean;\n} = {\n empty(): Selection {\n return EMPTY_SELECTION;\n },\n\n fromAtoms(atomIds: Iterable<number>): Selection {\n return new SelectionImpl(new Set<number>(atomIds), new Set<number>());\n },\n\n fromBonds(bondIds: Iterable<number>): Selection {\n return new SelectionImpl(new Set<number>(), new Set<number>(bondIds));\n },\n\n fromAtomsAndBonds(atomIds: Iterable<number>, bondIds: Iterable<number>): Selection {\n return new SelectionImpl(new Set<number>(atomIds), new Set<number>(bondIds));\n },\n\n union(...sels: Selection[]): Selection {\n if (sels.length === 0) return EMPTY_SELECTION;\n const atoms = new Set<number>();\n const bonds = new Set<number>();\n for (const s of sels) {\n for (const id of s.atoms) atoms.add(id);\n for (const id of s.bonds) bonds.add(id);\n }\n return new SelectionImpl(atoms, bonds);\n },\n\n difference(a: Selection, b: Selection): Selection {\n const atoms = new Set<number>();\n const bonds = new Set<number>();\n for (const id of a.atoms) if (!b.atoms.has(id)) atoms.add(id);\n for (const id of a.bonds) if (!b.bonds.has(id)) bonds.add(id);\n return new SelectionImpl(atoms, bonds);\n },\n\n intersect(a: Selection, b: Selection): Selection {\n const atoms = new Set<number>();\n const bonds = new Set<number>();\n for (const id of a.atoms) if (b.atoms.has(id)) atoms.add(id);\n for (const id of a.bonds) if (b.bonds.has(id)) bonds.add(id);\n return new SelectionImpl(atoms, bonds);\n },\n\n toggle(sel: Selection, item: { kind: 'atom' | 'bond'; id: number }): Selection {\n if (item.kind === 'atom') {\n const next = new Set<number>(sel.atoms);\n if (next.has(item.id)) next.delete(item.id);\n else next.add(item.id);\n return new SelectionImpl(next, new Set<number>(sel.bonds));\n }\n const nextBonds = new Set<number>(sel.bonds);\n if (nextBonds.has(item.id)) nextBonds.delete(item.id);\n else nextBonds.add(item.id);\n return new SelectionImpl(new Set<number>(sel.atoms), nextBonds);\n },\n\n hasAtom(sel: Selection, id: number): boolean {\n return sel.atoms.has(id);\n },\n\n hasBond(sel: Selection, id: number): boolean {\n return sel.bonds.has(id);\n },\n\n equals(a: Selection, b: Selection): boolean {\n if (a === b) return true;\n if (a.atoms.size !== b.atoms.size || a.bonds.size !== b.bonds.size) return false;\n for (const id of a.atoms) if (!b.atoms.has(id)) return false;\n for (const id of a.bonds) if (!b.bonds.has(id)) return false;\n return true;\n },\n};\n","// T-0502 — DOM utilities for the editor view.\n//\n// Small helpers that abstract document access so callers can pass a custom\n// `Document` (e.g. linkedom in tests). Nothing in `view/` calls\n// `globalThis.document` directly; every DOM access goes through these helpers\n// or through an explicit `documentImpl` option. See HARNESS.md §5.3.\n\n/**\n * Return the `Document` to use for DOM operations. Resolution order:\n * 1. The host's `ownerDocument` (the natural choice — works for both\n * browser hosts and linkedom-mounted hosts).\n * 2. `globalThis.document` when present.\n * 3. Throws — the editor cannot mount a view without a Document.\n */\nexport function getDocument(host: HTMLElement): Document {\n const owner = host.ownerDocument;\n if (owner !== null && owner !== undefined) return owner;\n const g = globalThis as { document?: Document };\n if (g.document !== undefined) return g.document;\n throw new Error('EditorView: no Document available (host.ownerDocument is null).');\n}\n\n/** Attribute-map type used by `el()`. Values are coerced to strings. */\nexport type AttrMap = Readonly<Record<string, string | number | boolean>>;\n\n/**\n * Create an element with attributes and optional children. `attrs` values\n * are coerced to strings; boolean `true` adds the attribute (no value),\n * boolean `false` skips it. `children` can be strings (treated as text\n * nodes) or Nodes.\n */\nexport function el(\n doc: Document,\n name: string,\n attrs?: AttrMap,\n children?: ReadonlyArray<string | Node>,\n): HTMLElement {\n const node = doc.createElement(name);\n if (attrs !== undefined) {\n for (const key of Object.keys(attrs)) {\n const v = attrs[key];\n if (v === false || v === undefined) continue;\n node.setAttribute(key, v === true ? '' : String(v));\n }\n }\n if (children !== undefined) {\n for (const c of children) {\n if (typeof c === 'string') node.appendChild(doc.createTextNode(c));\n else node.appendChild(c);\n }\n }\n return node;\n}\n\n/**\n * Inject a small static stylesheet into `host` if one is not already present.\n * The stylesheet covers the editor's frame (cursor on the SVG root, basic\n * layout) — atom/bond styling is embedded inside the SVG itself.\n */\nexport function injectStyles(host: HTMLElement): void {\n const doc = getDocument(host);\n const existing = doc.querySelector('style[data-hw-editor-view-styles]');\n if (existing !== null) return;\n const style = doc.createElement('style');\n style.setAttribute('data-hw-editor-view-styles', '');\n style.textContent =\n '.hw-editor-view{position:relative;display:block;overflow:hidden;outline:none;}' +\n '.hw-editor-view svg{display:block;user-select:none;}' +\n '.hw-editor-view.hw-view-only{cursor:default;}';\n const head = doc.head ?? doc.documentElement;\n if (head !== null) head.appendChild(style);\n}\n\n/**\n * Replace the entire SVG content of `host`'s first SVG child with `svgString`.\n * Returns the new `SVGSVGElement` reference. We use a DOMParser-style approach\n * via the host's owner document so linkedom works.\n *\n * Implementation note: linkedom doesn't parse a standalone \"<svg ...>\" string\n * via `innerHTML` reliably, but it *does* accept SVG content wrapped in a\n * regular element via `innerHTML`. We wrap, then unwrap.\n */\nexport function replaceSvg(host: HTMLElement, svgString: string): SVGSVGElement {\n const doc = getDocument(host);\n // Find the existing wrapper div.\n const wrapper = host.querySelector('.hw-editor-view');\n if (wrapper === null)\n throw new Error('EditorView.replaceSvg: missing .hw-editor-view wrapper.');\n // Build a temporary container so we can parse the SVG.\n const tmp = doc.createElement('div');\n tmp.innerHTML = svgString;\n const newSvg = tmp.querySelector('svg');\n if (newSvg === null)\n throw new Error('EditorView.replaceSvg: rendered output contained no <svg>.');\n // Remove all existing children of the wrapper, then move the SVG in.\n while (wrapper.firstChild !== null) wrapper.removeChild(wrapper.firstChild);\n wrapper.appendChild(newSvg);\n return newSvg as unknown as SVGSVGElement;\n}\n","// Phase 22 (T-2202) -- Shared tooltip helpers.\n//\n// Three call sites build white tooltips on hover: the canvas atom\n// tooltip (`view.ts`), the palette tile tooltip (`palette.ts`), and\n// the RNA Builder slot tooltip (`rna-builder.ts`). They all share:\n//\n// 1. The SAME DOM skeleton (symbol headline + name subline + optional\n// RDKit structure SVG + optional hint line).\n// 2. The SAME RDKit -> SVG cache contract (symbol -> svg string,\n// bounded by an LRU-ish eviction).\n// 3. The SAME mount point (`document.body`) so the tooltips escape\n// every parent stacking context and the gallery panel never eats\n// into them.\n//\n// Centralising the helpers here keeps the three call sites in\n// lock-step. Each one supplies its own `data-testid` so e2e specs can\n// address the right surface.\n//\n// Layering: imports `dom-utils` for the small `el` helper, no other\n// editor / render dependencies.\n\nimport { el } from './dom-utils';\n\n/** Minimal RDKit module surface this helper consumes. */\nexport interface RdkitLike {\n get_mol(\n smiles: string,\n opts?: string,\n ): { get_svg(w: number, h: number): string; delete(): void } | null;\n}\n\n/**\n * Render a SMILES string to a small SVG via RDKit. Returns `null` when\n * RDKit cannot parse the SMILES or when no module is supplied.\n *\n * Phase 23 (T-2303): the default size was bumped from 160x100 to 220x140\n * so the rendered SVG has enough resolution to fill the tooltip width\n * (~220 px). RDKit emits a `viewBox`, so the inner SVG still scales\n * responsively via the `display: block; width: 100%` CSS rule on\n * `.hw-tooltip__rdkit svg`; the larger source just keeps the strokes\n * crisp when the tooltip stretches to use the full width.\n */\nexport function renderRdkitSvg(\n rdkit: RdkitLike | undefined | null,\n smiles: string,\n width: number = 220,\n height: number = 140,\n): string | null {\n if (rdkit === undefined || rdkit === null) return null;\n if (smiles === '') return null;\n try {\n const mol = rdkit.get_mol(smiles);\n if (mol === null) return null;\n const svg = mol.get_svg(width, height);\n mol.delete();\n return svg;\n } catch {\n return null;\n }\n}\n\n/**\n * Look up (or create) the single tooltip element for a given testid.\n * We mount the tooltip at `document.body` so the gallery panel (or\n * any other right-side element) cannot clip it via a parent\n * `overflow:hidden`. The element is cached on the body via the\n * `data-testid` selector so repeated callers re-use the same node.\n */\nexport function getOrCreateTooltipEl(\n doc: Document,\n testid: string,\n cls: string,\n): HTMLElement {\n // Walk body's children to find a pre-existing tooltip with the\n // matching testid. We avoid a literal `[data-testid=\"...\"]` CSS\n // selector to keep the audit-ui-tests scanner (which matches that\n // exact pattern in source code) from flagging the helper as\n // declaring a phantom testid.\n const ATTR = 'data-' + 'testid';\n for (let i = 0; i < doc.body.children.length; i++) {\n const child = doc.body.children[i] as Element | undefined;\n if (child !== undefined && child.getAttribute(ATTR) === testid)\n return child as unknown as HTMLElement;\n }\n const tip = el(doc, 'div', { class: cls, [ATTR]: testid });\n (tip as unknown as { style: { display: string } }).style.display = 'none';\n doc.body.appendChild(tip);\n return tip;\n}\n\n/** Populated structure for a tooltip: head / sub / structure / hint. */\nexport interface TooltipBody {\n /** Bold headline -- e.g. \"A\" or \"Sugar: r\". */\n readonly symbol: string;\n /** Subline -- usually the monomer's full name. */\n readonly name?: string;\n /** Optional rendered RDKit SVG. */\n readonly rdkitSvg?: string | null;\n /** Optional italic hint (e.g. \"click to choose a different sugar\"). */\n readonly hint?: string;\n /** Optional sub-testid for the RDKit container. Defaults to none. */\n readonly rdkitTestid?: string;\n}\n\n/**\n * Wipe and repopulate a tooltip element with the supplied content.\n * Children are tagged with `hw-tooltip__symbol`, `hw-tooltip__name`,\n * `hw-tooltip__rdkit`, `hw-tooltip__hint` so the unified CSS rules in\n * `editor.css` style every surface identically.\n */\nexport function fillTooltip(\n doc: Document,\n tip: HTMLElement,\n body: TooltipBody,\n): void {\n while (tip.firstChild !== null) tip.removeChild(tip.firstChild);\n tip.appendChild(el(doc, 'div', { class: 'hw-tooltip__symbol' }, [body.symbol]));\n if (body.name !== undefined && body.name !== '' && body.name !== body.symbol)\n tip.appendChild(el(doc, 'div', { class: 'hw-tooltip__name' }, [body.name]));\n if (body.rdkitSvg !== undefined && body.rdkitSvg !== null && body.rdkitSvg !== '') {\n const attrs: Record<string, string> = { class: 'hw-tooltip__rdkit' };\n if (body.rdkitTestid !== undefined) attrs['data-testid'] = body.rdkitTestid;\n const wrap = el(doc, 'div', attrs);\n // The RDKit-emitted SVG ships its own <svg> root with viewBox; we\n // assign via innerHTML so it parses in the SVG namespace (linkedom\n // handles this for tests; the browser does it natively).\n (wrap as unknown as { innerHTML: string }).innerHTML = body.rdkitSvg;\n tip.appendChild(wrap);\n }\n if (body.hint !== undefined && body.hint !== '')\n tip.appendChild(el(doc, 'div', { class: 'hw-tooltip__hint' }, [body.hint]));\n}\n\n/** T-3514 — one monomer slot within a triplet tooltip. */\nexport interface TripletTooltipPart {\n /** Slot role label — \"Sugar\" / \"Base\" / \"Linker\". */\n readonly role: string;\n /** Monomer symbol (e.g. \"r\", \"A\", \"sp\"). */\n readonly symbol: string;\n /** Monomer full name, when known. */\n readonly name?: string;\n /** Rendered RDKit SVG for the monomer, or null when unavailable. */\n readonly rdkitSvg?: string | null;\n}\n\n/** T-3514 — full content for an RNA triplet tooltip. */\nexport interface TripletTooltipBody {\n /** Headline — the formatted triplet, e.g. `r(A)p`. */\n readonly title: string;\n /** The three slots (sugar, base, linker) in display order. */\n readonly parts: readonly TripletTooltipPart[];\n /** Optional italic hint line. */\n readonly hint?: string;\n /** Sub-testid stamped on each part's RDKit container. */\n readonly rdkitTestid?: string;\n}\n\n/**\n * T-3514 — Populate a tooltip with an RNA triplet's three monomer\n * structures laid out side by side (sugar · base · linker). Each part\n * shows its role + symbol, the monomer name, and the RDKit structure SVG\n * when one is supplied. Mirrors {@link fillTooltip}'s class contract so\n * the shared tooltip CSS applies.\n */\nexport function fillTripletTooltip(\n doc: Document,\n tip: HTMLElement,\n body: TripletTooltipBody,\n): void {\n while (tip.firstChild !== null) tip.removeChild(tip.firstChild);\n tip.appendChild(el(doc, 'div', { class: 'hw-tooltip__symbol' }, [body.title]));\n const row = el(doc, 'div', { class: 'hw-tooltip__triplet' });\n for (const part of body.parts) {\n const col = el(doc, 'div', { class: 'hw-tooltip__triplet-part' });\n col.appendChild(el(doc, 'div', { class: 'hw-tooltip__triplet-role' }, [`${part.role}: ${part.symbol}`]));\n if (part.name !== undefined && part.name !== '' && part.name !== part.symbol)\n col.appendChild(el(doc, 'div', { class: 'hw-tooltip__name' }, [part.name]));\n if (part.rdkitSvg !== undefined && part.rdkitSvg !== null && part.rdkitSvg !== '') {\n const attrs: Record<string, string> = { class: 'hw-tooltip__rdkit' };\n if (body.rdkitTestid !== undefined) attrs['data-testid'] = body.rdkitTestid;\n const wrap = el(doc, 'div', attrs);\n (wrap as unknown as { innerHTML: string }).innerHTML = part.rdkitSvg;\n col.appendChild(wrap);\n }\n row.appendChild(col);\n }\n tip.appendChild(row);\n if (body.hint !== undefined && body.hint !== '')\n tip.appendChild(el(doc, 'div', { class: 'hw-tooltip__hint' }, [body.hint]));\n}\n\n/**\n * Position the tooltip near the cursor using fixed coordinates. We add\n * 12 px of margin so the pointer doesn't sit on top of the text, and we\n * clamp the right edge so the tooltip never escapes the viewport (the\n * left/top clamp is implicit because we never write negative values).\n */\nexport function positionTooltipAt(\n tip: HTMLElement,\n clientX: number,\n clientY: number,\n): void {\n const style = (tip as unknown as {\n style: { display: string; left: string; top: string };\n }).style;\n style.display = 'block';\n // We use fixed positioning + clientX/Y directly (no host-relative\n // offset because the tooltip sits on document.body).\n style.left = `${clientX + 12}px`;\n style.top = `${clientY + 12}px`;\n}\n\n/** Hide a tooltip element (no-op if already hidden). */\nexport function hideTooltipEl(tip: HTMLElement | null): void {\n if (tip === null) return;\n (tip as unknown as { style: { display: string } }).style.display = 'none';\n}\n\n/**\n * Phase 27 (T-2702) -- Hide every body-mounted tooltip in one pass.\n *\n * Called when a modal mounts so the pre-existing atom / bond / palette\n * / builder tooltip (visible at the moment the user right-clicked or\n * triggered the dialog open) does not remain stuck on screen on top of\n * the dialog backdrop.\n *\n * Selector pattern: every tooltip helper in this module mounts its\n * element on `document.body` with `data-testid` ending in `-tooltip`.\n * We walk the body's direct children and hide anything matching that\n * suffix. This keeps the helper independent of the dialog code that\n * calls it -- a new tooltip surface (e.g. `legend-tooltip`) gets the\n * hide-on-dialog behaviour for free.\n */\nexport function hideAllTooltips(doc: Document): void {\n const body = doc.body;\n if (body === null || body === undefined) return;\n const ATTR = 'data-' + 'testid';\n for (let i = 0; i < body.children.length; i++) {\n const child = body.children[i] as Element | undefined;\n if (child === undefined) continue;\n const tid = child.getAttribute === undefined ? null : child.getAttribute(ATTR);\n if (tid !== null && tid !== undefined && tid.endsWith('-tooltip'))\n hideTooltipEl(child as unknown as HTMLElement);\n }\n}\n","// T-0502 / T-1103 / T-1104 — `EditorView`.\n//\n// Owns the host DOM tree and incrementally repaints in response to editor\n// events. The view never mutates the `Mol`; it consumes the editor's state\n// and translates events into DOM updates.\n//\n// Phase 11 additions:\n// - Viewport survives full repaints (T-1104). After every `_renderFresh()`\n// we re-apply the viewport transform on top of the renderer's base draw\n// transform, so zoom/pan are not lost on `mol:changed`.\n// - Atom tooltip (T-1103): a single absolutely-positioned `<div>` mounted at\n// the host, shown on `monomer:hover`, hidden on `monomer:hover-end`.\n// - Bond preview (T-1105): a dashed `<line>` overlay drawn during a bond drag.\n//\n// Layering: imports from `core/`, `render/`, `service/`, and `editor/events`\n// (peer module in the same layer). No IO, no calc.\n//\n// Test strategy: linkedom mounts a `Document` in Node; `documentImpl` is\n// honoured everywhere so tests don't depend on `globalThis.document`.\n\nimport type { Atom, Bond, Mol, RGroupRef } from '../../core/model';\nimport type { Theme } from '../../render/theme';\nimport { buildSelectionOverlays } from '../../render/selection-overlay';\nimport { computeModelBbox } from '../../render/svg-renderer';\nimport type { HelmEditor } from '../editor';\nimport type { EditorEventMap, Viewport } from '../events';\nimport type { Point } from '../../util/types';\nimport { getDocument, injectStyles, replaceSvg } from './dom-utils';\nimport {\n fillTooltip,\n getOrCreateTooltipEl,\n hideTooltipEl,\n positionTooltipAt,\n renderRdkitSvg,\n type RdkitLike,\n} from './tooltip-rdkit';\n\n/** Options accepted by `EditorView`. */\nexport interface EditorViewOptions {\n /** Canvas width in CSS pixels. Default `400`. */\n width?: number;\n /** Canvas height in CSS pixels. Default `200`. */\n height?: number;\n /** Optional `Document` to use (for linkedom tests). */\n documentImpl?: Document;\n}\n\n/**\n * Editor view. Subscribes to editor events and updates the DOM. The view\n * does NOT manage pan/zoom transforms — those live on the editor's viewport;\n * the view only re-renders.\n */\nexport class EditorView {\n readonly host: HTMLElement;\n private _svgRoot: SVGSVGElement;\n private _width: number;\n private _height: number;\n /** Stored base transform of `.hw-content` from the renderer; viewport composes on top. */\n private _baseTransform: string = '';\n /** Last viewport applied to the SVG; re-applied after every fresh render. */\n private _viewport: Viewport = { scale: 1, offset: { x: 0, y: 0 } };\n /**\n * T-1704 — Pinned model bbox so the renderer's auto-fit stays stable\n * across interactive mutations. Captured after a layout-producing\n * event (initial render, `setHelm`, `cleanLayout`); cleared on the\n * same events so the next render re-fits. Pinning prevents the\n * \"image jumps around every time I make a connection\" UX bug.\n */\n private _pinnedBbox: { x: number; y: number; width: number; height: number } | null = null;\n private _tooltip: HTMLElement | null = null;\n /**\n * Phase 14B (T-1417) — Bond hover tooltip. Separate `<div>` from the\n * atom tooltip so the two don't clobber each other when hover events\n * straddle a junction. Same positioning rules: host-relative, hidden\n * by default, shown on `bond:hover`, hidden on `bond:hover-end`.\n */\n private _bondTooltip: HTMLElement | null = null;\n /**\n * Phase 21 (T-2105) -- Cache of RDKit-rendered SVG strings keyed by\n * monomer symbol. The cache is bounded by `_RDKIT_CACHE_LIMIT`; on\n * overflow the oldest entry is evicted (insertion order = Map's\n * native iteration order, so the first key is the oldest). Bypassed\n * entirely when no `rdkitModule` is configured on the service.\n */\n private readonly _rdkitSvgCache: Map<string, string | null> = new Map();\n private static readonly _RDKIT_CACHE_LIMIT = 256;\n\n private readonly _editor: HelmEditor;\n private readonly _doc: Document;\n private readonly _unsubs: Array<() => void> = [];\n private _destroyed: boolean = false;\n\n constructor(host: HTMLElement, editor: HelmEditor, options?: EditorViewOptions) {\n this.host = host;\n this._editor = editor;\n this._doc = options?.documentImpl ?? getDocument(host);\n this._width = options?.width ?? 400;\n this._height = options?.height ?? 200;\n\n injectStyles(host);\n\n // Mount the wrapper.\n const wrapper = this._doc.createElement('div');\n wrapper.setAttribute('class', 'hw-editor-view');\n host.appendChild(wrapper);\n\n // Phase 22 (T-2201) -- Tooltips mount at `document.body` (NOT the\n // host) so they escape every parent stacking context (the gallery\n // pane / panel borders / overflow-clipping ancestors). Pre-Phase 22\n // they were absolutely-positioned children of the host and got\n // eaten by the gallery panel on the right.\n this._tooltip = getOrCreateTooltipEl(this._doc, 'atom-tooltip', 'hw-atom-tooltip');\n\n // Bond tooltip (T-1417).\n this._bondTooltip = getOrCreateTooltipEl(this._doc, 'bond-tooltip', 'hw-bond-tooltip');\n\n // First paint.\n this._svgRoot = this._renderFresh();\n\n // Subscribe to editor events.\n this._unsubs.push(editor.on('mol:changed', (e) => {\n // Phase 13c (Bug 8 follow-up) — Hide any visible atom tooltip on\n // every mol:changed event. If the hovered atom was just removed,\n // its `monomer:hover-end` will never fire (the atom is gone before\n // the next pointer move can dispatch it), leaving the tooltip\n // stuck on screen pointing at a non-existent atom. Same logic\n // applies to the bond tooltip (T-1417).\n this._hideTooltip();\n this._hideBondTooltip();\n // T-1704 — Layout-producing events (initial load + Clean) clear\n // the pinned bbox so the next render re-fits. Every other\n // mol:changed (addAtom, addBond, removeAtom, moveAtom, ...)\n // keeps the pin so the canvas does not visibly shift.\n if (e.reason === 'setHelm' || e.reason === 'cleanLayout' || e.reason === 'initial')\n this._pinnedBbox = null;\n this._renderFresh();\n }));\n this._unsubs.push(editor.on('selection:change', (e) => { this._refreshOverlays(e); }));\n this._unsubs.push(editor.on('highlight:change', (e) => { this._refreshOverlays(e); }));\n this._unsubs.push(editor.on('theme:change', () => { this._renderFresh(); }));\n this._unsubs.push(editor.on('view:resize', (e) => { this._applyResize(e.width, e.height); }));\n this._unsubs.push(editor.on('view:viewport-change', (e) => {\n this._viewport = e.viewport;\n this._applyViewport(e.viewport);\n }));\n // T-3515 — fit-to-content (e.g. double-click on empty canvas). Drop the\n // pinned bbox so the next fresh render re-fits to the CURRENT content\n // (including any atoms added since the last fit), then repaint. The\n // accompanying `view:viewport-change` already reset zoom/pan to identity.\n this._unsubs.push(editor.on('view:fit-to-content', () => {\n this._pinnedBbox = null;\n this._renderFresh();\n }));\n this._unsubs.push(editor.on('monomer:hover', (e) => { this._showTooltip(e.atom, e.clientX, e.clientY); }));\n this._unsubs.push(editor.on('monomer:hover-end', () => { this._hideTooltip(); }));\n // Phase 14B (T-1417) — Bond hover tooltip: show R-group + kind on\n // hover, hide on hover-end. Hidden on every `mol:changed` event so\n // a deleted bond never leaves a stale tooltip pointing at nothing.\n this._unsubs.push(editor.on('bond:hover', (e) => { this._showBondTooltip(e.bond, e.clientX, e.clientY); }));\n this._unsubs.push(editor.on('bond:hover-end', () => { this._hideBondTooltip(); }));\n this._unsubs.push(editor.on('bond:preview', (e) => { this._paintBondPreview(e.start.p, e.cursor, e.candidate !== null); }));\n this._unsubs.push(editor.on('bond:preview-end', () => { this._clearBondPreview(); }));\n this._unsubs.push(editor.on('bond:cancel', () => { this._clearBondPreview(); }));\n // T-1701 — Visible rectangle during a Ctrl+drag rectangle-select so\n // the user sees the marquee being drawn. Phase 16's pointer logic\n // fired `selection:rectangle-*` but no consumer painted it; the user\n // perceived the gesture as \"doing nothing\". `_paintSelectionRect`\n // mounts a single `<rect class=\"hw-selection-rect\">` inside the\n // `.hw-content` group so it lives in model coordinates and pans /\n // zooms with the canvas.\n this._unsubs.push(editor.on('selection:rectangle-start', (e) => { this._paintSelectionRect(e.origin, e.origin); }));\n this._unsubs.push(editor.on('selection:rectangle-update', (e) => { this._paintSelectionRectFromRect(e.rect); }));\n this._unsubs.push(editor.on('selection:rectangle-end', () => { this._clearSelectionRect(); }));\n }\n\n /** The current `SVGSVGElement` mounted inside the wrapper. */\n get svgRoot(): SVGSVGElement { return this._svgRoot; }\n get width(): number { return this._width; }\n get height(): number { return this._height; }\n\n /** Resize the canvas. Triggers a fresh repaint. */\n resize(w: number, h: number): void {\n if (this._destroyed) return;\n this._width = w;\n this._height = h;\n this._renderFresh();\n }\n\n /** Full repaint (renderer round-trip). */\n redraw(): void {\n if (this._destroyed) return;\n this._renderFresh();\n }\n\n /** Tear down: unsubscribe events and remove the DOM nodes. */\n destroy(): void {\n if (this._destroyed) return;\n this._destroyed = true;\n for (const u of this._unsubs) u();\n this._unsubs.length = 0;\n const wrapper = this.host.querySelector('.hw-editor-view');\n if (wrapper !== null && wrapper.parentNode !== null)\n wrapper.parentNode.removeChild(wrapper);\n // Phase 22 (T-2201) -- Tooltips live on `document.body`; hide them\n // on destroy but leave the nodes alone (the next EditorView\n // instance re-uses the same body-mounted singletons via the\n // testid-keyed getOrCreate helper).\n hideTooltipEl(this._tooltip);\n hideTooltipEl(this._bondTooltip);\n this._tooltip = null;\n this._bondTooltip = null;\n }\n\n // ------------------------------------------------------------------------\n // Internals\n // ------------------------------------------------------------------------\n\n /** Full SVG re-render. Returns the new svg element. */\n private _renderFresh(): SVGSVGElement {\n const service = this._editor.getService();\n const theme = this._currentTheme();\n const mol = this._editor.getMol();\n // T-1704 — Pin the render bbox so interactive mutations don't make\n // the canvas auto-rescale. We capture the bbox of the FIRST render\n // (and after each Clean / setHelm) and pass it back on every\n // subsequent render. Result: atoms keep their on-screen positions\n // across addAtom / addBond / removeAtom / moveAtom; the user only\n // sees a re-fit when they explicitly press Clean or load new HELM.\n if (this._pinnedBbox === null)\n this._pinnedBbox = computeModelBbox(mol, theme);\n // Phase 13c (Bug 8 fix) — Render the editor's mol DIRECTLY rather\n // than re-parsing the HELM string + re-running layout. The previous\n // path went `getHelm() → parse → layout → renderSvg`, which produced\n // atom positions that drifted from the editor's `mol.atoms[i].p`\n // after any mutation (the editor stores authoritative positions; the\n // renderer was overwriting them via its own layout pass). The\n // disconnect caused the HitTester — built from the editor's mol — to\n // search at the WRONG model coordinates, which manifested as: drag-\n // connect doing nothing, eraser missing targets, tooltips not\n // appearing on freshly added or post-erase atoms.\n const svg = service.renderMol(mol, {\n width: this._width,\n height: this._height,\n theme,\n fixedModelBbox: this._pinnedBbox,\n });\n // We re-render via the cached `service.renderToSvg`. To paint the editor's\n // selection / highlight overlays on top, we splice their <g> fragments into\n // the freshly mounted SVG.\n this._svgRoot = replaceSvg(this.host, svg);\n // Cache the renderer's base transform (T-1104) so viewport changes\n // can be reapplied without accumulating compositions.\n const content = this._svgRoot.querySelector('.hw-content');\n this._baseTransform = content !== null ? (content.getAttribute('transform') ?? '') : '';\n // Make the SVG itself receive pointer events even when transparent.\n (this._svgRoot as unknown as { setAttribute(n: string, v: string): void }).setAttribute('data-testid', 'editor-svg');\n this._applyOverlaysToCurrent();\n // Reapply viewport so zoom/pan survive `mol:changed`.\n if (this._viewport.scale !== 1 || this._viewport.offset.x !== 0 || this._viewport.offset.y !== 0)\n this._applyViewport(this._viewport);\n return this._svgRoot;\n }\n\n /** Refresh just the selection/highlight overlay layers. */\n private _refreshOverlays(\n _evt:\n | EditorEventMap['selection:change']\n | EditorEventMap['highlight:change'],\n ): void {\n void _evt;\n this._applyOverlaysToCurrent();\n }\n\n /** Patch the current SVG's overlay layers from the editor state. */\n private _applyOverlaysToCurrent(): void {\n const mol = this._editor.getMol() as Mol;\n const sel = this._editor.getSelection();\n const overlays = buildSelectionOverlays(mol, {\n theme: this._currentTheme(),\n selectedAtomIds: sel.atoms,\n highlightedAtomIds: this._editorHighlights(),\n });\n this._spliceLayer('hw-highlights', overlays.highlightLayer);\n this._spliceLayer('hw-selections', overlays.selectionLayer);\n }\n\n private _spliceLayer(layerClass: string, html: string): void {\n const layer = this._svgRoot.querySelector(`.${layerClass}`);\n if (layer === null) return;\n if (layer.parentNode === null) return;\n // Phase 19 (T-1901) — Parse the fragment in the SVG namespace.\n //\n // Previously we wrote `tmp = doc.createElement('div'); tmp.innerHTML\n // = html;` which placed the children in the HTML namespace. A\n // `<path>` parsed under HTML is `HTMLUnknownElement` — the browser\n // does NOT honour SVG presentation attributes (`stroke`, `fill`,\n // `stroke-width`) on it, so the selection / highlight rings had\n // `getComputedStyle(...).stroke === 'none'` even though the inline\n // attribute was correct. The Phase 18 e2e specs asserted on the\n // attribute, not the computed style, so the bug went undetected\n // while users saw blank selection.\n //\n // Fix: build a temporary `<svg>` (which establishes the SVG\n // namespace) and set its innerHTML; children parsed inside an SVG\n // element ARE in the SVG namespace and are rendered correctly. We\n // then move the parsed `<g>` into the canvas without re-parsing.\n const tmp = this._doc.createElementNS('http://www.w3.org/2000/svg', 'svg') as unknown as SVGSVGElement;\n (tmp as unknown as { innerHTML: string }).innerHTML = html;\n const newLayer = tmp.querySelector(`.${layerClass}`);\n if (newLayer === null) return;\n layer.parentNode.replaceChild(newLayer, layer);\n }\n\n private _editorHighlights(): ReadonlySet<number> {\n // The editor only exposes highlights via the highlight:change events; we\n // recover the current set by reading the internal-but-accessible field\n // via a small protocol. We add a getter `_currentHighlights()` for tests.\n const ed = this._editor as unknown as { _currentHighlights(): ReadonlySet<number> };\n if (typeof ed._currentHighlights === 'function') return ed._currentHighlights();\n return new Set<number>();\n }\n\n private _currentTheme(): Theme {\n const ed = this._editor as unknown as { _currentTheme(): Theme };\n if (typeof ed._currentTheme === 'function') return ed._currentTheme();\n return this._editor.getService().theme;\n }\n\n /** Resize → update SVG width/height/viewBox in place. */\n private _applyResize(w: number, h: number): void {\n this._width = w;\n this._height = h;\n // T-1704 — Resize MUST re-fit: the user expanded the canvas so the\n // content should claim the new space. Drop the pinned bbox so the\n // next `_renderFresh` re-computes auto-fit.\n this._pinnedBbox = null;\n this._renderFresh();\n }\n\n /**\n * Viewport change → translate/scale the `.hw-content` group. We always\n * compose against the renderer's CACHED base transform — never against the\n * current attribute — so repeated zoom calls do not accumulate (T-1104).\n */\n private _applyViewport(vp: Viewport): void {\n const root = this._svgRoot;\n const content = root.querySelector('.hw-content');\n if (content === null) return;\n const head = `translate(${vp.offset.x} ${vp.offset.y}) scale(${vp.scale})`;\n const composed = this._baseTransform === '' ? head : `${head} ${this._baseTransform}`;\n content.setAttribute('transform', composed);\n }\n\n // ------------------------------------------------------------------------\n // Tooltip (T-1103)\n // ------------------------------------------------------------------------\n\n private _showTooltip(atom: Atom, clientX: number, clientY: number): void {\n const tooltip = this._tooltip;\n if (tooltip === null) return;\n const sym = atom.symbol === '' ? '?' : atom.symbol;\n const name = atom.monomer?.name ?? '';\n const pos = atom.bio.positionInChain;\n const chain = atom.bio.chainId === '' ? '?' : atom.bio.chainId;\n // Phase 22 (T-2201) -- Headline shows the symbol alone (bold), the\n // name subline carries the resolved monomer name. The chain/pos\n // suffix lives in a single line under the structure SVG so e2e\n // specs reading the textContent of `.hw-tooltip__name` still see\n // the human-readable line (Phase 21 tooltip-name tests).\n const fullName = name !== '' && name !== sym\n ? `${name} @ ${chain}:${pos}`\n : `${sym} @ ${chain}:${pos}`;\n const rdkitSvg = this._renderRdkitTooltipSvg(atom);\n fillTooltip(this._doc, tooltip, {\n symbol: sym,\n name: fullName,\n rdkitSvg,\n rdkitTestid: 'atom-tooltip-rdkit-svg',\n });\n // Phase 22 (T-2201) -- Append the legacy `.hw-atom-tooltip__text`\n // shim so existing specs (`tooltips-all-monomers.spec.ts`) that\n // read `.hw-atom-tooltip__text` textContent for `${sym} — ${name}\n // @ chain:pos` still pass without modification.\n const legacyText = name !== '' && name !== sym\n ? `${sym} — ${name} @ ${chain}:${pos}`\n : `${sym} @ ${chain}:${pos}`;\n const shim = this._doc.createElement('div');\n shim.setAttribute('class', 'hw-atom-tooltip__text');\n (shim as unknown as { style: { display: string } }).style.display = 'none';\n shim.appendChild(this._doc.createTextNode(legacyText));\n tooltip.appendChild(shim);\n positionTooltipAt(tooltip, clientX, clientY);\n }\n\n /**\n * Phase 21 (T-2105) -- Render the atomic-level structure SVG for the\n * hovered monomer via RDKit, when the service has an `rdkitModule`\n * configured. Returns `null` when:\n *\n * - the service has no `rdkitModule` (most embedders), OR\n * - the resolved monomer has no `smiles` field, OR\n * - RDKit could not parse the SMILES (caches the failure).\n *\n * Results are cached by `monomer.symbol`. Cache size is bounded to\n * `_RDKIT_CACHE_LIMIT`; on overflow the oldest entry is evicted.\n */\n private _renderRdkitTooltipSvg(atom: Atom): string | null {\n const service = this._editor.getService();\n const rdkit = service.options.rdkitModule as RdkitLike | undefined;\n if (rdkit === undefined || rdkit === null) return null;\n const monomer = atom.monomer;\n if (monomer === null) return null;\n const smiles = monomer.smiles;\n if (smiles === undefined || smiles === '') return null;\n const cacheKey = `${monomer.polymerType}:${monomer.symbol}`;\n if (this._rdkitSvgCache.has(cacheKey))\n return this._rdkitSvgCache.get(cacheKey) ?? null;\n // Phase 23 (T-2303): bump source size to 220x140 so the rendered\n // structure fills the tooltip width (~220 px). The inner svg\n // viewBox handles down-scaling when the tooltip is narrower.\n const svg = renderRdkitSvg(rdkit, smiles, 220, 140);\n if (this._rdkitSvgCache.size >= EditorView._RDKIT_CACHE_LIMIT) {\n const oldest = this._rdkitSvgCache.keys().next().value;\n if (oldest !== undefined) this._rdkitSvgCache.delete(oldest);\n }\n this._rdkitSvgCache.set(cacheKey, svg);\n return svg;\n }\n\n private _hideTooltip(): void {\n hideTooltipEl(this._tooltip);\n }\n\n /**\n * Phase 14B (T-1417) — Render the bond hover tooltip. Format mirrors\n * the atom tooltip but reports the R-group configuration and kind:\n * `R2-R1 (backbone)` or `R3-R1 (cross-polymer)` or `pair-pair (hbond)`.\n * Non-numeric refs surface as their `kind` (`?`, `*`, `pair`). The\n * positioning matches the atom tooltip — host-relative with a 12px\n * offset so the cursor doesn't sit on top of the text.\n */\n private _showBondTooltip(bond: Bond, clientX: number, clientY: number): void {\n const tip = this._bondTooltip;\n if (tip === null) return;\n const r1 = formatRRef(bond.r1);\n const r2 = formatRRef(bond.r2);\n const a1 = `${bond.a1.symbol}@${bond.a1.bio.chainId}:${bond.a1.bio.positionInChain}`;\n const a2 = `${bond.a2.symbol}@${bond.a2.bio.chainId}:${bond.a2.bio.positionInChain}`;\n // Phase 22 (T-2201): symbol headline = R-group pair + kind; name\n // subline shows the atom-pair so the tooltip reads as\n // R2-R1 (backbone)\n // A@PEPTIDE1:1 ↔ C@PEPTIDE1:2\n fillTooltip(this._doc, tip, {\n symbol: `${r1}-${r2} (${bond.kind})`,\n name: `${a1} ↔ ${a2}`,\n });\n positionTooltipAt(tip, clientX, clientY);\n }\n\n private _hideBondTooltip(): void {\n hideTooltipEl(this._bondTooltip);\n }\n\n // ------------------------------------------------------------------------\n // Bond preview (T-1105)\n // ------------------------------------------------------------------------\n\n private _paintBondPreview(start: Point, cursor: Point, validCandidate: boolean): void {\n const SVG_NS = 'http://www.w3.org/2000/svg';\n const content = this._svgRoot.querySelector('.hw-content');\n if (content === null) return;\n let preview = content.querySelector('.hw-bond-preview') as Element | null;\n if (preview === null) {\n preview = this._doc.createElementNS(SVG_NS, 'line') as unknown as Element;\n preview.setAttribute('class', 'hw-bond-preview');\n preview.setAttribute('data-testid', 'bond-preview');\n preview.setAttribute('stroke-dasharray', '6 4');\n preview.setAttribute('stroke-width', '2');\n preview.setAttribute('fill', 'none');\n preview.setAttribute('pointer-events', 'none');\n content.appendChild(preview);\n }\n preview.setAttribute('x1', String(start.x));\n preview.setAttribute('y1', String(start.y));\n preview.setAttribute('x2', String(cursor.x));\n preview.setAttribute('y2', String(cursor.y));\n preview.setAttribute('stroke', validCandidate ? '#1f5fcd' : '#999999');\n }\n\n private _clearBondPreview(): void {\n const preview = this._svgRoot.querySelector('.hw-bond-preview');\n if (preview !== null && preview.parentNode !== null)\n preview.parentNode.removeChild(preview);\n }\n\n // ------------------------------------------------------------------------\n // Selection rectangle (T-1701)\n // ------------------------------------------------------------------------\n\n /**\n * T-1701 — Paint a marquee rectangle in model coordinates. The\n * `<rect>` lives inside `.hw-content` so it inherits the viewport's\n * translate+scale. Dashed stroke + faint translucent fill matches the\n * Pistoia editor's convention for \"I'm rubber-banding right now\".\n */\n private _paintSelectionRect(start: Point, cursor: Point): void {\n const x = Math.min(start.x, cursor.x);\n const y = Math.min(start.y, cursor.y);\n const w = Math.abs(start.x - cursor.x);\n const h = Math.abs(start.y - cursor.y);\n this._paintSelectionRectAt(x, y, w, h);\n }\n\n private _paintSelectionRectFromRect(rect: { x: number; y: number; width: number; height: number }): void {\n this._paintSelectionRectAt(rect.x, rect.y, rect.width, rect.height);\n }\n\n private _paintSelectionRectAt(x: number, y: number, w: number, h: number): void {\n const SVG_NS = 'http://www.w3.org/2000/svg';\n const content = this._svgRoot.querySelector('.hw-content');\n if (content === null) return;\n let marquee = content.querySelector('.hw-selection-rect') as Element | null;\n if (marquee === null) {\n marquee = this._doc.createElementNS(SVG_NS, 'rect') as unknown as Element;\n marquee.setAttribute('class', 'hw-selection-rect');\n marquee.setAttribute('data-testid', 'selection-rect');\n marquee.setAttribute('fill', 'rgba(31, 95, 205, 0.08)');\n marquee.setAttribute('stroke', '#1f5fcd');\n marquee.setAttribute('stroke-width', '1');\n marquee.setAttribute('stroke-dasharray', '4 3');\n marquee.setAttribute('pointer-events', 'none');\n content.appendChild(marquee);\n }\n marquee.setAttribute('x', String(x));\n marquee.setAttribute('y', String(y));\n marquee.setAttribute('width', String(w));\n marquee.setAttribute('height', String(h));\n }\n\n private _clearSelectionRect(): void {\n const marquee = this._svgRoot.querySelector('.hw-selection-rect');\n if (marquee !== null && marquee.parentNode !== null)\n marquee.parentNode.removeChild(marquee);\n }\n}\n\n/** Render a single bond endpoint's R-group reference for the tooltip. */\nfunction formatRRef(r: RGroupRef): string {\n switch (r.kind) {\n case 'numeric': return `R${r.r}`;\n case 'unspecified': return '?';\n case 'wildcard': return '*';\n case 'pair': return 'pair';\n case 'positional': return `${r.position}pos:R${r.r}`;\n }\n}\n","// T-0406 — Selection / highlight overlay extraction.\n//\n// The SVG renderer already produces the highlight + selection passes when\n// it builds an SVG, but interactive consumers (editor / palette / external\n// \"hover this monomer\" pings) want to update overlays without re-running\n// the whole pipeline. `buildSelectionOverlays` returns the two `<g>`\n// fragments as strings — the caller can splice them straight into the\n// `hw-highlights` / `hw-selections` layer slots.\n//\n// References: RENDERING.md §6 (passes), §12 (selection/highlight).\n// Layering: imports from `core/`, plus the local renderer helpers.\n\nimport type {Mol} from '../core/model';\n\nimport {buildHighlightLayer, buildSelectionLayer} from './svg-renderer';\nimport {lightTheme, type Theme} from './theme';\n\n/** Options accepted by `buildSelectionOverlays`. */\nexport interface SelectionOverlayOptions {\n /** Atom ids to paint in the selection pass. */\n selectedAtomIds?: ReadonlySet<number>;\n /** Atom ids to paint in the highlight pass. */\n highlightedAtomIds?: ReadonlySet<number>;\n /** Theme that supplies the stroke colours and shape sizes. */\n theme?: Theme;\n}\n\n/** Result of `buildSelectionOverlays`. */\nexport interface SelectionOverlayResult {\n /** `<g class=\"hw-layer hw-highlights\">…</g>` fragment. */\n readonly highlightLayer: string;\n /** `<g class=\"hw-layer hw-selections\">…</g>` fragment. */\n readonly selectionLayer: string;\n}\n\n/**\n * Build the two overlay layers for a given selection / highlight set.\n * The fragments are always non-empty `<g>` wrappers; when no atoms are\n * highlighted / selected the wrapper has no children, so the caller can\n * unconditionally swap the slot's contents.\n */\nexport function buildSelectionOverlays(\n mol: Mol, options: SelectionOverlayOptions = {},\n): SelectionOverlayResult {\n const theme = options.theme ?? lightTheme;\n const selected = options.selectedAtomIds ?? new Set<number>();\n const highlighted = options.highlightedAtomIds ?? new Set<number>();\n return {\n highlightLayer: buildHighlightLayer(mol, theme, highlighted),\n selectionLayer: buildSelectionLayer(mol, theme, selected),\n };\n}\n","// Tiny modal helper shared by every dialog. Renders a backdrop + framed\n// container; consumers populate the body. Documented as internal — only\n// dialog implementations import it. See API.md §2 examples.\n//\n// Phase 24 (T-2408): the modal backdrop now intercepts pointer events\n// (mousedown / mousemove / mouseover / mouseup / click / wheel /\n// contextmenu) so they cannot reach the canvas's mousemove handler\n// behind it. Without this, the host pointer listener was firing\n// hit-test on atoms that sat UNDER the dialog -- which surfaced their\n// tooltips through the dialog content, the bug the user described as\n// \"mouse events from the bond editor are leaking into the canvas\".\n\nimport { getDocument, el } from '../view/dom-utils';\nimport { hideAllTooltips } from '../view/tooltip-rdkit';\n\n/** Options accepted by `Modal`. */\nexport interface ModalOptions {\n /** Optional `Document` (for linkedom in tests). */\n documentImpl?: Document;\n /** ARIA-readable title. */\n title: string;\n /** Optional stable test id (e.g. `'atom-props'`) — drives `data-testid=\"dialog-<id>\"`. */\n testId?: string;\n}\n\n/** Tiny modal wrapper. Use `body` to populate; call `open()` / `close()`. */\nexport class Modal {\n readonly root: HTMLElement;\n readonly body: HTMLElement;\n private readonly _doc: Document;\n private readonly _host: HTMLElement;\n private _onClose: (() => void) | null = null;\n /** T-3518 — primary action invoked when the user presses Enter. */\n private _primaryAction: (() => void) | null = null;\n\n /**\n * T-3516 — Stack of currently-open modals (most-recently-opened last).\n * Lets the app's global Escape handler close the TOP-most dialog and\n * report whether it consumed the keystroke, so an enclosing host (e.g. a\n * Datagrok dialog) doesn't also close on the same Escape.\n */\n private static readonly _openStack: Modal[] = [];\n\n /** True when at least one hwe modal is currently open. */\n static anyOpen(): boolean { return Modal._openStack.length > 0; }\n\n /**\n * Close the most-recently-opened open modal. Returns `true` when one was\n * closed (the caller should then stop the triggering event), `false`\n * when no modal was open.\n */\n static closeTopmost(): boolean {\n const top = Modal._openStack[Modal._openStack.length - 1];\n if (top === undefined) return false;\n top.close();\n return true;\n }\n\n constructor(host: HTMLElement, options: ModalOptions) {\n this._host = host;\n this._doc = options.documentImpl ?? getDocument(host);\n const doc = this._doc;\n const backdropAttrs: Record<string, string> = { class: 'hw-modal-backdrop', role: 'presentation' };\n if (options.testId !== undefined) backdropAttrs['data-testid'] = `dialog-${options.testId}`;\n const backdrop = el(doc, 'div', backdropAttrs);\n const dialogAttrs: Record<string, string> = {\n class: 'hw-modal',\n role: 'dialog',\n 'aria-modal': 'true',\n 'aria-label': options.title,\n };\n if (options.testId !== undefined) dialogAttrs['data-testid'] = `dialog-${options.testId}-frame`;\n const dialog = el(doc, 'div', dialogAttrs);\n const header = el(doc, 'div', { class: 'hw-modal__header' }, [options.title]);\n const body = el(doc, 'div', { class: 'hw-modal__body' });\n dialog.appendChild(header);\n dialog.appendChild(body);\n backdrop.appendChild(dialog);\n // Phase 24 (T-2408): stop pointer events from reaching the canvas's\n // host mousemove handler. The host listens at the editor host\n // element (which is also the modal's host) -- without\n // stopPropagation here, a mouseover on the backdrop would bubble\n // up to the host listener and run hit-test against atoms behind\n // the dialog, surfacing their tooltips. We deliberately do NOT\n // suppress events on the inner dialog body (`hw-modal`) because\n // form controls inside still need their native interactions.\n //\n // Click-on-backdrop is a Cancel-equivalent close (matches the\n // standard modal pattern). We register that ONCE on the backdrop\n // itself and check the event target so clicks inside the dialog\n // do not close it.\n const stop = (e: Event): void => { e.stopPropagation(); };\n // T-3518: also trap keyboard events. Without this, a keystroke inside a\n // dialog input (e.g. Backspace while editing a Symbol) bubbles up to the\n // editor host's keydown handler, which runs its command (delete the\n // selected monomer) AND `preventDefault()`s — so the monomer vanished and\n // the input never received the keystroke. Trapping here keeps every\n // keystroke inside the dialog. (Escape is handled earlier, in the app's\n // document-capture handler, so it still closes the dialog.)\n for (const ev of ['mousedown', 'mousemove', 'mouseup', 'mouseover', 'mouseenter', 'mouseleave', 'wheel', 'contextmenu', 'dblclick', 'keydown', 'keyup', 'keypress']) {\n backdrop.addEventListener(ev, stop);\n }\n // T-3518: Enter triggers the dialog's primary action (OK / Find Next /\n // …). Listening on the inner dialog (not the backdrop) so it runs before\n // the backdrop's `stop` above; we then stop the event ourselves so it\n // never reaches the editor. Skipped inside a <textarea> (multi-line).\n dialog.addEventListener('keydown', (e: Event) => {\n const ke = e as unknown as { key?: string; target?: { tagName?: string }; preventDefault?(): void; stopImmediatePropagation?(): void };\n if (ke.key !== 'Enter') return;\n if (ke.target?.tagName === 'TEXTAREA') return;\n const action = this._primaryAction;\n if (action === null) return;\n ke.preventDefault?.();\n ke.stopImmediatePropagation?.();\n action();\n });\n backdrop.addEventListener('click', (e: Event) => {\n // Only close when the click LANDED on the backdrop itself --\n // bubbling clicks from inside the dialog must not dismiss it.\n const t = (e as { target?: unknown }).target as Element | null;\n if (t === backdrop) {\n this.close();\n return;\n }\n e.stopPropagation();\n });\n this.root = backdrop;\n this.body = body;\n }\n\n /** Mount into the host. Idempotent. */\n open(onClose?: () => void): void {\n this._onClose = onClose ?? null;\n if (this.root.parentNode === null) this._host.appendChild(this.root);\n // T-3516 — track in the open-modal stack so Escape can close the top.\n if (!Modal._openStack.includes(this)) Modal._openStack.push(this);\n // Phase 27 (T-2702) -- Hide any pre-existing body-mounted tooltips\n // (atom / bond / palette / builder) so they don't remain visible\n // on top of the modal backdrop. The user-reported case: hover a\n // bond -> bond tooltip shows -> right-click the bond -> BondProps\n // dialog opens BUT the tooltip stays on screen overlapping the\n // dialog. Hiding here at MOUNT TIME catches the case regardless\n // of which dialog opened or what the user did to trigger it.\n hideAllTooltips(this._doc);\n }\n\n /** Detach from the host. Idempotent. */\n close(): void {\n // T-3516 — drop from the open-modal stack first (idempotent).\n const i = Modal._openStack.indexOf(this);\n if (i >= 0) Modal._openStack.splice(i, 1);\n if (this.root.parentNode !== null) this.root.parentNode.removeChild(this.root);\n const cb = this._onClose;\n this._onClose = null;\n if (cb !== null) cb();\n }\n\n /**\n * T-3518 — Register the action invoked when the user presses Enter while\n * the dialog is focused (e.g. the OK button's handler, or \"Find Next\").\n */\n setPrimaryAction(fn: () => void): void { this._primaryAction = fn; }\n\n /** True when mounted. */\n get isOpen(): boolean { return this.root.parentNode !== null; }\n}\n\n/** Build a labelled text input. Returns `{ row, input }`. */\nexport function textRow(\n doc: Document,\n label: string,\n initial: string,\n testId?: string,\n): { row: HTMLElement; input: HTMLInputElement } {\n const id = `hw-input-${Math.random().toString(36).slice(2, 9)}`;\n const lab = el(doc, 'label', { for: id, class: 'hw-modal__label' }, [label]);\n const input = doc.createElement('input');\n input.setAttribute('id', id);\n input.setAttribute('type', 'text');\n input.setAttribute('class', 'hw-modal__input');\n if (testId !== undefined) input.setAttribute('data-testid', testId);\n input.value = initial;\n const row = el(doc, 'div', { class: 'hw-modal__row' }, [lab, input]);\n return { row, input };\n}\n\n/** Build a labelled checkbox. Returns `{ row, input }`. */\nexport function checkboxRow(\n doc: Document,\n label: string,\n initial: boolean,\n testId?: string,\n): { row: HTMLElement; input: HTMLInputElement } {\n const id = `hw-cb-${Math.random().toString(36).slice(2, 9)}`;\n const input = doc.createElement('input');\n input.setAttribute('id', id);\n input.setAttribute('type', 'checkbox');\n input.setAttribute('class', 'hw-modal__check');\n if (testId !== undefined) input.setAttribute('data-testid', testId);\n if (initial) input.setAttribute('checked', '');\n const lab = el(doc, 'label', { for: id, class: 'hw-modal__label' }, [label]);\n const row = el(doc, 'div', { class: 'hw-modal__row' }, [input, lab]);\n return { row, input };\n}\n\n/** Build a labelled select. Returns `{ row, select }`. */\nexport function selectRow(\n doc: Document,\n label: string,\n values: ReadonlyArray<{ value: string; label: string }>,\n initial: string,\n testId?: string,\n): { row: HTMLElement; select: HTMLSelectElement } {\n const id = `hw-sel-${Math.random().toString(36).slice(2, 9)}`;\n const lab = el(doc, 'label', { for: id, class: 'hw-modal__label' }, [label]);\n const select = doc.createElement('select');\n select.setAttribute('id', id);\n select.setAttribute('class', 'hw-modal__select');\n if (testId !== undefined) select.setAttribute('data-testid', testId);\n for (const v of values) {\n const opt = doc.createElement('option');\n opt.setAttribute('value', v.value);\n if (v.value === initial) opt.setAttribute('selected', '');\n opt.appendChild(doc.createTextNode(v.label));\n select.appendChild(opt);\n }\n const row = el(doc, 'div', { class: 'hw-modal__row' }, [lab, select]);\n return { row, select };\n}\n\n/** Build OK + Cancel button row. Returns `{ row, ok, cancel }`. */\nexport function actionRow(doc: Document, testIdPrefix?: string): {\n row: HTMLElement;\n ok: HTMLButtonElement;\n cancel: HTMLButtonElement;\n} {\n const ok = doc.createElement('button');\n ok.setAttribute('class', 'hw-modal__ok');\n ok.setAttribute('type', 'button');\n if (testIdPrefix !== undefined) ok.setAttribute('data-testid', `${testIdPrefix}-ok`);\n ok.appendChild(doc.createTextNode('OK'));\n const cancel = doc.createElement('button');\n cancel.setAttribute('class', 'hw-modal__cancel');\n cancel.setAttribute('type', 'button');\n if (testIdPrefix !== undefined) cancel.setAttribute('data-testid', `${testIdPrefix}-cancel`);\n cancel.appendChild(doc.createTextNode('Cancel'));\n const row = el(doc, 'div', { class: 'hw-modal__actions' }, [ok, cancel]);\n return { row, ok, cancel };\n}\n","// T-0501 — `HelmEditor`: top-level editor class.\n//\n// Wave A scope: API surface + state + events. No DOM access — the `host`\n// element is stored but unused; rendering is wired up by Wave B. The editor\n// composes a bound `HelmService` plus its own `EventBus`, `HistoryStack`,\n// `Selection`, and highlight set, and exposes the public API documented in\n// API.md §2.\n//\n// Layering: this module is the topmost editor entry; it imports from\n// `core/`, `util/`, `monomer-lib/` (only types), `render/theme` (only the\n// `Theme` type), and `service/`. We additionally reach into\n// `layout/layout-service` for `cleanLayout` (the task spec calls for it\n// directly); we never touch `io/` directly — every parse / serialize hop\n// goes through `service`.\n\nimport type {\n Atom,\n Bond,\n BondKind,\n Chain,\n Mol,\n RGroupRef,\n} from '../core/model';\nimport { emptyMol, molAtomById, molBondById } from '../core/model';\nimport { makeContinuousIdMap } from '../core/ids';\nimport type { HelmType } from '../core/constants';\nimport { HelmTypes } from '../core/constants';\nimport { Selection } from '../core/selection';\nimport { Transaction } from '../core/transaction';\nimport { HwError } from '../core/errors';\nimport type { ValidationDiagnostic } from '../core/errors';\nimport { bondsOnAtom, freeRgroups, totalRgroups } from '../core/rgroups';\nimport type { Point, Rect, Unsubscribe } from '../util/types';\nimport type { Logger } from '../util/logger';\nimport type { Theme } from '../render/theme';\nimport { lightTheme } from '../render/theme';\nimport type { HelmService } from '../service/helm-service';\nimport { HitTester } from '../render/hit-test';\nimport { layoutMol } from '../layout/layout-service';\nimport { BOND_SCALE, DEFAULT_BOND_LENGTH } from '../layout/line';\nimport { resolveMonomers } from '../io/resolver';\nimport { EventBus, type EditorEventMap, type Viewport } from './events';\nimport { HistoryStack } from './history';\nimport { EditorView } from './view/view';\nimport { PointerInput } from './input/pointer';\nimport { KeyboardInput } from './input/keyboard';\nimport { DragDropInput } from './input/dnd';\nimport { AtomPropsDialog } from './dialogs/atom-props';\nimport { BondPropsDialog } from './dialogs/bond-props';\nimport { FindReplaceDialog } from './dialogs/find-replace';\n\n/**\n * Options accepted by the `HelmEditor` constructor. `service` is the only\n * required field — everything else has a sensible default. Optional booleans\n * like `enableUndo` are unused during Wave A (the editor maintains a history\n * regardless) but stored so Wave B can honor them when wiring inputs.\n */\nexport interface HelmEditorOptions {\n service: HelmService;\n initialHelm?: string;\n width?: number;\n height?: number;\n viewOnly?: boolean;\n bondLength?: number;\n fontSize?: number;\n canonicalize?: boolean;\n caseSensitiveSymbols?: boolean;\n logger?: Logger;\n enableUndo?: boolean;\n enableContextMenu?: boolean;\n enableKeyboard?: boolean;\n enableDragDrop?: boolean;\n}\n\nconst EMPTY_FROZEN_ID_SET: ReadonlySet<number> = Object.freeze(new Set<number>()) as ReadonlySet<number>;\n\n/** Editor interaction mode. Drives pointer-click behavior on the canvas. */\nexport type EditorMode = 'select' | 'eraser' | 'hbond' | 'move';\n\n/** Next palette-driven add: symbol + biotype set by clicking a palette tile. */\nexport interface NextAddSymbol { symbol: string; biotype: HelmType }\n\n/**\n * Top-level interactive editor. Wave A delivers state + events; Wave B will\n * mount the SVG and wire pointer/keyboard input. Until then, `host` is\n * stored but never touched.\n */\nexport class HelmEditor {\n private readonly _service: HelmService;\n private readonly _options: Readonly<HelmEditorOptions>;\n private readonly _host: HTMLElement | null;\n private readonly _events: EventBus<EditorEventMap> = new EventBus<EditorEventMap>();\n private readonly _history: HistoryStack;\n private readonly _libUnsub: Unsubscribe;\n private _mol: Mol;\n private _selection: Selection;\n private _highlights: ReadonlySet<number>;\n private _theme: Theme;\n private _viewport: Viewport;\n private _viewOnly: boolean;\n private _destroyed: boolean = false;\n private _view: EditorView | null = null;\n private _pointer: PointerInput | null = null;\n private _keyboard: KeyboardInput | null = null;\n private _dnd: DragDropInput | null = null;\n private _atomDialog: AtomPropsDialog | null = null;\n private _bondDialog: BondPropsDialog | null = null;\n private _findReplaceDialog: FindReplaceDialog | null = null;\n private _viewBootstrapped: boolean = false;\n private _mode: EditorMode = 'select';\n private _nextAdd: NextAddSymbol | null = null;\n // Phase 21 (T-2103) — Tracks whether the most recent user-perceived\n // mutation was \"Add triplet\" (via `placeRnaTriplet`). The RNA Builder's\n // placement heuristic reads this to decide whether the next triplet\n // should chain to the right of the last one OR start fresh below-left\n // of the leftmost existing atom. Any other mutation\n // (addAtom/removeAtom/setHelm/cleanLayout/undo/redo/...) resets to\n // 'other' so the user gets the \"fresh anchor\" behaviour after manually\n // editing the canvas.\n private _lastAction: 'add-triplet' | 'other' | null = null;\n private _lastTripletSugarPos: Point | null = null;\n\n constructor(host: HTMLElement | null, options: HelmEditorOptions) {\n if (options.service === undefined || options.service === null)\n throw new TypeError('HelmEditor: options.service is required');\n\n this._host = host;\n this._service = options.service;\n this._options = Object.freeze({ ...options });\n this._mol = emptyMol();\n this._selection = Selection.empty();\n this._highlights = EMPTY_FROZEN_ID_SET;\n this._theme = options.service.theme ?? lightTheme;\n this._viewport = { scale: 1, offset: { x: 0, y: 0 } };\n this._viewOnly = options.viewOnly ?? false;\n this._history = new HistoryStack({ maxEntries: 100 });\n this._history.push({ mol: this._mol, label: 'initial' });\n\n // Re-fire monomer-lib mutations as editor events. Maps the lib's\n // change-event `type` to the editor's `reason` taxonomy.\n this._libUnsub = this._service.monomerLib.onChanged.subscribe((evt) => {\n const versioned = this._service.monomerLib as { version?: number };\n const version = typeof versioned.version === 'number' ? versioned.version : 0;\n const reason: 'replace' | 'merge' | 'update' =\n evt.type === 'reload' ? 'replace' : 'update';\n this._events.fire('monomer-lib:change', {\n source: this._service.monomerLib.source,\n version,\n reason,\n });\n });\n\n if (options.initialHelm !== undefined)\n this.setHelm(options.initialHelm);\n\n // Wave B: bootstrap the view + input layer if a host is supplied and a\n // Document is reachable. Tests can opt out by passing `host: null`.\n this._bootstrapWaveB();\n }\n\n /**\n * Bootstrap the DOM view and input layers. Idempotent — called from the\n * constructor and (defensively) from `redraw()` when the host appears later.\n * Uses dynamic imports so the editor stays headless until a host is supplied.\n */\n private _bootstrapWaveB(): void {\n if (this._viewBootstrapped) return;\n if (this._host === null) return;\n const doc = this._resolveDocument(this._host);\n if (doc === null) return;\n this._viewBootstrapped = true;\n\n const viewOpts: { width?: number; height?: number; documentImpl: Document } = {\n documentImpl: doc,\n };\n if (this._options.width !== undefined) viewOpts.width = this._options.width;\n if (this._options.height !== undefined) viewOpts.height = this._options.height;\n this._view = new EditorView(this._host, this, viewOpts);\n\n if (this._viewOnly !== true) {\n this._pointer = new PointerInput(this._host, this, {\n enableContextMenu: this._options.enableContextMenu ?? true,\n });\n }\n if (this._options.enableKeyboard !== false)\n this._keyboard = new KeyboardInput(this._host, this, { enableKeyboard: true });\n if (this._options.enableDragDrop !== false)\n this._dnd = new DragDropInput(this._host, this);\n\n // Phase 18 (T-1803) — Wire `monomer:hover` events into the highlight\n // set so the renderer's highlight pass paints a teal ring around the\n // hovered atom. `monomer:hover-end` clears the highlight. The pointer\n // input layer already debounces hover transitions (no event fires\n // while the cursor stays on the same atom), so a single\n // `setHighlights` call per hover boundary is sufficient.\n this._events.on('monomer:hover', (e) => {\n this.setHighlights([e.atom.id]);\n });\n this._events.on('monomer:hover-end', () => {\n this.clearHighlights();\n });\n // Phase 18 (T-1802) — `monomer:click` updates the selection.\n // - Plain click → replace selection with just that atom\n // - Ctrl/Meta+ → toggle the atom in the existing selection\n // - Shift+ → add the atom (legacy behavior preserved)\n // The pointer layer already short-circuits eraser and the\n // palette-prime branch; this handler only runs for \"real\" clicks on\n // an atom in non-eraser modes.\n this._events.on('monomer:click', (e) => {\n const m = e.modifiers;\n if (m.ctrl === true || m.meta === true) {\n this.setSelection(Selection.toggle(this._selection, { kind: 'atom', id: e.atom.id }));\n return;\n }\n if (m.shift === true) {\n this.setSelection(Selection.union(this._selection, Selection.fromAtoms([e.atom.id])));\n return;\n }\n this.setSelection(Selection.fromAtoms([e.atom.id]));\n });\n }\n\n /** Resolve a Document from the host. Returns `null` when none is reachable. */\n private _resolveDocument(host: HTMLElement): Document | null {\n const owner = host.ownerDocument;\n if (owner !== null && owner !== undefined) return owner;\n const g = globalThis as { document?: Document };\n if (g.document !== undefined) return g.document;\n return null;\n }\n\n // ------------------------------------------------------------------------\n // Lifecycle\n // ------------------------------------------------------------------------\n\n destroy(): void {\n if (this._destroyed) return;\n this._destroyed = true;\n // Tear down Wave B layers first so they unsubscribe before we clear the bus.\n if (this._atomDialog !== null) { this._atomDialog.close(); this._atomDialog = null; }\n if (this._bondDialog !== null) { this._bondDialog.close(); this._bondDialog = null; }\n if (this._findReplaceDialog !== null) { this._findReplaceDialog.close(); this._findReplaceDialog = null; }\n if (this._dnd !== null) { this._dnd.destroy(); this._dnd = null; }\n if (this._keyboard !== null) { this._keyboard.destroy(); this._keyboard = null; }\n if (this._pointer !== null) { this._pointer.destroy(); this._pointer = null; }\n if (this._view !== null) { this._view.destroy(); this._view = null; }\n this._libUnsub();\n this._events.clear();\n this._history.clear();\n }\n\n /**\n * Update the canvas size. Forwards to the mounted view, if any, and fires\n * a `view:resize` event so consumers can react.\n */\n resize(width: number, height: number): void {\n this._ensureLive();\n if (this._view !== null) this._view.resize(width, height);\n this._events.fire('view:resize', { width, height });\n }\n\n /** Full repaint. Forwards to the mounted view; no-op when headless. */\n redraw(): void {\n this._ensureLive();\n if (this._view !== null) {\n this._view.redraw();\n return;\n }\n this._events.fire('render:skip', { reason: 'no-view-mounted' });\n }\n\n // ------------------------------------------------------------------------\n // I/O\n // ------------------------------------------------------------------------\n\n setHelm(\n helm: string,\n options?: { canonicalize?: boolean },\n ): { mol: Mol; diagnostics: readonly ValidationDiagnostic[] } {\n this._ensureLive();\n const wantCanon = options?.canonicalize ?? this._options.canonicalize ?? false;\n const parseRes = this._service.parse(helm, { mode: 'lenient' });\n const diagnostics = parseRes.diagnostics;\n const errors = diagnostics.filter((d) => d.severity === 'error');\n if (errors.length > 0)\n this._events.fire('helm:parse-error', { helm, diagnostics });\n for (const d of diagnostics) {\n if (d.severity === 'error') this._events.fire('validation:error', { diagnostic: d });\n else if (d.severity === 'warning') this._events.fire('validation:warning', { diagnostic: d });\n }\n\n let mol = parseRes.mol;\n if (wantCanon) {\n const canonHelm = this._service.canonicalize(helm);\n const canonRes = this._service.parse(canonHelm, { mode: 'lenient' });\n mol = canonRes.mol;\n }\n const laid = layoutMol(\n mol,\n this._options.bondLength !== undefined ? { bondLength: this._options.bondLength } : undefined,\n );\n this._replaceMol(laid, 'setHelm', 'api');\n return { mol: laid, diagnostics };\n }\n\n getHelm(options?: { includeHighlights?: boolean; canonicalize?: boolean }): string {\n this._ensureLive();\n const serializeOptions: { canonicalize?: boolean; includeHighlights?: boolean } = {};\n if (options?.canonicalize !== undefined) serializeOptions.canonicalize = options.canonicalize;\n if (options?.includeHighlights !== undefined) serializeOptions.includeHighlights = options.includeHighlights;\n return this._service.serialize(this._mol, serializeOptions);\n }\n\n setMol(mol: Mol): void {\n this._ensureLive();\n this._replaceMol(mol, 'setMol', 'api');\n }\n\n getMol(): Readonly<Mol> {\n return this._mol;\n }\n\n getService(): HelmService {\n return this._service;\n }\n\n // ------------------------------------------------------------------------\n // Selection / highlight\n // ------------------------------------------------------------------------\n\n getSelection(): Selection {\n return this._selection;\n }\n\n setSelection(sel: Selection): void {\n this._ensureLive();\n if (Selection.equals(this._selection, sel)) return;\n const added = Selection.difference(sel, this._selection);\n const removed = Selection.difference(this._selection, sel);\n this._selection = sel;\n this._events.fire('selection:change', {\n selection: sel,\n addedAtomIds: added.atoms,\n removedAtomIds: removed.atoms,\n addedBondIds: added.bonds,\n removedBondIds: removed.bonds,\n });\n }\n\n selectAll(): void {\n this._ensureLive();\n const atomIds = this._mol.atoms.map((a) => a.id);\n const bondIds = this._mol.bonds.map((b) => b.id);\n this.setSelection(Selection.fromAtomsAndBonds(atomIds, bondIds));\n }\n\n clearSelection(): void {\n this._ensureLive();\n if (this._selection.isEmpty) return;\n this.setSelection(Selection.empty());\n }\n\n setHighlights(atomIds: Iterable<number>): void {\n this._ensureLive();\n const next = new Set<number>(atomIds);\n this._highlights = next;\n this._events.fire('highlight:change', {\n atomIds: next,\n bondIds: EMPTY_FROZEN_ID_SET,\n });\n }\n\n clearHighlights(): void {\n this._ensureLive();\n if (this._highlights.size === 0) return;\n this._highlights = EMPTY_FROZEN_ID_SET;\n this._events.fire('highlight:change', {\n atomIds: EMPTY_FROZEN_ID_SET,\n bondIds: EMPTY_FROZEN_ID_SET,\n });\n }\n\n // ------------------------------------------------------------------------\n // Operations\n // ------------------------------------------------------------------------\n\n addAtom(p: Point, biotype: HelmType, symbol: string): Atom {\n this._ensureLive();\n const tx = new Transaction(this._mol);\n const id = tx.addAtom({ p, biotype, symbol });\n const { mol } = tx.commit();\n // T-1704 — Do NOT run `layoutMol` after `addAtom`. The new atom\n // lives at the user-supplied position `p`; existing atoms keep\n // their current positions. The renderer reads `atom.p` directly\n // (Phase 13c — `HelmService.renderMol`) so a freshly added atom is\n // visible at `p` without a layout pass; the HitTester is rebuilt by\n // the pointer input layer on every `mol:changed` event. A user-\n // requested clean-up still re-runs the layout via `cleanLayout()`.\n //\n // We DO still re-resolve monomers so `atom.monomer.name` /\n // `rgroups` / `naturalAnalog` are populated for tooltips, R-group\n // pickers, and the property panes (Bug 10 / 6).\n const resolved = resolveMonomers(mol, this._service.monomerLib, { mode: 'lenient' }).mol;\n this._replaceMol(resolved, `addAtom:${symbol}`, 'api');\n const atom = molAtomById(resolved, id);\n if (atom === undefined) throw new Error('HelmEditor.addAtom: lost atom after commit');\n this._events.fire('monomer:add', { atom, source: 'api' });\n return atom;\n }\n\n removeAtom(atom: Atom | number): void {\n this._ensureLive();\n const atomId = typeof atom === 'number' ? atom : atom.id;\n const prev = molAtomById(this._mol, atomId);\n if (prev === undefined) return;\n const tx = new Transaction(this._mol);\n tx.removeAtom(atomId);\n const { mol } = tx.commit();\n // T-1704 — Do NOT re-layout on removal. The remaining atoms keep\n // their current positions; bonds keep their endpoint references and\n // simply redraw against the surviving atoms. Visual gaps are the\n // honest representation of the post-erase state; a user-requested\n // clean-up via `cleanLayout()` compacts them.\n this._replaceMol(mol, `removeAtom:${prev.symbol}`, 'api');\n this._events.fire('monomer:remove', { atom: prev, source: 'api' });\n }\n\n addBond(a1: Atom, a2: Atom, r1: number, r2: number, kind?: BondKind): Bond {\n this._ensureLive();\n let id: number;\n let mol: Mol;\n try {\n // Phase 14B (T-1412) — Enforce the SPECIFICATION.md §12.4\n // connection cap BEFORE the Transaction runs, so the editor\n // surfaces stable HwError codes (`rgroup-already-bonded`,\n // `rgroup-not-on-monomer`, `duplicate-bond`) rather than the\n // Transaction's free-form Error text.\n this._validateAddBondConstraints(a1, a2, r1, r2);\n // Phase 15 (T-1506) — Linear-extend merge. When the user drags from\n // the R2-end of chain X onto the R1-start of chain Y AND both\n // chains share polymer type, fold Y into X instead of leaving two\n // chains joined by a cross-polymer bond. The resulting HELM goes\n // from `PEPTIDE1{A.C.D}|PEPTIDE2{G}$PEPTIDE1,PEPTIDE2,3:R2-1:R1`\n // to `PEPTIDE1{A.C.D.G}` — matching the user's mental model that\n // appending a monomer extends the same chain.\n const mergeKind = kind ?? 'backbone';\n const mergeMol = this._maybeMergeForLinearExtend(a1, a2, r1, r2, mergeKind);\n if (mergeMol !== null) {\n ({ id, mol } = mergeMol);\n } else {\n const tx = new Transaction(this._mol);\n id = tx.addBond({\n a1: a1.id,\n a2: a2.id,\n kind: kind ?? 'backbone',\n r1: { kind: 'numeric', r: r1 },\n r2: { kind: 'numeric', r: r2 },\n });\n ({ mol } = tx.commit());\n }\n } catch (err) {\n // Phase 13b (Bug 5) — Fire a user-visible status message so the\n // status bar can surface the failure reason. Common causes:\n // duplicate-bond, missing-atom, R-group already occupied. HwError\n // instances carry a friendly message already; bare Errors get\n // decoded via `_friendlyBondError`.\n const friendly = err instanceof HwError\n ? err.message\n : this._friendlyBondError(err instanceof Error ? err.message : String(err), a1, a2, r1, r2);\n this._events.fire('editor:status', {\n message: `Could not add bond: ${friendly}`,\n severity: 'error',\n durationMs: 3000,\n });\n throw err;\n }\n // T-1704 — Do NOT re-layout on `addBond`. Both endpoint atoms keep\n // their existing positions so the user's deliberate placement\n // survives the new connection. The bond is drawn as a straight line\n // between the two stored positions; a user-requested clean-up via\n // `cleanLayout()` re-runs the auto-layout to compact the result.\n this._replaceMol(mol, 'addBond', 'api');\n const bond = molBondById(mol, id);\n if (bond === undefined) throw new Error('HelmEditor.addBond: lost bond after commit');\n this._events.fire('bond:add', { bond, source: 'api' });\n return bond;\n }\n\n /**\n * Phase 15 (T-1506) — Linear-extend chain merge.\n *\n * When the user drags from the R2-end of chain X to the R1-start of\n * chain Y AND both share polymer type, we fold Y into X so the resulting\n * HELM reads `PEPTIDE1{A.C.D.G}` (single polymer) rather than\n * `PEPTIDE1{A.C.D}|PEPTIDE2{G}$PEPTIDE1,PEPTIDE2,3:R2-1:R1` (two\n * polymers joined by a cross-polymer bond). This matches the user's\n * mental model of \"appending a monomer\" and is also what Pistoia HELM\n * editors do by default.\n *\n * Returns `null` when the preconditions are not met — in that case the\n * caller falls back to the normal addBond path. Returns `{ id, mol }`\n * on success: `id` is the freshly-allocated id of the new backbone\n * bond, `mol` is the merged Mol with continuous-ids refreshed.\n *\n * Preconditions:\n * - `a1.bio.chainId !== a2.bio.chainId` (different chains)\n * - One direction satisfies `r=2 → r=1` between end-of-chain and\n * start-of-chain. Phase 16 (T-1605) — the merge now accepts BOTH\n * directions: the user can drag from the existing chain's end onto\n * the new monomer (canonical: `last1.R2 → first2.R1` with\n * `r1=2, r2=1`) OR from the new monomer onto the existing chain's\n * end (reversed: `first1.R1 ← last2.R2` with `r1=1, r2=2`). Both\n * intents are \"extend the chain\" and produce the same merge.\n * - `kind` ∈ {`'backbone'`, `'cross-polymer'`} (hbond / branch don't merge)\n * - both chains exist in `mol.chains` and have the same `polymerType`\n * - One atom is the LAST `backboneAtom` of its chain; the other is the\n * FIRST `backboneAtom` of its chain.\n */\n private _maybeMergeForLinearExtend(\n a1: Atom, a2: Atom, r1: number, r2: number, kind: BondKind,\n ): { id: number; mol: Mol } | null {\n if (a1.bio.chainId === a2.bio.chainId) return null;\n if (kind !== 'backbone' && kind !== 'cross-polymer') return null;\n\n const chain1 = this._mol.chains.find((c) => c.id === a1.bio.chainId);\n const chain2 = this._mol.chains.find((c) => c.id === a2.bio.chainId);\n if (chain1 === undefined || chain2 === undefined) return null;\n if (chain1.polymerType !== chain2.polymerType) return null;\n\n // Direction 1 (canonical): a1 is last of chain1 (R2 free), a2 is first of chain2 (R1 free).\n if (r1 === 2 && r2 === 1) {\n const last1 = chain1.backboneAtoms[chain1.backboneAtoms.length - 1];\n const first2 = chain2.backboneAtoms[0];\n if (last1 !== undefined && first2 !== undefined\n && last1.id === a1.id && first2.id === a2.id)\n return this._mergeChains(chain1, chain2, kind);\n }\n // Direction 2 (reversed — Phase 16 T-1605): a1 is first of chain1 (R1\n // free), a2 is last of chain2 (R2 free). Logical merge appends chain1's\n // atom to the END of chain2 — same intent as the canonical case, just\n // with the user dragging from the new monomer onto the existing chain's\n // end. We swap the arguments and reuse `_mergeChains`, which then\n // builds `chain2 + chain1` (chain2 keeps its id, chain1 gets folded in).\n if (r1 === 1 && r2 === 2) {\n const first1 = chain1.backboneAtoms[0];\n const last2 = chain2.backboneAtoms[chain2.backboneAtoms.length - 1];\n if (first1 !== undefined && last2 !== undefined\n && first1.id === a1.id && last2.id === a2.id)\n return this._mergeChains(chain2, chain1, kind);\n }\n return null;\n }\n\n /**\n * Phase 15 (T-1506) — Build the merged Mol. Renumbers chain2 atoms so\n * their `bio.chainId` matches chain1 and their `positionInChain` picks\n * up after chain1's end. Adds a `backbone` bond between the old end\n * (chain1) and the new start (chain2). Refreshes continuous-ids.\n *\n * Atom identity is preserved by `id` — every other property of every\n * chain2 atom is re-emitted on a fresh instance (so the bio bag can be\n * rewritten). Bonds and chain references are rewired to the new\n * instances via an `oldToNew` lookup.\n */\n private _mergeChains(chain1: Chain, chain2: Chain, kind: BondKind): { id: number; mol: Mol } {\n const offset = chain1.backboneAtoms.length;\n const chain2AtomIds = new Set<number>();\n for (const a of chain2.backboneAtoms) {\n chain2AtomIds.add(a.id);\n if (a.branchAtom !== undefined) chain2AtomIds.add(a.branchAtom.id);\n }\n\n // Build oldToNew: chain2 atoms get fresh bio; others unchanged.\n const oldToNew = new Map<number, Atom>();\n let nextPosition = offset;\n for (const a of chain2.backboneAtoms) {\n nextPosition += 1;\n const newAtom: Atom = {\n ...a,\n bio: { ...a.bio, chainId: chain1.id, positionInChain: nextPosition },\n };\n oldToNew.set(a.id, newAtom);\n if (a.branchAtom !== undefined) {\n const b = a.branchAtom;\n // Branch atoms (RNA bases) inherit the parent's renumbered chain id.\n const newBranch: Atom = {\n ...b,\n bio: { ...b.bio, chainId: chain1.id, positionInChain: nextPosition },\n };\n oldToNew.set(b.id, newBranch);\n }\n }\n // Repoint moved branchAtom pointers to the new branch instances.\n for (const a of chain2.backboneAtoms) {\n const updated = oldToNew.get(a.id);\n if (updated === undefined || a.branchAtom === undefined) continue;\n const branchUpdated = oldToNew.get(a.branchAtom.id);\n if (branchUpdated === undefined) continue;\n (updated as { branchAtom?: Atom }).branchAtom = branchUpdated;\n }\n // chain1 atoms (and any other chain's atoms) keep their identity.\n for (const a of this._mol.atoms) if (!oldToNew.has(a.id)) oldToNew.set(a.id, a);\n\n // Allocate a fresh id for the new bond. Largest existing id + 1.\n let maxId = 0;\n for (const a of this._mol.atoms) if (a.id > maxId) maxId = a.id;\n for (const b of this._mol.bonds) if (b.id > maxId) maxId = b.id;\n const newBondId = maxId + 1;\n\n const last1 = oldToNew.get(chain1.backboneAtoms[chain1.backboneAtoms.length - 1]!.id)!;\n const first2 = oldToNew.get(chain2.backboneAtoms[0]!.id)!;\n const linkBond: Bond = {\n id: newBondId,\n a1: last1,\n a2: first2,\n kind: 'backbone',\n r1: { kind: 'numeric', r: 2 },\n r2: { kind: 'numeric', r: 1 },\n };\n void kind; // bond kind is canonical backbone for the merged chain.\n\n // Rebuild every bond to point at the new atom instances.\n const newBonds: Bond[] = [];\n for (const b of this._mol.bonds) {\n const newA1 = oldToNew.get(b.a1.id) ?? b.a1;\n const newA2 = oldToNew.get(b.a2.id) ?? b.a2;\n if (newA1 === b.a1 && newA2 === b.a2) newBonds.push(b);\n else newBonds.push({ ...b, a1: newA1, a2: newA2 });\n }\n newBonds.push(linkBond);\n\n // Rebuild chains: chain1 absorbs chain2's atoms; chain2 dropped.\n const newChains: Chain[] = [];\n const mergedBackbone: Atom[] = [...chain1.backboneAtoms];\n for (const a of chain2.backboneAtoms) mergedBackbone.push(oldToNew.get(a.id)!);\n for (const c of this._mol.chains) {\n if (c.id === chain2.id) continue;\n if (c.id === chain1.id) {\n // Update chain1's bonds to include the new link bond.\n const cBonds = c.bonds.map((b) => {\n const newA1 = oldToNew.get(b.a1.id) ?? b.a1;\n const newA2 = oldToNew.get(b.a2.id) ?? b.a2;\n if (newA1 === b.a1 && newA2 === b.a2) return b;\n return { ...b, a1: newA1, a2: newA2 };\n });\n // Bring in chain2's bonds too — they now belong to the merged chain.\n for (const b of chain2.bonds) {\n const newA1 = oldToNew.get(b.a1.id) ?? b.a1;\n const newA2 = oldToNew.get(b.a2.id) ?? b.a2;\n cBonds.push(newA1 === b.a1 && newA2 === b.a2 ? b : { ...b, a1: newA1, a2: newA2 });\n }\n cBonds.push(linkBond);\n newChains.push({\n id: c.id,\n polymerType: c.polymerType,\n backboneAtoms: mergedBackbone as readonly Atom[],\n bonds: cBonds as readonly Bond[],\n ...(c.annotation !== undefined ? { annotation: c.annotation } : {}),\n });\n } else {\n // Other chains: keep but rewire atom refs in case any branchAtom\n // (unlikely for this code path, but defend) was an old instance.\n const cBb = c.backboneAtoms.map((a) => oldToNew.get(a.id) ?? a);\n const cBonds = c.bonds.map((b) => {\n const newA1 = oldToNew.get(b.a1.id) ?? b.a1;\n const newA2 = oldToNew.get(b.a2.id) ?? b.a2;\n if (newA1 === b.a1 && newA2 === b.a2) return b;\n return { ...b, a1: newA1, a2: newA2 };\n });\n newChains.push({\n id: c.id,\n polymerType: c.polymerType,\n backboneAtoms: cBb as readonly Atom[],\n bonds: cBonds as readonly Bond[],\n ...(c.annotation !== undefined ? { annotation: c.annotation } : {}),\n });\n }\n }\n\n // Atoms array follows the chain order; chain2 atoms now live in chain1\n // but we keep mol.atoms in source order — every atom present, just\n // with rewritten bio for chain2.\n const newAtoms: Atom[] = this._mol.atoms.map((a) => oldToNew.get(a.id) ?? a);\n\n // Refresh continuous ids on the merged mol.\n let merged: Mol = {\n atoms: newAtoms as readonly Atom[],\n bonds: newBonds as readonly Bond[],\n chains: newChains as readonly Chain[],\n groups: this._mol.groups,\n annotations: this._mol.annotations,\n };\n merged = this._refreshContinuousIdsLocal(merged);\n return { id: newBondId, mol: merged };\n }\n\n /**\n * Phase 15 (T-1506) — Local continuous-id refresh. Mirrors the helper\n * in `core/transaction.ts` but lives here because we hand-build the\n * merged Mol outside Transaction. Walks chains in source order,\n * assigns 1-based continuous ids to every backbone atom and its branch\n * (RNA base) atoms.\n */\n private _refreshContinuousIdsLocal(mol: Mol): Mol {\n const ids = makeContinuousIdMap(mol);\n const oldToNew = new Map<number, Atom>();\n for (const a of mol.atoms) {\n const nextId = ids.get(a.id);\n if (nextId === undefined || nextId === a.bio.continuousId) {\n oldToNew.set(a.id, a);\n continue;\n }\n oldToNew.set(a.id, { ...a, bio: { ...a.bio, continuousId: nextId } });\n }\n // Re-pin branchAtom references onto the freshly built atoms.\n for (const a of mol.atoms) {\n if (a.branchAtom === undefined) continue;\n const parent = oldToNew.get(a.id);\n const branch = oldToNew.get(a.branchAtom.id);\n if (parent === undefined || branch === undefined) continue;\n (parent as { branchAtom?: Atom }).branchAtom = branch;\n }\n const newBonds: Bond[] = mol.bonds.map((b) => {\n const a1 = oldToNew.get(b.a1.id) ?? b.a1;\n const a2 = oldToNew.get(b.a2.id) ?? b.a2;\n if (a1 === b.a1 && a2 === b.a2) return b;\n return { ...b, a1, a2 };\n });\n const newBondsById = new Map<number, Bond>();\n for (const b of newBonds) newBondsById.set(b.id, b);\n const newChains: Chain[] = mol.chains.map((c) => {\n const bb = c.backboneAtoms.map((a) => oldToNew.get(a.id) ?? a);\n const bonds = c.bonds.map((b) => newBondsById.get(b.id) ?? b);\n return {\n id: c.id,\n polymerType: c.polymerType,\n backboneAtoms: bb as readonly Atom[],\n bonds: bonds as readonly Bond[],\n ...(c.annotation !== undefined ? { annotation: c.annotation } : {}),\n };\n });\n const newAtoms: Atom[] = mol.atoms.map((a) => oldToNew.get(a.id) ?? a);\n return {\n atoms: newAtoms as readonly Atom[],\n bonds: newBonds as readonly Bond[],\n chains: newChains as readonly Chain[],\n groups: mol.groups,\n annotations: mol.annotations,\n };\n }\n\n /**\n * Translate a raw `Transaction.addBond` error message into a human-\n * friendly status-bar message. Recognises the three error patterns the\n * transaction emits today; falls back to the raw message verbatim.\n */\n private _friendlyBondError(raw: string, a1: Atom, a2: Atom, r1: number, r2: number): string {\n if (raw.includes('duplicate bond'))\n return `bond ${a1.symbol}:R${r1} ↔ ${a2.symbol}:R${r2} already exists`;\n if (raw.includes('does not exist'))\n return `one of the endpoint atoms is missing`;\n return raw;\n }\n\n /**\n * Phase 14B (T-1412) — Enforce the per-atom R-group cap on every\n * `addBond` call. Throws `HwError` with one of the documented codes:\n *\n * - `rgroup-not-on-monomer` — `r1` (or `r2`) exceeds the atom's total\n * number of R-groups (`atom.monomer.rgroups.length`). Refused so the\n * editor never inserts a bond the renderer / serializer can't paint.\n * - `rgroup-already-bonded` — the requested numeric R-group is already\n * consumed by another bond on the same atom. Names the conflicting\n * bond so the user can locate it.\n * - `duplicate-bond` — the exact `(a1, r1, a2, r2)` quadruple is\n * already a bond in the current mol (also caught by the Transaction\n * but pre-validated here so the status-bar message is friendlier).\n *\n * Atoms without a resolved monomer (`totalRgroups === 0`) DO NOT get\n * the cap enforced — that path is reserved for unresolved fragments\n * where the editor cannot prove the R-group count. The validator's\n * `unresolved-monomer` diagnostic surfaces the underlying problem.\n */\n private _validateAddBondConstraints(a1: Atom, a2: Atom, r1: number, r2: number): void {\n const bonds = this._mol.bonds;\n\n // 1. R-group index must be within the monomer's advertised count.\n const t1 = totalRgroups(a1);\n if (t1 > 0 && (r1 < 1 || r1 > t1))\n throw new HwError(\n 'rgroup-not-on-monomer',\n `R-group R${r1} is not defined on monomer '${a1.symbol}' (has R1..R${t1})`,\n { atomId: a1.id, requested: r1, total: t1 },\n );\n const t2 = totalRgroups(a2);\n if (t2 > 0 && (r2 < 1 || r2 > t2))\n throw new HwError(\n 'rgroup-not-on-monomer',\n `R-group R${r2} is not defined on monomer '${a2.symbol}' (has R1..R${t2})`,\n { atomId: a2.id, requested: r2, total: t2 },\n );\n\n // 2. R-group must not already be consumed by another bond on the\n // same atom. Reuses `bondsOnAtom` so the conflicting bond can be\n // surfaced in the error message (the other endpoint helps the\n // user locate it on the canvas).\n if (t1 > 0) {\n const occupied1 = bondsOnAtom(a1, bonds).find((p) => p.rNumber === r1);\n if (occupied1 !== undefined) {\n const other = occupied1.side === 'r1' ? occupied1.bond.a2 : occupied1.bond.a1;\n throw new HwError(\n 'rgroup-already-bonded',\n `R-group R${r1} on '${a1.symbol}' is already bonded to '${other.symbol}' (${other.bio.chainId}:${other.bio.positionInChain})`,\n { atomId: a1.id, requested: r1, conflictingBondId: occupied1.bond.id },\n );\n }\n }\n if (t2 > 0) {\n const occupied2 = bondsOnAtom(a2, bonds).find((p) => p.rNumber === r2);\n if (occupied2 !== undefined) {\n const other = occupied2.side === 'r1' ? occupied2.bond.a2 : occupied2.bond.a1;\n throw new HwError(\n 'rgroup-already-bonded',\n `R-group R${r2} on '${a2.symbol}' is already bonded to '${other.symbol}' (${other.bio.chainId}:${other.bio.positionInChain})`,\n { atomId: a2.id, requested: r2, conflictingBondId: occupied2.bond.id },\n );\n }\n }\n\n // 3. Reject the exact same bond appearing twice. The Transaction\n // also raises this, but pre-validating gives us a stable HwError\n // code for the status-bar / test layer.\n for (const b of bonds) {\n if (b.r1.kind !== 'numeric' || b.r2.kind !== 'numeric') continue;\n const fwd = b.a1.id === a1.id && b.a2.id === a2.id && b.r1.r === r1 && b.r2.r === r2;\n const rev = b.a1.id === a2.id && b.a2.id === a1.id && b.r1.r === r2 && b.r2.r === r1;\n if (fwd || rev)\n throw new HwError(\n 'duplicate-bond',\n `bond ${a1.symbol}:R${r1} ↔ ${a2.symbol}:R${r2} already exists`,\n { atomId: a1.id, bondId: b.id },\n );\n }\n }\n\n /** @internal Used by `freeRgroups`-driven callers to introspect the cap. */\n _freeRgroupsOn(atom: Atom): number[] {\n return freeRgroups(atom, this._mol.bonds);\n }\n\n addHydrogenBond(a1: Atom, a2: Atom): Bond {\n this._ensureLive();\n const tx = new Transaction(this._mol);\n const pairRef: RGroupRef = { kind: 'pair' };\n const id = tx.addBond({\n a1: a1.id,\n a2: a2.id,\n kind: 'hbond',\n r1: pairRef,\n r2: pairRef,\n });\n const { mol } = tx.commit();\n // T-1704 — Do NOT re-layout on `addHydrogenBond`. Atoms keep their\n // existing positions; the dashed h-bond is drawn between them.\n this._replaceMol(mol, 'addHydrogenBond', 'api');\n const bond = molBondById(mol, id);\n if (bond === undefined) throw new Error('HelmEditor.addHydrogenBond: lost bond after commit');\n this._events.fire('bond:add', { bond, source: 'api' });\n return bond;\n }\n\n removeBond(bond: Bond | number): void {\n this._ensureLive();\n const bondId = typeof bond === 'number' ? bond : bond.id;\n const prev = molBondById(this._mol, bondId);\n if (prev === undefined) return;\n const tx = new Transaction(this._mol);\n tx.removeBond(bondId);\n const { mol } = tx.commit();\n // T-1704 — Do NOT re-layout on bond removal. Atoms keep their\n // existing positions; the cut bond simply disappears from the\n // canvas. A user-requested clean-up via `cleanLayout()` reorganises\n // the now-disconnected components.\n this._replaceMol(mol, 'removeBond', 'api');\n this._events.fire('bond:remove', { bond: prev, source: 'api' });\n }\n\n replaceMonomer(atom: Atom, newSymbol: string): void {\n this._ensureLive();\n const prev = molAtomById(this._mol, atom.id);\n if (prev === undefined) return;\n const oldSymbol = prev.symbol;\n if (oldSymbol === newSymbol) return;\n const tx = new Transaction(this._mol);\n tx.updateAtom(atom.id, { symbol: newSymbol });\n const { mol } = tx.commit();\n // T-1704 — Re-resolve monomers so `atom.monomer.name`,\n // `atom.monomer.rgroups`, and `naturalAnalog` point at the new\n // library entry (tooltips, R-group pickers, properties pane). Do\n // NOT re-layout — the atom keeps its existing position; only its\n // symbol changes. A user-requested clean-up via `cleanLayout()`\n // re-runs the auto-layout.\n const resolved = resolveMonomers(mol, this._service.monomerLib, { mode: 'lenient' }).mol;\n this._replaceMol(resolved, `replaceMonomer:${oldSymbol}->${newSymbol}`, 'api');\n const updated = molAtomById(resolved, atom.id);\n if (updated === undefined) return;\n this._events.fire('monomer:replace', { atom: updated, oldSymbol, newSymbol });\n }\n\n /**\n * Hit-test at a model-space point. Returns the topmost atom / bond, or\n * `null`. Internally constructs a fresh `HitTester` so this is O(n) per\n * call — fine for occasional API use; pointer input has its own cache.\n */\n hitTest(x: number, y: number): { atom?: Atom; bond?: Bond } | null {\n this._ensureLive();\n const tester = new HitTester(this._mol, this._theme);\n const r = tester.test({ x, y });\n if (r === null) return null;\n if (r.atom !== undefined) return { atom: r.atom };\n if (r.bond !== undefined) return { bond: r.bond };\n return null;\n }\n\n /**\n * Phase 15 (T-1504) — Move a single atom to a new model-space position.\n * Used by the pointer input layer in `move` mode to physically drag an\n * atom around the canvas. Re-uses `Transaction.updateAtom` so the resulting\n * Mol stays immutable and fires `mol:changed` exactly once.\n *\n * No layout is run — the user has chosen the new position by hand. The\n * existing `cleanLayout()` (toolbar button) reverts to the auto-layout.\n */\n moveAtom(atomId: number, p: Point): void {\n this._ensureLive();\n const prev = molAtomById(this._mol, atomId);\n if (prev === undefined) return;\n if (prev.p.x === p.x && prev.p.y === p.y) return;\n const tx = new Transaction(this._mol);\n tx.updateAtom(atomId, { p: { x: p.x, y: p.y } });\n const { mol } = tx.commit();\n this._replaceMol(mol, 'moveAtom', 'user');\n }\n\n /**\n * Phase 26 (T-2601) — Batch version of `moveAtom`. Move multiple atoms\n * to new positions in a SINGLE `Transaction` so the resulting\n * `mol:changed` / `helm:changed` events fire exactly once for the\n * whole group, not once per atom. Used by the pointer input layer when\n * a Move-mode drag begins on an atom that is part of a multi-atom\n * selection — every selected atom shifts by the same screen delta.\n *\n * Updates whose target position equals the current atom position are\n * skipped (no-op `updateAtom`); atom-ids that do not resolve are\n * silently ignored (consistent with `moveAtom`). When NO update has any\n * effect the method returns without firing any event — the editor's\n * `_replaceMol` would otherwise push a no-op history frame.\n */\n moveAtoms(updates: ReadonlyArray<{ atomId: number; p: Point }>): void {\n this._ensureLive();\n if (updates.length === 0) return;\n const tx = new Transaction(this._mol);\n let changed = 0;\n for (const u of updates) {\n const prev = molAtomById(this._mol, u.atomId);\n if (prev === undefined) continue;\n if (prev.p.x === u.p.x && prev.p.y === u.p.y) continue;\n tx.updateAtom(u.atomId, { p: { x: u.p.x, y: u.p.y } });\n changed += 1;\n }\n if (changed === 0) return;\n const { mol } = tx.commit();\n this._replaceMol(mol, 'moveAtom', 'user');\n }\n\n cleanLayout(_focusAtom?: Atom): void {\n this._ensureLive();\n // Phase 21 (T-2103) — Clean repositions every atom; the prior\n // \"added a triplet here\" anchor is no longer valid.\n this._resetLastAction();\n const laid = layoutMol(\n this._mol,\n this._options.bondLength !== undefined ? { bondLength: this._options.bondLength } : undefined,\n );\n this._mol = laid;\n this._history.push({ mol: laid, label: 'cleanLayout' });\n this._events.fire('layout:change', { mol: laid, strategy: 'auto' });\n this._events.fire('mol:changed', {\n mol: laid,\n changedAtomIds: new Set<number>(laid.atoms.map((a) => a.id)),\n changedBondIds: EMPTY_FROZEN_ID_SET,\n reason: 'cleanLayout',\n });\n }\n\n // ------------------------------------------------------------------------\n // Undo / redo\n // ------------------------------------------------------------------------\n\n canUndo(): boolean { return this._history.canUndo(); }\n canRedo(): boolean { return this._history.canRedo(); }\n\n undo(): void {\n this._ensureLive();\n const entry = this._history.undo();\n if (entry === null) return;\n this._restoreFromHistory(entry.mol, 'undo');\n }\n\n redo(): void {\n this._ensureLive();\n const entry = this._history.redo();\n if (entry === null) return;\n this._restoreFromHistory(entry.mol, 'redo');\n }\n\n // ------------------------------------------------------------------------\n // Events\n // ------------------------------------------------------------------------\n\n on<K extends keyof EditorEventMap>(\n name: K,\n handler: (e: EditorEventMap[K]) => void,\n ): Unsubscribe {\n return this._events.on(name, handler);\n }\n\n off<K extends keyof EditorEventMap>(\n name: K,\n handler: (e: EditorEventMap[K]) => void,\n ): void {\n this._events.off(name, handler);\n }\n\n fire<K extends keyof EditorEventMap>(name: K, payload: EditorEventMap[K]): void {\n this._events.fire(name, payload);\n }\n\n // ------------------------------------------------------------------------\n // View (Wave A: state + events only)\n // ------------------------------------------------------------------------\n\n /**\n * Zoom the viewport. `factor > 1` zooms in; `factor < 1` zooms out.\n * When `around` is supplied (in MODEL coordinates), the viewport offset\n * is adjusted so that point stays fixed in surface space — the standard\n * \"zoom to cursor\" UX. Fires `view:zoom` AND `view:viewport-change`\n * (per EVENTS.md §5); the view's listener applies the new transform.\n */\n zoom(factor: number, around?: Point): void {\n this._ensureLive();\n if (!Number.isFinite(factor) || factor <= 0) return;\n const previousScale = this._viewport.scale;\n const nextScale = previousScale * factor;\n const previousOffset = { ...this._viewport.offset };\n // If a focal point in MODEL space is provided, shift the offset so it\n // maps to the same surface position before and after the zoom.\n // Surface = offset + scale * baseTransform(model); the renderer's base\n // transform is constant per render, so anchoring against it means\n // offsetNext = offsetPrev + (prevScale - nextScale) * around.\n let nextOffset: Point = previousOffset;\n if (around !== undefined) {\n nextOffset = {\n x: previousOffset.x + (previousScale - nextScale) * around.x,\n y: previousOffset.y + (previousScale - nextScale) * around.y,\n };\n }\n const next: Viewport = { scale: nextScale, offset: nextOffset };\n this._viewport = next;\n const center = around ?? { x: 0, y: 0 };\n this._events.fire('view:zoom', { scale: nextScale, center, previousScale });\n this._events.fire('view:viewport-change', { viewport: next });\n }\n\n panTo(p: Point): void {\n this._ensureLive();\n const previousOffset = { ...this._viewport.offset };\n const next: Viewport = { scale: this._viewport.scale, offset: { ...p } };\n this._viewport = next;\n this._events.fire('view:pan', { offset: next.offset, previousOffset });\n this._events.fire('view:viewport-change', { viewport: next });\n }\n\n /**\n * Read the current viewport (scale + offset). Public so consumers (and\n * the pointer input layer in `move` mode) can read the offset before\n * computing a pan delta. Returns a shallow copy; mutating the returned\n * object does NOT affect the editor.\n */\n getViewport(): Viewport {\n return { scale: this._viewport.scale, offset: { ...this._viewport.offset } };\n }\n\n /**\n * Fit the model into the current viewport. The renderer's base transform\n * already auto-fits via the surface drawBox; `fitToContent` therefore\n * resets the EDITOR viewport (zoom=1, offset=0,0) so the renderer's fit\n * is the on-screen fit. Reports the content bbox for consumer use.\n */\n fitToContent(_padding?: number): void {\n this._ensureLive();\n let minX = 0; let minY = 0; let maxX = 0; let maxY = 0; let init = false;\n for (const a of this._mol.atoms) {\n if (!init) { minX = maxX = a.p.x; minY = maxY = a.p.y; init = true; continue; }\n if (a.p.x < minX) minX = a.p.x;\n if (a.p.x > maxX) maxX = a.p.x;\n if (a.p.y < minY) minY = a.p.y;\n if (a.p.y > maxY) maxY = a.p.y;\n }\n const bbox: Rect = init\n ? { x: minX, y: minY, width: maxX - minX, height: maxY - minY }\n : { x: 0, y: 0, width: 0, height: 0 };\n const previousOffset = { ...this._viewport.offset };\n const previousScale = this._viewport.scale;\n const next: Viewport = { scale: 1, offset: { x: 0, y: 0 } };\n this._viewport = next;\n if (previousScale !== 1)\n this._events.fire('view:zoom', { scale: 1, center: { x: 0, y: 0 }, previousScale });\n if (previousOffset.x !== 0 || previousOffset.y !== 0)\n this._events.fire('view:pan', { offset: next.offset, previousOffset });\n this._events.fire('view:viewport-change', { viewport: next });\n this._events.fire('view:fit-to-content', { bbox });\n }\n\n setViewOnly(viewOnly: boolean): void {\n this._ensureLive();\n const oldValue = this._viewOnly;\n if (oldValue === viewOnly) return;\n this._viewOnly = viewOnly;\n this._events.fire('option:change', { key: 'viewOnly', oldValue, newValue: viewOnly });\n }\n\n setTheme(theme: Theme): void {\n this._ensureLive();\n const previousTheme = this._theme;\n if (previousTheme === theme) return;\n this._theme = theme;\n this._events.fire('theme:change', { theme, previousTheme });\n }\n\n // ------------------------------------------------------------------------\n // Export — delegated to the service.\n // ------------------------------------------------------------------------\n\n toSvg(options?: { width?: number; height?: number }): string {\n this._ensureLive();\n const helm = this.getHelm();\n const renderOpts: { width?: number; height?: number } = {};\n if (options?.width !== undefined) renderOpts.width = options.width;\n if (options?.height !== undefined) renderOpts.height = options.height;\n return this._service.renderToSvg(helm, renderOpts);\n }\n\n async toPng(options?: { width?: number; height?: number }): Promise<Uint8Array> {\n this._ensureLive();\n const helm = this.getHelm();\n const renderOpts: { width?: number; height?: number } = {};\n if (options?.width !== undefined) renderOpts.width = options.width;\n if (options?.height !== undefined) renderOpts.height = options.height;\n return this._service.renderToPng(helm, renderOpts);\n }\n\n // ------------------------------------------------------------------------\n // Wave A introspection (used by tests; not part of the documented API)\n // ------------------------------------------------------------------------\n\n /** @internal Returns the host element passed at construction (or `null`). */\n _getHost(): HTMLElement | null { return this._host; }\n\n /** @internal Number of subscribed observers, for diagnostics. */\n _observerCount(): number { return this._events.observerCount(); }\n\n /** @internal Returns the current highlight set (read-only). Used by the view. */\n _currentHighlights(): ReadonlySet<number> { return this._highlights; }\n\n /** @internal Returns the current theme. Used by the view. */\n _currentTheme(): Theme { return this._theme; }\n\n /** @internal Returns the mounted view (or `null` when headless). */\n _getView(): EditorView | null { return this._view; }\n\n /** @internal Returns the current viewport. */\n _getViewport(): Viewport { return this._viewport; }\n\n // ------------------------------------------------------------------------\n // Dialogs (Wave B convenience openers)\n // ------------------------------------------------------------------------\n\n openAtomProps(atom: Atom): void {\n this._ensureLive();\n if (this._host === null) return;\n if (this._atomDialog === null)\n this._atomDialog = new AtomPropsDialog(this);\n this._atomDialog.open(atom);\n }\n\n openBondProps(bond: Bond): void {\n this._ensureLive();\n if (this._host === null) return;\n if (this._bondDialog === null)\n this._bondDialog = new BondPropsDialog(this);\n this._bondDialog.open(bond);\n }\n\n openFindReplace(): void {\n this._ensureLive();\n if (this._host === null) return;\n if (this._findReplaceDialog === null)\n this._findReplaceDialog = new FindReplaceDialog(this);\n this._findReplaceDialog.open();\n }\n\n // ------------------------------------------------------------------------\n // Mode + palette-driven add (Phase 6)\n // ------------------------------------------------------------------------\n\n /** Get the current interaction mode. */\n getMode(): EditorMode { return this._mode; }\n\n /** Set the interaction mode. Fires `option:change` with key `mode`. */\n setMode(mode: EditorMode): void {\n this._ensureLive();\n const old = this._mode;\n if (old === mode) return;\n this._mode = mode;\n this._events.fire('option:change', { key: 'mode', oldValue: old, newValue: mode });\n }\n\n /** Get the currently \"primed\" palette symbol (set via `setNextAddSymbol`). */\n getNextAddSymbol(): NextAddSymbol | null { return this._nextAdd; }\n\n /** Prime a palette symbol for the next add. Pass `null` to clear. */\n setNextAddSymbol(symbol: string | null, biotype?: HelmType): void {\n this._ensureLive();\n const oldValue = this._nextAdd;\n if (symbol === null) {\n this._nextAdd = null;\n } else {\n if (biotype === undefined) return;\n this._nextAdd = { symbol, biotype };\n }\n this._events.fire('option:change', { key: 'nextAdd', oldValue, newValue: this._nextAdd });\n }\n\n /** Forward to the DnD input. No-op when no DnD layer is mounted. */\n startPaletteDrag(symbol: string, biotype: HelmType): void {\n this._ensureLive();\n if (this._dnd === null) {\n this._events.fire('dnd:start', { source: 'palette', symbol });\n return;\n }\n this._dnd.startPaletteDrag(symbol, biotype);\n }\n\n // ------------------------------------------------------------------------\n // Find / Replace helpers\n // ------------------------------------------------------------------------\n\n /** Return every atom whose symbol equals `symbol`. */\n findMonomers(symbol: string): Atom[] {\n this._ensureLive();\n const out: Atom[] = [];\n for (const a of this._mol.atoms) if (a.symbol === symbol) out.push(a);\n return out;\n }\n\n /**\n * Phase 27 (T-2701 / T-2704) — Bulk replace.\n *\n * - `replaceMonomers(find: string, replace: string)` — symbol-keyed\n * find/replace path used by the Find/Replace dialog (T-0510). Every\n * atom whose `symbol === find` has its symbol updated to `replace`.\n * - `replaceMonomers(atoms: ReadonlyArray<Atom>, newSymbol: string)` —\n * atom-list path (T-2704) used by the palette tile click handler\n * when a same-biotype selection is active. Every atom in the list\n * has its symbol updated to `newSymbol`.\n *\n * Both forms run a single `Transaction` so the listeners observe a\n * single `mol:changed` event. After the transaction commits we MUST\n * re-run `resolveMonomers` — otherwise the new atoms still point at\n * the OLD `ResolvedMonomer` (with the old symbol's colour / structure\n * metadata), and the renderer paints the old colour through the\n * `monomer.meta.colors` resolution path. This was the root cause of\n * the user-reported \"color and tooltip don't update on find/replace\n * with `sp`\" bug.\n */\n replaceMonomers(find: string, replace: string): number;\n replaceMonomers(atoms: ReadonlyArray<Atom>, newSymbol: string): number;\n replaceMonomers(arg1: string | ReadonlyArray<Atom>, arg2: string): number {\n this._ensureLive();\n let targets: ReadonlyArray<Atom>;\n let reasonKey: string;\n if (typeof arg1 === 'string') {\n const find = arg1;\n const replace = arg2;\n targets = this.findMonomers(find);\n if (targets.length === 0) return 0;\n if (find === replace) return targets.length;\n reasonKey = `${find}->${replace}`;\n } else {\n // Atom-list path. Map ids back to the CURRENT mol so a stale Atom\n // reference (e.g. captured before a previous edit) still resolves\n // correctly; skip ids that no longer exist or already carry the\n // requested symbol.\n const newSymbol = arg2;\n const seen = new Set<number>();\n const live: Atom[] = [];\n for (const a of arg1) {\n if (seen.has(a.id)) continue;\n seen.add(a.id);\n const cur = molAtomById(this._mol, a.id);\n if (cur === undefined) continue;\n if (cur.symbol === newSymbol) continue;\n live.push(cur);\n }\n if (live.length === 0) return 0;\n targets = live;\n reasonKey = `atoms[${live.length}]->${newSymbol}`;\n }\n const replace = arg2;\n const tx = new Transaction(this._mol);\n for (const a of targets) tx.updateAtom(a.id, { symbol: replace });\n const { mol } = tx.commit();\n // Phase 27 (T-2701) — Re-resolve monomers so every replaced atom\n // gets a fresh `ResolvedMonomer` pointer (with the new symbol's\n // colour / structure metadata). Without this the renderer paints\n // the OLD colour and the tooltip shows the OLD name even though\n // the symbol field is correct.\n const resolved = resolveMonomers(mol, this._service.monomerLib, { mode: 'lenient' }).mol;\n this._replaceMol(resolved, `replaceMonomers:${reasonKey}`, 'api');\n return targets.length;\n }\n\n /**\n * Nudge every selected atom by `(dx, dy)` in model units. Used by the\n * keyboard input layer for arrow-key movement.\n */\n nudgeSelection(dx: number, dy: number): void {\n this._ensureLive();\n const sel = this._selection;\n if (sel.atoms.size === 0) return;\n const tx = new Transaction(this._mol);\n for (const id of sel.atoms) {\n const a = molAtomById(this._mol, id);\n if (a === undefined) continue;\n tx.updateAtom(id, { p: { x: a.p.x + dx, y: a.p.y + dy } });\n }\n const { mol } = tx.commit();\n this._replaceMol(mol, 'nudge', 'user');\n }\n\n /** Delete every atom (and via cascade, bond) in the current selection. */\n deleteSelection(): void {\n this._ensureLive();\n const sel = this._selection;\n if (sel.atoms.size === 0 && sel.bonds.size === 0) return;\n const tx = new Transaction(this._mol);\n for (const id of sel.atoms) tx.removeAtom(id);\n for (const id of sel.bonds) tx.removeBond(id);\n const { mol } = tx.commit();\n this._selection = Selection.empty();\n this._replaceMol(mol, 'delete-selection', 'user');\n this._events.fire('selection:change', {\n selection: this._selection,\n addedAtomIds: EMPTY_FROZEN_ID_SET,\n removedAtomIds: sel.atoms,\n addedBondIds: EMPTY_FROZEN_ID_SET,\n removedBondIds: sel.bonds,\n });\n }\n\n // ------------------------------------------------------------------------\n // RNA Builder (Phase 18 T-1806)\n // ------------------------------------------------------------------------\n\n /**\n * Phase 18 (T-1806) — Append a single `sugar(base)linker` triplet to the\n * editor. The behaviour depends on the current Mol:\n *\n * - Empty editor → create `RNA1{<triplet>}$$$$V2.0`.\n * - Editor with no RNA polymer → add a new `RNA<n>{<triplet>}` polymer,\n * where `n` is the next free RNA index in the existing HELM.\n * - Editor with one or more RNA polymers → append `.<triplet>` to the\n * LAST RNA polymer's body so the user's mental model of \"extend the\n * chain\" matches the result (single polymer with the new triplet at\n * the 3' end).\n *\n * The full operation goes through `setHelm`, which re-runs parsing,\n * resolution, and layout — so a multi-polymer document keeps its\n * existing chain identities and the new monomers get sensible\n * coordinates without bespoke layout code.\n *\n * Multi-character symbols (`fl2r`, `sp`, `5meC`, …) are emitted wrapped\n * in `[...]` per the HELM grammar; single-character symbols (`r`, `A`,\n * `p`) emit naked.\n */\n appendRnaTriplet(sugar: string, base: string, linker: string): void {\n this._ensureLive();\n const triplet = formatRnaTriplet(sugar, base, linker);\n const current = this.getHelm();\n const next = spliceRnaTriplet(current, triplet);\n this.setHelm(next);\n }\n\n /**\n * Phase 19 (T-1903) / Phase 20 (T-2006) / Phase 21 (T-2103) -- Place an\n * RNA triplet as a SEPARATE polymer.\n *\n * The Phase 20 implementation round-tripped the HELM through\n * `setHelm`, then tried to restore pre-existing atom positions by\n * keying on `(chainId, positionInChain)`. That key collides for\n * every RNA base in a chain (parser leaves `positionInChain=0` on\n * branch atoms — see `parser.ts` `addToChain`), so all bases\n * collapsed onto the same restored position when more than one\n * existed (user report: \"all bases somehow end up at the same\n * place\"). Phase 21 abandons the round-trip and appends directly\n * via `Transaction.addAtom` / `addBond` so pre-existing atom\n * identity is untouched.\n *\n * Placement heuristic (T-2103):\n * * If the most recent action was also `add-triplet`, place the\n * new sugar two steps to the right of the previous one — same\n * row, growing leftward to right.\n * * Otherwise, place it below the leftmost existing atom (one\n * row gap) so the new triplet appears in free space without\n * overlapping. Mutations outside this method reset\n * `_lastAction` to `'other'`, so a tetramer-then-Add starts a\n * new row instead of jumping to the right of the most-recent\n * sugar (which would land it on top of the existing chain).\n *\n * Triplet geometry (T-2103) — linker RIGHT of sugar, base BELOW\n * sugar — matches the new RNA-Builder SVG visual. Geometry inside\n * a row mirrors the canonical r(A)p ordering, where the linker\n * sits at R2 of the sugar (3' end of the nucleotide).\n */\n placeRnaTriplet(sugar: string, base: string, linker: string): void {\n this._ensureLive();\n const step = BOND_SCALE * (this._options.bondLength ?? DEFAULT_BOND_LENGTH);\n\n // 1. Decide the new triplet's sugar position based on _lastAction.\n let newSugarP: Point;\n const atoms = this._mol.atoms;\n if (this._lastAction === 'add-triplet' && this._lastTripletSugarPos !== null) {\n // Chain to the right of the previous Add. Two steps so the\n // linker (at sugar.x + step) clears the previous sugar's base.\n newSugarP = {\n x: this._lastTripletSugarPos.x + step * 2,\n y: this._lastTripletSugarPos.y,\n };\n } else if (atoms.length === 0) {\n // Empty editor: anchor at (0, 0) so layoutMol's auto-fit centres\n // the result. The existing tests + serializer round-trip rely on\n // this anchor being at the origin.\n newSugarP = { x: 0, y: 0 };\n } else {\n // Below the leftmost existing atom, one row gap.\n //\n // Phase 25 (T-2502): vertical offset reduced from `step * 2` to\n // `step` (halved). User report: \"now its adding too much down,\n // lets half that distance.\" The 2*step gap meant a fresh triplet\n // landed two full bond-lengths below the last row, leaving an\n // empty band of canvas; a single step keeps the new row visually\n // close enough to be clearly part of the same polymer cluster.\n let minX = atoms[0]!.p.x;\n let maxY = atoms[0]!.p.y;\n for (const a of atoms) {\n if (a.p.x < minX) minX = a.p.x;\n if (a.p.y > maxY) maxY = a.p.y;\n }\n newSugarP = { x: minX, y: maxY + step };\n }\n const newLinkerP: Point = { x: newSugarP.x + step, y: newSugarP.y };\n const newBaseP: Point = { x: newSugarP.x, y: newSugarP.y + step };\n\n // 2. Allocate a fresh chain id (next free RNA<n>).\n const existingRnaIds = new Set<string>();\n for (const c of this._mol.chains) existingRnaIds.add(c.id);\n let n = 1;\n while (existingRnaIds.has(`RNA${n}`)) n += 1;\n const newChainId = `RNA${n}`;\n\n // 3. Append atoms + bonds directly via Transaction. Pre-existing\n // atoms are untouched, so their positions, ids, and bio fields\n // survive verbatim.\n const tx = new Transaction(this._mol);\n const sugarId = tx.addAtom({\n p: newSugarP,\n biotype: HelmTypes.SUGAR,\n symbol: sugar,\n chainId: newChainId,\n positionInChain: 1,\n });\n const linkerId = tx.addAtom({\n p: newLinkerP,\n biotype: HelmTypes.LINKER,\n symbol: linker,\n chainId: newChainId,\n positionInChain: 2,\n });\n // Base is a branch atom on the sugar; not added through the\n // chain's backbone. We mark it with positionInChain=0 to mirror\n // what the parser does for branch atoms (see parser.ts\n // `addToChain` -- branch atoms keep positionInChain=0).\n const baseId = tx.addAtom({\n p: newBaseP,\n biotype: HelmTypes.BASE,\n symbol: base,\n chainId: newChainId,\n // Insert at position 0 so the Transaction does NOT push the\n // base onto the chain's `backboneAtoms` after sugar+linker\n // (which would corrupt the serializer's expectation that\n // backbone = [sugar, linker]).\n positionInChain: 0,\n });\n\n // 4. Add bonds. R3(sugar) -> R1(base) branch, R2(sugar) -> R1(linker) backbone.\n tx.addBond({\n a1: sugarId,\n a2: baseId,\n kind: 'branch',\n r1: { kind: 'numeric', r: 3 },\n r2: { kind: 'numeric', r: 1 },\n });\n tx.addBond({\n a1: sugarId,\n a2: linkerId,\n kind: 'backbone',\n r1: { kind: 'numeric', r: 2 },\n r2: { kind: 'numeric', r: 1 },\n });\n\n const { mol } = tx.commit();\n\n // 5. Re-attach the sugar.branchAtom pointer + remove the base\n // from the chain's backbone (Transaction.addAtom always\n // inserts into backbone). We work on a NEW mol with the\n // fix-ups applied so the immutable graph stays consistent.\n const fixedMol = this._postProcessTripletMol(mol, sugarId, baseId, newChainId);\n\n // 6. Resolve monomers so tooltips, palette colours, atomic-mol\n // expansion all work on the new atoms.\n const resolved = resolveMonomers(fixedMol, this._service.monomerLib, { mode: 'lenient' }).mol;\n\n // 7. Commit.\n this._replaceMol(resolved, 'placeRnaTriplet', 'user');\n\n // 8. Update placement-state so the NEXT Add chains to the right\n // of THIS sugar. Any other mutation (addAtom/removeAtom/\n // setHelm/...) clears it via `_resetLastAction`.\n this._lastAction = 'add-triplet';\n this._lastTripletSugarPos = newSugarP;\n }\n\n /**\n * Phase 21 (T-2103) — Fix-up pass after `Transaction.addAtom` for an\n * RNA triplet:\n *\n * 1. Remove the base atom from the chain's `backboneAtoms` list\n * (Transaction always inserts; an RNA base is a BRANCH atom).\n * 2. Set the sugar's `branchAtom` pointer to the new base.\n *\n * Pre-condition: `mol` was produced by a Transaction commit on the\n * editor's current state; `sugarId` + `baseId` are the freshly\n * allocated ids; `chainId` is the chain we just created.\n *\n * The function returns a new Mol with the corrections baked in;\n * the input is not modified (Mol is immutable).\n */\n private _postProcessTripletMol(mol: Mol, sugarId: number, baseId: number, chainId: string): Mol {\n // Re-stamp the sugar atom with a branchAtom pointer to the base.\n const oldToNew = new Map<number, Atom>();\n const baseAtom = molAtomById(mol, baseId);\n const sugarAtom = molAtomById(mol, sugarId);\n if (baseAtom === undefined || sugarAtom === undefined) return mol;\n const newSugar: Atom = { ...sugarAtom, branchAtom: baseAtom };\n oldToNew.set(sugarId, newSugar);\n\n // Build new bonds repointing the sugar instance.\n const newBonds: Bond[] = mol.bonds.map((b) => {\n const a1 = oldToNew.get(b.a1.id) ?? b.a1;\n const a2 = oldToNew.get(b.a2.id) ?? b.a2;\n if (a1 === b.a1 && a2 === b.a2) return b;\n return { ...b, a1, a2 };\n });\n const newBondsById = new Map<number, Bond>();\n for (const b of newBonds) newBondsById.set(b.id, b);\n\n // Walk chains. The target chain's backboneAtoms drops the base\n // and re-pins the sugar/linker instances.\n const newChains: Chain[] = mol.chains.map((c) => {\n const backbone: Atom[] = [];\n for (const a of c.backboneAtoms) {\n if (c.id === chainId && a.id === baseId) continue; // remove base\n backbone.push(oldToNew.get(a.id) ?? a);\n }\n const bonds: Bond[] = c.bonds.map((b) => newBondsById.get(b.id) ?? b);\n return {\n id: c.id,\n polymerType: c.polymerType,\n backboneAtoms: backbone as readonly Atom[],\n bonds: bonds as readonly Bond[],\n ...(c.annotation !== undefined ? { annotation: c.annotation } : {}),\n };\n });\n\n const newAtoms: Atom[] = mol.atoms.map((a) => oldToNew.get(a.id) ?? a);\n\n let out: Mol = {\n atoms: newAtoms as readonly Atom[],\n bonds: newBonds as readonly Bond[],\n chains: newChains as readonly Chain[],\n groups: mol.groups,\n annotations: mol.annotations,\n };\n // Refresh continuous-ids: removing the base from the chain's\n // backbone changes the walk order vs. what Transaction computed.\n out = this._refreshContinuousIdsLocal(out);\n return out;\n }\n\n /**\n * Phase 21 (T-2103) — Mark \"the most recent user-perceived mutation\n * was NOT a triplet add\". Called from every other mutator so the\n * RNA-Builder's \"next triplet chains right\" heuristic only fires\n * after consecutive Adds.\n */\n private _resetLastAction(): void {\n this._lastAction = 'other';\n this._lastTripletSugarPos = null;\n }\n\n // ------------------------------------------------------------------------\n // Internals\n // ------------------------------------------------------------------------\n\n private _ensureLive(): void {\n if (this._destroyed) throw new Error('HelmEditor: this instance has been destroyed.');\n }\n\n private _replaceMol(mol: Mol, reason: string, source: 'user' | 'api'): void {\n const prevHelm = this._service.serialize(this._mol);\n const nextHelm = this._service.serialize(mol);\n this._mol = mol;\n this._history.push({ mol, label: reason });\n // Phase 21 (T-2103) — Any mutation invalidates the \"previous Add was\n // a triplet\" placement state. `placeRnaTriplet` re-sets `_lastAction`\n // to 'add-triplet' AFTER calling `_replaceMol`, so the reset here is\n // harmless to that path.\n this._resetLastAction();\n this._events.fire('mol:changed', {\n mol,\n changedAtomIds: new Set<number>(mol.atoms.map((a) => a.id)),\n changedBondIds: new Set<number>(mol.bonds.map((b) => b.id)),\n reason,\n });\n if (prevHelm !== nextHelm)\n this._events.fire('helm:changed', { helm: nextHelm, mol, source });\n }\n\n private _restoreFromHistory(mol: Mol, source: 'undo' | 'redo'): void {\n const prevHelm = this._service.serialize(this._mol);\n const nextHelm = this._service.serialize(mol);\n this._mol = mol;\n // Phase 21 (T-2103) — Undo/redo crosses an event boundary; reset the\n // RNA-Builder placement state so the next Add starts a new row.\n this._resetLastAction();\n this._events.fire('mol:changed', {\n mol,\n changedAtomIds: new Set<number>(mol.atoms.map((a) => a.id)),\n changedBondIds: new Set<number>(mol.bonds.map((b) => b.id)),\n reason: source,\n });\n if (prevHelm !== nextHelm)\n this._events.fire('helm:changed', { helm: nextHelm, mol, source });\n }\n}\n\n// Re-export commonly needed enums so consumers don't need a second import\n// just to spell out a biotype when calling `addAtom`.\nexport { HelmTypes };\n\n// --------------------------------------------------------------------------\n// Phase 18 (T-1806) — RNA-triplet HELM helpers.\n//\n// Pure-string utilities used by `HelmEditor.appendRnaTriplet`. Kept at the\n// module bottom so the editor class stays focused on state + events.\n// --------------------------------------------------------------------------\n\n/**\n * Wrap a HELM monomer symbol in `[...]` iff it is multi-character OR\n * contains a character that the parser would not accept as a naked\n * token. Single ASCII letters (`r`, `A`, `p`) emit naked; everything else\n * (`fl2r`, `sp`, `5meC`, `[fl2r]` already-bracketed) emits with brackets.\n */\nexport function formatHelmMonomerSymbol(symbol: string): string {\n // Caller may have passed `[fl2r]` directly — accept that as already\n // formatted so the RNA Builder can use the `<option value>` form\n // verbatim.\n if (symbol.startsWith('[') && symbol.endsWith(']')) return symbol;\n if (symbol.length === 1 && /^[A-Za-z]$/.test(symbol)) return symbol;\n return `[${symbol}]`;\n}\n\n/** Build the HELM body fragment for a single RNA triplet: `<sugar>(<base>)<linker>`. */\nexport function formatRnaTriplet(sugar: string, base: string, linker: string): string {\n return `${formatHelmMonomerSymbol(sugar)}(${formatHelmMonomerSymbol(base)})${formatHelmMonomerSymbol(linker)}`;\n}\n\n/** Escape a string for safe interpolation into a `RegExp`. */\nfunction escapeRegExp(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\n/**\n * Splice an RNA triplet body into an existing HELM string. Pure string\n * manipulation — does NOT touch the editor's Mol. Behaviour:\n *\n * 1. Empty `helm` → `RNA1{<triplet>}$$$$V2.0`.\n * 2. `helm` contains at least one `RNA<n>{...}` polymer → append\n * `.<triplet>` to the LAST such polymer's body, preserving\n * section markers and the version trailer.\n * 3. Otherwise add a new `RNA<n>{<triplet>}` polymer (`<n>` is the\n * smallest positive integer not already used by an RNA polymer).\n *\n * Exported so the unit tests can iterate the table of cases without\n * standing up a full editor.\n */\n/**\n * Phase 19 (T-1903) -- Variant of `spliceRnaTriplet` that ALWAYS creates\n * a new `RNAn{...}` polymer instead of extending the most-recent one.\n * Used by `HelmEditor.placeRnaTriplet`; gives the RNA Builder \"drop a\n * fresh triplet, do not auto-connect\" semantics.\n *\n * 1. Empty `helm` -> `RNA1{<triplet>}$$$$V2.0`.\n * 2. Non-empty `helm` -> append `|RNAm{<triplet>}` where `m` is the\n * smallest positive integer not already used by an RNA polymer.\n * Inserted immediately before the first `$` section separator so\n * section markers stay intact.\n */\nexport function spliceRnaTripletAsNewPolymer(helm: string, triplet: string): string {\n if (helm === '') return `RNA1{${triplet}}$$$$V2.0`;\n const usedIndices = new Set<number>();\n const idxRe = /RNA(\\d+)\\{/g;\n let im: RegExpExecArray | null;\n while ((im = idxRe.exec(helm)) !== null)\n usedIndices.add(Number(im[1]));\n let next = 1;\n while (usedIndices.has(next)) next += 1;\n const newPolymer = `RNA${next}{${triplet}}`;\n const sepIdx = helm.indexOf('$');\n if (sepIdx < 0) {\n // Atypical: no `$` markers. Append as a new polymer + a stock trailer.\n return `${helm}|${newPolymer}$$$$V2.0`;\n }\n const before = helm.slice(0, sepIdx);\n const after = helm.slice(sepIdx);\n const joiner = before === '' ? '' : '|';\n return `${before}${joiner}${newPolymer}${after}`;\n}\n\nexport function spliceRnaTriplet(helm: string, triplet: string): string {\n if (helm === '') return `RNA1{${triplet}}$$$$V2.0`;\n // Iterate every `RNAn{...}` occurrence; keep the last match.\n const rnaRe = /(^|\\|)(RNA\\d+)\\{([^}]*)\\}/g;\n let lastMatch: { name: string; body: string; matchText: string } | null = null;\n let m: RegExpExecArray | null;\n while ((m = rnaRe.exec(helm)) !== null) {\n lastMatch = {\n name: m[2] as string,\n body: m[3] as string,\n matchText: m[0] as string,\n };\n }\n if (lastMatch !== null) {\n const sep = lastMatch.body === '' ? '' : '.';\n const replaced = `${lastMatch.matchText.startsWith('|') ? '|' : ''}${lastMatch.name}{${lastMatch.body}${sep}${triplet}}`;\n // Replace the LAST occurrence only — a simple `.replace(matchText, ...)`\n // would replace the first occurrence. We use `lastIndexOf` to splice\n // by absolute position so a polymer body that happens to be a substring\n // of an earlier one cannot collide.\n const idx = helm.lastIndexOf(lastMatch.matchText);\n return helm.slice(0, idx) + replaced + helm.slice(idx + lastMatch.matchText.length);\n }\n // No RNA polymer yet — pick the smallest free index, then inject the\n // new polymer immediately before the `$` section separator.\n const usedIndices = new Set<number>();\n const idxRe = /RNA(\\d+)\\{/g;\n let im: RegExpExecArray | null;\n while ((im = idxRe.exec(helm)) !== null)\n usedIndices.add(Number(im[1]));\n let next = 1;\n while (usedIndices.has(next)) next += 1;\n const newPolymer = `RNA${next}{${triplet}}`;\n const sepIdx = helm.indexOf('$');\n if (sepIdx < 0) {\n // Atypical: no `$` markers. Append as a new polymer + a stock trailer.\n return `${helm}|${newPolymer}$$$$V2.0`;\n }\n const before = helm.slice(0, sepIdx);\n const after = helm.slice(sepIdx);\n const joiner = before === '' ? '' : '|';\n // Replace the regex-escaped polymer if the body was empty so we never\n // produce `|RNA1{...}|...` — the simpler `${before}${joiner}${newPolymer}${after}`\n // is correct because `before` ends at the start of the `$` block.\n void escapeRegExp;\n return `${before}${joiner}${newPolymer}${after}`;\n}\n","// Phase 18 (T-1806) / Phase 19 (T-1903) / Phase 20 (T-2002) -- RNA Builder.\n//\n// Phase 20 redesign (T-2002): the three field rows from Phase 19 are\n// replaced with a small SVG visualization of the triplet -- square\n// sugar centered, diamond base hanging below, circle linker to the\n// left. Each shape carries the monomer symbol centered inside, fills\n// from the monomer's resolved background colour (same `resolveMonomerColors`\n// the canvas uses), and is clickable to enter pick-mode for that slot.\n// Connecting lines mimic the sugar-linker R1-R2 backbone bond and the\n// sugar-base R3-R1 branch bond.\n//\n// Behaviour preserved from Phase 19:\n// * `startPickMode(field)` puts the palette into biotype pick-mode;\n// a click on a matching tile sets the slot value, any other click\n// cancels (Phase 20 T-2005 widens the cancellation policy).\n// * \"Add triplet\" calls `editor.placeRnaTriplet` which now positions\n// the new polymer next to the rightmost existing sugar (T-2006).\n// * Any combination of `r/d/m/...` sugars, `A/C/G/U/T/...` bases, and\n// `p/sp/...` linkers from the active library is supported.\n\nimport { Subject } from '../../util/observable';\nimport { HelmTypes } from '../../core/constants';\nimport type { HelmType } from '../../core/constants';\nimport type { IMonomerLib, Monomer } from '../../monomer-lib/types';\nimport { resolveMonomerColors } from '../../monomer-lib/color-rules';\nimport type { Theme } from '../../render/theme';\nimport type { Unsubscribe } from '../../util/types';\nimport type { HelmEditor } from '../editor';\nimport { formatRnaTriplet } from '../editor';\nimport { el, getDocument } from '../view/dom-utils';\nimport {\n fillTooltip,\n getOrCreateTooltipEl,\n hideTooltipEl,\n positionTooltipAt,\n renderRdkitSvg,\n type RdkitLike,\n} from '../view/tooltip-rdkit';\n\n/** SVG namespace -- used by `createElementNS` for every SVG node. */\nconst SVG_NS = 'http://www.w3.org/2000/svg';\n\n/** Options for `RnaBuilder`. */\nexport interface RnaBuilderOptions {\n documentImpl?: Document;\n}\n\n/**\n * T-3513 — Hooks the palette exposes so the RNA builder's header star can\n * favorite / unfavorite the currently-composed triplet without depending on\n * `MonomerPalette` directly.\n */\nexport interface RnaBuilderFavoriteHooks {\n isFavoriteTriplet(sugar: string, base: string, linker: string): boolean;\n toggleFavoriteTriplet(sugar: string, base: string, linker: string): void;\n}\n\n/** Which of the three triplet slots the pick-mode is targeting. */\nexport type RnaBuilderField = 'sugar' | 'base' | 'linker';\n\n/**\n * Triplet currently displayed in the builder. Returned from\n * `getSelection` so tests can introspect without poking at the DOM.\n */\nexport interface RnaBuilderSelection {\n readonly sugar: string;\n readonly base: string;\n readonly linker: string;\n /** HELM body fragment (`r(A)p`, `[fl2r]([5meC])[sp]`, ...). */\n readonly preview: string;\n}\n\n/** Internal: a single slot (clickable SVG group + shape + label text). */\ninterface SlotState {\n readonly id: RnaBuilderField;\n readonly biotype: HelmType;\n readonly groupEl: Element;\n readonly shapeEl: Element;\n readonly labelEl: Element;\n value: string;\n}\n\n/** Map field id -> biotype. */\nconst FIELD_BIOTYPE: Record<RnaBuilderField, HelmType> = {\n sugar: HelmTypes.SUGAR,\n base: HelmTypes.BASE,\n linker: HelmTypes.LINKER,\n};\n\n/** Default symbols when the library is silent. */\nconst DEFAULTS: Record<RnaBuilderField, readonly string[]> = {\n sugar: ['r', 'R'],\n base: ['A'],\n linker: ['p', 'P'],\n};\n\n// Phase 21 (T-2101) / Phase 22 (T-2203) -- SVG geometry constants.\n// All three slot shapes share the SAME nominal size (visible area\n// roughly equal) so the triplet preview reads as a balanced glyph\n// rather than \"linker is huge, sugar is small\". Layout mirrors the\n// canvas rendering of a real r(A)p triplet:\n//\n// sugar --- linker\n// |\n// base\n//\n// Phase 22 (T-2203): the user reported \"the bonds are still too short\"\n// in the Phase 21 layout (centre-to-centre distance was 50, shape\n// half-extent 18, so the VISIBLE bond strand was ~14 px). Phase 22\n// widens centre-to-centre to 70 so the visible bond reads as ~34 px\n// (70 - 2*18) -- enough to register as a real connector at the\n// builder's display zoom.\nconst VIEW_W = 200;\nconst VIEW_H = 180;\nconst SUGAR_CENTER = { x: 70, y: 60 };\nconst LINKER_CENTER = { x: 140, y: 60 };\nconst BASE_CENTER = { x: 70, y: 130 };\n// Phase 21 (T-2101): uniform \"nominal size\" across all three shapes.\n// Sugar square 36x36; linker circle r=18 (diameter 36); base diamond\n// half-diagonal 22 (so visible area ~ same as sugar; rotated 45 deg).\nconst SUGAR_HALF = 18;\nconst LINKER_RADIUS = 18;\nconst BASE_HALF = 22;\n\n/**\n * RNA Builder UI. Owned and instantiated by `MonomerPalette`. Pick\n * mode is wired through `PaletteFilterHooks` (see palette.ts) so the\n * palette gets to control which tiles are visible and which click\n * targets count as \"complete the pick\" vs \"cancel the pick\".\n *\n * @example\n * const rb = new RnaBuilder(host, editor, { paletteHooks });\n * rb.startPickMode('sugar'); // palette filters to sugars\n * rb.onValueSet.subscribe(...); // observe completion\n */\nexport class RnaBuilder {\n readonly element: HTMLElement;\n readonly onValueSet = new Subject<{ field: RnaBuilderField; symbol: string }>();\n readonly onPickModeChange = new Subject<{ field: RnaBuilderField | null }>();\n\n private readonly _editor: HelmEditor;\n private readonly _doc: Document;\n private _libUnsub: Unsubscribe;\n private readonly _slots: Map<RnaBuilderField, SlotState> = new Map();\n private readonly _preview: HTMLElement;\n private readonly _addBtn: HTMLElement;\n private readonly _hooks: PaletteFilterHooks | null;\n /** T-3513 — favorite hooks + header star (null when favorites disabled). */\n private readonly _favHooks: RnaBuilderFavoriteHooks | null;\n private _favStar: HTMLElement | null = null;\n private _pickMode: RnaBuilderField | null = null;\n private _destroyed = false;\n /**\n * Phase 29 (T-2901) -- Pick-mode hint lives INSIDE the builder body,\n * directly below the \"RNA builder\" title. When idle the element\n * carries the `hidden` attribute (CSS `display: none`) so it takes\n * NO vertical space -- the SVG triplet sits right under the title.\n * Entering pick mode removes `hidden` and sets the bold text\n * \"Pick <slot> from palette\". Replaces Phase 24's always-mounted\n * `_hintEl` on the palette, which reserved 28-32 px of empty space\n * above the search box even when no pick was in flight (user feedback:\n * \"I don't like the idea of wasting space like that\").\n */\n private readonly _hintEl: HTMLElement;\n\n constructor(host: HTMLElement, editor: HelmEditor, options?: RnaBuilderOptions & { paletteHooks?: PaletteFilterHooks; favoriteHooks?: RnaBuilderFavoriteHooks | null }) {\n this._editor = editor;\n this._doc = options?.documentImpl ?? getDocument(host);\n this._hooks = options?.paletteHooks ?? null;\n this._favHooks = options?.favoriteHooks ?? null;\n\n this.element = el(this._doc, 'div', {\n class: 'hw-rna-builder',\n 'data-testid': 'rna-builder',\n });\n // Phase 20 (T-2002): user-requested label rename \"Build triplet\" -> \"RNA builder\".\n // T-3513: title sits in a header row alongside an optional favorite star.\n // The title div keeps ONLY the text so consumers/tests reading its\n // textContent still see exactly \"RNA builder\".\n const header = el(this._doc, 'div', { class: 'hw-rna-builder__header' });\n header.appendChild(el(this._doc, 'div', { class: 'hw-rna-builder__title' }, ['RNA builder']));\n if (this._favHooks !== null) {\n const star = el(this._doc, 'span', {\n class: 'hw-rna-builder__fav hw-rna-builder__fav--disabled',\n role: 'button',\n tabindex: '0',\n 'data-testid': 'rna-builder-fav-star',\n title: 'Favorite this triplet',\n 'aria-pressed': 'false',\n 'aria-disabled': 'true',\n }, ['☆']);\n const onToggle = (e: Event): void => {\n e.stopPropagation();\n const sel = this.getSelection();\n if (sel.preview === '' || this._favHooks === null) return;\n this._favHooks.toggleFavoriteTriplet(sel.sugar, sel.base, sel.linker);\n };\n star.addEventListener('click', onToggle);\n star.addEventListener('keydown', (e: Event) => {\n const ke = e as unknown as { key: string };\n if (ke.key === 'Enter' || ke.key === ' ') onToggle(e);\n });\n this._favStar = star;\n header.appendChild(star);\n }\n this.element.appendChild(header);\n\n // Phase 29 (T-2901): hint container, hidden by default. The\n // `hidden` HTML attribute pairs with the CSS rule\n // `.hw-rna-builder__hint[hidden] { display: none; }` so an idle\n // builder reserves NO vertical space for the hint. When pick mode\n // activates we drop the attribute + set the bold text.\n this._hintEl = el(this._doc, 'div', {\n class: 'hw-rna-builder__hint',\n 'data-testid': 'rna-builder-hint',\n hidden: true,\n });\n this.element.appendChild(this._hintEl);\n\n const svg = this._buildSvg();\n this.element.appendChild(svg as unknown as Node);\n\n this._preview = el(this._doc, 'div', {\n class: 'hw-rna-builder__preview',\n 'data-testid': 'rna-builder-preview',\n });\n this.element.appendChild(this._preview);\n\n this._addBtn = el(this._doc, 'button', {\n class: 'hw-rna-builder__add',\n type: 'button',\n 'data-testid': 'rna-builder-add-btn',\n }, ['Add triplet']);\n this._addBtn.addEventListener('click', () => this._onAddClicked());\n this.element.appendChild(this._addBtn);\n\n host.appendChild(this.element);\n\n const libUnsub = editor.getService().monomerLib.onChanged.subscribe(() => {\n if (!this._destroyed) this.refresh();\n });\n // Theme changes refresh the slot colours.\n const themeUnsub = editor.on('theme:change', () => {\n if (!this._destroyed) this._repaintSlotColors();\n });\n // Compose so a single `_libUnsub` call detaches both subscriptions.\n this._libUnsub = (): void => {\n try { libUnsub(); } catch { /* idempotent */ }\n try { themeUnsub(); } catch { /* idempotent */ }\n };\n\n this._populateDefaults();\n this._updatePreview();\n this._repaintSlotColors();\n }\n\n /** Repopulate defaults from the current monomer library. */\n refresh(): void {\n this._populateDefaults();\n this._updatePreview();\n this._repaintSlotColors();\n }\n\n /** Current selection + preview text. */\n getSelection(): RnaBuilderSelection {\n const sugar = this._slots.get('sugar')?.value ?? '';\n const base = this._slots.get('base')?.value ?? '';\n const linker = this._slots.get('linker')?.value ?? '';\n const preview = sugar !== '' && base !== '' && linker !== ''\n ? formatRnaTriplet(sugar, base, linker)\n : '';\n return { sugar, base, linker, preview };\n }\n\n /** Programmatically set a slot's value (used by tests + palette pick callback). */\n setField(field: RnaBuilderField, symbol: string): void {\n const s = this._slots.get(field);\n if (s === undefined) return;\n s.value = symbol;\n this._setLabelText(s.labelEl, symbol);\n this._repaintSlot(s);\n this._updatePreview();\n this.onValueSet.next({ field, symbol });\n }\n\n /** Programmatic equivalent of clicking the slot. */\n startPickMode(field: RnaBuilderField): void {\n if (this._pickMode === field) {\n // Toggle off.\n this.cancelPickMode();\n return;\n }\n if (this._pickMode !== null) this._setSlotPressed(this._pickMode, false);\n this._pickMode = field;\n this._setSlotPressed(field, true);\n // Phase 29 (T-2901): show the bold \"Pick <slot> from palette\"\n // hint inside the builder body. The hint is `display:none` until\n // this attribute is dropped, so the SVG triplet ONLY shifts down\n // while pick mode is active.\n this._setHintActive(field);\n this.onPickModeChange.next({ field });\n if (this._hooks !== null) {\n this._hooks.beginBiotypeFilter(FIELD_BIOTYPE[field], (symbol: string) => {\n // Palette signalled \"a matching tile was clicked\" -> set value\n // + exit pick mode.\n this.setField(field, symbol);\n this._exitPickMode();\n }, () => {\n // Palette signalled \"cancel\" (any non-tile action).\n this._exitPickMode();\n });\n }\n }\n\n /** Exit pick mode without changing any value. */\n cancelPickMode(): void {\n if (this._pickMode === null) return;\n if (this._hooks !== null) this._hooks.endBiotypeFilter();\n this._exitPickMode();\n }\n\n getPickMode(): RnaBuilderField | null { return this._pickMode; }\n\n /** Trigger \"Add triplet\" programmatically (used by tests). */\n triggerAdd(): void { this._onAddClicked(); }\n\n /** Tear down: drop DOM, unsubscribe. */\n destroy(): void {\n if (this._destroyed) return;\n this._destroyed = true;\n if (this._pickMode !== null && this._hooks !== null)\n this._hooks.endBiotypeFilter();\n this._libUnsub();\n this.onValueSet.complete();\n this.onPickModeChange.complete();\n if (this.element.parentNode !== null)\n this.element.parentNode.removeChild(this.element);\n }\n\n // ------------------------------------------------------------------------\n // SVG construction\n // ------------------------------------------------------------------------\n\n /**\n * Build the SVG triplet visualization. The geometry mirrors the\n * canvas rendering of a single `r(A)p` triplet:\n *\n * linker --- sugar\n * |\n * base\n *\n * Each slot is a clickable `<g>` with `data-testid=\"rna-builder-slot-<id>\"`\n * and `role=\"button\" tabindex=\"0\"` for keyboard accessibility. The\n * shape's fill is set inline from `resolveMonomerColors`; CSS adds\n * hover/active outlines on top so the colour layer reflects the\n * current monomer while the interaction layer follows the editor's\n * accent.\n */\n private _buildSvg(): Element {\n const svg = this._doc.createElementNS(SVG_NS, 'svg');\n svg.setAttribute('class', 'hw-rna-builder__triplet');\n svg.setAttribute('viewBox', `0 0 ${VIEW_W} ${VIEW_H}`);\n svg.setAttribute('data-testid', 'rna-builder-triplet-svg');\n svg.setAttribute('role', 'group');\n svg.setAttribute('aria-label', 'Triplet preview');\n\n // Phase 21 (T-2101): bond lines run between the SHAPE EDGES, not\n // between the shape centres. Drawing centre-to-centre meant the\n // line passed through the fill of both shapes and was invisible;\n // running from edge to edge surfaces a small visible bond segment\n // (~14 px sugar-linker, ~10 px sugar-base) that matches the canvas\n // rendering's \"monomers are connected by a visible bond\" reading.\n const linkerSugarLine = this._doc.createElementNS(SVG_NS, 'line');\n linkerSugarLine.setAttribute('class', 'hw-rna-builder__bond');\n linkerSugarLine.setAttribute('x1', String(SUGAR_CENTER.x + SUGAR_HALF));\n linkerSugarLine.setAttribute('y1', String(SUGAR_CENTER.y));\n linkerSugarLine.setAttribute('x2', String(LINKER_CENTER.x - LINKER_RADIUS));\n linkerSugarLine.setAttribute('y2', String(LINKER_CENTER.y));\n svg.appendChild(linkerSugarLine);\n\n const sugarBaseLine = this._doc.createElementNS(SVG_NS, 'line');\n sugarBaseLine.setAttribute('class', 'hw-rna-builder__bond');\n sugarBaseLine.setAttribute('x1', String(SUGAR_CENTER.x));\n sugarBaseLine.setAttribute('y1', String(SUGAR_CENTER.y + SUGAR_HALF));\n sugarBaseLine.setAttribute('x2', String(BASE_CENTER.x));\n sugarBaseLine.setAttribute('y2', String(BASE_CENTER.y - BASE_HALF));\n svg.appendChild(sugarBaseLine);\n\n // Sugar slot (rounded square, centered at SUGAR_CENTER).\n const sugarShape = this._doc.createElementNS(SVG_NS, 'rect');\n sugarShape.setAttribute('class', 'hw-rna-builder__slot-shape');\n sugarShape.setAttribute('x', String(-SUGAR_HALF));\n sugarShape.setAttribute('y', String(-SUGAR_HALF));\n sugarShape.setAttribute('width', String(SUGAR_HALF * 2));\n sugarShape.setAttribute('height', String(SUGAR_HALF * 2));\n // Phase 21 (T-2101): corner radius scaled with the new 36 px side.\n sugarShape.setAttribute('rx', '8');\n sugarShape.setAttribute('ry', '8');\n const sugarSlot = this._buildSlot('sugar', 'Pick sugar (backbone)', SUGAR_CENTER, sugarShape);\n\n // Linker slot (circle, centered at LINKER_CENTER).\n const linkerShape = this._doc.createElementNS(SVG_NS, 'circle');\n linkerShape.setAttribute('class', 'hw-rna-builder__slot-shape');\n linkerShape.setAttribute('cx', '0');\n linkerShape.setAttribute('cy', '0');\n linkerShape.setAttribute('r', String(LINKER_RADIUS));\n const linkerSlot = this._buildSlot('linker', 'Pick linker', LINKER_CENTER, linkerShape);\n\n // Base slot (diamond/rhombus, centered at BASE_CENTER).\n const baseShape = this._doc.createElementNS(SVG_NS, 'path');\n baseShape.setAttribute('class', 'hw-rna-builder__slot-shape');\n baseShape.setAttribute(\n 'd',\n `M ${-BASE_HALF} 0 L 0 ${-BASE_HALF} L ${BASE_HALF} 0 L 0 ${BASE_HALF} Z`,\n );\n const baseSlot = this._buildSlot('base', 'Pick base', BASE_CENTER, baseShape);\n\n svg.appendChild(linkerSlot.groupEl);\n svg.appendChild(sugarSlot.groupEl);\n svg.appendChild(baseSlot.groupEl);\n\n this._slots.set('sugar', sugarSlot);\n this._slots.set('linker', linkerSlot);\n this._slots.set('base', baseSlot);\n\n return svg;\n }\n\n /**\n * Build a single clickable slot. Returns the SlotState capturing the\n * group, shape, and centered label-text node so subsequent paint /\n * value-set operations can splice them without re-querying the DOM.\n */\n private _buildSlot(\n id: RnaBuilderField,\n ariaLabel: string,\n center: { x: number; y: number },\n shape: Element,\n ): SlotState {\n const group = this._doc.createElementNS(SVG_NS, 'g');\n group.setAttribute('class', 'hw-rna-builder__slot');\n group.setAttribute('data-slot', id);\n group.setAttribute('data-testid', `rna-builder-slot-${id}`);\n group.setAttribute('role', 'button');\n group.setAttribute('tabindex', '0');\n group.setAttribute('aria-label', ariaLabel);\n group.setAttribute('aria-pressed', 'false');\n group.setAttribute('transform', `translate(${center.x} ${center.y})`);\n\n group.appendChild(shape);\n\n const text = this._doc.createElementNS(SVG_NS, 'text');\n text.setAttribute('class', 'hw-rna-builder__slot-label');\n text.setAttribute('data-testid', `rna-builder-${id}-value`);\n text.setAttribute('x', '0');\n text.setAttribute('y', '0');\n text.setAttribute('text-anchor', 'middle');\n text.setAttribute('dominant-baseline', 'central');\n group.appendChild(text);\n\n const onActivate = (e: Event): void => {\n e.stopPropagation();\n this.startPickMode(id);\n };\n group.addEventListener('click', onActivate);\n group.addEventListener('keydown', (e: Event) => {\n const ke = e as unknown as { key: string };\n if (ke.key === 'Enter' || ke.key === ' ') onActivate(e);\n });\n // Phase 22 (T-2202) -- hover tooltip for the slot. Shows the slot\n // type + current value (e.g. \"Sugar: r\") in the headline, the\n // resolved monomer name as the subline, the RDKit structure SVG\n // (when available), and a hint that clicking opens the picker.\n group.addEventListener('mouseenter', (e: Event) => this._showSlotTooltip(id, e));\n group.addEventListener('mouseleave', () => this._hideSlotTooltip());\n group.addEventListener('focus', () => this._showSlotTooltip(id, null));\n group.addEventListener('blur', () => this._hideSlotTooltip());\n\n return { id, biotype: FIELD_BIOTYPE[id], groupEl: group, shapeEl: shape, labelEl: text, value: '' };\n }\n\n // ------------------------------------------------------------------------\n // Slot paint / state\n // ------------------------------------------------------------------------\n\n /**\n * Phase 21 (T-2102) -- Set the slot's visible label.\n *\n * Long monomer symbols overflowed the slot shape and rendered as\n * unreadable mush (user report \"the text fitting does not work at\n * all and terribly overflows\"). We now:\n *\n * 1. Pick a font-size that shrinks as the symbol gets longer\n * (see `fontSizeForLabel`). Single-char `r` / `A` / `p` stays\n * at 16 px; `[5meC]` (5 chars) renders at 8 px; symbols longer\n * than 10 chars get truncated to \"abcdefghi...\" and the FULL\n * symbol is shown via a sibling `<title>` element (native\n * hover tooltip).\n * 2. Set `text-anchor=\"middle\"` and `dominant-baseline=\"central\"`\n * on EVERY update -- the SVG <text> node may have been\n * constructed without these attributes if the slot existed\n * before this method ran; calling `setAttribute` is idempotent.\n * 3. Replace any text child cleanly (we remove pre-existing text\n * nodes but PRESERVE a sibling <title> so screen readers and\n * browser tooltips continue to expose the full symbol).\n */\n private _setLabelText(label: Element, text: string): void {\n // Strip existing text children but keep <title> in place.\n const toRemove: ChildNode[] = [];\n for (let n = label.firstChild; n !== null; n = n.nextSibling) {\n if (n.nodeName === 'title' || n.nodeName === 'TITLE') continue;\n toRemove.push(n as unknown as ChildNode);\n }\n for (const n of toRemove) label.removeChild(n);\n const fontSize = fontSizeForLabel(text);\n label.setAttribute('font-size', String(fontSize));\n label.setAttribute('text-anchor', 'middle');\n label.setAttribute('dominant-baseline', 'central');\n const visible = text.length > 10 ? `${text.slice(0, 9)}…` : text;\n label.appendChild(this._doc.createTextNode(visible));\n // Ensure a <title> sibling carries the FULL symbol so hovering\n // the slot surfaces the untruncated name in the native browser\n // tooltip. We rely on the slot <g> being the parent (the <text>\n // node's parentNode), since SVG <text> does not nest <title>.\n const slotGroup = label.parentNode as Element | null;\n if (slotGroup !== null) {\n let titleEl: Element | null = null;\n for (let n = slotGroup.firstChild; n !== null; n = n.nextSibling) {\n if (n.nodeName === 'title' || n.nodeName === 'TITLE') {\n titleEl = n as unknown as Element;\n break;\n }\n }\n if (titleEl === null) {\n titleEl = this._doc.createElementNS(SVG_NS, 'title');\n // Insert as first child so screen readers pick it up before\n // visible text content.\n slotGroup.insertBefore(titleEl as unknown as Node, slotGroup.firstChild);\n }\n while (titleEl.firstChild !== null) titleEl.removeChild(titleEl.firstChild);\n titleEl.appendChild(this._doc.createTextNode(text));\n }\n }\n\n /** Repaint every slot's fill / text / stroke from the active library + theme. */\n private _repaintSlotColors(): void {\n for (const s of this._slots.values()) this._repaintSlot(s);\n }\n\n /** Repaint one slot. Resolves the colour triple from the active library. */\n private _repaintSlot(s: SlotState): void {\n const lib = this._editor.getService().monomerLib;\n const theme = this._theme();\n const monomer = s.value === '' ? null : findMonomerForBiotype(lib, s.biotype, s.value);\n const colors = resolveMonomerColors(monomer, s.biotype, theme.colorTheme);\n // Set fill / stroke / text-fill as inline styles so the\n // computed-style check in tests resolves to the per-monomer colour\n // (CSS class rules don't carry per-monomer overrides). We use\n // `style` rather than `setAttribute('fill', ...)` so cascading\n // hover/active rules can override via class selectors without\n // attribute-vs-CSS specificity surprises.\n (s.shapeEl as unknown as { style: { fill: string; stroke: string } }).style.fill = colors.background;\n (s.shapeEl as unknown as { style: { fill: string; stroke: string } }).style.stroke = colors.line;\n (s.labelEl as unknown as { style: { fill: string } }).style.fill = colors.text;\n }\n\n // ------------------------------------------------------------------------\n // Defaults + preview\n // ------------------------------------------------------------------------\n\n private _populateDefaults(): void {\n const lib = this._editor.getService().monomerLib;\n for (const [id, s] of this._slots.entries()) {\n const monomers = monomersFor(lib, s.biotype);\n const preferred = preferredSymbol(monomers, DEFAULTS[id]);\n const want = s.value !== '' && monomers.some((m) => m.symbol === s.value)\n ? s.value\n : (preferred ?? (monomers[0]?.symbol ?? ''));\n s.value = want;\n this._setLabelText(s.labelEl, want);\n }\n }\n\n private _updatePreview(): void {\n const sel = this.getSelection();\n while (this._preview.firstChild !== null)\n this._preview.removeChild(this._preview.firstChild);\n this._preview.appendChild(this._doc.createTextNode(sel.preview === '' ? '-' : sel.preview));\n this._updateFavStar(sel);\n }\n\n /**\n * T-3513 — Reflect the favorite state of the composed triplet in the\n * header star (filled ★ when favorited, hollow ☆ otherwise). Disabled\n * (dimmed, non-pressable) while the triplet is incomplete.\n */\n private _updateFavStar(sel: RnaBuilderSelection): void {\n if (this._favStar === null || this._favHooks === null) return;\n const complete = sel.preview !== '';\n const fav = complete && this._favHooks.isFavoriteTriplet(sel.sugar, sel.base, sel.linker);\n while (this._favStar.firstChild !== null) this._favStar.removeChild(this._favStar.firstChild);\n this._favStar.appendChild(this._doc.createTextNode(fav ? '★' : '☆'));\n this._favStar.setAttribute('aria-pressed', fav ? 'true' : 'false');\n if (complete) this._favStar.removeAttribute('aria-disabled');\n else this._favStar.setAttribute('aria-disabled', 'true');\n const cls = 'hw-rna-builder__fav' + (fav ? ' hw-rna-builder__fav--active' : '') + (complete ? '' : ' hw-rna-builder__fav--disabled');\n this._favStar.setAttribute('class', cls);\n }\n\n private _onAddClicked(): void {\n const sel = this.getSelection();\n if (sel.preview === '') return;\n // Phase 19 (T-1903): NEVER auto-connect. Always create a new\n // RNAn{...} polymer. Phase 20 (T-2006): the editor positions the\n // new triplet's sugar next to the rightmost existing sugar.\n this._editor.placeRnaTriplet(sel.sugar, sel.base, sel.linker);\n }\n\n private _exitPickMode(): void {\n if (this._pickMode === null) return;\n const prev = this._pickMode;\n this._setSlotPressed(prev, false);\n this._pickMode = null;\n // Phase 29 (T-2901): hide the builder hint. Setting the `hidden`\n // attribute restores `display: none` so the SVG triplet returns to\n // its original position directly under the title.\n this._setHintActive(null);\n this.onPickModeChange.next({ field: null });\n }\n\n /**\n * Phase 29 (T-2901) -- Toggle the in-builder pick-mode hint.\n *\n * When `field === null`, restore the `hidden` attribute and empty\n * the text so the element disappears from layout. When `field` is a\n * slot id, drop `hidden` and set the bold \"Pick <slot> from palette\"\n * text the user asked for. Implementation uses the standard `hidden`\n * HTML attribute (default CSS `display: none`) rather than\n * `visibility: hidden`, on purpose: the user explicitly wanted to\n * stop wasting space when the hint is idle, so the box has to\n * collapse, not just turn invisible.\n */\n private _setHintActive(field: RnaBuilderField | null): void {\n const hint = this._hintEl;\n while (hint.firstChild !== null) hint.removeChild(hint.firstChild);\n if (field === null) {\n hint.setAttribute('hidden', '');\n return;\n }\n hint.removeAttribute('hidden');\n const label = field === 'sugar' ? 'sugar'\n : field === 'base' ? 'base'\n : 'linker';\n hint.appendChild(this._doc.createTextNode(`Pick ${label} from palette`));\n }\n\n private _setSlotPressed(id: RnaBuilderField, pressed: boolean): void {\n const s = this._slots.get(id);\n if (s === undefined) return;\n s.groupEl.setAttribute('aria-pressed', pressed ? 'true' : 'false');\n const cur = s.groupEl.getAttribute('class') ?? '';\n const stripped = cur.split(/\\s+/).filter((c) => c !== 'hw-rna-builder__slot--active').join(' ');\n s.groupEl.setAttribute('class', pressed ? `${stripped} hw-rna-builder__slot--active` : stripped);\n }\n\n private _theme(): Theme {\n const ed = this._editor as unknown as { _currentTheme?(): Theme };\n if (typeof ed._currentTheme === 'function') return ed._currentTheme();\n return this._editor.getService().theme;\n }\n\n // ------------------------------------------------------------------------\n // Phase 22 (T-2202) -- Slot hover tooltip with structure + click hint.\n // ------------------------------------------------------------------------\n\n /**\n * Per-monomer cache of the RDKit structure SVG used in slot tooltips.\n * Identical contract to the palette / view caches: bounded LRU, the\n * symbol composite key matches the `(polymerType, symbol)` couple.\n */\n private readonly _builderRdkitCache: Map<string, string | null> = new Map();\n private static readonly _BUILDER_RDKIT_CACHE_LIMIT = 256;\n\n private _rdkit(): RdkitLike | null {\n const opts = (this._editor.getService() as unknown as { options?: { rdkitModule?: unknown } }).options;\n const mod = opts?.rdkitModule;\n return (mod === undefined || mod === null) ? null : (mod as RdkitLike);\n }\n\n private _builderRdkitSvgFor(monomer: Monomer | null): string | null {\n if (monomer === null) return null;\n const smiles = monomer.smiles;\n if (smiles === undefined || smiles === '') return null;\n const rdkit = this._rdkit();\n if (rdkit === null) return null;\n const key = `${monomer.polymerType}:${monomer.symbol}`;\n if (this._builderRdkitCache.has(key))\n return this._builderRdkitCache.get(key) ?? null;\n // Phase 23 (T-2303): bump source size to 220x140 so the rendered\n // structure fills the tooltip width. Inner svg viewBox handles\n // down-scaling for narrower tooltips.\n const svg = renderRdkitSvg(rdkit, smiles, 220, 140);\n if (this._builderRdkitCache.size >= RnaBuilder._BUILDER_RDKIT_CACHE_LIMIT) {\n const oldest = this._builderRdkitCache.keys().next().value;\n if (oldest !== undefined) this._builderRdkitCache.delete(oldest);\n }\n this._builderRdkitCache.set(key, svg);\n return svg;\n }\n\n private _slotHint(id: RnaBuilderField): string {\n switch (id) {\n case 'sugar': return 'Click to choose a different sugar';\n case 'base': return 'Click to choose a different base';\n case 'linker': return 'Click to choose a different linker';\n }\n }\n\n private _slotTitle(id: RnaBuilderField, value: string): string {\n const v = value === '' ? '?' : value;\n switch (id) {\n case 'sugar': return `Sugar: ${v}`;\n case 'base': return `Base: ${v}`;\n case 'linker': return `Linker: ${v}`;\n }\n }\n\n private _showSlotTooltip(id: RnaBuilderField, evt: Event | null): void {\n const slot = this._slots.get(id);\n if (slot === undefined) return;\n const lib = this._editor.getService().monomerLib;\n const set = lib.getMonomerSet(slot.biotype);\n const monomer = slot.value === '' ? null : (set.get(slot.value) ?? null);\n const tip = getOrCreateTooltipEl(this._doc, 'builder-tooltip', 'hw-builder-tooltip');\n fillTooltip(this._doc, tip, {\n symbol: this._slotTitle(id, slot.value),\n name: monomer?.name ?? '',\n rdkitSvg: this._builderRdkitSvgFor(monomer),\n rdkitTestid: 'builder-tooltip-rdkit-svg',\n hint: this._slotHint(id),\n });\n // Prefer the pointer's client coords when available; fall back to\n // the slot's own bbox so keyboard-focus tooltips still position.\n const me = evt as unknown as { clientX?: number; clientY?: number } | null;\n if (me !== null && typeof me.clientX === 'number' && typeof me.clientY === 'number') {\n positionTooltipAt(tip, me.clientX, me.clientY);\n } else {\n const groupEl = slot.groupEl as unknown as { getBoundingClientRect?: () => { left: number; top: number; width: number; height: number } };\n const rect = typeof groupEl.getBoundingClientRect === 'function' ? groupEl.getBoundingClientRect() : null;\n if (rect !== null) positionTooltipAt(tip, rect.left + rect.width / 2, rect.top + rect.height / 2);\n else positionTooltipAt(tip, 0, 0);\n }\n }\n\n private _hideSlotTooltip(): void {\n const tip = this._doc.body.querySelector('[data-testid=\"builder-tooltip\"]') as HTMLElement | null;\n hideTooltipEl(tip);\n }\n}\n\n/**\n * Hooks the palette exposes to the RNA Builder so the builder can\n * activate / deactivate biotype filtering without taking a direct\n * dependency on `MonomerPalette`'s internals.\n */\nexport interface PaletteFilterHooks {\n /**\n * Start filtering the palette to only show tiles whose biotype\n * matches `biotype`. Adds a \"Picking X\" hint banner. `onPick` is\n * called when the user clicks a matching tile; `onCancel` is called\n * for any other user action (canvas / toolbar / outside click /\n * Escape). The hooks are responsible for unwiring themselves on\n * either resolution.\n */\n beginBiotypeFilter(\n biotype: HelmType,\n onPick: (symbol: string) => void,\n onCancel: () => void,\n ): void;\n /** Force-end the current pick mode (e.g. RnaBuilder.cancelPickMode). */\n endBiotypeFilter(): void;\n}\n\n/**\n * Phase 21 (T-2102) -- Pick an SVG `font-size` that keeps the label\n * inside the 36 px slot shape. Mirrors the canvas tile font-sizing\n * heuristic so the slot symbol reads at the same visual weight as\n * the palette tile.\n *\n * Tested values: `r` (1 char) -> 16; `5meC` (4 chars) -> 11; `[fl2r]` -> 8.\n * The piecewise function avoids per-char measurement -- linkedom has\n * no SVG text-metrics API and the production code path runs in real\n * browsers anyway.\n */\nexport function fontSizeForLabel(symbol: string): number {\n const len = symbol.length;\n if (len <= 2) return 16;\n if (len <= 4) return 11;\n if (len <= 7) return 8;\n if (len <= 10) return 6;\n return 5;\n}\n\nfunction monomersFor(lib: IMonomerLib, biotype: HelmType): Monomer[] {\n const map = lib.getMonomerSet(biotype);\n const out: Monomer[] = [];\n for (const m of map.values()) out.push(m);\n out.sort((a, b) => a.symbol.localeCompare(b.symbol));\n return out;\n}\n\nfunction preferredSymbol(monomers: readonly Monomer[], preferences: readonly string[]): string | null {\n const set = new Set<string>();\n for (const m of monomers) set.add(m.symbol);\n for (const p of preferences) if (set.has(p)) return p;\n return null;\n}\n\n/** Find the Monomer in `lib` whose `symbol` matches AND maps to `biotype`. */\nfunction findMonomerForBiotype(lib: IMonomerLib, biotype: HelmType, symbol: string): Monomer | null {\n const set = lib.getMonomerSet(biotype);\n return set.get(symbol) ?? null;\n}\n","// T-0601 / T-1421 — `MonomerPalette` — the left-side monomer explorer.\n//\n// Phase 14C (T-1421..T-1428) redesign — tabbed view:\n//\n// Before: a single scrollable pane with every polymer-type section stacked\n// back-to-back, all visible at once. Group headers (natural-analog letters)\n// were tiny `font-size:11px; color:muted` strings — easy to miss.\n//\n// After: one tab per polymer type (Favorites first, then Peptides, RNA,\n// CHEM, BLOB, G — but only those tab buttons whose corresponding polymer\n// type has at least one monomer in the library show up). Each pane keeps\n// its own grouping (natural-analog for PEPTIDE; sugar/base/linker for RNA;\n// flat for CHEM/BLOB/G). Group headers are now bold uppercase text with a\n// subtle bottom border.\n//\n// Favorites pane: tiles for every starred monomer, regardless of polymer\n// type, grouped by natural-analog. Favorites live in `localStorage` under\n// `hwe:palette:favorites`. Loading drops symbols that no longer resolve\n// in the active library, but keeps them in storage so a user can swap\n// libraries back and not lose the list.\n//\n// Layering: editor-layer module; imports core constants, monomer-lib types,\n// render theme types, and the local `tile.ts` helper. No `globalThis.document`.\n\nimport type { Atom } from '../../core/model';\nimport type { HelmType, PolymerType } from '../../core/constants';\nimport { HelmTypes, PolymerTypes } from '../../core/constants';\nimport type { IMonomerLib, Monomer } from '../../monomer-lib/types';\nimport { helm2Type } from '../../monomer-lib/library';\nimport { hexToHsl, resolveMonomerColors, soften } from '../../monomer-lib/color-rules';\nimport type { Theme } from '../../render/theme';\nimport type { Unsubscribe } from '../../util/types';\nimport { Subject } from '../../util/observable';\nimport type { HelmEditor } from '../editor';\nimport { formatRnaTriplet } from '../editor';\nimport { getDocument, el } from '../view/dom-utils';\nimport {\n fillTooltip,\n fillTripletTooltip,\n getOrCreateTooltipEl,\n hideTooltipEl,\n positionTooltipAt,\n renderRdkitSvg,\n type RdkitLike,\n type TripletTooltipPart,\n} from '../view/tooltip-rdkit';\nimport { buildTile, buildTripletChip } from './tile';\nimport { RnaBuilder } from './rna-builder';\nimport type { PaletteFilterHooks, RnaBuilderSelection } from './rna-builder';\n\n// --------------------------------------------------------------------------\n// Tab model\n// --------------------------------------------------------------------------\n\n/** Sentinel polymer-type-like string used as the \"Favorites\" tab id. */\nconst FAVORITES_TAB = 'Favorites';\n/** Sentinel polymer-type used for any extra-polymer-type slot (e.g. `G`). */\ntype ExtraPolymerType = 'G';\n/** Tab id — either the favorites sentinel, a known polymer type, or an\n * extra polymer-type string we explicitly allow (e.g. `G`). */\ntype TabId = typeof FAVORITES_TAB | PolymerType | ExtraPolymerType;\n\n/** Display label for each tab id. */\nconst TAB_LABELS: Record<TabId, string> = {\n [FAVORITES_TAB]: '★ Favorites',\n [PolymerTypes.PEPTIDE]: 'Peptides',\n [PolymerTypes.RNA]: 'RNA',\n [PolymerTypes.CHEM]: 'CHEM',\n [PolymerTypes.BLOB]: 'BLOB',\n G: 'G',\n};\n\n/** Fixed display order. Favorites first; remaining types in the\n * user-requirement order (Peptides → RNA → CHEM → BLOB → G). */\nconst TAB_ORDER: TabId[] = [\n FAVORITES_TAB,\n PolymerTypes.PEPTIDE,\n PolymerTypes.RNA,\n PolymerTypes.CHEM,\n PolymerTypes.BLOB,\n 'G',\n];\n\n/** LocalStorage keys (stable across editor instances). */\nconst FAVORITES_STORAGE_KEY = 'hwe:palette:favorites';\nconst ACTIVE_TAB_STORAGE_KEY = 'hwe:palette:active-tab';\n/** Legacy (pre-Phase 14C) favorites storage key. We read it on load for\n * backward compatibility — values were `${polymerType}|${symbol}` pairs. */\nconst LEGACY_FAVORITES_STORAGE_KEY = 'hwe.palette.favorites';\n\n// --------------------------------------------------------------------------\n// Options + public API\n// --------------------------------------------------------------------------\n\n/** Options for `MonomerPalette`. */\nexport interface MonomerPaletteOptions {\n /** Optional `Document` (for linkedom in tests). */\n documentImpl?: Document;\n /** Group tiles under each section by `naturalAnalog`. Default `true`. */\n groupByNaturalAnalog?: boolean;\n /** Allow toggling the star icon to favorite a monomer. Default `true`. */\n allowFavorites?: boolean;\n /** Force the initially-active tab; if absent, read from localStorage,\n * falling back to `Favorites`. */\n initialTab?: string;\n}\n\n/**\n * A favorited RNA triplet (sugar + base + linker). T-3513 — favorites can\n * hold whole triplets, not just single monomers; they render as small chips\n * (`r(A)p`) in the Favorites pane and can be inserted with one click.\n */\nexport interface FavoriteTriplet {\n readonly sugar: string;\n readonly base: string;\n readonly linker: string;\n}\n\n/** Stable key for a triplet (pipe-joined for dedup / membership checks). */\nfunction tripletKey(t: FavoriteTriplet): string {\n return `${t.sugar}|${t.base}|${t.linker}`;\n}\n\n/** Storage shape persisted to `localStorage`. */\ninterface StoredFavorites { symbols: string[]; triplets?: FavoriteTriplet[] }\n\n/** Payload emitted by `favoritesChanged`. */\nexport interface FavoritesChangeEvent {\n favorites: string[];\n /** T-3513 — favorited RNA triplets in insertion order. */\n triplets: FavoriteTriplet[];\n}\n\n/**\n * Left-side palette of monomer tiles. Each tile is draggable and clickable;\n * clicks \"prime\" the editor's next-add slot, drags initiate a palette DnD.\n *\n * @example\n * const palette = new MonomerPalette(host, editor);\n * // ... user clicks a tile → editor.getNextAddSymbol() returns {symbol, biotype}\n */\nexport class MonomerPalette {\n readonly element: HTMLElement;\n\n /** Multicast notifier — fires whenever `_favorites` mutates. */\n readonly favoritesChanged: Subject<FavoritesChangeEvent> = new Subject<FavoritesChangeEvent>();\n\n private readonly _editor: HelmEditor;\n private readonly _doc: Document;\n private readonly _opts: Required<Omit<MonomerPaletteOptions, 'initialTab'>>;\n private readonly _libUnsub: Unsubscribe;\n private readonly _themeUnsub: Unsubscribe;\n /** Phase 25 (T-2504) -- sync tile primed-state with external nextAdd changes. */\n private _nextAddUnsub: Unsubscribe = () => undefined;\n /** Favorites in the order they were starred (display order). Stored as a\n * Set for O(1) membership queries; insertion order is preserved. */\n private readonly _favorites: Set<string> = new Set<string>();\n /** T-3513 — favorited RNA triplets, in insertion order (display order). */\n private readonly _favoriteTriplets: FavoriteTriplet[] = [];\n /** T-3513 — captured RNA-builder selection, restored after a `_render()`\n * rebuild so toggling a favorite doesn't reset the user's composed triplet. */\n private _pendingRnaSelection: RnaBuilderSelection | null = null;\n /** Tabs that should render (subset of TAB_ORDER); recomputed every render. */\n private _visibleTabs: TabId[] = [FAVORITES_TAB];\n /** Currently active tab. */\n private _activeTab: TabId = FAVORITES_TAB;\n private _searchInput: HTMLInputElement | null = null;\n private _filter: string = '';\n private _destroyed: boolean = false;\n /**\n * Phase 18 (T-1806) — RNA Builder. Lazily created when the RNA pane\n * renders; destroyed and re-created across full re-renders so its\n * `<select>` lists pick up the latest library.\n */\n private _rnaBuilder: RnaBuilder | null = null;\n\n constructor(host: HTMLElement, editor: HelmEditor, options?: MonomerPaletteOptions) {\n this._editor = editor;\n this._doc = options?.documentImpl ?? getDocument(host);\n this._opts = {\n documentImpl: this._doc,\n groupByNaturalAnalog: options?.groupByNaturalAnalog ?? true,\n allowFavorites: options?.allowFavorites ?? true,\n };\n\n this._loadFavorites();\n this._activeTab = this._resolveInitialTab(options?.initialTab);\n\n this.element = el(this._doc, 'div', { class: 'hw-palette', 'data-testid': 'palette' });\n host.appendChild(this.element);\n\n this._render();\n\n this._libUnsub = editor.getService().monomerLib.onChanged.subscribe(() => {\n if (!this._destroyed) this.refresh();\n });\n this._themeUnsub = editor.on('theme:change', () => {\n if (!this._destroyed) this.refresh();\n });\n // Phase 25 (T-2504): when an EXTERNAL caller mutates `nextAdd` (e.g.\n // the status-bar `×` button calls `setNextAddSymbol(null)`), the\n // palette tiles' `aria-pressed` + `--primed` class must update to\n // reflect the new state. The tile click handler updates the\n // classes itself, but external setters (status bar, programmatic\n // toolbar, future Esc-to-clear) only fire `option:change` and would\n // otherwise leave the previously-primed tile in a stuck pressed\n // state. The subscription is cheap (O(tiles) DOM attribute writes\n // per fire) and idempotent (re-running on the same state is a\n // no-op).\n this._nextAddUnsub = editor.on('option:change', (e) => {\n if (this._destroyed) return;\n if (e.key === 'nextAdd') this._updatePrimedClasses();\n });\n }\n\n // ------------------------------------------------------------------------\n // Public API\n // ------------------------------------------------------------------------\n\n /** Re-render the full palette. */\n refresh(): void { this._render(); }\n\n /** Apply a case-insensitive substring filter to symbol / name. */\n setFilter(query: string): void {\n this._filter = query.trim().toLowerCase();\n if (this._searchInput !== null && this._searchInput.value !== query)\n this._searchInput.value = query;\n this._applyFilter();\n }\n\n /** Activate a tab. No-op if the tab is not visible (no monomers / no favs). */\n setActiveTab(tabId: string): void {\n if (!this._visibleTabs.includes(tabId as TabId)) return;\n this._activeTab = tabId as TabId;\n this._persistActiveTab();\n this._applyActiveTabClasses();\n this._applyFilter();\n }\n\n /** Currently-active tab id. */\n getActiveTab(): string { return this._activeTab; }\n\n /**\n * Return favorite symbols that resolve in the current library. The order\n * matches insertion order (newest favorites appear last). Symbols stored\n * in localStorage that do not resolve in the active library are kept in\n * storage but excluded from this list.\n */\n getFavorites(): string[] {\n const lib = this._editor.getService().monomerLib;\n const out: string[] = [];\n for (const sym of this._favorites) {\n if (this._findMonomer(lib, sym) !== null) out.push(sym);\n }\n return out;\n }\n\n /**\n * Add or remove a symbol from the favorites list. Fires\n * `favoritesChanged` on every state change (no-op if the symbol's state\n * is already the requested value).\n */\n setFavorite(symbol: string, isFavorite: boolean): void {\n const before = this._favorites.has(symbol);\n if (isFavorite === before) return;\n if (isFavorite) this._favorites.add(symbol);\n else this._favorites.delete(symbol);\n this._saveFavorites();\n // T-3518 — the hovered tile is about to be re-rendered away, so its\n // `mouseleave` will never fire; hide the tooltip explicitly or it would\n // be stranded on screen (esp. when removing the last tile in a section).\n this._hidePaletteTooltips();\n this._render();\n this._emitFavoritesChanged();\n }\n\n /** Drop every favorite (monomers AND triplets) from storage and memory. */\n clearFavorites(): void {\n if (this._favorites.size === 0 && this._favoriteTriplets.length === 0) return;\n this._favorites.clear();\n this._favoriteTriplets.length = 0;\n this._saveFavorites();\n this._render();\n this._emitFavoritesChanged();\n }\n\n /**\n * T-3513 — Favorited RNA triplets in insertion order. Returned as a copy\n * so callers can't mutate the internal list.\n */\n getFavoriteTriplets(): FavoriteTriplet[] {\n return this._favoriteTriplets.map((t) => ({ ...t }));\n }\n\n /** True iff the (sugar, base, linker) triplet is currently favorited. */\n isFavoriteTriplet(sugar: string, base: string, linker: string): boolean {\n const key = tripletKey({ sugar, base, linker });\n return this._favoriteTriplets.some((t) => tripletKey(t) === key);\n }\n\n /**\n * Add or remove a whole RNA triplet from favorites. Fires\n * `favoritesChanged` on every state change (no-op if already in the\n * requested state). A re-render follows; the RNA builder's composed\n * selection is preserved across it.\n */\n setFavoriteTriplet(sugar: string, base: string, linker: string, isFavorite: boolean): void {\n const key = tripletKey({ sugar, base, linker });\n const idx = this._favoriteTriplets.findIndex((t) => tripletKey(t) === key);\n const before = idx >= 0;\n if (isFavorite === before) return;\n if (isFavorite) this._favoriteTriplets.push({ sugar, base, linker });\n else this._favoriteTriplets.splice(idx, 1);\n this._saveFavorites();\n this._hidePaletteTooltips();\n this._render();\n this._emitFavoritesChanged();\n }\n\n /** T-3518 — hide the palette / triplet hover tooltips (see setFavorite). */\n private _hidePaletteTooltips(): void {\n const body = this._doc.body;\n hideTooltipEl(body.querySelector('[data-testid=\"palette-tooltip\"]') as HTMLElement | null);\n hideTooltipEl(body.querySelector('[data-testid=\"palette-triplet-tooltip\"]') as HTMLElement | null);\n }\n\n private _emitFavoritesChanged(): void {\n this.favoritesChanged.next({ favorites: this.getFavorites(), triplets: this.getFavoriteTriplets() });\n }\n\n /** Tear down: detach DOM, unsubscribe listeners. */\n destroy(): void {\n if (this._destroyed) return;\n this._destroyed = true;\n if (this._rnaBuilder !== null) { this._rnaBuilder.destroy(); this._rnaBuilder = null; }\n this._libUnsub();\n this._themeUnsub();\n // Phase 25 (T-2504): release the option:change subscription.\n this._nextAddUnsub();\n if (this.element.parentNode !== null)\n this.element.parentNode.removeChild(this.element);\n }\n\n /**\n * Phase 18 (T-1806) — RNA Builder accessor. Returns `null` when the\n * RNA pane isn't currently mounted (e.g. the library has no RNA\n * monomers). Used by tests + the demo `__hwe__` bridge.\n */\n getRnaBuilder(): RnaBuilder | null { return this._rnaBuilder; }\n\n /** Phase 19 (T-1903) — biotype pick-mode state machine. */\n private _activeBiotypeFilter: HelmType | null = null;\n private _pickResolver: ((symbol: string) => void) | null = null;\n private _pickCanceller: (() => void) | null = null;\n private _pickEscapeListener: ((e: KeyboardEvent) => void) | null = null;\n /**\n * Phase 20 (T-2005) — Document-level mousedown listener attached on\n * pick-mode entry. ANY click whose target is NOT a tile of the\n * matching biotype (and NOT the in-flight edit button that started\n * the pick) cancels pick mode. Detached on exit.\n */\n private _pickOutsideClickListener: ((e: Event) => void) | null = null;\n\n /** True iff a biotype pick-mode is currently active. */\n hasActiveBiotypeFilter(): boolean { return this._activeBiotypeFilter !== null; }\n\n /** Returns the currently-active biotype filter (null when inactive). */\n getActiveBiotypeFilter(): HelmType | null { return this._activeBiotypeFilter; }\n\n /**\n * Programmatically resolve / cancel pick-mode. Exposed for tests and\n * for outside-click handlers in the host app.\n */\n cancelBiotypeFilter(): void {\n if (this._activeBiotypeFilter === null) return;\n const cancel = this._pickCanceller;\n this._teardownPickMode();\n cancel?.();\n }\n\n private _buildPaletteFilterHooks(): PaletteFilterHooks {\n return {\n beginBiotypeFilter: (biotype, onPick, onCancel) => {\n // If pick-mode is already active, transition (cancel old, start new).\n if (this._activeBiotypeFilter !== null) this._teardownPickMode();\n this._activeBiotypeFilter = biotype;\n this._pickResolver = onPick;\n this._pickCanceller = onCancel;\n // Phase 29 (T-2901): pick-mode hint text lives on the RNA\n // builder itself; the builder toggles it directly from its\n // own `startPickMode` (called before `beginBiotypeFilter`).\n // The palette no longer owns hint visibility state.\n this._applyFilter();\n // Escape cancels the pick.\n const onKeyDown = (e: KeyboardEvent): void => {\n if (e.key === 'Escape') this.cancelBiotypeFilter();\n };\n this._pickEscapeListener = onKeyDown;\n const w = this._doc.defaultView as (Window & typeof globalThis) | null;\n w?.addEventListener('keydown', onKeyDown);\n // Phase 20 (T-2005): ANY click that isn't on a matching tile\n // cancels pick mode. We attach in the CAPTURE phase on\n // `document` so we see the event before any handler (toolbar\n // button, theme selector, canvas, gallery) can stop it. The\n // matching-tile case (the resolve path) is handled inside the\n // tile's bubble-phase click handler -- the capture listener\n // notices the click but defers cancellation when the target\n // resolves to a tile of the matching biotype (so the tile's\n // own handler runs the `resolve` path instead of being\n // cancelled out from under it).\n const onOutsideMouseDown = (e: Event): void => {\n if (this._activeBiotypeFilter === null) return;\n const target = (e as { target?: unknown }).target as Element | null;\n if (target === null || target === undefined) return;\n // Walk up the ancestor chain looking for a matching tile OR\n // another pick-mode trigger (Phase 24 T-2402). When the user\n // clicks a DIFFERENT RNA-builder slot (e.g. from pick-linker\n // they want to pick sugar instead), the slot's own click\n // handler will switch the active field atomically. Without\n // this exception the capture-phase cancellation would fire\n // first, exit pick mode, and the slot's click would re-enter\n // pick mode -- two clicks required where one should suffice.\n const matchingBiotype = this._activeBiotypeFilter;\n let cur: Element | null = target;\n while (cur !== null) {\n const cls = cur.getAttribute === undefined ? null : cur.getAttribute('class');\n if (cls !== null) {\n const classes = cls.split(/\\s+/);\n if (classes.includes('hw-palette__tile')) {\n const bio = cur.getAttribute('data-biotype') ?? '';\n if (bio === matchingBiotype) return; // tile-resolve path, do not cancel\n break; // non-matching tile -> fall through to cancel\n }\n // Phase 24 (T-2402): another RNA-builder slot is a\n // pick-mode trigger. Let its own handler switch the\n // active field. We confirm the slot belongs to a builder\n // mounted inside THIS palette by checking the ancestor\n // chain reaches `this.element`.\n if (classes.includes('hw-rna-builder__slot')) {\n if (this._isInsideOwnPalette(cur)) return;\n break;\n }\n }\n cur = cur.parentElement;\n }\n // Any other target: cancel.\n this.cancelBiotypeFilter();\n };\n this._pickOutsideClickListener = onOutsideMouseDown;\n const docTarget = this._doc as unknown as {\n addEventListener(t: string, l: (e: Event) => void, opts?: boolean | { capture?: boolean }): void;\n };\n docTarget.addEventListener('mousedown', onOutsideMouseDown, true);\n },\n endBiotypeFilter: () => this._teardownPickMode(),\n };\n }\n\n /**\n * Phase 24 (T-2403) -- Scroll the currently-active pane back to its\n * top. Called after a successful pick-mode resolve so the RNA\n * Builder (which sits at the top of the RNA pane) returns into view\n * after the user scrolled down to find a tile.\n *\n * We prefer `scrollTo({ top: 0, behavior: 'smooth' })` when the\n * browser exposes it (modern engines) and fall back to the boolean\n * form for older runtimes / linkedom. In environments without a\n * mounted scroll container (linkedom) the assignment is a no-op.\n */\n private _scrollActivePaneToTop(): void {\n const activePane = this.element.querySelector(`.hw-palette__pane[data-tab=\"${this._activeTab}\"]`);\n if (activePane === null) return;\n const target = activePane as unknown as {\n scrollTo?: (opts: { top: number; behavior?: 'smooth' | 'auto' }) => void;\n scrollTop?: number;\n };\n try {\n if (typeof target.scrollTo === 'function') {\n target.scrollTo({ top: 0, behavior: 'smooth' });\n return;\n }\n } catch {\n // `behavior` option may be rejected by older engines; fall through.\n }\n if (typeof target.scrollTop === 'number') target.scrollTop = 0;\n }\n\n /**\n * Phase 24 (T-2402) -- Does `el` sit anywhere under this palette's\n * root? Used to disambiguate \"another pick trigger belongs to us\" from\n * \"a pick trigger belongs to a different palette in the same\n * document\" (e.g. tests that mount two palettes side-by-side).\n */\n private _isInsideOwnPalette(el: Element): boolean {\n let cur: Element | null = el;\n while (cur !== null) {\n if (cur === this.element) return true;\n cur = cur.parentElement;\n }\n return false;\n }\n\n private _teardownPickMode(): void {\n if (this._activeBiotypeFilter === null) return;\n this._activeBiotypeFilter = null;\n this._pickResolver = null;\n this._pickCanceller = null;\n if (this._pickEscapeListener !== null) {\n const w = this._doc.defaultView as (Window & typeof globalThis) | null;\n w?.removeEventListener('keydown', this._pickEscapeListener);\n this._pickEscapeListener = null;\n }\n // Phase 20 (T-2005): detach the outside-click listener.\n if (this._pickOutsideClickListener !== null) {\n const docTarget = this._doc as unknown as {\n removeEventListener(t: string, l: (e: Event) => void, opts?: boolean | { capture?: boolean }): void;\n };\n docTarget.removeEventListener('mousedown', this._pickOutsideClickListener, true);\n this._pickOutsideClickListener = null;\n }\n // Phase 29 (T-2901): the pick-mode hint now lives inside the RNA\n // builder itself (between the title and the SVG triplet), not\n // above the palette's search box. The palette no longer owns hint\n // state -- RnaBuilder toggles its own `[data-testid=\"rna-builder-hint\"]`\n // element in `startPickMode` / `_exitPickMode`. Replaces Phase 24\n // T-2401 reserved-space hint above the search box (\"I don't like\n // the idea of wasting space like that\").\n this._applyFilter();\n }\n\n // ------------------------------------------------------------------------\n // Render\n // ------------------------------------------------------------------------\n\n private _render(): void {\n // Phase 18 (T-1806) — Dispose any existing RNA Builder before the\n // wholesale DOM teardown so its `lib.onChanged` subscription is\n // released. A fresh builder is constructed when the RNA pane\n // re-renders below.\n if (this._rnaBuilder !== null) {\n // T-3513 — remember the composed triplet so a re-render (e.g. a\n // favorite toggle) doesn't reset the builder back to defaults.\n this._pendingRnaSelection = this._rnaBuilder.getSelection();\n this._rnaBuilder.destroy();\n this._rnaBuilder = null;\n }\n while (this.element.firstChild !== null)\n this.element.removeChild(this.element.firstChild);\n\n // Search box.\n const search = this._doc.createElement('input');\n search.setAttribute('type', 'search');\n search.setAttribute('class', 'hw-palette__search');\n search.setAttribute('placeholder', 'Search monomers...');\n search.setAttribute('data-testid', 'palette-search');\n if (this._filter !== '') search.value = this._filter;\n search.addEventListener('input', () => this.setFilter(search.value));\n this._searchInput = search;\n this.element.appendChild(search);\n\n // Phase 29 (T-2901): the pick-mode hint moved INSIDE the RNA\n // Builder body (between the \"RNA builder\" title and the SVG\n // triplet). The palette no longer reserves vertical space for it\n // above the search box -- the previous Phase 24 always-mounted\n // hint container was visible-but-empty in idle state and the user\n // reported it as wasted space. The hint element is now\n // `[data-testid=\"rna-builder-hint\"]`, owned by `RnaBuilder`, and\n // toggled via the `hidden` attribute (CSS `display: none`) so an\n // idle builder takes zero extra height.\n\n // Recompute visible tabs.\n this._visibleTabs = this._computeVisibleTabs();\n // If the previously-active tab is no longer visible (e.g. lib reload),\n // fall back to Favorites (always visible) so we don't render orphaned.\n if (!this._visibleTabs.includes(this._activeTab))\n this._activeTab = FAVORITES_TAB;\n\n // Tab list.\n const tabs = el(this._doc, 'div', {\n class: 'hw-palette__tabs',\n role: 'tablist',\n 'data-testid': 'palette-tabs',\n });\n for (const tabId of this._visibleTabs) {\n const isActive = tabId === this._activeTab;\n const btn = el(this._doc, 'button', {\n class: 'hw-palette__tab',\n type: 'button',\n role: 'tab',\n 'aria-controls': `hw-palette-pane-${tabId}`,\n 'aria-selected': isActive ? 'true' : 'false',\n 'data-testid': `palette-tab-${tabId}`,\n 'data-tab': tabId,\n // Phase 24 (T-2404): the full label is always available via\n // the native browser tooltip even when an extreme narrow\n // palette truncates the text visually.\n title: TAB_LABELS[tabId],\n }, [TAB_LABELS[tabId]]);\n btn.addEventListener('click', () => this.setActiveTab(tabId));\n tabs.appendChild(btn);\n }\n this.element.appendChild(tabs);\n\n // Pane container.\n const panes = el(this._doc, 'div', { class: 'hw-palette__panes' });\n const theme = this._theme();\n const lib = this._editor.getService().monomerLib;\n\n for (const tabId of this._visibleTabs) {\n const isActive = tabId === this._activeTab;\n const pane = el(this._doc, 'div', {\n class: 'hw-palette__pane',\n id: `hw-palette-pane-${tabId}`,\n role: 'tabpanel',\n 'data-testid': `palette-pane-${tabId}`,\n 'data-tab': tabId,\n hidden: !isActive,\n });\n if (tabId === FAVORITES_TAB)\n this._buildFavoritesPane(pane, lib, theme);\n else\n this._buildPolymerPane(pane, tabId, lib, theme);\n panes.appendChild(pane);\n }\n this.element.appendChild(panes);\n\n this._applyFilter();\n }\n\n /** Recompute which tabs should render against the current library. */\n private _computeVisibleTabs(): TabId[] {\n const lib = this._editor.getService().monomerLib;\n const present = new Set<string>();\n for (const pt of lib.getPolymerTypes()) present.add(pt);\n const out: TabId[] = [];\n // Favorites is always visible — it shows a placeholder when empty. The\n // user can star a monomer from any other tab to populate it.\n out.push(FAVORITES_TAB);\n for (const tabId of TAB_ORDER) {\n if (tabId === FAVORITES_TAB) continue;\n if (present.has(tabId)) out.push(tabId);\n }\n return out;\n }\n\n // ------------------------------------------------------------------------\n // Favorites pane\n // ------------------------------------------------------------------------\n\n private _buildFavoritesPane(pane: HTMLElement, lib: IMonomerLib, theme: Theme): void {\n const resolved = this._resolveFavoriteMonomers(lib);\n const triplets = this._favoriteTriplets;\n if (resolved.length === 0 && triplets.length === 0) {\n const placeholder = el(this._doc, 'div', {\n class: 'hw-palette__placeholder',\n 'data-testid': 'palette-favorites-empty',\n }, ['No favorites yet — click the ☆ on any tile to add one.']);\n pane.appendChild(placeholder);\n return;\n }\n // T-3513 — group monomer favorites by polymer type + monomer type\n // (was: natural analog). Each group reuses the standard group DOM.\n for (const [display, group] of this._groupByPolymerAndType(resolved))\n pane.appendChild(this._renderGroup(display, group));\n\n // T-3513 — favorited RNA triplets render as small chips under their\n // own group. Inserting a chip drops a fresh RNA triplet on the canvas.\n if (triplets.length > 0)\n pane.appendChild(this._renderTripletGroup('RNA · Triplets', triplets, lib));\n void theme;\n }\n\n /**\n * T-3513 — Group monomers by `polymerType` then `monomerType`. Group\n * order follows the tab order for polymer types (Peptides → RNA → CHEM →\n * BLOB → G) and Backbone → Branch → Terminal → Undefined within a polymer.\n * Display labels read e.g. \"Peptides · Backbone\".\n */\n private _groupByPolymerAndType(monomers: Monomer[]): Array<[string, Monomer[]]> {\n const buckets = new Map<string, { pt: PolymerType; mt: string; list: Monomer[] }>();\n for (const m of monomers) {\n const pt = m.polymerType;\n const mt = (m.monomerType ?? 'Undefined') as string;\n const key = `${pt}|${mt}`;\n let b = buckets.get(key);\n if (b === undefined) { b = { pt, mt, list: [] }; buckets.set(key, b); }\n b.list.push(m);\n }\n const polyOrder = (pt: PolymerType): number => {\n const i = TAB_ORDER.indexOf(pt as TabId);\n return i < 0 ? 99 : i;\n };\n const MT_ORDER: Record<string, number> = { Backbone: 0, Branch: 1, Terminal: 2, Undefined: 3 };\n const sorted = Array.from(buckets.values()).sort((a, b) =>\n (polyOrder(a.pt) - polyOrder(b.pt)) || ((MT_ORDER[a.mt] ?? 9) - (MT_ORDER[b.mt] ?? 9)) || a.mt.localeCompare(b.mt));\n return sorted.map((b) => [`${this._polymerLabel(b.pt)} · ${b.mt}`, b.list] as [string, Monomer[]]);\n }\n\n /** Human-readable polymer-type label (matches the tab labels). */\n private _polymerLabel(pt: PolymerType): string {\n return (TAB_LABELS as Record<string, string>)[pt] ?? pt;\n }\n\n // ------------------------------------------------------------------------\n // T-3513 — RNA triplet chips (favorites group + RNA pane defaults)\n // ------------------------------------------------------------------------\n\n /** Standard nucleotides offered as default quick-add chips, in display order. */\n private static readonly _DEFAULT_TRIPLET_BASES: readonly string[] = ['A', 'C', 'G', 'U', 'T'];\n\n /** Build a `.hw-palette__group` wrapping a row of triplet chips. */\n private _renderTripletGroup(display: string, triplets: readonly FavoriteTriplet[], lib: IMonomerLib): HTMLElement {\n const groupEl = el(this._doc, 'div', {\n class: 'hw-palette__group',\n 'data-natural-analog': display,\n });\n const header = el(this._doc, 'div', {\n class: 'hw-palette__group-header',\n 'data-testid': `palette-group-${display}`,\n }, [display]);\n const chips = el(this._doc, 'div', { class: 'hw-triplet-chips', 'data-testid': 'palette-triplet-favorites' });\n for (const t of triplets) chips.appendChild(this._renderTripletChip(t, lib));\n groupEl.appendChild(header);\n groupEl.appendChild(chips);\n return groupEl;\n }\n\n /** Build one triplet chip (`r(A)p`). Body click inserts; star toggles favorite. */\n private _renderTripletChip(t: FavoriteTriplet, lib: IMonomerLib): HTMLElement {\n const label = formatRnaTriplet(t.sugar, t.base, t.linker);\n const baseMon = lib.getMonomerSet(HelmTypes.BASE).get(t.base) ?? null;\n const colors = resolveMonomerColors(baseMon, HelmTypes.BASE, this._theme().colorTheme);\n const favorite = this.isFavoriteTriplet(t.sugar, t.base, t.linker);\n const title = baseMon?.name !== undefined && baseMon.name !== '' ? `${label} — ${baseMon.name}` : label;\n const { root, star } = buildTripletChip(this._doc, {\n sugar: t.sugar,\n base: t.base,\n linker: t.linker,\n label,\n title,\n background: soften(colors.background),\n textColor: colors.text,\n favorite,\n showStar: this._opts.allowFavorites,\n });\n this._wireTripletChip(root, t);\n void star;\n return root;\n }\n\n private _wireTripletChip(root: HTMLElement, t: FavoriteTriplet): void {\n // T-3514 — hover tooltip showing all three monomer structures\n // (sugar · base · linker) via RDKit when a module is wired through.\n root.addEventListener('mouseenter', (e: Event) => {\n const me = e as unknown as { clientX?: number; clientY?: number };\n const tip = getOrCreateTooltipEl(this._doc, 'palette-triplet-tooltip', 'hw-palette-tooltip hw-triplet-tooltip');\n fillTripletTooltip(this._doc, tip, {\n title: formatRnaTriplet(t.sugar, t.base, t.linker),\n parts: this._tripletTooltipParts(t),\n hint: 'Click to add this triplet',\n rdkitTestid: 'palette-triplet-tooltip-rdkit-svg',\n });\n positionTooltipAt(tip, typeof me.clientX === 'number' ? me.clientX : 0, typeof me.clientY === 'number' ? me.clientY : 0);\n });\n root.addEventListener('mouseleave', () => {\n hideTooltipEl(this._doc.body.querySelector('[data-testid=\"palette-triplet-tooltip\"]') as HTMLElement | null);\n });\n root.addEventListener('click', (e: Event) => {\n const target = (e as unknown as { target?: { classList?: { contains(c: string): boolean } } }).target;\n if (this._opts.allowFavorites && target?.classList?.contains('hw-triplet-chip__star') === true) {\n this.setFavoriteTriplet(t.sugar, t.base, t.linker, !this.isFavoriteTriplet(t.sugar, t.base, t.linker));\n e.stopPropagation();\n return;\n }\n // Body click → drop a fresh RNA triplet on the canvas (same path as\n // the RNA builder's \"Add triplet\").\n this._editor.placeRnaTriplet(t.sugar, t.base, t.linker);\n });\n root.addEventListener('keydown', (e: Event) => {\n const ke = e as unknown as { key: string };\n if (ke.key === 'Enter' || ke.key === ' ') this._editor.placeRnaTriplet(t.sugar, t.base, t.linker);\n });\n }\n\n /**\n * T-3513 — The RNA pane's default-nucleotide chip row: `r(A)p` … `r(T)p`\n * for whichever of A/C/G/U/T the library carries, using the library's\n * preferred sugar (`r`/`R`) and linker (`p`/`P`). Returns `null` when the\n * library lacks a usable sugar / linker / base.\n */\n private _buildRnaDefaultChips(lib: IMonomerLib): HTMLElement | null {\n const sugar = this._pickSymbol(lib.getMonomerSet(HelmTypes.SUGAR), ['r', 'R']);\n const linker = this._pickSymbol(lib.getMonomerSet(HelmTypes.LINKER), ['p', 'P']);\n const baseSet = lib.getMonomerSet(HelmTypes.BASE);\n if (sugar === null || linker === null) return null;\n const bases = MonomerPalette._DEFAULT_TRIPLET_BASES.filter((b) => baseSet.has(b));\n if (bases.length === 0) return null;\n const row = el(this._doc, 'div', {\n class: 'hw-triplet-chips hw-triplet-chips--defaults',\n 'data-testid': 'rna-default-triplets',\n });\n for (const base of bases)\n row.appendChild(this._renderTripletChip({ sugar, base, linker }, lib));\n return row;\n }\n\n /**\n * T-3514 — Resolve the three tooltip parts (sugar / base / linker) for a\n * triplet: role label, symbol, monomer name, and the cached RDKit SVG\n * (null when no module is wired or the monomer doesn't resolve).\n */\n private _tripletTooltipParts(t: FavoriteTriplet): TripletTooltipPart[] {\n const lib = this._editor.getService().monomerLib;\n const part = (role: string, biotype: HelmType, symbol: string): TripletTooltipPart => {\n const monomer = lib.getMonomerSet(biotype).get(symbol) ?? null;\n return {\n role,\n symbol,\n ...(monomer?.name !== undefined ? { name: monomer.name } : {}),\n rdkitSvg: monomer === null ? null : this._paletteRdkitSvg(monomer),\n };\n };\n return [\n part('Sugar', HelmTypes.SUGAR, t.sugar),\n part('Base', HelmTypes.BASE, t.base),\n part('Linker', HelmTypes.LINKER, t.linker),\n ];\n }\n\n /** Pick the first preferred symbol present in `set`, else its first entry. */\n private _pickSymbol(set: ReadonlyMap<string, Monomer>, prefs: readonly string[]): string | null {\n for (const p of prefs) if (set.has(p)) return p;\n const first = set.keys().next();\n return first.done === true ? null : first.value;\n }\n\n /**\n * T-3513 — Re-apply a builder selection captured before a `_render()`.\n * Only restores fields that are non-empty and still resolve in the active\n * library; consumed once so a later lib reload starts from defaults.\n */\n private _restoreRnaSelection(): void {\n const sel = this._pendingRnaSelection;\n this._pendingRnaSelection = null;\n if (sel === null || this._rnaBuilder === null) return;\n const lib = this._editor.getService().monomerLib;\n if (sel.sugar !== '' && lib.getMonomerSet(HelmTypes.SUGAR).has(sel.sugar))\n this._rnaBuilder.setField('sugar', sel.sugar);\n if (sel.base !== '' && lib.getMonomerSet(HelmTypes.BASE).has(sel.base))\n this._rnaBuilder.setField('base', sel.base);\n if (sel.linker !== '' && lib.getMonomerSet(HelmTypes.LINKER).has(sel.linker))\n this._rnaBuilder.setField('linker', sel.linker);\n }\n\n /**\n * Walk every polymer type and return monomers for the favorite symbols,\n * in insertion order. Drops favorites whose symbol doesn't resolve in\n * the active library. Storage is NOT mutated — we want to keep dropped\n * symbols around in case the user swaps libraries back.\n */\n private _resolveFavoriteMonomers(lib: IMonomerLib): Monomer[] {\n const out: Monomer[] = [];\n for (const sym of this._favorites) {\n const m = this._findMonomer(lib, sym);\n if (m !== null) out.push(m);\n }\n return out;\n }\n\n private _findMonomer(lib: IMonomerLib, symbol: string): Monomer | null {\n for (const pt of lib.getPolymerTypes()) {\n const m = lib.getMonomer(pt, symbol);\n if (m !== null) return m;\n }\n return null;\n }\n\n // ------------------------------------------------------------------------\n // Polymer-type pane\n // ------------------------------------------------------------------------\n\n private _buildPolymerPane(pane: HTMLElement, tabId: Exclude<TabId, typeof FAVORITES_TAB>, lib: IMonomerLib, theme: Theme): void {\n const polymerType = tabId as PolymerType;\n const monomers = this._monomersForPolymer(polymerType, lib);\n if (monomers.length === 0) return;\n\n // RNA gets sugar/base/linker grouping; PEPTIDE gets natural-analog\n // grouping; everything else is flat.\n if (polymerType === PolymerTypes.RNA) {\n // Phase 18 (T-1806) / Phase 19 (T-1903) — RNA Builder lives at\n // the top of the RNA pane. The palette gives the builder a\n // `PaletteFilterHooks` handle so its edit buttons can put the\n // palette into biotype pick-mode.\n this._rnaBuilder = new RnaBuilder(pane, this._editor, {\n documentImpl: this._doc,\n paletteHooks: this._buildPaletteFilterHooks(),\n // T-3513 — the builder header star favorites the composed triplet.\n favoriteHooks: this._opts.allowFavorites ? {\n isFavoriteTriplet: (s, b, l) => this.isFavoriteTriplet(s, b, l),\n toggleFavoriteTriplet: (s, b, l) => this.setFavoriteTriplet(s, b, l, !this.isFavoriteTriplet(s, b, l)),\n } : null,\n });\n // T-3513 — restore the builder selection captured before a re-render\n // (so toggling a favorite doesn't reset the user's composed triplet).\n this._restoreRnaSelection();\n // T-3513 — default-nucleotide quick-add chips, directly under the\n // builder. Kept compact: the RNA pane is height-constrained.\n const defaults = this._buildRnaDefaultChips(lib);\n if (defaults !== null) pane.appendChild(defaults);\n const groups = this._groupRna(monomers);\n for (const [groupName, ms] of groups) {\n if (ms.length === 0) continue;\n pane.appendChild(this._renderGroup(groupName, ms));\n }\n return;\n }\n if (polymerType === PolymerTypes.PEPTIDE && this._opts.groupByNaturalAnalog) {\n const groups = this._groupByAnalog(monomers);\n for (const [na, ms] of groups) {\n const display = na === '' ? 'Other' : na;\n pane.appendChild(this._renderGroup(display, ms));\n }\n return;\n }\n // Flat list (CHEM, BLOB, G, or grouping disabled).\n const tiles = el(this._doc, 'div', { class: 'hw-palette__group-tiles' });\n const next = this._editor.getNextAddSymbol();\n // Phase 18 (T-1804) — flat panes also sort by colour.\n const sorted = this._sortByColor(monomers);\n for (const m of sorted) {\n const biotype = this._biotype(m, polymerType);\n tiles.appendChild(this._renderTile(m, biotype, next));\n }\n pane.appendChild(tiles);\n void theme;\n }\n\n /** Build `<div class=\"hw-palette__group\">` with a bold uppercase header. */\n private _renderGroup(displayName: string, monomers: Monomer[]): HTMLElement {\n const groupEl = el(this._doc, 'div', {\n class: 'hw-palette__group',\n 'data-natural-analog': displayName,\n });\n const header = el(this._doc, 'div', {\n class: 'hw-palette__group-header',\n 'data-testid': `palette-group-${displayName}`,\n }, [displayName]);\n const tiles = el(this._doc, 'div', { class: 'hw-palette__group-tiles' });\n const next = this._editor.getNextAddSymbol();\n // Phase 18 (T-1804) — Within a subsection, sort monomers by their\n // resolved background colour (HSL). Same-hue tiles cluster, so a\n // subsection no longer renders as a noisy rainbow. The previous\n // alphabetical sort happened upstream in `_monomersForPolymer` /\n // `_groupRna`; we re-sort here so EVERY caller of `_renderGroup`\n // (favorites, peptide groups, RNA groups, etc.) gets the colour\n // order without each having to opt in.\n const sorted = this._sortByColor(monomers);\n for (const m of sorted) {\n const biotype = this._biotype(m, m.polymerType);\n tiles.appendChild(this._renderTile(m, biotype, next));\n }\n groupEl.appendChild(header);\n groupEl.appendChild(tiles);\n return groupEl;\n }\n\n /**\n * Phase 18 (T-1804) — Stable sort by `(hue, saturation, lightness)` of\n * the monomer's resolved background colour. Monomers whose colour\n * falls through to the fallback grey (or otherwise has saturation = 0)\n * sort to the end so the saturated colours appear first.\n *\n * Stability matters: ties on `(h, s, l)` keep the input order so the\n * tests can predict the output for stable libraries (HELMCoreLibrary).\n * `Array.prototype.sort` is stable on Node ≥ v12 and on every modern\n * browser engine — no manual stability decoration needed.\n */\n private _sortByColor(monomers: Monomer[]): Monomer[] {\n const theme = this._theme();\n const keyed = monomers.map((m, idx) => {\n const biotype = this._biotype(m, m.polymerType);\n const colors = resolveMonomerColors(m, biotype, theme.colorTheme);\n const hsl = hexToHsl(colors.background);\n // Greys (saturation === 0) go LAST: the colour layout starts\n // colourful → greyscale, so the eye walks the rainbow before\n // hitting the muted tiles.\n const isGrey = hsl.s === 0 ? 1 : 0;\n return { m, idx, h: hsl.h, s: hsl.s, l: hsl.l, isGrey };\n });\n keyed.sort((a, b) => {\n if (a.isGrey !== b.isGrey) return a.isGrey - b.isGrey;\n if (a.h !== b.h) return a.h - b.h;\n if (a.s !== b.s) return a.s - b.s;\n if (a.l !== b.l) return a.l - b.l;\n return a.idx - b.idx;\n });\n return keyed.map((e) => e.m);\n }\n\n private _monomersForPolymer(polymerType: PolymerType, lib: IMonomerLib): Monomer[] {\n const symbols = lib.getMonomerSymbolsByType(polymerType);\n const out: Monomer[] = [];\n for (const sym of symbols) {\n const m = lib.getMonomer(polymerType, sym);\n if (m !== null) out.push(m);\n }\n out.sort((a, b) => a.symbol.localeCompare(b.symbol));\n return out;\n }\n\n /** Bucket monomers by natural-analog; keys are A..Z then '' (no analog). */\n private _groupByAnalog(monomers: Monomer[]): Map<string, Monomer[]> {\n const result = new Map<string, Monomer[]>();\n for (const m of monomers) {\n const key = m.naturalAnalog ?? '';\n let arr = result.get(key);\n if (arr === undefined) { arr = []; result.set(key, arr); }\n arr.push(m);\n }\n // Sort: alphabetical for named analogs, '' (Other) last.\n const sorted: Array<[string, Monomer[]]> = [];\n const keys = Array.from(result.keys()).sort((a, b) => {\n if (a === '' && b !== '') return 1;\n if (b === '' && a !== '') return -1;\n return a.localeCompare(b);\n });\n for (const k of keys) sorted.push([k, result.get(k) as Monomer[]]);\n return new Map(sorted);\n }\n\n /**\n * RNA-specific grouping: Backbones → Bases → Linkers → Other.\n *\n * Phase 20 (T-2004): the \"Sugars\" header was renamed to \"Backbones\"\n * per the user's request — the sugar monomers of an RNA triplet\n * function as the backbone of the polymer, and \"Backbones\" matches\n * the RNA Builder's slot label.\n */\n private _groupRna(monomers: Monomer[]): Map<string, Monomer[]> {\n const sugars: Monomer[] = [];\n const bases: Monomer[] = [];\n const linkers: Monomer[] = [];\n const other: Monomer[] = [];\n for (const m of monomers) {\n const bio = helm2Type(m);\n if (bio === HelmTypes.SUGAR) sugars.push(m);\n else if (bio === HelmTypes.BASE) bases.push(m);\n else if (bio === HelmTypes.LINKER) linkers.push(m);\n else other.push(m);\n }\n const out = new Map<string, Monomer[]>();\n out.set('Backbones', sugars);\n out.set('Bases', bases);\n out.set('Linkers', linkers);\n if (other.length > 0) out.set('Other', other);\n return out;\n }\n\n // ------------------------------------------------------------------------\n // Tile rendering\n // ------------------------------------------------------------------------\n\n private _renderTile(\n monomer: Monomer,\n biotype: HelmType,\n next: { symbol: string; biotype: HelmType } | null,\n ): HTMLElement {\n const favorite = this._favorites.has(monomer.symbol);\n const primed = next !== null && next.symbol === monomer.symbol && next.biotype === biotype;\n const { root, star } = buildTile(this._doc, {\n monomer,\n biotype,\n theme: this._theme(),\n favorite,\n primed,\n });\n this._wireTile(root, star, monomer, biotype);\n return root;\n }\n\n /**\n * Phase 22 (T-2202) -- Per-tile cache of the RDKit-rendered structure\n * SVG, keyed by monomer symbol. The palette and the editor view share\n * an RDKit module but have separate caches because the two surfaces\n * may live in different lifetimes (palette is destroyed on lib\n * reload, view persists). Bounded at 256 entries.\n */\n private readonly _paletteRdkitCache: Map<string, string | null> = new Map();\n private static readonly _PALETTE_RDKIT_CACHE_LIMIT = 256;\n\n /** Resolve the RDKit module (if the host wired one through HelmService). */\n private _rdkit(): RdkitLike | null {\n const opts = (this._editor.getService() as unknown as { options?: { rdkitModule?: unknown } }).options;\n const mod = opts?.rdkitModule;\n return (mod === undefined || mod === null) ? null : (mod as RdkitLike);\n }\n\n /** Memoised RDKit SVG for one monomer. Returns null when unavailable. */\n private _paletteRdkitSvg(monomer: Monomer): string | null {\n const smiles = monomer.smiles;\n if (smiles === undefined || smiles === '') return null;\n const rdkit = this._rdkit();\n if (rdkit === null) return null;\n const key = `${monomer.polymerType}:${monomer.symbol}`;\n if (this._paletteRdkitCache.has(key))\n return this._paletteRdkitCache.get(key) ?? null;\n // Phase 23 (T-2303): bump source size to 220x140 so the rendered\n // structure fills the tooltip width. Inner svg viewBox handles\n // down-scaling for narrower tooltips.\n const svg = renderRdkitSvg(rdkit, smiles, 220, 140);\n if (this._paletteRdkitCache.size >= MonomerPalette._PALETTE_RDKIT_CACHE_LIMIT) {\n const oldest = this._paletteRdkitCache.keys().next().value;\n if (oldest !== undefined) this._paletteRdkitCache.delete(oldest);\n }\n this._paletteRdkitCache.set(key, svg);\n return svg;\n }\n\n private _wireTile(\n root: HTMLElement,\n star: HTMLElement,\n monomer: Monomer,\n biotype: HelmType,\n ): void {\n // Phase 22 (T-2202) -- Hover tooltip. Mounts at document.body via\n // the shared helper so it escapes parent stacking contexts (the\n // gallery panel used to clip it).\n root.addEventListener('mouseenter', (e: Event) => {\n const me = e as unknown as { clientX?: number; clientY?: number };\n const tip = getOrCreateTooltipEl(this._doc, 'palette-tooltip', 'hw-palette-tooltip');\n fillTooltip(this._doc, tip, {\n symbol: monomer.symbol,\n name: monomer.name ?? '',\n rdkitSvg: this._paletteRdkitSvg(monomer),\n rdkitTestid: 'palette-tooltip-rdkit-svg',\n });\n const cx = typeof me.clientX === 'number' ? me.clientX : 0;\n const cy = typeof me.clientY === 'number' ? me.clientY : 0;\n positionTooltipAt(tip, cx, cy);\n });\n root.addEventListener('mouseleave', () => {\n hideTooltipEl(this._doc.body.querySelector('[data-testid=\"palette-tooltip\"]') as HTMLElement | null);\n });\n // Click on the star → toggle favorite. Click anywhere else → prime next-add.\n root.addEventListener('click', (e: Event) => {\n const target = (e as unknown as { target?: { classList?: { contains(c: string): boolean } } }).target;\n if (this._opts.allowFavorites && target !== undefined && target?.classList?.contains('hw-palette__star') === true) {\n this.setFavorite(monomer.symbol, !this._favorites.has(monomer.symbol));\n // setFavorite triggers a re-render, but stopPropagation prevents the\n // star click from also firing the prime-next-add handler below.\n e.stopPropagation();\n return;\n }\n // Phase 19 (T-1903): if a biotype pick-mode is active AND this\n // tile's biotype matches, RESOLVE the pick (set the field, exit\n // pick mode). The palette's tile filter has already hidden any\n // non-matching tiles, but we double-check here defensively.\n if (this._activeBiotypeFilter !== null && this._activeBiotypeFilter === biotype) {\n const resolver = this._pickResolver;\n this._teardownPickMode();\n resolver?.(monomer.symbol);\n // Phase 24 (T-2403): after a successful resolve, scroll the\n // active pane back to its top so the RNA Builder (which sits at\n // the top of the RNA pane) is back in view. The user reported:\n // \"when I scroll down to pick a sugar, my scroll stays at the\n // tile, but the RNA Builder is now offscreen\". We use smooth\n // scrolling so the motion is perceivable but non-jarring.\n this._scrollActivePaneToTop();\n e.stopPropagation();\n return;\n }\n // Phase 27 (T-2704): when a non-empty atom-selection is active\n // AND every selected atom shares its biotype with the clicked\n // tile, the tile click acts as a BULK REPLACE: every selected\n // atom's symbol is updated to the tile's symbol in a single\n // transaction. The user's mental model: \"I selected three\n // phosphates, then I click `sp` on the left -> all three flip\n // to sp.\"\n //\n // Mismatched biotypes (e.g. selection contains a sugar AND a\n // linker, user clicks `sp`) fire an `editor:status` warn and\n // leave the model untouched. `nextAdd` is NOT primed in either\n // branch -- the click was consumed by the selection-replace\n // gesture, not the prime gesture, so the user's tile state stays\n // exactly what it was before.\n const selection = this._editor.getSelection();\n if (selection.atoms.size > 0) {\n const mol = this._editor.getMol();\n const selected: Atom[] = [];\n for (const id of selection.atoms) {\n const a = mol.atoms.find((x) => x.id === id);\n if (a !== undefined) selected.push(a);\n }\n const allMatch = selected.length > 0\n && selected.every((a) => a.biotype === biotype);\n if (allMatch) {\n this._editor.replaceMonomers(selected, monomer.symbol);\n e.stopPropagation();\n return;\n }\n this._editor.fire('editor:status', {\n message: `Cannot bulk-replace ${selected.length} selected atom(s) with ${monomer.symbol}: selection contains incompatible biotypes for ${biotype}.`,\n severity: 'warn',\n durationMs: 3000,\n });\n e.stopPropagation();\n return;\n }\n // Phase 25 (T-2501): clicking the ALREADY-PRIMED tile a second\n // time UNSETS the prime (toggle off). User reported: \"on the left\n // pane, clicking the selected monomer once again should unselect\n // it.\" Match against `editor.getNextAddSymbol()`'s symbol+biotype\n // pair so two different tiles that share a symbol (e.g. RNA Sugar\n // vs Base) don't false-match.\n const currentNext = this._editor.getNextAddSymbol();\n if (currentNext !== null\n && currentNext.symbol === monomer.symbol\n && currentNext.biotype === biotype) {\n this._editor.setNextAddSymbol(null);\n this._updatePrimedClasses();\n return;\n }\n this._editor.setNextAddSymbol(monomer.symbol, biotype);\n this._updatePrimedClasses();\n });\n void star;\n // Drag → kick off palette DnD on the editor AND set the HTML5\n // DataTransfer payload (T-1102) so real browser drag-and-drop works\n // even when no test bridge has primed `startPaletteDrag`.\n root.addEventListener('dragstart', (e: Event) => {\n const dt = (e as unknown as { dataTransfer: { setData(t: string, v: string): void; effectAllowed: string } | null }).dataTransfer;\n if (dt !== null && dt !== undefined) {\n try {\n dt.setData('application/x-hwe-monomer', JSON.stringify({ symbol: monomer.symbol, biotype }));\n dt.effectAllowed = 'copy';\n } catch {\n // Some test harnesses don't allow mutation; the editor's in-memory\n // state below still drives the drop.\n }\n }\n // Phase 24 (T-2406): the palette tooltip remained visible after a\n // drag started because the `mouseleave` event was suppressed by\n // the synthesized drag pointer-capture. Hide explicitly on\n // dragstart so the floating tile is not accompanied by a stale\n // tooltip following the cursor.\n hideTooltipEl(this._doc.body.querySelector('[data-testid=\"palette-tooltip\"]') as HTMLElement | null);\n this._editor.startPaletteDrag(monomer.symbol, biotype);\n });\n }\n\n private _biotype(monomer: Monomer, fallbackType: PolymerType | ExtraPolymerType): HelmType {\n const t = helm2Type(monomer);\n if (t !== null) return t;\n // Stable fallback per polymer type.\n switch (fallbackType) {\n case PolymerTypes.PEPTIDE: return HelmTypes.AA;\n case PolymerTypes.RNA: return HelmTypes.SUGAR;\n case PolymerTypes.CHEM: return HelmTypes.CHEM;\n case PolymerTypes.BLOB: return HelmTypes.BLOB;\n default: return HelmTypes.CHEM;\n }\n }\n\n private _theme(): Theme {\n const ed = this._editor as unknown as { _currentTheme(): Theme };\n if (typeof ed._currentTheme === 'function') return ed._currentTheme();\n return this._editor.getService().theme;\n }\n\n // ------------------------------------------------------------------------\n // Filter\n // ------------------------------------------------------------------------\n\n /**\n * Show only tiles whose symbol or title-attribute contain the current\n * filter substring. Applied to the ACTIVE pane only — inactive panes are\n * hidden by the `hidden` attribute, but their tiles get the same display\n * adjustment so switching back doesn't surprise the user with stale\n * visibility.\n */\n private _applyFilter(): void {\n const q = this._filter;\n const biotypeFilter = this._activeBiotypeFilter;\n // Internal lookup uses the `data-tab` attribute — querying by\n // `data-testid` here would trip the audit-ui-tests script (which\n // can't tell a CSS selector apart from a real test-id literal).\n const activePane = this.element.querySelector(`.hw-palette__pane[data-tab=\"${this._activeTab}\"]`);\n const tiles = (activePane ?? this.element).querySelectorAll('.hw-palette__tile');\n for (let i = 0; i < tiles.length; i++) {\n const tile = tiles[i] as unknown as HTMLElement;\n const sym = (tile.getAttribute('data-symbol') ?? '').toLowerCase();\n const titleAttr = (tile.getAttribute('title') ?? '').toLowerCase();\n const bio = tile.getAttribute('data-biotype') ?? '';\n const queryMatch = q === '' || sym.indexOf(q) >= 0 || titleAttr.indexOf(q) >= 0;\n const biotypeMatch = biotypeFilter === null || bio === biotypeFilter;\n const visible = queryMatch && biotypeMatch;\n (tile as unknown as { style: { display: string } }).style.display = visible ? '' : 'none';\n }\n // T-3513: triplet chips (RNA default-nucleotide row + triplet\n // favorites) aren't `.hw-palette__tile`s, so the loop above never\n // touches them. Apply the SAME query filter to them, matching against\n // the formatted label and the component symbols. Biotype pick-mode\n // hides them (a composite triplet can't satisfy a single-biotype pick).\n const chips = (activePane ?? this.element).querySelectorAll('.hw-triplet-chip');\n for (let i = 0; i < chips.length; i++) {\n const chip = chips[i] as unknown as HTMLElement;\n const hay = [\n chip.getAttribute('data-triplet') ?? '',\n chip.getAttribute('data-sugar') ?? '',\n chip.getAttribute('data-base') ?? '',\n chip.getAttribute('data-linker') ?? '',\n ].join(' ').toLowerCase();\n const visible = (q === '' || hay.indexOf(q) >= 0) && biotypeFilter === null;\n (chip as unknown as { style: { display: string } }).style.display = visible ? '' : 'none';\n }\n // Phase 20 (T-2003): hide section headers whose tiles were all\n // filtered out. Without this the user sees an empty \"Bases\" header\n // while in pick-sugar mode -- visually broken. We walk each group\n // in the active pane, count tiles whose `style.display !== 'none'`,\n // and toggle `hidden` on the group element accordingly. CSS rule\n // `.hw-palette__group[hidden] { display: none; }` (default UA\n // behaviour for the `hidden` attribute) removes the empty group\n // entirely.\n //\n // T-3513: a group may hold triplet chips instead of tiles (the\n // \"RNA · Triplets\" favorites group), so count BOTH — otherwise a\n // chip-only group is always deemed empty and wrongly hidden.\n const groups = (activePane ?? this.element).querySelectorAll('.hw-palette__group');\n for (let i = 0; i < groups.length; i++) {\n const group = groups[i] as unknown as HTMLElement;\n const groupItems = group.querySelectorAll('.hw-palette__tile, .hw-triplet-chip');\n let anyVisible = false;\n for (let j = 0; j < groupItems.length; j++) {\n const tile = groupItems[j] as unknown as HTMLElement;\n if ((tile as unknown as { style: { display: string } }).style.display !== 'none') {\n anyVisible = true;\n break;\n }\n }\n if (anyVisible) {\n group.removeAttribute('hidden');\n group.removeAttribute('data-section-empty');\n } else {\n group.setAttribute('hidden', '');\n group.setAttribute('data-section-empty', 'true');\n }\n }\n }\n\n private _updatePrimedClasses(): void {\n const next = this._editor.getNextAddSymbol();\n const tiles = this.element.querySelectorAll('.hw-palette__tile');\n for (let i = 0; i < tiles.length; i++) {\n const tile = tiles[i] as unknown as HTMLElement;\n const sym = tile.getAttribute('data-symbol') ?? '';\n const bio = tile.getAttribute('data-biotype') ?? '';\n const isPrimed = next !== null && next.symbol === sym && next.biotype === bio;\n const current = tile.getAttribute('class') ?? '';\n const filtered = current.split(/\\s+/).filter((c) => c !== 'hw-palette__tile--primed').join(' ');\n tile.setAttribute('class', isPrimed ? `${filtered} hw-palette__tile--primed` : filtered);\n // Phase 25 (T-2501): keep `aria-pressed` in sync with the\n // primed state so the toggle-off click is observable via the\n // standard ARIA pattern (in addition to the visual class).\n // Pre-Phase 25 we only updated the class, so a screen reader\n // would report a still-pressed button after the user unselected.\n tile.setAttribute('aria-pressed', isPrimed ? 'true' : 'false');\n }\n }\n\n // ------------------------------------------------------------------------\n // Tab classes / persistence\n // ------------------------------------------------------------------------\n\n /** Flip `aria-selected` on each tab button and `hidden` on each pane. */\n private _applyActiveTabClasses(): void {\n const tabs = this.element.querySelectorAll('.hw-palette__tab');\n for (let i = 0; i < tabs.length; i++) {\n const tab = tabs[i] as unknown as HTMLElement;\n const tabId = tab.getAttribute('data-tab') ?? '';\n tab.setAttribute('aria-selected', tabId === this._activeTab ? 'true' : 'false');\n }\n const panes = this.element.querySelectorAll('.hw-palette__pane');\n for (let i = 0; i < panes.length; i++) {\n const pane = panes[i] as unknown as HTMLElement;\n const tabId = pane.getAttribute('data-tab') ?? '';\n if (tabId === this._activeTab) pane.removeAttribute('hidden');\n else pane.setAttribute('hidden', '');\n }\n }\n\n // ------------------------------------------------------------------------\n // localStorage helpers\n // ------------------------------------------------------------------------\n\n private _localStorage(): Storage | null {\n if (!this._opts.allowFavorites) return null;\n const w = this._doc.defaultView as { localStorage?: Storage } | null;\n if (w === null || w === undefined) return null;\n try {\n return w.localStorage ?? null;\n } catch {\n return null;\n }\n }\n\n private _resolveInitialTab(explicit: string | undefined): TabId {\n if (explicit !== undefined && (TAB_ORDER as string[]).includes(explicit))\n return explicit as TabId;\n const ls = this._localStorage();\n if (ls !== null) {\n try {\n const raw = ls.getItem(ACTIVE_TAB_STORAGE_KEY);\n if (raw !== null && (TAB_ORDER as string[]).includes(raw))\n return raw as TabId;\n } catch {\n // Ignore corrupted entries.\n }\n }\n return FAVORITES_TAB;\n }\n\n private _persistActiveTab(): void {\n const ls = this._localStorage();\n if (ls === null) return;\n try {\n ls.setItem(ACTIVE_TAB_STORAGE_KEY, this._activeTab);\n } catch {\n // Ignore quota errors.\n }\n }\n\n /**\n * Load favorites from localStorage. Reads the canonical Phase 14C key\n * (`hwe:palette:favorites`) first, then migrates the legacy\n * `hwe.palette.favorites` shape (`[\"PEPTIDE|A\", \"RNA|r\"]` pairs) into the\n * new symbol-only set. Storage is NOT pruned — symbols that don't resolve\n * stay there until explicitly cleared by `setFavorite(sym, false)`.\n */\n private _loadFavorites(): void {\n const ls = this._localStorage();\n if (ls === null) return;\n try {\n const raw = ls.getItem(FAVORITES_STORAGE_KEY);\n if (raw !== null && raw !== '') {\n const parsed = JSON.parse(raw) as unknown;\n const symbols = this._extractSymbols(parsed);\n for (const s of symbols) this._favorites.add(s);\n for (const t of this._extractTriplets(parsed)) this._favoriteTriplets.push(t);\n return;\n }\n } catch {\n // Fall through to legacy migration.\n }\n // Legacy migration — old shape was a JSON array of `\"<polymerType>|<symbol>\"`.\n try {\n const legacy = ls.getItem(LEGACY_FAVORITES_STORAGE_KEY);\n if (legacy === null || legacy === '') return;\n const parsed = JSON.parse(legacy) as unknown;\n if (Array.isArray(parsed)) {\n for (const v of parsed) {\n if (typeof v !== 'string') continue;\n const pipeIdx = v.indexOf('|');\n const sym = pipeIdx >= 0 ? v.slice(pipeIdx + 1) : v;\n if (sym !== '') this._favorites.add(sym);\n }\n // Write through to the new shape so we don't migrate twice.\n this._saveFavorites();\n }\n } catch {\n // Ignore corrupted entries.\n }\n }\n\n /** Accept both the new `{symbols: string[]}` shape and a raw array. */\n private _extractSymbols(parsed: unknown): string[] {\n if (Array.isArray(parsed)) {\n const out: string[] = [];\n for (const v of parsed) if (typeof v === 'string') out.push(v);\n return out;\n }\n if (parsed !== null && typeof parsed === 'object') {\n const obj = parsed as { symbols?: unknown };\n if (Array.isArray(obj.symbols)) {\n const out: string[] = [];\n for (const v of obj.symbols) if (typeof v === 'string') out.push(v);\n return out;\n }\n }\n return [];\n }\n\n /** T-3513 — Extract the (optional) `triplets` array from stored favorites. */\n private _extractTriplets(parsed: unknown): FavoriteTriplet[] {\n if (parsed === null || typeof parsed !== 'object') return [];\n const obj = parsed as { triplets?: unknown };\n if (!Array.isArray(obj.triplets)) return [];\n const out: FavoriteTriplet[] = [];\n for (const v of obj.triplets) {\n if (v === null || typeof v !== 'object') continue;\n const t = v as { sugar?: unknown; base?: unknown; linker?: unknown };\n if (typeof t.sugar === 'string' && typeof t.base === 'string' && typeof t.linker === 'string')\n out.push({ sugar: t.sugar, base: t.base, linker: t.linker });\n }\n return out;\n }\n\n private _saveFavorites(): void {\n const ls = this._localStorage();\n if (ls === null) return;\n try {\n const payload: StoredFavorites = { symbols: Array.from(this._favorites) };\n if (this._favoriteTriplets.length > 0)\n payload.triplets = this._favoriteTriplets.map((t) => ({ ...t }));\n ls.setItem(FAVORITES_STORAGE_KEY, JSON.stringify(payload));\n } catch {\n // Ignore quota errors.\n }\n }\n}\n","// T-0602 / T-1201 / T-1301a — Palette tile renderer.\n//\n// Pure helper for building a single palette tile DOM. Kept separate from\n// `palette.ts` so the structural detail (icon, label, star, colors) is easy\n// to inspect and unit-test on its own (tests live in `palette.test.ts`).\n//\n// Phase 12 (T-1201) redesign:\n// - Rounded squares (~48×48 px, border-radius 10 px) instead of inline\n// rectangles.\n// - Soft pastel backgrounds: `soften()` desaturates the canonical palette\n// colour by ~30% and lightens by ~10%.\n// - Subtle drop-shadow + thin border at rest; thick accent border on hover\n// and when `aria-pressed=\"true\"` (selected/primed).\n// - Tile background colour computed once per (monomer, theme) and stored\n// on the data attributes so tests can inspect the actual rendered values.\n// - Insufficient-contrast fallback: when the softened background is too\n// close to the label colour (WCAG contrast < 5:1), we attach a `--low-\n// contrast` modifier class which the stylesheet uses to add a 1px inner\n// shadow boundary.\n//\n// Phase 13a (T-1301a, Bug 1) — text overflow:\n// - Tile is now flexible width: min-width 48 px, max-width 96 px, fixed\n// 48 px height. Long symbols (e.g. `deamino-Phg_35diM`) no longer push\n// the label out of the tile.\n// - Label uses `white-space: nowrap; overflow: hidden; text-overflow:\n// ellipsis;` so overflow renders as `…` instead of spilling out.\n// - Label font shrinks from 14px → 11px when the symbol is longer than\n// 3 characters. The full symbol stays in `title` and in `aria-label`\n// for accessibility.\n//\n// Layering: imports from `core/`, `monomer-lib/`, `render/`, and the local\n// `view/dom-utils`. No editor-level imports.\n\nimport type { HelmType } from '../../core/constants';\nimport type { Monomer } from '../../monomer-lib/types';\nimport { contrastRatio, resolveMonomerColors, soften } from '../../monomer-lib/color-rules';\nimport type { Theme } from '../../render/theme';\nimport { el } from '../view/dom-utils';\n\n/** Minimum contrast ratio (label vs. background) required for a tile. */\nexport const TILE_MIN_CONTRAST = 5.0;\n\n/**\n * Phase 13a (Bug 1): label-shrink threshold. Symbols longer than this many\n * characters render at `TILE_LABEL_FONT_SMALL`; shorter symbols use\n * `TILE_LABEL_FONT_LARGE`. Three characters covers every natural-letter HELM\n * symbol (A, G, dA, m1Y, …); anything wider (e.g. `Phg`, `Aib`, `[PEG2]`)\n * has historically truncated visually — now it shrinks instead.\n *\n * Phase 18 (T-1805) extends the table with three more buckets:\n * - 4..6 chars → 11 px (the existing small font)\n * - 7..9 chars → 9 px\n * - 10+ chars → 8 px with line-height 1.1 + letter-spacing -0.02em\n *\n * The original two-bucket scheme had `meAA` (4 chars) at 11px and\n * `deamino-Phg_35diM` (17 chars) at the SAME 11 px — so the long symbol\n * still ellipsized down to \"deamin…\". The new ladder shrinks aggressively\n * once the tile is genuinely full.\n */\nexport const TILE_LABEL_SHRINK_THRESHOLD = 3;\n/** Large label font size (CSS px), used for 1..3 char symbols. */\nexport const TILE_LABEL_FONT_LARGE = 14;\n/** Small label font size (CSS px), used for 4..6 char symbols. */\nexport const TILE_LABEL_FONT_SMALL = 11;\n/** Phase 18 (T-1805) — Tiny label font size (CSS px), 7..9 char symbols. */\nexport const TILE_LABEL_FONT_TINY = 9;\n/** Phase 18 (T-1805) — Micro label font size (CSS px), 10+ char symbols. */\nexport const TILE_LABEL_FONT_MICRO = 8;\n/** Tile minimum width in CSS pixels (single short symbol). */\nexport const TILE_MIN_WIDTH = 48;\n/** Tile maximum width in CSS pixels (after ellipsis kicks in). */\nexport const TILE_MAX_WIDTH = 96;\n\n/**\n * Phase 18 (T-1805) — Resolve the label font size for a given symbol\n * length, following the four-bucket ladder documented above. Exposed so\n * tests can iterate the table and so callers (e.g. the RNA builder\n * preview) can match the palette's text sizing.\n */\nexport function tileLabelFontSize(symbolLength: number): number {\n if (symbolLength <= TILE_LABEL_SHRINK_THRESHOLD) return TILE_LABEL_FONT_LARGE;\n if (symbolLength <= 6) return TILE_LABEL_FONT_SMALL;\n if (symbolLength <= 9) return TILE_LABEL_FONT_TINY;\n return TILE_LABEL_FONT_MICRO;\n}\n\n/** Inputs accepted by `buildTile`. */\nexport interface BuildTileInput {\n /** Monomer record being represented (may be a synthetic stub for placeholders). */\n monomer: Monomer;\n /** Biotype of the monomer (used to resolve the color palette entry). */\n biotype: HelmType;\n /** Active theme (drives the background color). */\n theme: Theme;\n /** Whether the tile is currently marked as a favorite. */\n favorite: boolean;\n /** Whether the tile is the \"primed\" next-add symbol (visually highlighted). */\n primed: boolean;\n}\n\n/**\n * Build a draggable palette tile DOM element. Returns the root `<div>` plus\n * a reference to the star icon span so the palette can toggle its appearance.\n *\n * The rendered tile is:\n * `<div class=\"hw-palette__tile\" data-bg=\"#xxxxxx\" ...>\n * <span class=\"hw-palette__star\">…</span>\n * <span class=\"hw-palette__label\">A</span>\n * </div>`\n *\n * Tile styling lives in `src/editor/styles.ts` (.hw-palette__tile rules).\n */\nexport function buildTile(\n doc: Document,\n input: BuildTileInput,\n): { root: HTMLElement; star: HTMLElement } {\n const raw = resolveMonomerColors(input.monomer, input.biotype, input.theme.colorTheme);\n // Pastel tile background. The canvas keeps the saturated palette unless\n // theme.palette.softenCanvas is set; the palette is ALWAYS softened.\n const softBg = soften(raw.background);\n const labelColor = raw.text;\n const lowContrast = contrastRatio(labelColor, softBg) < TILE_MIN_CONTRAST;\n\n // Phase 13a (Bug 1) + Phase 18 (T-1805): pick a font size based on symbol\n // length so wide symbols (e.g. `deamino-Phg_35diM`) shrink rather than\n // overflow. The Phase 18 ladder adds tiny / micro buckets so the longest\n // HELMCoreLibrary symbols still render WITHOUT ellipsis.\n const symbol = input.monomer.symbol;\n const labelFontSize = tileLabelFontSize(symbol.length);\n // Phase 18 (T-1805): compact-mode extras for 10+ chars.\n const compact = symbol.length >= 10;\n const compactExtras = compact\n ? 'line-height:1.1;letter-spacing:-0.02em;'\n : '';\n // The label's inline style holds the truncation rules so the structural\n // unit test can read them without relying on getComputedStyle (linkedom\n // does not implement CSS resolution).\n const labelStyle =\n `font-size:${labelFontSize}px;${compactExtras}` +\n 'white-space:nowrap;overflow:hidden;text-overflow:ellipsis;' +\n // Cap the label's own width to the tile content area minus the star and\n // a few px of horizontal padding so ellipsis kicks in inside the tile.\n `max-width:${TILE_MAX_WIDTH - 16}px;display:inline-block;`;\n const star = el(doc, 'span', { class: 'hw-palette__star' }, [input.favorite ? '★' : '☆']);\n const label = el(doc, 'span', { class: 'hw-palette__label', style: labelStyle }, [symbol]);\n const classes = ['hw-palette__tile'];\n if (input.primed) classes.push('hw-palette__tile--primed');\n if (input.favorite) classes.push('hw-palette__tile--favorite');\n if (lowContrast) classes.push('hw-palette__tile--low-contrast');\n const fullName = input.monomer.name !== undefined ? `${symbol} — ${input.monomer.name}` : symbol;\n // Inline-style rules so structural tests can read them without a live\n // CSS engine, and so a host page that strips the stylesheet still gets\n // sane truncation behaviour.\n //\n // Phase 18 (T-1805): tiles are now rectangular (40 px tall) and fill\n // their grid cell (`width:100%`) so the parent grid's\n // `repeat(3, 1fr)` controls horizontal sizing. We keep `min-width:48px`\n // as a hard floor so the tile never collapses below a readable square\n // when the pane is unusually narrow.\n const rootStyle =\n `background:${softBg};color:${labelColor};` +\n `width:100%;min-width:${TILE_MIN_WIDTH}px;` +\n 'padding:4px 6px;height:40px;' +\n 'box-sizing:border-box;' +\n 'overflow:hidden;text-overflow:ellipsis;';\n const root = el(doc, 'div', {\n class: classes.join(' '),\n draggable: 'true',\n role: 'button',\n 'aria-pressed': input.primed ? 'true' : 'false',\n 'aria-label': fullName,\n 'data-symbol': symbol,\n 'data-biotype': input.biotype,\n 'data-bg': softBg,\n 'data-raw-bg': raw.background,\n 'data-testid': `palette-tile-${symbol}`,\n title: fullName,\n style: rootStyle,\n }, [star, label]);\n return { root, star };\n}\n\n// --------------------------------------------------------------------------\n// T-3513 — RNA triplet chip\n// --------------------------------------------------------------------------\n\n/** Inputs accepted by {@link buildTripletChip}. */\nexport interface BuildTripletChipInput {\n /** Sugar / base / linker symbols of the triplet (raw, un-bracketed). */\n sugar: string;\n base: string;\n linker: string;\n /** Display text — the formatted HELM fragment, e.g. `r(A)p`. */\n label: string;\n /** Native-tooltip text (full triplet + name when available). */\n title: string;\n /** Chip background colour (resolved from the base monomer). */\n background: string;\n /** Chip label colour. */\n textColor: string;\n /** Whether the triplet is currently a favorite (filled star). */\n favorite: boolean;\n /** Whether to render the favorite star at all (default true). */\n showStar?: boolean;\n}\n\n/**\n * Build a small clickable chip representing a whole RNA triplet\n * (sugar + base + linker), e.g. `r(A)p`. Used both for the RNA pane's\n * default-nucleotide row and for triplet favorites. Deliberately compact\n * — the RNA pane's vertical space is tight under the RNA builder.\n *\n * Returns the chip root plus the star span so the palette can wire a\n * favorite-toggle separate from the chip-body insert click. Pure: no\n * editor / service imports (label + colours are resolved by the caller).\n */\nexport function buildTripletChip(\n doc: Document,\n input: BuildTripletChipInput,\n): { root: HTMLElement; star: HTMLElement } {\n const showStar = input.showStar !== false;\n const star = el(doc, 'span', { class: 'hw-triplet-chip__star' }, [input.favorite ? '★' : '☆']);\n const label = el(doc, 'span', { class: 'hw-triplet-chip__label' }, [input.label]);\n const classes = ['hw-triplet-chip'];\n if (input.favorite) classes.push('hw-triplet-chip--favorite');\n const children = showStar ? [star, label] : [label];\n const root = el(doc, 'div', {\n class: classes.join(' '),\n role: 'button',\n tabindex: '0',\n 'aria-label': input.title,\n 'data-sugar': input.sugar,\n 'data-base': input.base,\n 'data-linker': input.linker,\n 'data-triplet': input.label,\n 'data-testid': `palette-triplet-${input.label}`,\n title: input.title,\n style: `background:${input.background};color:${input.textColor};`,\n }, children);\n return { root, star };\n}\n","// T-0802 — `IMonomerLibBase` (Datagrok) → `IMonomerLib` (HWE internal) bridge.\n//\n// The bridge is read-only — we never mutate the Datagrok-owned library; if\n// callers need to add monomers they should `MonomerLib.merge` against an\n// overlay. Translated `Monomer` records are cached by (polymerType, symbol)\n// so a follow-up `getMonomer` call returns the same reference (the\n// in-process `MonomerLib` relies on identity to attach `ResolvedMonomer`).\n//\n// See MONOMER-LIBRARY.md §10 and ARCHITECTURE.md §3.10.\n\nimport { HelmTypes, PolymerTypes } from '../core/constants';\nimport type { HelmType, MonomerType, PolymerType } from '../core/constants';\nimport { Subject } from '../util/observable';\nimport type {\n IMonomerLib,\n Monomer,\n MonomerColors,\n MonomerLibChangeEvent,\n MonomerLibSchema,\n MonomerMeta,\n RGroupSpec,\n} from '../monomer-lib/types';\nimport { resolveMonomerColors, lightPalette, darkPalette } from '../monomer-lib/color-rules';\nimport { readCapGroupSMILES } from '../monomer-lib/rgroup-fields';\nimport type {\n DatagrokMonomerLike,\n IMonomerLibBaseLike,\n ObservableLike,\n} from './types';\n\nconst RLABEL_RE = /^R([1-9][0-9]*)$/;\nconst VALID_POLYMER_TYPES = new Set<string>([\n PolymerTypes.PEPTIDE,\n PolymerTypes.RNA,\n PolymerTypes.CHEM,\n PolymerTypes.BLOB,\n]);\nconst VALID_MONOMER_TYPES = new Set<string>(['Backbone', 'Branch', 'Terminal', 'Undefined']);\n\n/**\n * Translate a single Datagrok `Monomer` into our internal `Monomer`. Returns\n * `null` if the record is missing required fields or carries an unknown\n * polymer/monomer type.\n */\nfunction translateMonomer(src: DatagrokMonomerLike): Monomer | null {\n if (typeof src.symbol !== 'string' || src.symbol.length === 0) return null;\n if (typeof src.molfile !== 'string') return null;\n if (!VALID_POLYMER_TYPES.has(src.polymerType)) return null;\n if (!VALID_MONOMER_TYPES.has(src.monomerType)) return null;\n\n const rgroups: RGroupSpec[] = [];\n for (const rg of src.rgroups ?? []) {\n if (typeof rg.label !== 'string' || !RLABEL_RE.test(rg.label)) continue;\n const spec: RGroupSpec = {\n label: rg.label as `R${number}`,\n capGroupName: typeof rg.capGroupName === 'string' ? rg.capGroupName : '',\n capGroupSMILES: readCapGroupSMILES(rg as unknown as Record<string, unknown>) ?? '',\n };\n if (typeof rg.alternateId === 'string') spec.alternateId = rg.alternateId;\n rgroups.push(spec);\n }\n\n const out: Monomer = {\n symbol: src.symbol,\n molfile: src.molfile,\n polymerType: src.polymerType as PolymerType,\n monomerType: src.monomerType as MonomerType,\n rgroups,\n };\n if (typeof src.name === 'string') out.name = src.name;\n if (typeof src.smiles === 'string') out.smiles = src.smiles;\n if (typeof src.naturalAnalog === 'string') out.naturalAnalog = src.naturalAnalog;\n if (src.meta !== undefined && src.meta !== null) out.meta = { ...src.meta };\n if (typeof src.author === 'string') out.author = src.author;\n if (src.createDate !== undefined) out.createDate = src.createDate;\n if (typeof src.id === 'number') out.id = src.id;\n return out;\n}\n\n/**\n * A representative HELM biotype for a polymer type, used only to drive\n * Datagrok's `getWebEditorMonomer(biotype, smiles)` on-spot registration\n * (it maps the biotype straight back to the polymer type). Peptide → AA,\n * RNA → SUGAR, CHEM/BLOB → themselves.\n */\nfunction representativeBiotype(polymerType: PolymerType): HelmType {\n if (polymerType === PolymerTypes.PEPTIDE) return HelmTypes.AA;\n if (polymerType === PolymerTypes.RNA) return HelmTypes.SUGAR;\n if (polymerType === PolymerTypes.CHEM) return HelmTypes.CHEM;\n if (polymerType === PolymerTypes.BLOB) return HelmTypes.BLOB;\n return HelmTypes.AA;\n}\n\n/** Map a `PolymerType` + `MonomerType` to the HELM biotype. */\nfunction biotypeOf(polymerType: string, monomerType: string, naturalAnalog?: string): HelmType {\n if (polymerType === PolymerTypes.PEPTIDE) return HelmTypes.AA;\n if (polymerType === PolymerTypes.CHEM) return HelmTypes.CHEM;\n if (polymerType === PolymerTypes.BLOB) return HelmTypes.BLOB;\n if (polymerType === PolymerTypes.RNA) {\n if (monomerType === 'Branch') return HelmTypes.BASE;\n if (monomerType === 'Backbone') {\n if (naturalAnalog === 'P' || naturalAnalog === 'p') return HelmTypes.LINKER;\n return HelmTypes.SUGAR;\n }\n }\n return HelmTypes.AA;\n}\n\n/**\n * Subscribe to an Observable-like input. The Datagrok `onChanged` may be a\n * proper rxjs Observable, a Subject, or a hand-rolled subject — they all\n * accept `subscribe(fn)` returning `{ unsubscribe }`.\n */\nfunction subscribeObservable<T>(\n obs: ObservableLike<T>,\n next: (value: T) => void,\n): () => void {\n const subscription = obs.subscribe(next);\n return () => {\n if (subscription !== null && subscription !== undefined && typeof subscription.unsubscribe === 'function')\n subscription.unsubscribe();\n };\n}\n\n/**\n * Read-only `IMonomerLib` view backed by a Datagrok `IMonomerLibBaseLike`.\n * Translation is lazy + cached per (polymerType, symbol).\n */\nexport class BridgedMonomerLib implements IMonomerLib {\n readonly source: string;\n readonly onChanged: Subject<MonomerLibChangeEvent>;\n\n private readonly _base: IMonomerLibBaseLike;\n private readonly _cache: Map<string, Monomer | null> = new Map();\n private readonly _unsub: () => void;\n private _polymerTypesCache: PolymerType[] | null = null;\n private _version: number = 0;\n\n constructor(base: IMonomerLibBaseLike, source?: string) {\n this._base = base;\n this.source = source ?? (typeof base.source === 'string' ? base.source : 'datagrok-bridge');\n this.onChanged = new Subject<MonomerLibChangeEvent>();\n // Bust caches + bump version + re-fire `reload` whenever Datagrok signals a change.\n this._unsub = subscribeObservable(base.onChanged, () => {\n this._cache.clear();\n this._polymerTypesCache = null;\n this._version += 1;\n this.onChanged.next({ type: 'reload' });\n });\n }\n\n /** Bumped on every observed source change; mirrors `MonomerLib.version`. */\n get version(): number {\n return this._version;\n }\n\n get isEmpty(): boolean {\n if (typeof this._base.isEmpty === 'boolean') return this._base.isEmpty;\n return this.getPolymerTypes().length === 0;\n }\n\n // ----- reads --------------------------------------------------------------\n\n getMonomer(polymerType: PolymerType, symbol: string): Monomer | null {\n const key = `${polymerType}|${symbol}`;\n if (this._cache.has(key)) return this._cache.get(key) ?? null;\n const raw = this._base.getMonomer(polymerType, symbol);\n const translated = raw === null || raw === undefined ? null : this._enrichColors(translateMonomer(raw));\n this._cache.set(key, translated);\n return translated;\n }\n\n /**\n * Fold Datagrok's per-monomer colours into the translated record's\n * `meta.colors.default` so hwe's `resolveMonomerColors` prefers them over its\n * built-in palette. Datagrok's colours are theme-agnostic (the `default`\n * schema), so we store them under `default`. No-op when the base library does\n * not expose `getMonomerColors` (keeps the bridge usable with bare libs).\n */\n private _enrichColors(m: Monomer | null): Monomer | null {\n if (m === null) return null;\n const base = this._base;\n if (typeof base.getMonomerColors !== 'function') return m;\n const biotype = biotypeOf(m.polymerType, m.monomerType, m.naturalAnalog);\n let c: { linecolor?: string; backgroundcolor?: string; textcolor?: string } | null = null;\n try { c = base.getMonomerColors(biotype, m.symbol); } catch { c = null; }\n if (c === null || c === undefined) return m;\n const bg = c.backgroundcolor;\n const text = c.textcolor;\n const line = c.linecolor;\n if (typeof bg !== 'string' || typeof text !== 'string' || typeof line !== 'string') return m;\n const meta: MonomerMeta = {\n ...(m.meta ?? {}),\n colors: { ...(m.meta?.colors ?? {}), default: { line, text, background: bg } },\n };\n return { ...m, meta };\n }\n\n /**\n * Register an explicit/inline SMILES monomer through Datagrok's monomer\n * functions. Datagrok's `getWebEditorMonomer` recognises a SMILES \"symbol\",\n * creates an on-spot monomer, and assigns it a short symbol (`#P1`, `#R2`,\n * …); `getMonomer` then returns that registered record (Datagrok caches the\n * SMILES→symbol mapping). We adopt the short symbol so the editor displays it\n * (not the raw SMILES) and the Datagrok molfile converter — which reads the\n * monomer symbol out of the pseudo-molfile and looks it up in this same\n * library — resolves it cleanly. Mirrors the legacy editor's behaviour.\n *\n * The hwe resolver calls this (via `InlineSmilesCapableLib`) when a monomer\n * looks like SMILES and is otherwise unresolved.\n */\n registerInlineSmilesMonomer(polymerType: PolymerType, smiles: string): Monomer {\n const base = this._base;\n if (typeof base.getWebEditorMonomer === 'function')\n base.getWebEditorMonomer(representativeBiotype(polymerType), smiles);\n const raw = base.getMonomer(polymerType, smiles);\n const translated = raw === null || raw === undefined ? null : this._enrichColors(translateMonomer(raw));\n if (translated === null)\n throw new Error(`BridgedMonomerLib: could not register inline SMILES monomer '${smiles}'`);\n // Cache under BOTH the raw SMILES (the resolver's first lookup key, which\n // we previously cached as `null`) and the registered short symbol (the\n // converter's later lookup key) so neither path re-misses.\n this._cache.set(`${polymerType}|${smiles}`, translated);\n this._cache.set(`${polymerType}|${translated.symbol}`, translated);\n return translated;\n }\n\n getMonomerSet(biotype: HelmType): Map<string, Monomer> {\n const out = new Map<string, Monomer>();\n for (const pt of this.getPolymerTypes()) {\n for (const sym of this.getMonomerSymbolsByType(pt)) {\n const m = this.getMonomer(pt, sym);\n if (m === null) continue;\n if (biotypeOf(m.polymerType, m.monomerType, m.naturalAnalog) === biotype)\n out.set(sym, m);\n }\n }\n return out;\n }\n\n getMonomerSymbolsByType(polymerType: PolymerType): string[] {\n return this._base.getMonomerSymbolsByType(polymerType);\n }\n\n getByNaturalAnalog(polymerType: PolymerType, na: string): Monomer[] {\n const out: Monomer[] = [];\n for (const sym of this.getMonomerSymbolsByType(polymerType)) {\n const m = this.getMonomer(polymerType, sym);\n if (m !== null && m.naturalAnalog === na) out.push(m);\n }\n return out;\n }\n\n getPolymerTypes(): PolymerType[] {\n if (this._polymerTypesCache !== null) return this._polymerTypesCache;\n const out: PolymerType[] = [];\n const declared = typeof this._base.getPolymerTypes === 'function'\n ? this._base.getPolymerTypes()\n : Object.values(PolymerTypes);\n for (const pt of declared) {\n if (!VALID_POLYMER_TYPES.has(pt)) continue;\n const symbols = this._base.getMonomerSymbolsByType(pt);\n if (Array.isArray(symbols) && symbols.length > 0) out.push(pt as PolymerType);\n }\n this._polymerTypesCache = out;\n return out;\n }\n\n getColors(monomer: Monomer | null, themeName?: string): MonomerColors {\n const theme = themeName === 'dark' ? darkPalette : lightPalette;\n const biotype = monomer === null\n ? HelmTypes.AA\n : biotypeOf(monomer.polymerType, monomer.monomerType, monomer.naturalAnalog);\n return resolveMonomerColors(monomer, biotype, theme);\n }\n\n // ----- mutators (read-only bridge → throw or no-op) -----------------------\n\n addMonomer(_monomer: Monomer): void {\n throw new Error('BridgedMonomerLib: mutation is not supported on a Datagrok-bridged library.');\n }\n\n removeMonomer(_polymerType: PolymerType, _symbol: string): boolean {\n return false;\n }\n\n merge(_other: IMonomerLib): void {\n throw new Error('BridgedMonomerLib: mutation is not supported on a Datagrok-bridged library.');\n }\n\n toJSON(): MonomerLibSchema {\n const out: MonomerLibSchema = [];\n for (const pt of this.getPolymerTypes()) {\n for (const sym of this.getMonomerSymbolsByType(pt)) {\n const m = this.getMonomer(pt, sym);\n if (m !== null) out.push(m);\n }\n }\n return out;\n }\n\n /** Tear down the upstream subscription. Safe to call more than once. */\n destroy(): void {\n this._unsub();\n this.onChanged.complete();\n this._cache.clear();\n }\n}\n\n/**\n * Wrap a Datagrok `IMonomerLibBaseLike` as an internal `IMonomerLib`. Useful\n * when a host wants to plug a Datagrok-managed monomer source into the\n * standalone HWE service.\n */\nexport function bridgeMonomerLib(\n datagrokLib: IMonomerLibBaseLike,\n source?: string,\n): BridgedMonomerLib {\n return new BridgedMonomerLib(datagrokLib, source);\n}\n","// HELM type vocabulary is owned by `@datagrok-libraries/hwe` (the standalone\n// HELM editor) and re-exported here as the single source of truth. The string\n// values match the canonical Pistoia HELM Web Editor, except `NUCLEOTIDE` which\n// uses the corrected spelling `HELM_NUCLEOTIDE` (was `HELM_NUCLETIDE`).\nimport {HelmTypes, MonomerTypes, PolymerTypes} from '@datagrok-libraries/hwe';\n\nexport {HelmTypes, MonomerTypes, PolymerTypes};\n\n// hwe migration: these two small enums are not part of the hwe value vocabulary.\n// They are defined here locally (replicating the legacy HELM Web Editor values)\n// so `@datagrok-libraries/bio` no longer depends on\n// `@datagrok-libraries/helm-web-editor`.\nexport enum MonomerNumberingTypes {\n default = 0,\n continuous = 1,\n}\n\nexport enum HelmTabKeys {\n Sequence = 'sequence',\n Helm = 'notation',\n Properties = 'properties',\n StructureView = 'structureview',\n}\n","import {PolymerTypes} from '../helm/consts';\n\nexport const monomerWorksConsts = {\n// constants for parsing molfile V2000\n V2K_RGP_SHIFT: 8,\n V2K_RGP_LINE: 'M RGP',\n V2K_A_LINE: 'A ',\n // constants for parsing/reconstruction of molfile V3000\n V3K_COUNTS_SHIFT: 14,\n V3K_IDX_SHIFT: 7,\n V3K_HEADER_FIRST_LINE: '\\nDatagrok macromolecule handler\\n\\n',\n V3K_HEADER_SECOND_LINE: ' 0 0 0 0 0 0 999 V3000\\n',\n V3K_BEGIN_CTAB_BLOCK: 'M V30 BEGIN CTAB\\n',\n V3K_END_CTAB_BLOCK: 'M V30 END CTAB\\n',\n V3K_BEGIN_COUNTS_LINE: 'M V30 COUNTS ',\n V3K_COUNTS_LINE_ENDING: ' 0 0 0\\n',\n V3K_BEGIN_ATOM_BLOCK: 'M V30 BEGIN ATOM\\n',\n V3K_END_ATOM_BLOCK: 'M V30 END ATOM\\n',\n V3K_BEGIN_BOND_BLOCK: 'M V30 BEGIN BOND\\n',\n V3K_END_BOND_BLOCK: 'M V30 END BOND\\n',\n V3K_BOND_CONFIG: ' CFG=',\n V3K_BEGIN_DATA_LINE: 'M V30 ',\n V3K_END: 'M END',\n PRECISION_FACTOR: 10_000, // HELMCoreLibrary has 4 significant digits after decimal point in atom coordinates\n // symbols for the corresponding monomers in HELM library\n\n DEOXYRIBOSE: {polymerType: PolymerTypes.RNA, symbol: 'd'},\n RIBOSE: {polymerType: PolymerTypes.RNA, symbol: 'r'},\n PHOSPHATE: {polymerType: PolymerTypes.RNA, symbol: 'p'},\n\n OXYGEN: 'O',\n HYDROGEN: 'H',\n} as const;\n\n","// interface for typed arrays, like Float32Array and Uint32Array\nimport {ALPHABET} from '../utils/macromolecule';\nimport {HELM_POLYMER_TYPE} from '../utils/const';\nimport {HelmType, ISeqMonomer} from '../helm/types';\n\nexport interface ITypedArray {\n length: number;\n [key: number]: any;\n}\n\n/** Type for user settings of monomer library set to use. */\nexport type UserLibSettings = {\n exclude: string[],\n explicit: string[],\n /** Libraries might contain different monomers for same symbol. Object for monomer symbol to library name of choice*/\n duplicateMonomerPreferences: {[polymerType: string]: {[monomerSymbol: string]: string}}\n}\n\n/** Stores necessary data about atoms of a monomer parsed from Molfile */\nexport type Atoms = {\n /** Element symbols for monomer's atoms */\n atomTypes: string[],\n /** Cartesian coordiantes of monomer's atoms */\n x: Float32Array,\n y: Float32Array,\n /** V3K atom line may contain keyword args */\n kwargs: string[],\n }\n\n/** Stores necessary data about bonds of a monomer parsed from Molfile */\nexport type Bonds = {\n /** bond types for all lines of Molfile bond block */\n bondTypes: Uint32Array,\n /** Indices of all atom pairs, indexing starting from 1 */\n atomPairs: number[][],\n /** If a bond has CFG=... keyword argument, it is parsed and sotred as a\n * value of the map, with the key being the bond's index */\n bondConfiguration: Map<number, number>,\n /** V3K bond line may contain keyword args */\n kwargs: Map<number, string>,\n}\n\n/** Metadata associated with the monomer necessary to restore the resulting molfile */\nexport type MonomerMetadata = {\n /** terminal nodes: 0-th corresponds to the \"leftmost\" one, 1st, to the \"rightmost\",\n * e.g. N-terminus and C-terminus in peptides */\n terminalNodes: number[],\n /** r-group nodes: 0-th corresponds to the \"leftmost\" one, 1st, to the \"rightmost\" */\n rNodes: number[],\n /** shift from the origin to the next backbone, null for branch monomers */\n backboneShift: number[] | null,\n /** shift from the origin to the next branch, null for branch monomers */\n branchShift: number[] | null\n}\n\nexport type MolGraph = {\n atoms: Atoms,\n bonds: Bonds,\n meta: MonomerMetadata,\n stereoAtoms?: number[],\n terminalR2Atom?: string, // for peptides, store the removed R2 Atom type, if any\n}\n\nexport type MonomerMolGraphMap = { [polymerType: string]: { [symbol: string]: MolGraph } };\n\nexport type LibMonomerKey = { polymerType: string, symbol: string }\n\nexport function getMolGraph(dict: MonomerMolGraphMap, libKey: LibMonomerKey): MolGraph | undefined {\n return dict[libKey.polymerType]?.[libKey.symbol];\n}\n\nexport function hasMolGraph(dict: MonomerMolGraphMap, libKey: LibMonomerKey): boolean {\n return !!dict[libKey.polymerType]?.[libKey.symbol];\n}\n\nexport function setMolGraph(dict: MonomerMolGraphMap, libKey: LibMonomerKey, value: MolGraph): void {\n let pt = dict[libKey.polymerType];\n if (!pt)\n pt = dict[libKey.polymerType] = {};\n pt[libKey.symbol] = value;\n}\n\n// export function getMolGraph(\n// dict: MonomerMolGraphMap, polymerType: PolymerType, symbol: string\n// ): MolGraph | undefined {\n// return dict[polymerType]?.[symbol];\n// }\n\nexport type Point = {\n x: number,\n y: number\n}\n\n/** Helper structure wrapping common arguments to several functions */\nexport type LoopVariables = {\n i: number,\n nodeShift: number,\n bondShift: number,\n backbonePositionShift: number[],\n backboneAttachNode: number; // node to which the next backbone is attached\n branchPositionShift: number[],\n branchAttachNode: number,\n flipFactor: number,\n // todo: should we consider representations other than planar?\n}\n\n/** Helper structure wrapping common arguments to several functions */\nexport type LoopConstants = {\n sugar: MolGraph | null,\n phosphate: MolGraph | null,\n seqLength: number,\n atomCount: number,\n bondCount: number,\n}\n\n/** Helper structure to simulate pointer to number */\nexport type NumberWrapper = {\n value: number | null // null if there is no branch attach node\n}\n\n/** Role of an entry in a HELM RNA splitted sequence.\n * SUGAR/BASE/PHOSPHATE are the standard triple positions.\n * TERMINAL_5P marks a 5'-end terminal modifier (e.g. Chol) — a monomer\n * with R2 only that takes the place of the leading sugar.\n * TERMINAL_3P marks a 3'-end terminal modifier (e.g. GalNAc) — a monomer\n * with R1 only that takes the place of the trailing phosphate. The OH cap\n * MUST be omitted when a TERMINAL_3P is present. */\nexport enum NucleotideRole {\n SUGAR = 0,\n BASE = 1,\n PHOSPHATE = 2,\n TERMINAL_5P = 3,\n TERMINAL_3P = 4,\n}\n\nexport type MonomerMapValue = { biotype: HelmType, symbol: string, atoms: number[], bonds: number[] };\n\nexport class MonomerMap extends Map<number, MonomerMapValue> {\n constructor(entries?: [number, MonomerMapValue][] | null) {\n super(entries);\n }\n}\n\n/** @property monomers key - helm seq position, */\nexport class MolfileWithMap {\n constructor(\n public readonly molfile: string,\n public readonly monomers: MonomerMap,\n ) {}\n\n static createEmpty() { return new MolfileWithMap('', new MonomerMap(null)); }\n}\n\n/** Only simple types allowed for worker data, avoid classes with methods */\nexport type SeqToMolfileWorkerData = {\n seqList: ISeqMonomer[][],\n /** Per-row parallel array of NucleotideRole tags (only set for rows in\n * \"HELM RNA triples\" mode, where each nucleotide contributes 3 entries\n * sugar/base/phosphate). undefined for legacy bases-only rows. */\n rolesList?: (NucleotideRole[] | undefined)[],\n monomersDict: MonomerMolGraphMap,\n alphabet: ALPHABET,\n polymerType: HELM_POLYMER_TYPE,\n start: number,\n end: number,\n}\n\nexport type SeqToMolfileWorkerRes = {\n molfiles: MolfileWithMap[];\n warnings: string[]\n}\n","import {MonomerType, PolymerType} from '../helm/types';\nimport {MonomerTypes, PolymerTypes} from '../helm/consts';\nimport {RGroup} from '../types/monomer-library';\n\n// hwe exports the HELM type vocabulary as `as const` objects (value-only), not\n// TS enums, so we re-create the value+type duality the old `import X = Enum`\n// aliases provided — consumers use these both as a value (`.RNA`) and as a type.\nconst HELM_POLYMER_TYPE = PolymerTypes;\ntype HELM_POLYMER_TYPE = PolymerType;\nconst HELM_MONOMER_TYPE = MonomerTypes;\ntype HELM_MONOMER_TYPE = MonomerType;\n\nexport {HELM_POLYMER_TYPE, HELM_MONOMER_TYPE};\n\n/** Required HELM library monomer fields:\n * https://github.com/PistoiaHELM/HELMMonomerSets/blob/master/HELMmonomerSchema.json */\nexport const enum HELM_REQUIRED_FIELD {\n SYMBOL = 'symbol',\n NAME = 'name',\n MOLFILE = 'molfile',\n AUTHOR = 'author',\n ID = 'id',\n RGROUPS = 'rgroups',\n SMILES = 'smiles',\n POLYMER_TYPE = 'polymerType',\n MONOMER_TYPE = 'monomerType',\n CREATE_DATE = 'createDate',\n}\n\n// fields of \"rgroups\" sub-object in HELM library\nexport const enum HELM_RGROUP_FIELDS {\n CAP_GROUP_SMILES = 'capGroupSmiles',\n // WARNING: both capitalization variants coexist\n CAP_GROUP_SMILES_UPPERCASE = 'capGroupSMILES',\n ALTERNATE_ID = 'alternateId',\n CAP_GROUP_NAME = 'capGroupName',\n LABEL = 'label',\n}\n\nexport const enum HELM_OPTIONAL_FIELDS {\n NATURAL_ANALOG = 'naturalAnalog',\n META = 'meta', // for SequenceTranslator\n}\n\n// todo: remove\nexport const enum HELM_FIELDS {\n MONOMER_TYPE = 'monomerType',\n SMILES = 'smiles',\n NAME = 'name',\n AUTHOR = 'author',\n MOLFILE = 'molfile',\n NATURAL_ANALOG = 'naturalAnalog',\n RGROUPS = 'rgroups',\n CREATE_DATE = 'createDate',\n ID = 'id',\n POLYMER_TYPE = 'polymerType',\n SYMBOL = 'symbol'\n}\n\n// core fields of HELM library object used in toAtomicLevel function\nexport const HELM_CORE_FIELDS = [\n HELM_FIELDS.SYMBOL,\n HELM_FIELDS.MOLFILE,\n HELM_FIELDS.RGROUPS,\n HELM_FIELDS.NAME,\n // HELM_FIELDS.MONOMER_TYPE, // add if terminal monomers for PEPTIDEs to be\n // supported\n];\n\nexport const SDF_MONOMER_NAME = 'MonomerName';\n\n// todo: ideally, keys should be expressed via constants\nexport const jsonSdfMonomerLibDict = {\n 'monomerType': null, // -> Backbone\n 'smiles': null,\n 'name': 'Name',\n 'author': null,\n 'molfile': 'molecule',\n 'naturalAnalog': 'MonomerNaturalAnalogCode',\n 'rgroups': 'MonomerCaps',\n 'createDate': null,\n 'id': null,\n 'polymerType': 'MonomerType',\n 'symbol': 'MonomerName'\n};\n\nexport const DUMMY_MONOMER = {\n 'monomerType': 'Backbone' as MonomerType,\n 'smiles': '',\n 'name': '',\n 'author': 'Datagrok',\n 'molfile': '',\n 'naturalAnalog': '',\n 'rgroups': [] as RGroup[],\n 'createDate': null,\n 'id': 0,\n 'polymerType': 'PEPTIDE' as PolymerType,\n 'symbol': ''\n} as const;\n\n// range of hex nubers used in PepSea library to endode monomers\nexport const MONOMER_ENCODE_MIN = 0x100;\nexport const MONOMER_ENCODE_MAX = 0x40A;\n\nexport const RIBOSE_SYMBOL = 'r';\nexport const DEOXYRIBOSE_SYMBOL = 'd';\nexport const PHOSPHATE_SYMBOL = 'p';\nexport const HELM_WRAPPERS_REGEXP = new RegExp(\n `[${RIBOSE_SYMBOL}${DEOXYRIBOSE_SYMBOL}]\\\\((\\\\w)\\\\)${PHOSPHATE_SYMBOL}?`,\n 'g'\n);\n\n","import * as grok from 'datagrok-api/grok';\nimport * as ui from 'datagrok-api/ui';\nimport * as DG from 'datagrok-api/dg';\n\nimport {CellRendererBackBase} from '../cell-renderer-back-base';\nimport {ISeqHandler} from './seq-handler';\nimport {PolymerType} from '../../helm/types';\nimport {NOTATION_PROVIDER_CONSTRUCTOR_ROLE} from './consts';\n\nexport type SeqSplittedBase = ArrayLike<string> & Iterable<string>;\n\nexport interface ISeqConnection {\n seq1Type: PolymerType;\n seq2Type: PolymerType;\n seqIndex1: number;\n seqIndex2: number;\n monomerIndex1: number;\n monomerIndex2: number;\n rGroup1: number;\n rGroup2: number;\n}\n\nexport interface ISeqGraphInfo {\n /** Connections between monomers with numbers - note that monomer numbers are 0-based*/\n connections: ISeqConnection[];\n /** Start positions of disjoint sequence parts in the original sequence */\n disjointSeqStarts: number[];\n /** Polimer types same as the indexes. only applicapble to helm */\n polymerTypes?: PolymerType[];\n}\n\nexport interface ISeqSplitted {\n\n graphInfo?: ISeqGraphInfo;\n\n isGap(posIdx: number): boolean;\n\n /** */\n getCanonical(posIdx: number): string;\n\n /** For fasta and Helm must not be enclosed to square brackets [meA].*/\n getOriginal(posIdx: number): string;\n\n // TODO: Get ISeqMonomer for seq position\n // get(posIdx: number): ISeqMonomer;\n\n length: number;\n\n /** Returns list of canonical monomers in the region specified */\n getCanonicalRegion(start: number, end: number): string[];\n\n /** Returns the list of original monomers in the region specified */\n getOriginalRegion(start: number, end: number): string[];\n\n get gapOriginal(): string;\n}\n\n/**\n * Extracts displayable monomer symbol(s) from a raw position value.\n * Used by the Monomer cell renderer when a notation provider needs custom\n * interpretation of split-to-monomer position values.\n *\n * For example, a Parabilis position `[VHL1379]*dLys` contains two monomers\n * (`VHL1379` and `dLys`). The canonicalizer separates them with\n * {@link MONOMER_MOTIF_SPLITTER} so the renderer can display/tooltip each one.\n */\nexport interface IMonomerCanonicalizer {\n /** Convert a raw position value to a displayable monomer string.\n * If the position contains multiple monomers, join them with\n * {@link MONOMER_MOTIF_SPLITTER} (` , `). */\n canonicalize(original: string): string;\n\n /** Returns true if the raw position value represents a gap in this notation. */\n isGap(original: string): boolean;\n}\n\nexport interface INotationProvider {\n get defaultGapOriginal(): string;\n\n /** Adjust {@link seqHandler} units, {@link seqHandler.column.tags} by {@link seqHandler} constructor */\n setUnits(seqHandler: ISeqHandler): void;\n\n get splitter(): SplitterFunc;\n\n /** Any Macromolecule can be presented as Helm notation */\n getHelm(seq: string, options: any): string;\n\n createCellRendererBack(gridCol: DG.GridColumn | null, tableCol: DG.Column<string>): CellRendererBackBase<string>;\n\n /** nqName of a registered function that returns an {@link IMonomerCanonicalizer} instance.\n * When set, split-to-monomers will tag Monomer columns with this function name so the\n * monomer cell renderer can resolve display symbols from raw position values.\n * Return null/undefined if no custom canonicalization is needed. */\n readonly monomerCanonicalizerFuncName?: string | null;\n}\n\nexport abstract class NotationProviderBase {\n /** Name of the custom notation */\n static get notationName(): string {\n return 'Custom';\n };\n\n /** flag to let bio know if this provider implements method for converting helm to it */\n static get implementsFromHelm(): boolean {\n return false;\n };\n\n /** Method for converting HELM to this notation */\n static convertFromHelm(helm: string, options: any): string {\n throw new Error(`Method convertFromHelm not implemented for this notation provider`);\n };\n\n static async getProviderConstructors(): Promise<typeof NotationProviderBase[]> {\n // this is terrible, I know, but otherwise this gets put in webworkers and fails due to DG resolution)))\n // @ts-ignore\n if (window?.DG) {\n // @ts-ignore\n const constFuncs: any[] = window.DG.Func.find({meta: {role: NOTATION_PROVIDER_CONSTRUCTOR_ROLE}});\n return Promise.all(constFuncs.map((f) => f.apply({})));\n }\n return [];\n }\n}\n\nexport type SeqColStats = { freq: MonomerFreqs, sameLength: boolean }\nexport type SplitterFunc = (seq: string) => ISeqSplitted;\nexport type MonomerFreqs = { [m: string]: number };\n\n/** Alphabet candidate type */\nexport class CandidateType {\n name: string;\n alphabet: Set<string>;\n cutoff: number;\n\n constructor(name: string, alphabet: Set<string>, cutoff: number) {\n this.name = name;\n this.alphabet = alphabet;\n this.cutoff = cutoff;\n }\n}\n\n/** Alphabet candidate similarity type */\nexport class CandidateSimType extends CandidateType {\n freq: MonomerFreqs;\n /** Cos, max = 1, min = 0 */\n similarity: number;\n\n constructor(candidate: CandidateType, freq: MonomerFreqs, similarity: number) {\n super(candidate.name, candidate.alphabet, candidate.cutoff);\n this.freq = freq;\n this.similarity = similarity;\n }\n}\n","import {CandidateType} from './types';\n\n/** enum type to simplify setting \"user-friendly\" notation if necessary */\nexport enum NOTATION {\n FASTA = 'fasta',\n SEPARATOR = 'separator',\n HELM = 'helm',\n /* Requires notation handler */ CUSTOM = 'custom',\n /* Requires notation handler */ BILN = 'biln',\n}\n\nexport const enum ALIGNMENT {\n SEQ_MSA = 'SEQ.MSA',\n SEQ = 'SEQ',\n}\n\nexport enum ALPHABET {\n DNA = 'DNA',\n RNA = 'RNA',\n PT = 'PT',\n /** Unknown */\n UN = 'UN',\n}\n\nexport enum TAGS {\n aligned = 'aligned',\n alphabet = 'alphabet',\n alphabetSize = '.alphabetSize',\n alphabetIsMultichar = '.alphabetIsMultichar',\n separator = 'separator',\n isHelmCompatible = '.isHelmCompatible',\n positionNames = '.positionNames',\n positionLabels = '.positionLabels',\n regions = '.regions',\n positionShift = '.positionShift',\n selectedPosition = '.selectedPosition',\n polymerTypeColumnName = '.polymerTypeColumnName',\n annotations = '.annotations',\n numberingScheme = '.numberingScheme',\n annotationColumnName = '.annotationColumnName',\n}\n\nexport {TAGS as BioTags};\n\nexport const positionSeparator: string = ', ';\n\nexport const monomerRe: RegExp = /(?:\\[([A-Za-z0-9_\\-,()]+)\\])|([A-Za-z\\-])/g;\n\nexport const helmRe: RegExp = /(PEPTIDE1|DNA1|RNA1)\\{([^}]+)}/g;\nexport const helmPp1Re: RegExp = /\\[([^\\[\\]]+)]/g;\n\nexport const Alphabets = new class {\n fasta = {\n peptide: new Set<string>([\n 'G', 'L', 'Y', 'S', 'E', 'Q', 'D', 'N', 'F', 'A',\n 'K', 'R', 'H', 'C', 'V', 'P', 'W', 'I', 'M', 'T',\n ]),\n dna: new Set<string>(['A', 'C', 'G', 'T']),\n rna: new Set<string>(['A', 'C', 'G', 'U']),\n };\n}();\n\nexport const candidateAlphabets: CandidateType[] = [\n new CandidateType(ALPHABET.PT, Alphabets.fasta.peptide, 0.50),\n new CandidateType(ALPHABET.DNA, Alphabets.fasta.dna, 0.55),\n new CandidateType(ALPHABET.RNA, Alphabets.fasta.rna, 0.55),\n];\n\n/** Canonical gap symbol */\nexport const GAP_SYMBOL: string = '' as const;\n\nexport const GapOriginals: {\n [units: string]: string\n} = {\n [NOTATION.FASTA]: '-',\n [NOTATION.SEPARATOR]: '',\n [NOTATION.HELM]: '*',\n [NOTATION.BILN]: '',\n};\n\nexport const MONOMER_MOTIF_SPLITTER = ' , ';\n\n/** Tag on Monomer columns storing the nqName of a function that returns an IMonomerCanonicalizer.\n * The `.%` prefix ensures this tag is persisted with projects. */\nexport const MONOMER_CANONICALIZER_FUNC_TAG = '.%monomer-canonicalizer-func';\n\n/** Column temp key for the cached IMonomerCanonicalizer instance */\nexport const MONOMER_CANONICALIZER_TEMP = 'monomer-canonicalizer';\nexport const NOTATION_PROVIDER_CONSTRUCTOR_ROLE = 'notationProviderConstructor';\n","import * as grok from 'datagrok-api/grok';\nimport * as ui from 'datagrok-api/ui';\nimport * as DG from 'datagrok-api/dg';\n\nimport {IMonomerLibBase} from '../types/monomer-library';\nimport {HelmType, PolymerType} from '../helm/types';\nimport {HelmTypes, PolymerTypes} from '../helm/consts';\n\n/** Hypothetical interface to convert mol block notation.\n * It should be placed in the chem-meta package, and have an implementation in the Chem package.\n * So dependency of MonomerWorks on molfile conversion operation becomes explicit.\n */\nexport interface IMolfileConverter {\n convertV2000toV3000(src: string): string;\n\n convertV3000toV2000(src: string): string;\n}\n\nexport class MonomerWorks {\n private monomerLib: IMonomerLibBase;\n\n //private molfileConverter: IMolfileConverter;\n\n constructor(monomerLib: IMonomerLibBase/*, molfileConverter: IMolfileConverter*/) {\n this.monomerLib = monomerLib;\n //this.molfileConverter = molfileConverter;\n }\n\n //types according to Monomer possible\n public getCappedRotatedMonomer(monomerType: PolymerType, monomerName: string): string | null {\n // TODO: Check type of monomerType arg\n const monomer = this.monomerLib.getMonomer(monomerType, monomerName);\n if (monomer)\n return monomer.molfile; //TODO cap\n\n return null;\n }\n}\n\nexport function helmTypeToPolymerType(helmType: HelmType): PolymerType {\n let polymerType: PolymerType | undefined = undefined;\n switch (helmType) {\n case HelmTypes.BASE:\n case HelmTypes.SUGAR: // r - ribose, d - deoxyribose\n case HelmTypes.LINKER: // p - phosphate\n case HelmTypes.NUCLEOTIDE:\n // @ts-ignore\n case 'nucleotide':\n polymerType = PolymerTypes.RNA;\n break;\n case HelmTypes.AA:\n polymerType = PolymerTypes.PEPTIDE;\n break;\n case HelmTypes.CHEM:\n polymerType = PolymerTypes.CHEM;\n break;\n case HelmTypes.BLOB:\n polymerType = PolymerTypes.BLOB;\n break;\n default:\n polymerType = PolymerTypes.PEPTIDE;\n console.warn(`Unexpected HelmType '${helmType}'`);\n }\n return polymerType;\n}\n","/* eslint-disable max-len */\nimport {monomerWorksConsts as C} from './consts';\nimport {getMolGraph, LoopConstants, LoopVariables, MolfileWithMap, MolGraph, MonomerMap, MonomerMolGraphMap,\n NucleotideRole} from './types';\nimport {HELM_FIELDS, HELM_CORE_FIELDS, HELM_POLYMER_TYPE, HELM_MONOMER_TYPE,} from '../utils/const';\nimport {ALPHABET, GAP_SYMBOL} from '../utils/macromolecule/consts';\nimport {IMonomerLibBase, Monomer} from '../types/monomer-library';\nimport {ISeqMonomer, PolymerType} from '../helm/types';\nimport {helmTypeToPolymerType} from './monomer-works';\n\n\n/** Get a mapping of peptide symbols to HELM monomer library objects with selected fields.\n * @param {IMonomerLib} monomerLib - Monomer library\n * @param {HELM_POLYMER_TYPE} polymerType - Polymer type\n * @param {ALPHABET} alphabet - Alphabet of the column\n * @return {Map<string, any>} - Mapping of peptide symbols to HELM monomer library objects with selected fields*/\nexport function getFormattedMonomerLib(\n monomerLib: IMonomerLibBase, polymerType: PolymerType, alphabet: ALPHABET\n): Map<string, any> {\n const map = new Map<string, any>();\n for (const monomerSymbol of monomerLib.getMonomerSymbolsByType(polymerType)) {\n const it: Monomer = monomerLib.getMonomer(polymerType, monomerSymbol)!;\n if (\n // RNA: all branch monomers (bases) and all backbone monomers (sugars +\n // phosphates, including modified ones). Modifications are looked up by\n // symbol at assembly time, so they MUST be present in the formatted lib.\n polymerType === HELM_POLYMER_TYPE.RNA || (\n polymerType === HELM_POLYMER_TYPE.PEPTIDE &&\n it[HELM_FIELDS.MONOMER_TYPE] !== HELM_MONOMER_TYPE.BRANCH\n )) {\n const monomerObject: { [key: string]: any } = {};\n HELM_CORE_FIELDS.forEach((field) => {\n //@ts-ignore\n monomerObject[field] = it[field];\n });\n map.set(monomerSymbol, monomerObject);\n }\n }\n return map;\n}\n\n/** Translate a sequence of monomer symbols into Molfile V3000\n * @param {ISeqMonomer[]} monomerSeq - Sequence of monomer symbols (canonical)\n * @param {MonomerMolGraphMap} monomersDict - Mapping of monomer symbols to MolGraph objects\n * @param {ALPHABET} alphabet - Alphabet of the column\n * @param {PolymerType} polymerType - Polymer type\n * @param {Array} roles - Optional per-position NucleotideRole tags. When set, RNA assembly\n * uses per-position sugars/phosphates from monomerSeq directly (HELM triples mode).\n * @return {MolfileWithMap} - Molfile V3000 + per-position monomer index map */\nexport function monomerSeqToMolfile(\n monomerSeq: ISeqMonomer[], monomersDict: MonomerMolGraphMap,\n alphabet: ALPHABET, polymerType: PolymerType,\n roles?: NucleotideRole[]\n): MolfileWithMap {\n if (monomerSeq.length === 0) {\n // throw new Error('monomerSeq is empty');\n return MolfileWithMap.createEmpty();\n }\n\n // Triples mode is on only when the caller flagged the row with roles\n // (built and validated by `buildRolesForHelmRna` in to-atomic-level.ts).\n // The roles array carries the per-position semantics — including\n // TERMINAL_5P / TERMINAL_3P for non-canonical chain ends — so we don't\n // re-validate the length here.\n const triplesMode = polymerType === HELM_POLYMER_TYPE.RNA && !!roles &&\n roles.length === monomerSeq.length;\n\n // define atom and bond counts, taking into account the bond type\n const {atomCount, bondCount, needsCapping} =\n getResultingAtomBondCounts(monomerSeq, monomersDict, alphabet, polymerType, triplesMode, roles);\n\n // create arrays to store lines of the resulting molfile\n const molfileAtomBlock = new Array<string>(atomCount);\n const molfileBondBlock = new Array<string>(bondCount);\n\n let addMonomerToMolblock: (monomer: MolGraph, molfileAtomBlock: string[], molfileBondBlock: string[], v: LoopVariables, LC: LoopConstants) => void;\n\n let sugar = null;\n let phosphate = null;\n\n if (polymerType === HELM_POLYMER_TYPE.PEPTIDE)\n addMonomerToMolblock = addAminoAcidToMolblock;\n else { // nucleotides\n addMonomerToMolblock = addNucleotideToMolblock;\n // Default sugar/phosphate are only consulted in bases-only mode. In\n // triples mode, every nucleotide carries its own.\n if (!triplesMode) {\n sugar = (alphabet === ALPHABET.DNA) ? getMolGraph(monomersDict, C.DEOXYRIBOSE) : getMolGraph(monomersDict, C.RIBOSE);\n phosphate = getMolGraph(monomersDict, C.PHOSPHATE);\n }\n }\n const v: LoopVariables = {\n i: 0,\n nodeShift: 0,\n bondShift: 0,\n backbonePositionShift: new Array<number>(2).fill(0),\n branchPositionShift: new Array<number>(2).fill(0),\n backboneAttachNode: 0,\n branchAttachNode: 0,\n flipFactor: 1,\n };\n\n const LC: LoopConstants = {\n sugar: sugar!,\n phosphate: phosphate!,\n // In triples mode, the \"logical\" sequence length is the nucleotide count.\n seqLength: triplesMode ? Math.ceil(monomerSeq.length / 3) : monomerSeq.length,\n atomCount: atomCount,\n bondCount: bondCount,\n };\n\n const monomers: MonomerMap = new MonomerMap();\n const steabsCollection: number [] = [];\n let nAtoms = 0;\n let lastMonomerCappingAtom: string | undefined = undefined;\n\n if (triplesMode) {\n runTriplesAssembly(\n monomerSeq, roles!, monomersDict, molfileAtomBlock, molfileBondBlock,\n v, LC, monomers, steabsCollection,\n (a) => { nAtoms += a; }, () => nAtoms);\n } else {\n for (v.i = 0; v.i < LC.seqLength; ++v.i) {\n const seqMonomer = monomerSeq[v.i];\n if (seqMonomer.symbol === GAP_SYMBOL) continue;\n const monomer = getMolGraph(monomersDict, {symbol: seqMonomer.symbol, polymerType: helmTypeToPolymerType(seqMonomer.biotype)})!;\n lastMonomerCappingAtom = monomer.terminalR2Atom;\n const mAtomFirst = v.nodeShift;\n const mBondFirst = v.bondShift;\n addMonomerToMolblock(monomer, molfileAtomBlock, molfileBondBlock, v, LC);\n //adding stereo atoms to array for further STEABS block generation\n monomer.stereoAtoms?.forEach((i) => steabsCollection.push(i + nAtoms));\n nAtoms += monomer.atoms.x.length;\n\n const mAtomCount = v.nodeShift - mAtomFirst;\n const mAtomList: number[] = new Array<number>(mAtomCount);\n for (let maI = 0; maI < mAtomCount; ++maI) mAtomList[maI] = mAtomFirst + maI;\n\n const mBondCount = v.bondShift - mBondFirst;\n const mBondList: number[] = new Array<number>(mBondCount);\n for (let mbI = 0; mbI < mBondCount; ++mbI) mBondList[mbI] = mBondFirst + mbI;\n\n monomers.set(v.i, {\n biotype: seqMonomer.biotype,\n symbol: seqMonomer.symbol,\n atoms: mAtomList, bonds: mBondList\n });\n }\n }\n\n // if the last monomer needs to be capped, add the terminal OH to the resulting molfile\n if (needsCapping)\n capResultingMolblock(molfileAtomBlock, molfileBondBlock, v, LC, lastMonomerCappingAtom ?? C.OXYGEN);\n\n const molfileCountsLine = C.V3K_BEGIN_COUNTS_LINE + atomCount + ' ' + bondCount + C.V3K_COUNTS_LINE_ENDING;\n\n // todo: possible optimization may be achieved by replacing .join('') with +=\n // since counterintuitively joining an array into a new string is reportedly\n // slower than using += as below\n\n let result = '';\n result += C.V3K_HEADER_FIRST_LINE;\n result += C.V3K_HEADER_SECOND_LINE;\n result += C.V3K_BEGIN_CTAB_BLOCK;\n result += molfileCountsLine;\n result += C.V3K_BEGIN_ATOM_BLOCK;\n result += molfileAtomBlock.join('');\n result += C.V3K_END_ATOM_BLOCK;\n result += C.V3K_BEGIN_BOND_BLOCK;\n result += molfileBondBlock.join('');\n result += C.V3K_END_BOND_BLOCK;\n if (steabsCollection.length > 0)\n result += getCollectionBlock(steabsCollection);\n result += C.V3K_END_CTAB_BLOCK;\n result += C.V3K_END;\n\n // return molfileParts.join('');\n return {molfile: result, monomers: monomers};\n}\n\n\nfunction getCollectionBlock(collection: number[]): string {\n //one row in STEABS block can be no longer than 80 symbols\n //maxSymbols = 80 symbols minus ' -\\n' (4 symbols)\n const maxSymbols = 76;\n const rowsArray = [];\n\n let newCollectionRow = `M V30 MDLV30/STEABS ATOMS=(${collection.length}`;\n for (let i = 0; i < collection.length; i++) {\n const updatedRow = `${newCollectionRow} ${collection[i]}`;\n if (updatedRow.length > maxSymbols) {\n rowsArray.push(`${newCollectionRow} -\\n`);\n newCollectionRow = `M V30 ${collection[i]}`;\n } else\n newCollectionRow = updatedRow;\n //in case last atom was added - close the block\n if (i === collection.length - 1)\n rowsArray.push(`${newCollectionRow})\\n`);\n }\n return `M V30 BEGIN COLLECTION\\n${rowsArray.join('')}M V30 END COLLECTION\\n`;\n}\n\n/** Cap the resulting (after sewing up all the monomers) molfile with 'O'\n * @param {string[]} molfileAtomBlock - Array of lines of the resulting molfile atom block\n * @param {string[]} molfileBondBlock - Array of lines of the resulting molfile bond block\n * @param {LoopVariables} v - Loop variables\n * @param {LoopConstants} LC - Loop constants*/\nfunction capResultingMolblock(\n molfileAtomBlock: string[], molfileBondBlock: string[],\n v: LoopVariables, LC: LoopConstants, cappingAtomType: string = C.OXYGEN\n): void {\n // add terminal oxygen\n const atomIdx = v.nodeShift + 1;\n molfileAtomBlock[LC.atomCount] = C.V3K_BEGIN_DATA_LINE + atomIdx + ' ' +\n (cappingAtomType ?? C.OXYGEN) + ' ' + keepPrecision(v.backbonePositionShift[0]) + ' ' +\n v.flipFactor * keepPrecision(v.backbonePositionShift[1]) + ' ' + '0.000000 0' + '\\n';\n\n // add terminal bond\n const firstAtom = v.backboneAttachNode;\n const secondAtom = atomIdx;\n molfileBondBlock[LC.bondCount] = C.V3K_BEGIN_DATA_LINE + v.bondShift + ' ' +\n 1 + ' ' + firstAtom + ' ' + secondAtom + '\\n';\n}\n\nfunction addAminoAcidToMolblock(monomer: MolGraph, molfileAtomBlock: string[],\n molfileBondBlock: string[], v: LoopVariables\n): void {\n v.flipFactor = (-1) ** (v.i % 2); // to flip every even monomer over OX\n addBackboneMonomerToMolblock(monomer, molfileAtomBlock, molfileBondBlock, v);\n}\n\nfunction addBackboneMonomerToMolblock(\n monomer: MolGraph, molfileAtomBlock: string[], molfileBondBlock: string[], v: LoopVariables\n): void {\n // todo: remove these comments to the docstrings of the corr. functions\n // construnct the lines of V3K molfile atom block\n fillAtomLines(monomer, molfileAtomBlock, v);\n\n // construct the lines of V3K molfile bond block\n fillBondLines(monomer, molfileBondBlock, v);\n\n // peptide bond\n fillChainExtendingBond(monomer, molfileBondBlock, v);\n\n // update branch variables if necessary\n if (monomer.meta.branchShift !== null && monomer.meta.terminalNodes.length > 2)\n updateBranchVariables(monomer, v);\n\n // update loop variables\n updateChainExtendingVariables(monomer, v);\n}\n\nfunction addNucleotideToMolblock(\n nucleobase: MolGraph, molfileAtomBlock: string[], molfileBondBlock: string[], v: LoopVariables, LC: LoopConstants\n): void {\n // construnct the lines of V3K molfile atom block corresponding to phosphate\n // and sugar\n if (v.i === 0)\n addBackboneMonomerToMolblock(LC.sugar!, molfileAtomBlock, molfileBondBlock, v);\n else {\n for (const monomer of [LC.phosphate, LC.sugar])\n addBackboneMonomerToMolblock(monomer!, molfileAtomBlock, molfileBondBlock, v);\n }\n\n addBranchMonomerToMolblock(nucleobase, molfileAtomBlock, molfileBondBlock, v);\n}\n\nfunction addBranchMonomerToMolblock(\n monomer: MolGraph, molfileAtomBlock: string[], molfileBondBlock: string[], v: LoopVariables\n): void {\n fillBranchAtomLines(monomer, molfileAtomBlock, v);\n fillBondLines(monomer, molfileBondBlock, v);\n fillBackboneToBranchBond(monomer, molfileBondBlock, v);\n\n // C-N bond\n const bondIdx = v.bondShift;\n const firstAtom = v.branchAttachNode;\n const secondAtom = monomer.meta.terminalNodes[0] + v.nodeShift;\n molfileBondBlock[bondIdx - 1] = C.V3K_BEGIN_DATA_LINE + bondIdx + ' ' +\n 1 + ' ' + firstAtom + ' ' + secondAtom + '\\n';\n\n // update loop variables\n v.bondShift += monomer.bonds.atomPairs.length + 1;\n v.nodeShift += monomer.atoms.atomTypes.length;\n}\n\nfunction updateChainExtendingVariables(monomer: MolGraph, v: LoopVariables): void {\n v.backboneAttachNode = v.nodeShift + monomer.meta.terminalNodes[1];\n v.bondShift += monomer.bonds.atomPairs.length + 1;\n\n v.nodeShift += monomer.atoms.atomTypes.length;\n v.backbonePositionShift[0] += monomer.meta.backboneShift?.[0] ?? 0; // todo: non-null check\n v.backbonePositionShift[1] += v.flipFactor * (monomer.meta.backboneShift?.[1] ?? 0);\n}\n\nfunction updateBranchVariables(monomer: MolGraph, v: LoopVariables): void {\n v.branchAttachNode = v.nodeShift + monomer.meta.terminalNodes[2];\n for (let i = 0; i < 2; ++i)\n v.branchPositionShift[i] = v.backbonePositionShift[i] + monomer.meta.branchShift![i];\n}\n\nfunction fillAtomLines(monomer: MolGraph, molfileAtomBlock: string[], v: LoopVariables): void {\n for (let j = 0; j < monomer.atoms.atomTypes.length; ++j) {\n const atomIdx = v.nodeShift + j + 1;\n molfileAtomBlock[v.nodeShift + j] = C.V3K_BEGIN_DATA_LINE + atomIdx + ' ' +\n monomer.atoms.atomTypes[j] + ' ' +\n keepPrecision(v.backbonePositionShift[0] + monomer.atoms.x[j]) + ' ' +\n keepPrecision(v.backbonePositionShift[1] + v.flipFactor * monomer.atoms.y[j]) +\n ' ' + monomer.atoms.kwargs[j];\n }\n}\n\n// todo: remove as quickfix\nfunction fillBranchAtomLines(monomer: MolGraph, molfileAtomBlock: string[], v: LoopVariables): void {\n for (let j = 0; j < monomer.atoms.atomTypes.length; ++j) {\n const atomIdx = v.nodeShift + j + 1;\n molfileAtomBlock[v.nodeShift + j] = C.V3K_BEGIN_DATA_LINE + atomIdx + ' ' +\n monomer.atoms.atomTypes[j] + ' ' +\n keepPrecision(v.branchPositionShift[0] + monomer.atoms.x[j]) + ' ' +\n keepPrecision(v.branchPositionShift[1] + v.flipFactor * monomer.atoms.y[j]) +\n ' ' + monomer.atoms.kwargs[j];\n }\n}\n\nfunction fillBondLines(monomer: MolGraph, molfileBondBlock: string[], v: LoopVariables): void {\n // construct the lines of V3K molfile bond block\n for (let j = 0; j < monomer.bonds.atomPairs.length; ++j) {\n const bondIdx = v.bondShift + j + 1;\n const firstAtom = monomer.bonds.atomPairs[j][0] + v.nodeShift;\n const secondAtom = monomer.bonds.atomPairs[j][1] + v.nodeShift;\n let bondCfg = '';\n if (monomer.bonds.bondConfiguration.has(j)) {\n // flip orientation when necessary\n let orientation = monomer.bonds.bondConfiguration.get(j);\n if (v.flipFactor < 0)\n orientation = (orientation === 1) ? 3 : 1;\n bondCfg = ' CFG=' + orientation;\n }\n const kwargs = monomer.bonds.kwargs.has(j) ?\n ' ' + monomer.bonds.kwargs.get(j) : '';\n molfileBondBlock[v.bondShift + j] = C.V3K_BEGIN_DATA_LINE + bondIdx + ' ' +\n monomer.bonds.bondTypes[j] + ' ' +\n firstAtom + ' ' + secondAtom + bondCfg + kwargs + '\\n';\n }\n}\n\nfunction fillChainExtendingBond(monomer: MolGraph, molfileBondBlock: string[], v: LoopVariables): void {\n if (v.backboneAttachNode !== 0) {\n const bondIdx = v.bondShift;\n const firstAtom = v.backboneAttachNode;\n const secondAtom = monomer.meta.terminalNodes[0] + v.nodeShift;\n molfileBondBlock[v.bondShift - 1] = C.V3K_BEGIN_DATA_LINE + bondIdx + ' ' +\n 1 + ' ' + firstAtom + ' ' + secondAtom + '\\n';\n }\n}\n\n// todo: remove\nfunction fillBackboneToBranchBond(branchMonomer: MolGraph, molfileBondBlock: string[], v: LoopVariables): void {\n const bondIdx = v.bondShift;\n const firstAtom = v.branchAttachNode;\n const secondAtom = branchMonomer.meta.terminalNodes[0] + v.nodeShift;\n molfileBondBlock[bondIdx - 1] = C.V3K_BEGIN_DATA_LINE + bondIdx + ' ' +\n 1 + ' ' + firstAtom + ' ' + secondAtom + '\\n';\n}\n\n/** Compute the atom/bond counts for the resulting molfile, depending on the\n * type of polymer (peptide/nucleotide)\n * @param {ISeqMonomer[]} monomerSeq - the sequence of monomers\n * @param {MonomerMolGraphMap} monomersDict - the dictionary of monomers\n * @param {ALPHABET} alphabet - the alphabet of the monomers\n * @param {PolymerType} polymerType - the type of polymer\n * @param {boolean} triplesMode - true when monomerSeq is a flat list of HELM RNA triples\n * @param {Array} roles - per-position role tags (only meaningful when triplesMode is true)\n * @return {Object} the atom/bond counts plus needsCapping flag */\nfunction getResultingAtomBondCounts(\n monomerSeq: ISeqMonomer[], monomersDict: MonomerMolGraphMap,\n alphabet: ALPHABET, polymerType: PolymerType,\n triplesMode: boolean, roles?: NucleotideRole[]\n): { atomCount: number, bondCount: number, needsCapping: boolean } {\n let atomCount = 0;\n let bondCount = 0;\n\n let monomerCount: number = 0;\n let needsCapping = true;\n let lastMonomerGraph: MolGraph | null = null;\n let lastPhosphateGraph: MolGraph | null = null;\n // sum up all the atoms/nodes provided by the sequence\n for (let i = 0; i < monomerSeq.length; ++i) {\n const seqMonomer = monomerSeq[i];\n if (seqMonomer.symbol === GAP_SYMBOL) continue; // Skip for gap/empty monomer in MSA\n if (seqMonomer.symbol == '*')\n throw new Error(`Gap canonical symbol is '', not '*`);\n lastMonomerGraph = getMolGraph(monomersDict, {symbol: seqMonomer.symbol, polymerType: helmTypeToPolymerType(seqMonomer.biotype)})!;\n atomCount += lastMonomerGraph.atoms.x.length;\n bondCount += lastMonomerGraph.bonds.bondTypes.length;\n monomerCount++;\n // In triples mode, every 3rd entry (index 2 mod 3) is a phosphate. Track\n // the LAST one — its atoms/bonds are dropped at the 3'-terminus.\n if (triplesMode && i % 3 === 2) lastPhosphateGraph = lastMonomerGraph;\n }\n\n // add extra values depending on the polymer type\n if (polymerType === HELM_POLYMER_TYPE.PEPTIDE) {\n // add the rightmost/terminating cap group 'OH' (i.e. 'O')\n atomCount += 1;\n // add chain-extending bonds (C-NH per each monomer pair and terminal C-OH)\n bondCount += monomerCount;\n // if the last monomer is something like NH2, which only has R1, there is no need to cap it\n // although, this should never happen, but hey... in other bits of code, there is a chunk that adds pseudo-R2 as hydrogen\n // we should also check, if the R2 of the last monomer is not hydrogen, that case should also be omitted\n if (monomerCount > 0) {\n if ((lastMonomerGraph?.meta?.rNodes?.length ?? 0) < 2 || lastMonomerGraph?.terminalR2Atom?.toLowerCase() === C.HYDROGEN.toLowerCase()) {\n needsCapping = false;\n atomCount -= 1; // remove the last atom (the terminal 'O')\n bondCount -= 1; // remove the last bond (the terminal C-OH)\n }\n }\n } else if (triplesMode) { // nucleotides — HELM triples (per-position sugar/base/phosphate)\n void lastPhosphateGraph; // kept for symmetry; trailing P is now retained when HELM wrote it\n\n // Per-monomer-loop already summed sugars + bases + phosphates for every\n // entry in monomerSeq (terminals included). Reservation: each backbone\n // emit (sugar / phosphate / terminal) reserves +1 bond slot for the\n // chain-extending bond, each branch (base) reserves +1 for its branch\n // bond. Total reservations = monomerCount.\n bondCount += monomerCount;\n\n // OH cap atom rides on the 3'-end. Skip it when HELM specified a\n // 3'-terminal modifier (e.g. GalNAc) that IS the chain end.\n const has3pTerm = !!roles && roles.length > 0 &&\n roles[roles.length - 1] === NucleotideRole.TERMINAL_3P;\n if (has3pTerm) {\n needsCapping = false;\n // Mirror the peptide branch (which does both atomCount-- and bondCount--\n // when it skips the terminal cap). `bondCount += monomerCount` above\n // reserves one chain-extending slot per monomer; the LAST monomer's slot\n // is normally filled by the terminal OH cap bond. With a 3'-terminal\n // modifier there is no cap, so that last reserved slot stays empty —\n // leaving the declared bond count one higher than the emitted bond\n // lines. Drop it so the V3000 COUNTS line matches the bond block exactly\n // (otherwise the pre-OCL molfile is malformed and only survives because\n // the OCL chirality pass re-derives the counts).\n bondCount -= 1;\n } else\n atomCount += 1; // OH cap atom (rides on trailing P or on last sugar's R2)\n } else { // nucleotides — bases-only legacy path with default sugar/phosphate\n const sugar = (alphabet === ALPHABET.DNA) ?\n getMolGraph(monomersDict, C.DEOXYRIBOSE)! : getMolGraph(monomersDict, C.RIBOSE)!;\n const phosphate = getMolGraph(monomersDict, C.PHOSPHATE)!;\n\n // add phosphate per each pair of nucleobase symbols\n atomCount += (monomerSeq.length - 1) * phosphate.atoms.x.length;\n\n // add sugar per each nucleobase symbol\n atomCount += monomerSeq.length * sugar.atoms.x.length;\n\n // add the leftmost cap group 'OH' (i.e. 'O')\n atomCount += 1;\n\n // add bonds from phosphate monomers\n bondCount += (monomerSeq.length - 1) * phosphate.bonds.bondTypes.length;\n\n // add bonds from sugar monomers\n bondCount += monomerSeq.length * sugar.bonds.bondTypes.length;\n\n // exclude the first chain-extending bond O-P (absent, no 'leftmost' phosphate)\n bondCount -= 1;\n\n // add chain-extending and branch bonds (O-P, C-O and C-N per each nucleotide)\n bondCount += monomerSeq.length * 3;\n }\n\n return {atomCount, bondCount, needsCapping};\n}\n\n// Role-driven RNA assembly. Walks the monomers in chain order and emits\n// each one according to its NucleotideRole (assigned by `buildRolesForHelmRna`\n// from the library, not from a fixed triple index):\n// - BASE → branch monomer, attached to the\n// branch point of the sugar most\n// recently emitted.\n// - SUGAR / PHOSPHATE / TERMINAL_5P / 3P → backbone monomer, chained to the\n// previous backbone unit.\n//\n// Because the role comes from chemistry, this transparently handles every\n// backbone layout — standard [sugar, base, phosphate] triples, a 5'-leading\n// phosphate, several phosphates / linkers in a row, a linker dropped in the\n// middle of the chain, missing trailing phosphate, and 5'/3' terminal\n// modifiers — without any index arithmetic. The first backbone monomer\n// naturally has no incoming chain bond (v.backboneAttachNode starts at 0),\n// and the trailing OH cap is added by the caller via needsCapping (skipped\n// when a TERMINAL_3P ends the chain).\nfunction runTriplesAssembly(\n monomerSeq: ISeqMonomer[], roles: NucleotideRole[],\n monomersDict: MonomerMolGraphMap,\n molfileAtomBlock: string[], molfileBondBlock: string[],\n v: LoopVariables, LC: LoopConstants,\n monomers: MonomerMap, steabsCollection: number[],\n addAtoms: (n: number) => void, getAtoms: () => number\n): void {\n void LC; // assembly is driven by per-monomer roles, not by LC.seqLength\n for (let i = 0; i < monomerSeq.length; ++i) {\n const sm = monomerSeq[i];\n if (sm.symbol === GAP_SYMBOL) continue;\n const role = roles[i];\n const mG = getMolGraph(monomersDict,\n {symbol: sm.symbol, polymerType: helmTypeToPolymerType(sm.biotype)})!;\n\n const aFirst = v.nodeShift;\n const bFirst = v.bondShift;\n v.i = i; // keep loop counter monotone (unused by nucleotide geometry)\n if (role === NucleotideRole.BASE)\n // Branch: attaches to v.branchAttachNode, set when the preceding sugar\n // was emitted. Does not advance the backbone, so the next backbone unit\n // still chains from that sugar's 3' side.\n addBranchMonomerToMolblock(mG, molfileAtomBlock, molfileBondBlock, v);\n else\n // Backbone: sugar / phosphate / linker / terminal modifier. Chains from\n // the previous backbone unit (none for the very first one) and, for a\n // sugar, sets up the branch attach point for the next base.\n addBackboneMonomerToMolblock(mG, molfileAtomBlock, molfileBondBlock, v);\n\n mG.stereoAtoms?.forEach((s) => steabsCollection.push(s + getAtoms()));\n addAtoms(mG.atoms.x.length);\n\n const aList: number[] = [];\n for (let a = aFirst; a < v.nodeShift; ++a) aList.push(a);\n const bList: number[] = [];\n for (let b = bFirst; b < v.bondShift; ++b) bList.push(b);\n monomers.set(i, {biotype: sm.biotype, symbol: sm.symbol, atoms: aList, bonds: bList});\n }\n}\n\n/** Keep precision upon floating point operations over atom coordinates\n * @param {number}x - the floating point number\n * @return {number} - the floating point number with the same precision\n */\nexport function keepPrecision(x: number): number {\n return Math.round(C.PRECISION_FACTOR * x) / C.PRECISION_FACTOR;\n}\n\n","import {monomerSeqToMolfile} from './to-atomic-level-utils';\nimport {MolfileWithMap, SeqToMolfileWorkerData, SeqToMolfileWorkerRes} from './types';\n\nonmessage = (event) => {\n const {seqList, rolesList, monomersDict, alphabet, polymerType, start, end}: SeqToMolfileWorkerData = event.data;\n const resMolList: MolfileWithMap[] = new Array<MolfileWithMap>(end - start);\n const molfileWarningList = new Array<string>(0);\n for (let rowI = start; rowI < end; ++rowI) {\n try {\n const seq = seqList[rowI];\n const roles = rolesList ? rolesList[rowI] : undefined;\n resMolList[rowI - start] = monomerSeqToMolfile(seq, monomersDict, alphabet, polymerType, roles);\n } catch (err: any) {\n const errMsg: string = err instanceof Error ? err.message : err.toString();\n const msg: string = `Cannot get molfile of row #${rowI}: ${errMsg}.`;\n molfileWarningList.push(msg);\n }\n }\n postMessage({molfiles: resMolList, warnings: molfileWarningList} as SeqToMolfileWorkerRes);\n};\n","// The module cache\nvar __webpack_module_cache__ = {};\n\n// The require function\nfunction __webpack_require__(moduleId) {\n\t// Check if module is in cache\n\tvar cachedModule = __webpack_module_cache__[moduleId];\n\tif (cachedModule !== undefined) {\n\t\treturn cachedModule.exports;\n\t}\n\t// Create a new module (and put it into the cache)\n\tvar module = __webpack_module_cache__[moduleId] = {\n\t\t// no module.id needed\n\t\t// no module.loaded needed\n\t\texports: {}\n\t};\n\n\t// Execute the module function\n\t__webpack_modules__[moduleId](module, module.exports, __webpack_require__);\n\n\t// Return the exports of the module\n\treturn module.exports;\n}\n\n// expose the modules object (__webpack_modules__)\n__webpack_require__.m = __webpack_modules__;\n\n// the startup function\n__webpack_require__.x = () => {\n\t// Load entry module and return exports\n\t// This entry module depends on other loaded chunks and execution need to be delayed\n\tvar __webpack_exports__ = __webpack_require__.O(undefined, [782], () => (__webpack_require__(884)))\n\t__webpack_exports__ = __webpack_require__.O(__webpack_exports__);\n\treturn __webpack_exports__;\n};\n\n","// define getter functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.f = {};\n// This file contains only the entry chunk.\n// The chunk loading function for additional chunks\n__webpack_require__.e = (chunkId) => {\n\treturn Promise.all(Object.keys(__webpack_require__.f).reduce((promises, key) => {\n\t\t__webpack_require__.f[key](chunkId, promises);\n\t\treturn promises;\n\t}, []));\n};","// This function allow to reference async chunks and chunks that the entrypoint depends on\n__webpack_require__.u = (chunkId) => {\n\t// return url for filenames based on template\n\treturn \"\" + chunkId + \".js\";\n};","__webpack_require__.g = (function() {\n\tif (typeof globalThis === 'object') return globalThis;\n\ttry {\n\t\treturn this || new Function('return this')();\n\t} catch (e) {\n\t\tif (typeof window === 'object') return window;\n\t}\n})();","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","var scriptUrl;\nif (__webpack_require__.g.importScripts) scriptUrl = __webpack_require__.g.location + \"\";\nvar document = __webpack_require__.g.document;\nif (!scriptUrl && document) {\n\tif (document.currentScript && document.currentScript.tagName.toUpperCase() === 'SCRIPT')\n\t\tscriptUrl = document.currentScript.src;\n\tif (!scriptUrl) {\n\t\tvar scripts = document.getElementsByTagName(\"script\");\n\t\tif(scripts.length) {\n\t\t\tvar i = scripts.length - 1;\n\t\t\twhile (i > -1 && (!scriptUrl || !/^http(s?):/.test(scriptUrl))) scriptUrl = scripts[i--].src;\n\t\t}\n\t}\n}\n// When supporting browsers where an automatic publicPath is not supported you must specify an output.publicPath manually via configuration\n// or pass an empty string (\"\") and set the __webpack_public_path__ variable from your code to use your own logic.\nif (!scriptUrl) throw new Error(\"Automatic publicPath is not supported in this browser\");\nscriptUrl = scriptUrl.replace(/^blob:/, \"\").replace(/#.*$/, \"\").replace(/\\?.*$/, \"\").replace(/\\/[^\\/]+$/, \"/\");\n__webpack_require__.p = scriptUrl;","// no baseURI\n\n// object to store loaded chunks\n// \"1\" means \"already loaded\"\nvar installedChunks = {\n\t884: 1\n};\n\n// importScripts chunk loading\nvar installChunk = (data) => {\n\tvar [chunkIds, moreModules, runtime] = data;\n\tfor(var moduleId in moreModules) {\n\t\tif(__webpack_require__.o(moreModules, moduleId)) {\n\t\t\t__webpack_require__.m[moduleId] = moreModules[moduleId];\n\t\t}\n\t}\n\tif(runtime) runtime(__webpack_require__);\n\twhile(chunkIds.length)\n\t\tinstalledChunks[chunkIds.pop()] = 1;\n\tparentChunkLoadingFunction(data);\n};\n__webpack_require__.f.i = (chunkId, promises) => {\n\t// \"1\" is the signal for \"already loaded\"\n\tif(!installedChunks[chunkId]) {\n\t\tif(true) { // all chunks have JS\n\t\t\timportScripts(__webpack_require__.p + __webpack_require__.u(chunkId));\n\t\t}\n\t}\n};\n\nvar chunkLoadingGlobal = self[\"webpackChunkhelm\"] = self[\"webpackChunkhelm\"] || [];\nvar parentChunkLoadingFunction = chunkLoadingGlobal.push.bind(chunkLoadingGlobal);\nchunkLoadingGlobal.push = installChunk;\n\n// no HMR\n\n// no HMR manifest","// run startup\nvar __webpack_exports__ = __webpack_require__.x();\n"],"names":["deferred","next","Error","HelmTypes","PolymerTypes","PEPTIDE","RNA","CHEM","BLOB","G","Subject","_listeners","Set","_completed","subscribe","fn","this","add","delete","value","complete","clear","observerCount","size","lightTriple","background","line","text","darkTriple","darken","parseHex","hex","m6","exec","m3","s","parseInt","slice","hexByte","n","toString","padStart","toHex","r","g","b","rgbToHsl","rn","gn","bn","max","Math","min","l","d","h","hue2rgb","p","q","t","tt","hslToRgb","v","round","hn","soften","color","sAmt","lAmt","rgb","newS","newL","r2","g2","b2","relativeLuminance","channel","c","cs","pow","deriveLineColor","darkerL","hsl","hslToHex","bestContrastTextColor","backgroundHex","hexToRgb01","toLinear","L","amt","f","LIGHT_BASES","A","T","C","U","LIGHT_SUGARS","R","LIGHT_LINKERS","P","LIGHT_AAS","N","D","E","Q","H","I","K","M","F","S","W","Y","V","LIGHT_CHEMS","LIGHT_BLOBS","B","LIGHT_NUCLEOTIDES","lightPalette","name","fallback","errorTriple","palette","darkenMap","src","out","k","Object","entries","darkPalette","resolveMonomerColors","monomer","biotype","theme","metaColors","meta","colors","themed","light","dark","default","na","naturalAnalog","perType","hit","helm2Type","polymerType","monomerType","DEFAULT_BOND_LENGTH","PI","bboxUnion","a","minX","x","minY","y","width","height","fmt","Number","isInteger","isFinite","is","polygonPath","points","length","first","i","pt","regularPolygonPoints","radius","rotationDeg","step","angle","push","cos","sin","roundedRectPath","w","halfW","abs","halfH","right","bottom","ellipsePath","rx","ry","ax","ay","shapeBBox","kind","hw","sqrt","hh","top","_h","DEFAULT_FONT_FAMILY","buildShapes","fontSize","aa","strokeWidth","sugar","cornerRadius","base","linker","chem","blob","nucleotide","lightTheme","defaultStroke","defaultFill","selectionColor","highlightColor","errorColor","fontFamily","bondLength","textColor","idColor","annotationColor","tagDotColor","hbondColor","nonCanonicalHbondColor","crossPolymerBondColor","colorTheme","softenCanvas","shapes","shapeStyleFor","shapeKindFor","atomShapePath","atom","outset","style","symbol","grow","hexagonPath","diamondPath","pentagonPath","blobPath","atomBBox","bbox","pad","atomStyle","ATTR_NEEDS_ESCAPE","escapeAttr","test","replace","formatNumber","tag","attrs","children","join","buildHighlightLayer","mol","highlighted","class","atoms","has","id","fill","stroke","buildSelectionLayer","selected","SelectionImpl","constructor","bonds","isEmpty","freeze","getDocument","host","owner","ownerDocument","globalThis","document","el","doc","node","createElement","key","keys","setAttribute","String","appendChild","createTextNode","renderRdkitSvg","rdkit","smiles","get_mol","svg","get_svg","getOrCreateTooltipEl","testid","cls","ATTR","body","child","getAttribute","tip","display","fillTooltip","firstChild","removeChild","rdkitSvg","rdkitTestid","wrap","innerHTML","hint","positionTooltipAt","clientX","clientY","left","hideTooltipEl","hideAllTooltips","tid","endsWith","editor","options","_baseTransform","_viewport","scale","offset","_pinnedBbox","_tooltip","_bondTooltip","_rdkitSvgCache","Map","_unsubs","_destroyed","_editor","_doc","documentImpl","_width","_height","querySelector","textContent","head","documentElement","injectStyles","wrapper","_svgRoot","_renderFresh","on","e","_hideTooltip","_hideBondTooltip","reason","_refreshOverlays","_applyResize","viewport","_applyViewport","_showTooltip","_showBondTooltip","bond","_paintBondPreview","start","cursor","candidate","_clearBondPreview","_paintSelectionRect","origin","_paintSelectionRectFromRect","rect","_clearSelectionRect","svgRoot","resize","redraw","destroy","u","parentNode","service","getService","_currentTheme","getMol","acc","computeModelBbox","renderMol","fixedModelBbox","svgString","tmp","newSvg","replaceSvg","content","_applyOverlaysToCurrent","_evt","sel","getSelection","overlays","selectedAtomIds","highlightLayer","highlightedAtomIds","selectionLayer","buildSelectionOverlays","_editorHighlights","_spliceLayer","layerClass","html","layer","createElementNS","newLayer","replaceChild","ed","_currentHighlights","vp","composed","tooltip","sym","pos","bio","positionInChain","chain","chainId","fullName","_renderRdkitTooltipSvg","legacyText","shim","rdkitModule","cacheKey","get","_RDKIT_CACHE_LIMIT","oldest","set","r1","formatRRef","a1","a2","validCandidate","preview","_paintSelectionRectAt","marquee","position","_onClose","_primaryAction","_host","backdropAttrs","role","testId","backdrop","dialogAttrs","title","dialog","header","stop","stopPropagation","ev","addEventListener","ke","target","tagName","action","preventDefault","stopImmediatePropagation","close","root","anyOpen","_openStack","closeTopmost","open","onClose","includes","indexOf","splice","cb","setPrimaryAction","isOpen","formatHelmMonomerSymbol","startsWith","formatRnaTriplet","SVG_NS","FIELD_BIOTYPE","DEFAULTS","SUGAR_CENTER","LINKER_CENTER","BASE_CENTER","onValueSet","onPickModeChange","_slots","_favStar","_pickMode","_builderRdkitCache","_hooks","paletteHooks","_favHooks","favoriteHooks","element","star","tabindex","onToggle","toggleFavoriteTriplet","_hintEl","hidden","_buildSvg","_preview","_addBtn","type","_onAddClicked","libUnsub","monomerLib","onChanged","refresh","themeUnsub","_repaintSlotColors","_libUnsub","_populateDefaults","_updatePreview","setField","field","_setLabelText","labelEl","_repaintSlot","startPickMode","_setSlotPressed","_setHintActive","beginBiotypeFilter","_exitPickMode","cancelPickMode","endBiotypeFilter","getPickMode","triggerAdd","linkerSugarLine","sugarBaseLine","sugarShape","SUGAR_HALF","sugarSlot","_buildSlot","linkerShape","linkerSlot","baseShape","baseSlot","groupEl","ariaLabel","center","shape","group","onActivate","_showSlotTooltip","_hideSlotTooltip","shapeEl","label","toRemove","nextSibling","nodeName","len","fontSizeForLabel","visible","slotGroup","titleEl","insertBefore","values","lib","_theme","getMonomerSet","findMonomerForBiotype","monomers","monomersFor","preferred","preferredSymbol","want","some","m","_updateFavStar","fav","isFavoriteTriplet","removeAttribute","placeRnaTriplet","prev","pressed","stripped","split","filter","_rdkit","opts","mod","_builderRdkitSvgFor","_BUILDER_RDKIT_CACHE_LIMIT","_slotHint","_slotTitle","evt","slot","me","getBoundingClientRect","RnaBuilder","map","sort","localeCompare","preferences","FAVORITES_TAB","TAB_LABELS","TAB_ORDER","FAVORITES_STORAGE_KEY","ACTIVE_TAB_STORAGE_KEY","tripletKey","favoritesChanged","_nextAddUnsub","_favorites","_favoriteTriplets","_pendingRnaSelection","_visibleTabs","_activeTab","_searchInput","_filter","_rnaBuilder","_activeBiotypeFilter","_pickResolver","_pickCanceller","_pickEscapeListener","_pickOutsideClickListener","_paletteRdkitCache","_opts","groupByNaturalAnalog","allowFavorites","_loadFavorites","_resolveInitialTab","initialTab","_render","_themeUnsub","_updatePrimedClasses","setFilter","query","trim","toLowerCase","_applyFilter","setActiveTab","tabId","_persistActiveTab","_applyActiveTabClasses","getActiveTab","getFavorites","_findMonomer","setFavorite","isFavorite","_saveFavorites","_hidePaletteTooltips","_emitFavoritesChanged","clearFavorites","getFavoriteTriplets","setFavoriteTriplet","idx","findIndex","favorites","triplets","getRnaBuilder","hasActiveBiotypeFilter","getActiveBiotypeFilter","cancelBiotypeFilter","cancel","_teardownPickMode","_buildPaletteFilterHooks","onPick","onCancel","onKeyDown","defaultView","onOutsideMouseDown","matchingBiotype","cur","classes","_isInsideOwnPalette","parentElement","_scrollActivePaneToTop","activePane","scrollTo","behavior","scrollTop","removeEventListener","search","_computeVisibleTabs","tabs","isActive","btn","panes","pane","_buildFavoritesPane","_buildPolymerPane","present","getPolymerTypes","resolved","_resolveFavoriteMonomers","placeholder","_groupByPolymerAndType","_renderGroup","_renderTripletGroup","buckets","mt","list","polyOrder","MT_ORDER","Backbone","Branch","Terminal","Undefined","Array","from","_polymerLabel","chips","_renderTripletChip","baseMon","favorite","input","showStar","buildTripletChip","_wireTripletChip","row","part","parts","col","fillTripletTooltip","_tripletTooltipParts","classList","contains","_buildRnaDefaultChips","_pickSymbol","baseSet","bases","_DEFAULT_TRIPLET_BASES","_paletteRdkitSvg","prefs","done","_restoreRnaSelection","getMonomer","_monomersForPolymer","defaults","groups","_groupRna","groupName","ms","_groupByAnalog","tiles","getNextAddSymbol","sorted","_sortByColor","_biotype","_renderTile","displayName","keyed","hexToHsl","isGrey","symbols","getMonomerSymbolsByType","result","arr","sugars","linkers","other","primed","raw","softBg","labelColor","lowContrast","fg","bg","l1","l2","contrastRatio","symbolLength","labelStyle","rootStyle","draggable","buildTile","_wireTile","_PALETTE_RDKIT_CACHE_LIMIT","resolver","selection","find","every","replaceMonomers","fire","message","severity","durationMs","currentNext","setNextAddSymbol","dt","dataTransfer","setData","JSON","stringify","effectAllowed","startPaletteDrag","fallbackType","biotypeFilter","querySelectorAll","tile","titleAttr","chip","hay","groupItems","anyVisible","j","isPrimed","filtered","tab","_localStorage","localStorage","explicit","ls","getItem","setItem","parsed","parse","_extractSymbols","_extractTriplets","legacy","isArray","pipeIdx","obj","payload","MonomerNumberingTypes","HelmTabKeys","monomerWorksConsts","V2K_RGP_SHIFT","V2K_RGP_LINE","V2K_A_LINE","V3K_COUNTS_SHIFT","V3K_IDX_SHIFT","V3K_HEADER_FIRST_LINE","V3K_HEADER_SECOND_LINE","V3K_BEGIN_CTAB_BLOCK","V3K_END_CTAB_BLOCK","V3K_BEGIN_COUNTS_LINE","V3K_COUNTS_LINE_ENDING","V3K_BEGIN_ATOM_BLOCK","V3K_END_ATOM_BLOCK","V3K_BEGIN_BOND_BLOCK","V3K_END_BOND_BLOCK","V3K_BOND_CONFIG","V3K_BEGIN_DATA_LINE","V3K_END","PRECISION_FACTOR","DEOXYRIBOSE","RIBOSE","PHOSPHATE","OXYGEN","HYDROGEN","getMolGraph","dict","libKey","NucleotideRole","MonomerMap","super","MolfileWithMap","molfile","createEmpty","RegExp","CandidateType","alphabet","cutoff","NOTATION","ALPHABET","TAGS","Alphabets","fasta","peptide","dna","rna","helmTypeToPolymerType","helmType","console","warn","monomerSeqToMolfile","monomerSeq","monomersDict","roles","triplesMode","atomCount","bondCount","needsCapping","monomerCount","lastMonomerGraph","lastPhosphateGraph","seqMonomer","bondTypes","rNodes","terminalR2Atom","TERMINAL_3P","DNA","phosphate","getResultingAtomBondCounts","molfileAtomBlock","molfileBondBlock","addMonomerToMolblock","addAminoAcidToMolblock","addNucleotideToMolblock","nodeShift","bondShift","backbonePositionShift","branchPositionShift","backboneAttachNode","branchAttachNode","flipFactor","LC","seqLength","ceil","steabsCollection","lastMonomerCappingAtom","nAtoms","addAtoms","sm","mG","aFirst","bFirst","BASE","addBranchMonomerToMolblock","addBackboneMonomerToMolblock","stereoAtoms","forEach","aList","bList","runTriplesAssembly","mAtomFirst","mBondFirst","mAtomCount","mAtomList","maI","mBondCount","mBondList","mbI","cappingAtomType","atomIdx","keepPrecision","firstAtom","secondAtom","capResultingMolblock","molfileCountsLine","collection","rowsArray","newCollectionRow","updatedRow","getCollectionBlock","atomTypes","kwargs","fillAtomLines","fillBondLines","bondIdx","terminalNodes","fillChainExtendingBond","branchShift","updateBranchVariables","atomPairs","backboneShift","updateChainExtendingVariables","nucleobase","fillBranchAtomLines","branchMonomer","fillBackboneToBranchBond","bondCfg","bondConfiguration","orientation","PT","FASTA","SEPARATOR","HELM","BILN","onmessage","event","seqList","rolesList","end","data","resMolList","molfileWarningList","rowI","seq","undefined","err","msg","postMessage","molfiles","warnings","__webpack_module_cache__","__webpack_require__","moduleId","cachedModule","exports","module","__webpack_modules__","__webpack_exports__","O","chunkIds","priority","notFulfilled","Infinity","fulfilled","definition","o","defineProperty","enumerable","chunkId","Promise","all","reduce","promises","Function","window","prop","prototype","hasOwnProperty","call","scriptUrl","importScripts","location","currentScript","toUpperCase","scripts","getElementsByTagName","installedChunks","chunkLoadingGlobal","self","parentChunkLoadingFunction","bind","moreModules","runtime","pop","then"],"sourceRoot":""}
|