@datagrok/sequence-translator 1.10.21 → 1.10.23

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.
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Cell-level user actions for OligoNucleotide cells.
3
+ *
4
+ * The corresponding decorated entry points live in `package.ts` so the
5
+ * platform can discover them — those methods are kept thin and forward to
6
+ * the implementations below. Keeping the logic here means `package.ts` stays
7
+ * structural and the oligo subsystem stays self-contained.
8
+ *
9
+ * - `openOligoCanvasDialog` — double-click handler: full-screen modal
10
+ * with a hi-res canvas rendering, hover interactions reused from the
11
+ * grid cell renderer.
12
+ * - `openOligoHelmEditorDialog` — "Edit HELM" action: HELM Web Editor;
13
+ * OK writes the edited HELM back to the cell.
14
+ * - `copyHelmToClipboard` — "Copy as HELM" action.
15
+ * - `copyDuplexImageToClipboard` — "Copy as Image" action: hi-res PNG with
16
+ * a transparent background, trimmed to the duplex's natural extent.
17
+ */
18
+
19
+ import * as grok from 'datagrok-api/grok';
20
+ import * as ui from 'datagrok-api/ui';
21
+ import * as DG from 'datagrok-api/dg';
22
+
23
+ import {getHelmHelper} from '@datagrok-libraries/bio/src/helm/helm-helper';
24
+
25
+ import {parseHelmDuplex} from './helm-parser';
26
+ import {drawDuplex, hitTest, DuplexLayout} from './canvas-renderer';
27
+ import {showMonomerTooltip} from './tooltip';
28
+
29
+ /** Reference layout dimensions used as the seed for sizing both the dialog
30
+ * canvas and the clipboard image. Wide enough that any real-world duplex
31
+ * fits without wrapping; the right edge is trimmed back to the real extent. */
32
+ const REF_W = 1500;
33
+ const REF_H = 120;
34
+ const RIGHT_PAD = 20;
35
+
36
+ /* -------------------------------------------------------------------------- *
37
+ * Canvas viewer dialog (double-click on a cell)
38
+ * -------------------------------------------------------------------------- */
39
+
40
+ export function openOligoCanvasDialog(cell: DG.GridCell): void {
41
+ const helm = String(cell.cell?.value ?? '');
42
+ if (!helm) return;
43
+ const model = parseHelmDuplex(helm);
44
+
45
+ // Probe layout at the reference dimensions so we discover the natural
46
+ // horizontal extent of this particular duplex (varies with conjugates,
47
+ // multi-char bases, strand length).
48
+ const refLayout = drawDuplex(
49
+ null as unknown as CanvasRenderingContext2D, 0, 0, REF_W, REF_H, model, undefined, true);
50
+ const trimmedW = Math.max(REF_H, computeMaxChipEndX(refLayout) + RIGHT_PAD);
51
+
52
+ // Scale the trimmed layout up to fill the modal — screen width minus 200px.
53
+ const targetW = Math.max(400, window.innerWidth - 200);
54
+ const scale = targetW / trimmedW;
55
+ const finalW = trimmedW * scale; // === targetW by construction
56
+ const finalH = REF_H * scale;
57
+
58
+ // Backing canvas: dimensions in CSS px, pixel-density bumped by devicePixelRatio.
59
+ const dpr = window.devicePixelRatio || 1;
60
+ const canvas = document.createElement('canvas');
61
+ canvas.width = Math.max(1, Math.round(finalW * dpr));
62
+ canvas.height = Math.max(1, Math.round(finalH * dpr));
63
+ canvas.style.width = `${finalW}px`;
64
+ canvas.style.height = `${finalH}px`;
65
+ canvas.style.display = 'block';
66
+ const g = canvas.getContext('2d');
67
+ if (!g) return;
68
+ g.scale(dpr, dpr);
69
+ const finalLayout = drawDuplex(g, 0, 0, finalW, finalH, model);
70
+
71
+ // Hover support — same hit-test → tooltip pipeline the grid renderer uses,
72
+ // just with canvas-local coords instead of GridCell.bounds-relative ones.
73
+ canvas.addEventListener('mousemove', (e) => {
74
+ const rect = canvas.getBoundingClientRect();
75
+ const localX = e.clientX - rect.left;
76
+ const localY = e.clientY - rect.top;
77
+ const hit = hitTest(localX, localY, model, finalLayout);
78
+ if (!hit) {
79
+ ui.tooltip.hide();
80
+ return;
81
+ }
82
+ showMonomerTooltip(hit, e.clientX, e.clientY);
83
+ });
84
+ canvas.addEventListener('mouseleave', () => ui.tooltip.hide());
85
+
86
+ const container = ui.div([canvas], {style: {
87
+ display: 'flex',
88
+ alignItems: 'center',
89
+ justifyContent: 'center',
90
+ width: '100%',
91
+ height: '100%',
92
+ }});
93
+
94
+ ui.dialog({title: 'Oligonucleotide', showHeader: true, showFooter: false})
95
+ .add(container)
96
+ .show({modal: true, fullScreen: true});
97
+ }
98
+
99
+ /* -------------------------------------------------------------------------- *
100
+ * HELM Web Editor action ("Edit HELM")
101
+ * -------------------------------------------------------------------------- */
102
+
103
+ export async function openOligoHelmEditorDialog(value: DG.SemanticValue): Promise<void> {
104
+ if (!value?.cell) return;
105
+ const cell = value.cell;
106
+ // Helm:editMoleculeCell can't be reused: it calls seqHelper.getSeqHandler(col),
107
+ // which throws unless semType === Macromolecule. OligoNucleotide columns have
108
+ // semType=OligoNucleotide, so we drive the HELM Web Editor directly.
109
+ const helmHelper = await getHelmHelper();
110
+ const view = ui.div();
111
+ const app = helmHelper.createWebEditorApp(view, String(cell.value ?? ''));
112
+ ui.dialog({showHeader: false, showFooter: true})
113
+ .add(view)
114
+ .onOK(() => {
115
+ const helmValue = app.canvas!.getHelm(true)
116
+ .replace(/<\/span>/g, '')
117
+ .replace(/<span style='background:#bbf;'>/g, '');
118
+ cell.value = helmValue;
119
+ })
120
+ .show({modal: true, fullScreen: true});
121
+ }
122
+
123
+ /* -------------------------------------------------------------------------- *
124
+ * Copy as HELM
125
+ * -------------------------------------------------------------------------- */
126
+
127
+ export function copyHelmToClipboard(value: DG.SemanticValue): void {
128
+ const helm = String(value?.value ?? '');
129
+ if (!helm) return;
130
+ navigator.clipboard.writeText(helm).then(() => grok.shell.info('HELM copied to clipboard'));
131
+ }
132
+
133
+ /* -------------------------------------------------------------------------- *
134
+ * Copy as Image
135
+ * -------------------------------------------------------------------------- */
136
+
137
+ const IMAGE_SCALE = 8;
138
+
139
+ export function copyDuplexImageToClipboard(value: DG.SemanticValue): void {
140
+ if (!value?.value) return;
141
+ const model = parseHelmDuplex(String(value.value));
142
+
143
+ // Probe layout at the reference size, then crop the canvas to the natural
144
+ // duplex extent so the resulting image isn't padded with blank pixels.
145
+ const refLayout = drawDuplex(
146
+ null as unknown as CanvasRenderingContext2D, 0, 0, REF_W, REF_H, model, undefined, true);
147
+ const width = Math.max(REF_H, computeMaxChipEndX(refLayout) + RIGHT_PAD);
148
+
149
+ const canvas = document.createElement('canvas');
150
+ canvas.width = Math.max(1, Math.round((width + RIGHT_PAD) * IMAGE_SCALE));
151
+ canvas.height = Math.max(1, Math.round(REF_H * IMAGE_SCALE));
152
+ const g = canvas.getContext('2d');
153
+ if (!g) return;
154
+ g.scale(IMAGE_SCALE, IMAGE_SCALE);
155
+ drawDuplex(g, 0, 0, width, REF_H, model);
156
+
157
+ canvas.toBlob((blob) => {
158
+ if (!blob) return;
159
+ navigator.clipboard.write([new ClipboardItem({'image/png': blob})])
160
+ .then(() => grok.shell.info('Image copied to clipboard'))
161
+ .catch((er) => grok.shell.error(`Failed to copy image: ${er?.message ?? er}`));
162
+ }, 'image/png');
163
+ }
164
+
165
+ /* -------------------------------------------------------------------------- *
166
+ * Internals
167
+ * -------------------------------------------------------------------------- */
168
+
169
+ function computeMaxChipEndX(layout: DuplexLayout): number {
170
+ const lastSense = layout.senseChips.length ?
171
+ layout.senseChips[layout.senseChips.length - 1] : null;
172
+ const lastAnti = layout.antiChips.length ?
173
+ layout.antiChips[layout.antiChips.length - 1] : null;
174
+ return Math.max(
175
+ (lastSense?.x ?? 0) + (lastSense?.w ?? 0),
176
+ (lastAnti?.x ?? 0) + (lastAnti?.w ?? 0),
177
+ );
178
+ }
@@ -20,6 +20,7 @@ import {
20
20
  ParsedNucleotide, resolveConjugate, resolvePhosphate, resolveSugar,
21
21
  } from './types';
