@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.
- package/CHANGELOG.md +12 -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/package.json +1 -1
- package/src/oligo-renderer/analog-cache.ts +5 -2
- package/src/oligo-renderer/canvas-renderer.ts +401 -123
- package/src/oligo-renderer/cell-actions.ts +178 -0
- package/src/oligo-renderer/legend-panel.ts +20 -12
- package/src/oligo-renderer/monomer-colors.ts +61 -0
- package/src/oligo-renderer/tooltip.ts +2 -2
- package/src/package-api.ts +21 -0
- package/src/package.g.ts +26 -2
- package/src/package.ts +57 -16
- package/src/polytool/pt-chem-enum-dialog.ts +6 -2
- package/src/tests/oligo-renderer-tests.ts +224 -14
- package/test-console-output-1.log +179 -179
- package/test-record-1.mp4 +0 -0
|
@@ -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.
|
|
114
|
-
*
|
|
115
|
-
*
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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 —
|
|
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])
|
|
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
|
-
|
|
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
|
|
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 = '';
|
package/src/package-api.ts
CHANGED
|
@@ -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
|
|
240
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
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}`);
|