22
22
  import {getNaturalAnalog} from './analog-cache';
23
+ import {getMonomerColors} from './monomer-colors';
23
24
 
24
25
  export function buildOligoPanel(value: DG.SemanticValue): DG.Widget {
25
26
  const helm: string = value.value ?? '';
@@ -110,9 +111,10 @@ function section(title: string, body: HTMLElement): HTMLElement {
110
111
 
111
112
  /** Build a legend filtered to modifications actually present in this cell.
112
113
  * One row per unique mod, with the count to the right. Items collapse by
113
- * canonical symbol so legacy/canonical aliases share a row. Custom bases
114
- * (non-A/C/G/U/T HELM symbols) are listed with their natural-analog color
115
- * resolved async via the central Bio monomer library. */
114
+ * canonical symbol so legacy/canonical aliases share a row. Colors come from
115
+ * the same `getMonomerColors()` path the canvas renderer uses — so what's
116
+ * drawn on the chip and what's in the legend swatch are always in sync. The
117
+ * local mod meta only supplies the fallback color and human-readable name. */
116
118
  function buildCellLegend(
117
119
  sugars: Map<string, number>, phos: Map<string, number>,
118
120
  conjs: Map<string, number>, bases: Map<string, number>,
@@ -120,34 +122,39 @@ function buildCellLegend(
120
122
  type Item = { label: string; color: string; count: number };
121
123
  const items: Item[] = [];
122
124
 
123
- // Sugar mods — collapse by canonical symbol so e.g. mR + m → one row
125
+ // Sugars — collapse by canonical symbol so e.g. mR + m → one row. Canonical
126
+ // ribose/deoxyribose are included too since the canvas now draws a stripe
127
+ // for every sugar, not just the modified ones.
124
128
  const sugarByCanon = new Map<string, number>();
125
129
  for (const [sym, n] of sugars.entries()) {
126
130
  const c = canonicalSugarSymbol(sym);
127
- if (c === 'r' || c === 'd') continue; // unmodified
128
131
  sugarByCanon.set(c, (sugarByCanon.get(c) ?? 0) + n);
129
132
  }
130
133
  for (const [c, n] of sugarByCanon.entries()) {
131
134
  const meta = resolveSugar(c, null).meta;
132
- items.push({label: meta.name, color: meta.color, count: n});
135
+ const libColor = getMonomerColors('sugar', c).backgroundcolor;
136
+ items.push({label: meta.name, color: libColor ?? meta.color, count: n});
133
137
  }
134
138
 
135
- // Phosphate / linkage mods
139
+ // Phosphate / linkage mods — show ALL linkages used in the cell (including
140
+ // canonical `p`) since the canvas now draws an apex for every linkage.
136
141
  const phosByCanon = new Map<string, number>();
137
142
  for (const [sym, n] of phos.entries()) {
138
143
  const c = canonicalPhosphateSymbol(sym);
139
- if (c === 'p') continue;
140
144
  phosByCanon.set(c, (phosByCanon.get(c) ?? 0) + n);
141
145
  }
142
146
  for (const [c, n] of phosByCanon.entries()) {
143
147
  const meta = resolvePhosphate(c).meta;
144
- items.push({label: `${meta.name} (linkage)`, color: meta.color, count: n});
148
+ const libColor = getMonomerColors('linker', c).backgroundcolor;
149
+ items.push({label: `${meta.name} (linkage)`, color: libColor ?? meta.color, count: n});
145
150
  }
146
151
 
147
- // Custom bases — color via natural analog from the central Bio lib (sync).
152
+ // Custom bases — prefer the library's base color; fall back to natural-
153
+ // analog palette and finally to FALLBACK_COLOR.
148
154
  for (const [sym, n] of bases.entries()) {
155
+ const libColor = getMonomerColors('base', sym).backgroundcolor;
149
156
  const analog = getNaturalAnalog(sym);
150
- const color = (analog && BASE_COLORS[analog]) ? BASE_COLORS[analog] : FALLBACK_COLOR;
157
+ const color = libColor ?? (analog && BASE_COLORS[analog]) ?? FALLBACK_COLOR;
151
158
  const label = analog ? `${sym} (base, analog ${analog})` : `${sym} (base)`;
152
159
  items.push({label, color, count: n});
153
160
  }
@@ -155,7 +162,8 @@ function buildCellLegend(
155
162
  // Conjugates
156
163
  for (const [sym, n] of conjs.entries()) {
157
164
  const meta = resolveConjugate(sym).meta;
158
- items.push({label: meta.name, color: meta.color, count: n});
165
+ const libColor = getMonomerColors('chem', sym).backgroundcolor;
166
+ items.push({label: meta.name, color: libColor ?? meta.color, count: n});
159
167
  }
160
168
 
161
169
  if (items.length === 0) {
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Synchronous color resolution against the central Bio monomer library.
3
+ *
4
+ * `_package.bioMonomerLib` is wired up at package init (see
5
+ * `initSequenceTranslatorInt`), so by the time any cell renders, the library
6
+ * is always present. The lib's `getMonomerColors(biotype, symbol)` returns
7
+ * `{ textcolor?, backgroundcolor?, linecolor? }` (all optional) — the
8
+ * library itself handles natural-analog fallback for custom symbols.
9
+ *
10
+ * Lookups are memoized in a small map keyed by `${kind}:${symbol}`.
11
+ */
12
+
13
+ import * as DG from 'datagrok-api/dg';
14
+
15
+ import {HelmTypes} from '@datagrok-libraries/js-draw-lite/src/types/org';
16
+ import {HelmType} from '@datagrok-libraries/bio/src/helm/types';
17
+ import {_package} from '../package';
18
+
19
+ /** The four "kinds" we render — maps to the appropriate `HelmType`. */
20
+ export type MonomerKind = 'sugar' | 'base' | 'linker' | 'chem';
21
+
22
+ const KIND_TO_HELM_TYPE: Record<MonomerKind, HelmType> = {
23
+ sugar: HelmTypes.SUGAR as HelmType,
24
+ base: HelmTypes.BASE as HelmType,
25
+ linker: HelmTypes.LINKER as HelmType,
26
+ chem: HelmTypes.CHEM as HelmType,
27
+ };
28
+
29
+ export interface MonomerColorTriple {
30
+ backgroundcolor: string | null;
31
+ textcolor: string | null;
32
+ linecolor: string | null;
33
+ }
34
+
35
+ const EMPTY: MonomerColorTriple = {backgroundcolor: null, textcolor: null, linecolor: null};
36
+ const _cache = new DG.LruCache<string, MonomerColorTriple>(256);
37
+
38
+ /** Resolve background/text/line colors for a HELM monomer. Returns `null`s
39
+ * when the library has no entry. Pure sync — backed by `_package.bioMonomerLib`. */
40
+ export function getMonomerColors(kind: MonomerKind, symbol: string): MonomerColorTriple {
41
+ if (!symbol) return EMPTY;
42
+ const key = `${kind}:${symbol}`;
43
+ const cached = _cache.get(key);
44
+ if (cached) return cached;
45
+
46
+ let result: MonomerColorTriple = EMPTY;
47
+ try {
48
+ const lib = _package.bioMonomerLib;
49
+ const colors = lib.getMonomerColors(KIND_TO_HELM_TYPE[kind], symbol);
50
+ if (colors) {
51
+ result = {
52
+ backgroundcolor: colors.backgroundcolor ?? null,
53
+ textcolor: colors.textcolor ?? null,
54
+ linecolor: colors.linecolor ?? null,
55
+ };
56
+ }
57
+ } catch { /* lib not yet initialized — return all-null */ }
58
+
59
+ _cache.set(key, result);
60
+ return result;
61
+ }
@@ -14,6 +14,7 @@
14
14
 
15
15
  import * as grok from 'datagrok-api/grok';
16
16
  import * as ui from 'datagrok-api/ui';
17
+ import * as DG from 'datagrok-api/dg';
17
18
 
18
19
  import {getMonomerLibHelper, IMonomerLib} from '@datagrok-libraries/bio/src/types/monomer-library';
19
20
 
@@ -208,14 +209,13 @@ function makeStructureCell(label: string): { root: HTMLElement; body: HTMLDivEle
208
209
  * Since only one tooltip is visible at a time, moving the cached node
209
210
  * between tooltip hosts via `appendChild` is safe: the previous host is
210
211
  * being torn down. */
211
- const _structCache = new Map<string, HTMLElement>();
212
+ const _structCache = new DG.LruCache<string, HTMLElement>(256);
212
213
 
213
214
  function drawMolfileCached(host: HTMLDivElement, molfile: string, cacheKey: string): void {
214
215
  try {
215
216
  let cached = _structCache.get(cacheKey);
216
217
  if (!cached) {
217
218
  cached = grok.chem.drawMolecule(molfile, STRUCT_W, STRUCT_H, false);
218
- if (_structCache.size > 256) _structCache.clear();
219
219
  _structCache.set(cacheKey, cached);
220
220
  }
221
221
  host.innerHTML = '';
@@ -162,6 +162,13 @@ export namespace funcs {
162
162
  return await grok.functions.call('SequenceTranslator:EditOligoNucleotideCell', { cell });
163
163
  }
164
164
 
165
+ /**
166
+ Edit the oligonucleotide HELM in the HELM Web Editor
167
+ */
168
+ export async function openOligoHelmEditor(value: any ): Promise<void> {
169
+ return await grok.functions.call('SequenceTranslator:OpenOligoHelmEditor', { value });
170
+ }
171
+
165
172
  /**
166
173
  Modifications, lengths, conjugates and color legend for an OligoNucleotide cell
167
174
  */
@@ -176,6 +183,20 @@ export namespace funcs {
176
183
  return await grok.functions.call('SequenceTranslator:OligoNucleotideStructuresPanel', { value });
177
184
  }
178
185
 
186
+ /**
187
+ Copy the HELM string of an oligo cell to the clipboard
188
+ */
189
+ export async function copyOligoAsHelm(value: any ): Promise<void> {
190
+ return await grok.functions.call('SequenceTranslator:CopyOligoAsHelm', { value });
191
+ }
192
+
193
+ /**
194
+ Copy a high-resolution image of the oligo duplex
195
+ */
196
+ export async function copyOligoAsImage(value: any ): Promise<void> {
197
+ return await grok.functions.call('SequenceTranslator:CopyOligoAsImage', { value });
198
+ }
199
+
179
200
  /**
180
201
  Create a new column tagged as OligoNucleotide so HELM duplex cells render with the oligo view
181
202
  */
package/src/package.g.ts CHANGED
@@ -236,8 +236,16 @@ export function oligoNucleotideCellRenderer() : any {
236
236
  //tags: cellEditor
237
237
  //input: grid_cell cell
238
238
  //meta.role: cellEditor
239
- export async function editOligoNucleotideCell(cell: any) : Promise<void> {
240
- await PackageFunctions.editOligoNucleotideCell(cell);
239
+ export function editOligoNucleotideCell(cell: any) : void {
240
+ PackageFunctions.editOligoNucleotideCell(cell);
241
+ }
242
+
243
+ //name: Open HELM Editor
244
+ //description: Edit the oligonucleotide HELM in the HELM Web Editor
245
+ //input: semantic_value value { semType: OligoNucleotide }
246
+ //meta.action: Edit HELM
247
+ export function openOligoHelmEditor(value: DG.SemanticValue) : void {
248
+ PackageFunctions.openOligoHelmEditor(value);
241
249
  }
242
250
 
243
251
  //name: Oligo-Nucleotide
@@ -258,6 +266,22 @@ export function oligoNucleotideStructuresPanel(value: DG.SemanticValue) : any {
258
266
  return PackageFunctions.oligoNucleotideStructuresPanel(value);
259
267
  }
260
268
 
269
+ //name: Copy as HELM
270
+ //description: Copy the HELM string of an oligo cell to the clipboard
271
+ //input: semantic_value value { semType: OligoNucleotide }
272
+ //meta.action: Copy as HELM
273
+ export function copyOligoAsHelm(value: DG.SemanticValue) : void {
274
+ PackageFunctions.copyOligoAsHelm(value);
275
+ }
276
+
277
+ //name: Copy as Image
278
+ //description: Copy a high-resolution image of the oligo duplex
279
+ //input: semantic_value value { semType: OligoNucleotide }
280
+ //meta.action: Copy as Image
281
+ export function copyOligoAsImage(value: DG.SemanticValue) : void {
282
+ PackageFunctions.copyOligoAsImage(value);
283
+ }
284
+
261
285
  //description: Create a new column tagged as OligoNucleotide so HELM duplex cells render with the oligo view
262
286
  //input: dataframe table
263
287
  //input: column helmCol { caption: HELM column; semType: Macromolecule }
package/src/package.ts CHANGED
@@ -20,6 +20,10 @@ import {OligoNucleotideCellRenderer} from './oligo-renderer/cell-renderer';
20
20
  import {buildOligoPanel} from './oligo-renderer/legend-panel';
21
21
  import {buildOligoStructuresPanel} from './oligo-renderer/structures-panel';
22
22
  import {combineSenseAntisenseToOligo, convertHelmColumnToOligo} from './oligo-renderer/converters';
23
+ import {
24
+ openOligoCanvasDialog, openOligoHelmEditorDialog,
25
+ copyHelmToClipboard, copyDuplexImageToClipboard,
26
+ } from './oligo-renderer/cell-actions';
23
27
 
24
28
  import {polyToolConvert, polyToolConvertUI} from './polytool/pt-dialog';
25
29
  import {polyToolEnumerateChemApp, polyToolEnumerateChemUI} from './polytool/pt-chem-enum-dialog';
@@ -443,6 +447,10 @@ export class PackageFunctions {
443
447
  return new OligoNucleotideCellRenderer();
444
448
  }
445
449
 
450
+ /** Double-click cell editor: opens a full-screen modal with a nicely-rendered
451
+ * canvas view of the duplex. Hover interactions (monomer/linkage tooltip
452
+ * with cached RDKit structures) mirror what works in the grid cell. Editing
453
+ * the HELM itself happens through the separate `Open HELM Editor` action. */
446
454
  @grok.decorators.func({
447
455
  name: 'editOligoNucleotideCell',
448
456
  description: 'OligoNucleotide',
@@ -451,24 +459,25 @@ export class PackageFunctions {
451
459
  role: 'cellEditor',
452
460
  },
453
461
  })
454
- static async editOligoNucleotideCell(
462
+ static editOligoNucleotideCell(
455
463
  @grok.decorators.param({type: 'grid_cell'}) cell: DG.GridCell,
464
+ ): void {
465
+ openOligoCanvasDialog(cell);
466
+ }
467
+
468
+ /** Cell context-menu action: open the HELM Web Editor for the cell's
469
+ * sequence and write the edited HELM back on OK. Lives in the "Actions"
470
+ * group on the cell's context menu (same surfacing convention as
471
+ * `Copy as HELM`). */
472
+ @grok.decorators.func({
473
+ name: 'Open HELM Editor',
474
+ description: 'Edit the oligonucleotide HELM in the HELM Web Editor',
475
+ meta: {'action': 'Edit HELM'},
476
+ })
477
+ static openOligoHelmEditor(
478
+ @grok.decorators.param({options: {semType: 'OligoNucleotide'}}) value: DG.SemanticValue,
456
479
  ): Promise<void> {
457
- // Helm:editMoleculeCell can't be reused: it calls seqHelper.getSeqHandler(col),
458
- // which throws unless semType === Macromolecule. OligoNucleotide columns have
459
- // semType=OligoNucleotide, so we open the HELM Web Editor directly.
460
- const helmHelper = await getHelmHelper();
461
- const view = ui.div();
462
- const app = helmHelper.createWebEditorApp(view, (cell.cell.value as string | null) ?? '');
463
- ui.dialog({showHeader: false, showFooter: true})
464
- .add(view)
465
- .onOK(() => {
466
- const helmValue = app.canvas!.getHelm(true)
467
- .replace(/<\/span>/g, '')
468
- .replace(/<span style='background:#bbf;'>/g, '');
469
- cell.setValue(helmValue);
470
- })
471
- .show({modal: true, fullScreen: true});
480
+ return openOligoHelmEditorDialog(value);
472
481
  }
473
482
 
474
483
  @grok.decorators.func({
@@ -495,6 +504,38 @@ export class PackageFunctions {
495
504
  return buildOligoStructuresPanel(value);
496
505
  }
497
506
 
507
+ /** Cell context-menu action: copy the raw HELM string. Surfaced automatically
508
+ * by the platform under the cell's "Copy" submenu because of `meta.action:
509
+ * 'Copy as HELM'`; we set `exclude-actions-panel` so it doesn't also show up
510
+ * in the right-side actions panel. */
511
+ @grok.decorators.func({
512
+ name: 'Copy as HELM',
513
+ description: 'Copy the HELM string of an oligo cell to the clipboard',
514
+ meta: {'action': 'Copy as HELM'},
515
+ })
516
+ static copyOligoAsHelm(
517
+ @grok.decorators.param({options: {semType: 'OligoNucleotide'}}) value: DG.SemanticValue,
518
+ ): void {
519
+ copyHelmToClipboard(value);
520
+ }
521
+
522
+ /** Cell context-menu action: render the duplex to a high-resolution PNG with
523
+ * transparent background and copy it to the system clipboard. Canvas pixel
524
+ * dimensions are scaled up but the logical layout sees the original
525
+ * gridCell bounds — so chip sizes match what's on-screen, just at higher
526
+ * pixel density. drawDuplex itself never paints a backdrop, which keeps
527
+ * the alpha channel clean. */
528
+ @grok.decorators.func({
529
+ name: 'Copy as Image',
530
+ description: 'Copy a high-resolution image of the oligo duplex',
531
+ meta: {'action': 'Copy as Image'},
532
+ })
533
+ static copyOligoAsImage(
534
+ @grok.decorators.param({options: {semType: 'OligoNucleotide'}}) value: DG.SemanticValue,
535
+ ): void {
536
+ copyDuplexImageToClipboard(value);
537
+ }
538
+
498
539
  // Invoked from the column / cell context menu via detectors.js (no top-menu).
499
540
  @grok.decorators.func({
500
541
  name: 'convertHelmToOligoNucleotide',
@@ -1269,14 +1269,18 @@ async function executeEnumeration(state: ChemEnumDialogState, _rdkit: RDModule):
1269
1269
  // Stage 2 — canonicalize the whole Enumerated column in parallel via Chem workers.
1270
1270
  pi.update(40, `Canonicalizing ${results.length.toLocaleString()} molecule(s)...`);
1271
1271
  try {
1272
- await grok.functions.call('Chem:convertNotation', {
1272
+ const res: DG.Column = await grok.functions.call('Chem:convertNotation', {
1273
1273
  data: df,
1274
1274
  molecules: smilesCol,
1275
1275
  targetNotation: DG.chem.Notation.Smiles,
1276
- overwrite: true,
1276
+ overwrite: false,
1277
1277
  join: false,
1278
1278
  kekulize: false,
1279
1279
  });
1280
+ // in older version of the chem, overwrite is super slow, it has been updated but we can do it like this here
1281
+ const resArr = res.toList();
1282
+ smilesCol.init((i) => resArr[i]);
1283
+ smilesCol.meta.units = DG.chem.Notation.Smiles;
1280
1284
  } catch (err: any) {
1281
1285
  // Canonicalization is a nice-to-have; the uncanonical SMILES are still valid output.
1282
1286
  _package.logger.warning(`Canonicalization skipped: ${err?.message ?? err}`);