@datagrok/sequence-translator 1.10.13 → 1.10.15

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.
Files changed (36) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/detectors.js +30 -2
  3. package/dist/455.js +1 -1
  4. package/dist/455.js.map +1 -1
  5. package/dist/package-test.js +1 -1
  6. package/dist/package-test.js.map +1 -1
  7. package/dist/package.js +1 -1
  8. package/dist/package.js.map +1 -1
  9. package/files/samples/sirna-demo.csv +38 -0
  10. package/files/tests/chem_enum_cores.csv +5 -0
  11. package/files/tests/chem_enum_rgroups.csv +5 -0
  12. package/package.json +2 -2
  13. package/src/apps/structure/view/ui.ts +1 -1
  14. package/src/apps/translator/view/ui.ts +1 -1
  15. package/src/oligo-renderer/canvas-renderer.ts +500 -0
  16. package/src/oligo-renderer/cell-renderer.ts +105 -0
  17. package/src/oligo-renderer/converters.ts +77 -0
  18. package/src/oligo-renderer/helm-parser.ts +154 -0
  19. package/src/oligo-renderer/legend-panel.ts +154 -0
  20. package/src/oligo-renderer/structures-panel.ts +96 -0
  21. package/src/oligo-renderer/tooltip.ts +223 -0
  22. package/src/oligo-renderer/types.ts +221 -0
  23. package/src/package-api.ts +43 -1
  24. package/src/package-test.ts +2 -0
  25. package/src/package.g.ts +56 -3
  26. package/src/package.ts +92 -5
  27. package/src/polytool/const.ts +1 -1
  28. package/src/polytool/pt-chem-enum-dialog.ts +940 -0
  29. package/src/polytool/pt-chem-enum.ts +553 -0
  30. package/src/polytool/pt-dialog.ts +4 -125
  31. package/src/polytool/pt-enumerate-seq-dialog.ts +3 -3
  32. package/src/tests/oligo-renderer-tests.ts +299 -0
  33. package/src/tests/polytool-enumerate-chem-tests.ts +408 -0
  34. package/test-console-output-1.log +303 -97
  35. package/test-record-1.mp4 +0 -0
  36. package/src/polytool/pt-enumeration-chem.ts +0 -100
@@ -0,0 +1,223 @@
1
+ /**
2
+ * Hover tooltip for OligoNucleotide cells.
3
+ *
4
+ * Shows monomer details synchronously, then async-loads RDKit structures
5
+ * for whatever was hovered:
6
+ * - Nucleotide chip → sugar + base + 3'-phosphate (3 small structures)
7
+ * - PS / phosphate-mod linkage in the gap → just the phosphate structure
8
+ * - Conjugate pill → that conjugate's structure
9
+ *
10
+ * Structures come from the **central Bio monomer library** (not the
11
+ * SequenceTranslator-internal one). Symbols are resolved via the alias map
12
+ * so legacy / vendor shorthand still finds the canonical entry.
13
+ */
14
+
15
+ import * as grok from 'datagrok-api/grok';
16
+ import * as ui from 'datagrok-api/ui';
17
+
18
+ import {getMonomerLibHelper, IMonomerLib} from '@datagrok-libraries/bio/src/types/monomer-library';
19
+
20
+ import {HitResult} from './canvas-renderer';
21
+ import {
22
+ canonicalPhosphateSymbol, canonicalSugarSymbol,
23
+ ParsedNucleotide, resolveConjugate, resolvePhosphate, resolveSugar,
24
+ } from './types';
25
+
26
+ const STRUCT_W = 110;
27
+ const STRUCT_H = 90;
28
+
29
+ type StructureKind = 'sugar' | 'base' | 'phosphate' | 'conjugate';
30
+
31
+ /** Show a tooltip for the hovered hit. Idempotent — replaces any current tooltip. */
32
+ export function showMonomerTooltip(hit: HitResult, clientX: number, clientY: number): void {
33
+ const root = buildTooltipRoot(hit);
34
+ ui.tooltip.show(root, clientX + 14, clientY + 14);
35
+ void renderStructuresAsync(root, hit);
36
+ }
37
+
38
+ function buildTooltipRoot(hit: HitResult): HTMLElement {
39
+ const m = hit.monomer;
40
+ const title = ui.divText('', {style: {fontWeight: '600', marginBottom: '4px'}});
41
+ const detailRows: HTMLElement[] = [];
42
+
43
+ // Linkage hover (PS marker in the gap) — shape the tooltip around the linkage
44
+ if (hit.linkage) {
45
+ const phos = resolvePhosphate(hit.linkage.phosphateSymbol);
46
+ title.textContent = `${phos.meta.name} linkage`;
47
+ detailRows.push(
48
+ kv('Strand', hit.strand === 'sense' ? 'Sense' : 'Antisense'),
49
+ kv('Between', `pos ${hit.position + 1} – ${hit.position + 2}`),
50
+ kv('HELM symbol', `[${canonicalPhosphateSymbol(hit.linkage.phosphateSymbol)}]`, true),
51
+ );
52
+ } else if (m.kind === 'nucleotide') {
53
+ const nt = m as ParsedNucleotide;
54
+ const sugarRes = resolveSugar(nt.sugar, nt.base);
55
+ const phosRes = resolvePhosphate(nt.phosphate);
56
+ title.textContent = `${nt.base ?? '?'}${hit.position + 1} — ${sugarRes.meta.name}`;
57
+ detailRows.push(
58
+ kv('Strand', hit.strand === 'sense' ? 'Sense' : 'Antisense'),
59
+ kv('Position', String(hit.position + 1)),
60
+ kv('Base', nt.base ?? '—'),
61
+ kv('Sugar', sugarRes.meta.name),
62
+ kv('3\'-linkage', phosRes.meta.name),
63
+ kv('HELM', nt.raw, true),
64
+ );
65
+ } else {
66
+ const conj = resolveConjugate(m.symbol);
67
+ title.textContent = conj.meta.name;
68
+ detailRows.push(
69
+ kv('Strand', hit.strand === 'sense' ? 'Sense' : 'Antisense'),
70
+ kv('Position', String(hit.position + 1)),
71
+ kv('Type', 'Terminal conjugate / linker'),
72
+ kv('HELM', m.raw, true),
73
+ );
74
+ }
75
+
76
+ // Reserved area for structures — populated async.
77
+ // Holds 1 (linkage / conjugate) or 3 (full nucleotide) small canvases side by side.
78
+ const structHost = ui.div([], {style: {
79
+ marginTop: '8px',
80
+ display: 'flex', flexDirection: 'row', gap: '6px',
81
+ flexWrap: 'wrap',
82
+ }});
83
+ structHost.dataset.role = 'struct';
84
+ const placeholder = ui.divText('Loading structures…', {style: {
85
+ fontSize: '11px', color: 'var(--grey-4, #888)',
86
+ }});
87
+ structHost.appendChild(placeholder);
88
+
89
+ return ui.divV([title, ...detailRows, structHost], {
90
+ style: {fontSize: '12px', lineHeight: '1.45', minWidth: '240px', maxWidth: '380px'},
91
+ });
92
+ }
93
+
94
+ function kv(k: string, v: string, mono = false): HTMLElement {
95
+ return ui.divH([
96
+ ui.divText(k, {style: {color: 'var(--grey-4, #888)', width: '90px', flexShrink: '0'}}),
97
+ ui.divText(v, {style: mono ? {fontFamily: 'ui-monospace, Menlo, monospace', wordBreak: 'break-all'} : {}}),
98
+ ], {style: {gap: '6px'}});
99
+ }
100
+
101
+ /** Cached merged Bio monomer library. */
102
+ let _bioLibPromise: Promise<IMonomerLib> | null = null;
103
+ function getBioLib(): Promise<IMonomerLib> {
104
+ if (!_bioLibPromise) {
105
+ _bioLibPromise = (async () => {
106
+ const helper = await getMonomerLibHelper();
107
+ return helper.getMonomerLib();
108
+ })();
109
+ }
110
+ return _bioLibPromise;
111
+ }
112
+
113
+ /** Try the original symbol first, then the canonical alias, across all polymer types. */
114
+ function findMonomerMolfile(lib: IMonomerLib, rawSymbol: string, kind: StructureKind): string | null {
115
+ const candidates: string[] = [rawSymbol];
116
+ if (kind === 'sugar') candidates.push(canonicalSugarSymbol(rawSymbol));
117
+ else if (kind === 'phosphate') candidates.push(canonicalPhosphateSymbol(rawSymbol));
118
+ // Bases (A/C/G/U/T) and conjugates: try the symbol as-is.
119
+
120
+ const polymerTypes = lib.getPolymerTypes();
121
+ for (const sym of candidates) {
122
+ for (const pt of polymerTypes) {
123
+ const monomer = lib.getMonomer(pt, sym);
124
+ if (monomer && monomer.molfile) return monomer.molfile;
125
+ }
126
+ }
127
+ return null;
128
+ }
129
+
130
+ async function renderStructuresAsync(root: HTMLElement, hit: HitResult): Promise<void> {
131
+ const host = root.querySelector('[data-role="struct"]') as HTMLDivElement | null;
132
+ if (!host) return;
133
+
134
+ // Determine which structures to show
135
+ let requests: { label: string; symbol: string; kind: StructureKind }[];
136
+ if (hit.linkage) {
137
+ requests = [{label: 'Linkage', symbol: hit.linkage.phosphateSymbol, kind: 'phosphate'}];
138
+ } else if (hit.monomer.kind === 'nucleotide') {
139
+ const nt = hit.monomer as ParsedNucleotide;
140
+ requests = [
141
+ {label: 'Sugar', symbol: nt.sugar, kind: 'sugar'},
142
+ ];
143
+ if (nt.base) requests.push({label: 'Base', symbol: nt.base, kind: 'base'});
144
+ if (nt.phosphate) requests.push({label: '3\'-linkage', symbol: nt.phosphate, kind: 'phosphate'});
145
+ } else {
146
+ requests = [{label: 'Conjugate', symbol: hit.monomer.symbol, kind: 'conjugate'}];
147
+ }
148
+
149
+ let lib: IMonomerLib;
150
+ try {
151
+ lib = await getBioLib();
152
+ } catch {
153
+ host.innerHTML = '';
154
+ host.appendChild(ui.divText('Monomer library unavailable', {
155
+ style: {fontSize: '11px', color: 'var(--grey-4, #888)'},
156
+ }));
157
+ return;
158
+ }
159
+
160
+ host.innerHTML = '';
161
+
162
+ for (const req of requests) {
163
+ const cell = makeStructureCell(req.label);
164
+ host.appendChild(cell.root);
165
+ const molfile = findMonomerMolfile(lib, req.symbol, req.kind);
166
+ if (!molfile) {
167
+ cell.body.textContent = `Not in HELMCore: ${req.symbol}`;
168
+ continue;
169
+ }
170
+ void drawMolfileCached(cell.body, molfile, cacheKeyFor(req));
171
+ }
172
+ }
173
+
174
+ /** Cache key for a structure render. */
175
+ function cacheKeyFor(req: {kind: StructureKind; symbol: string}): string {
176
+ let canonical = req.symbol;
177
+ if (req.kind === 'sugar') canonical = canonicalSugarSymbol(req.symbol);
178
+ else if (req.kind === 'phosphate') canonical = canonicalPhosphateSymbol(req.symbol);
179
+ return `${req.kind}:${canonical}`;
180
+ }
181
+
182
+ function makeStructureCell(label: string): { root: HTMLElement; body: HTMLDivElement } {
183
+ const body = ui.div([], {style: {
184
+ // width: `${STRUCT_W}px`, height: `${STRUCT_H}px`,
185
+ // background: 'var(--grey-1, #f4f4f4)', border: '1px solid var(--grey-2, #ddd)',
186
+ borderRadius: '4px',
187
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
188
+ fontSize: '10px', color: 'var(--grey-4, #888)', textAlign: 'center', padding: '4px',
189
+ }}) as HTMLDivElement;
190
+ const cap = ui.divText(label, {style: {
191
+ fontSize: '10px', color: 'var(--grey-5, #555)', textAlign: 'center', marginTop: '2px',
192
+ letterSpacing: '0.04em', textTransform: 'uppercase', fontWeight: '600',
193
+ }});
194
+ const root = ui.divV([body, cap], {style: {display: 'inline-flex'}});
195
+ return {root, body};
196
+ }
197
+
198
+ /** Cache of rendered structures by kind+canonical-symbol.
199
+ *
200
+ * We cache the actual element returned by grok.chem.drawMolecule and reuse
201
+ * the same instance across tooltips. We deliberately do NOT clone — when
202
+ * `drawMolecule` returns a canvas, its bitmap is painted asynchronously
203
+ * (RDKit init etc.), so a clone taken right after the call ends up blank.
204
+ * Since only one tooltip is visible at a time, moving the cached node
205
+ * between tooltip hosts via `appendChild` is safe: the previous host is
206
+ * being torn down. */
207
+ const _structCache = new Map<string, HTMLElement>();
208
+
209
+ function drawMolfileCached(host: HTMLDivElement, molfile: string, cacheKey: string): void {
210
+ try {
211
+ let cached = _structCache.get(cacheKey);
212
+ if (!cached) {
213
+ cached = grok.chem.drawMolecule(molfile, STRUCT_W, STRUCT_H, false);
214
+ if (_structCache.size > 256) _structCache.clear();
215
+ _structCache.set(cacheKey, cached);
216
+ }
217
+ host.innerHTML = '';
218
+ // appendChild moves the node from any previous parent automatically.
219
+ host.appendChild(cached);
220
+ } catch {
221
+ host.textContent = 'Render failed';
222
+ }
223
+ }
@@ -0,0 +1,221 @@
1
+ /**
2
+ * Types and modification dictionary for the OligoNucleotide cell renderer.
3
+ *
4
+ * The renderer is intentionally self-contained — it does NOT depend on
5
+ * `_package.initLibData()` having completed, so the grid can render oligo
6
+ * cells before any app has opened. Only the tooltip's RDKit structure
7
+ * depends on the monomer library (which is loaded async there).
8
+ */
9
+
10
+ export const OLIGO_SEM_TYPE = 'OligoNucleotide';
11
+ export const OLIGO_UNITS = 'helm';
12
+
13
+ /** Per-nucleotide parsed model. One per chip in the rendered output. */
14
+ export interface ParsedNucleotide {
15
+ kind: 'nucleotide';
16
+ /** Index inside the strand (0-based). */
17
+ position: number;
18
+ /** Sugar HELM symbol, normalized (no brackets). HELMCore canonical: 'r', 'd', 'm', 'fl2r', 'lna'. */
19
+ sugar: string;
20
+ /** Single-letter base — A / C / G / U / T or null when missing. */
21
+ base: string | null;
22
+ /** Phosphate HELM symbol normalized (no brackets). HELMCore canonical: 'p', 'sp'. Empty for terminal. */
23
+ phosphate: string;
24
+ /** Original HELM monomer text for tooltip + structure lookup. */
25
+ raw: string;
26
+ }
27
+
28
+ /** Terminal conjugate (GalNAc, cholesterol, biotin, L3 linker, …). */
29
+ export interface ParsedConjugate {
30
+ kind: 'conjugate';
31
+ position: number;
32
+ symbol: string;
33
+ raw: string;
34
+ }
35
+
36
+ export type ParsedMonomer = ParsedNucleotide | ParsedConjugate;
37
+
38
+ export interface ParsedStrand {
39
+ /** 'RNA' | 'DNA' | 'CHEM' */
40
+ type: string;
41
+ monomers: ParsedMonomer[];
42
+ }
43
+
44
+ export interface ParsedDuplex {
45
+ sense: ParsedStrand;
46
+ antisense: ParsedStrand | null;
47
+ /** Original HELM string (kept verbatim for tooltip / debug). */
48
+ raw: string;
49
+ }
50
+
51
+ /** Visual classification used for chip coloring. */
52
+ export type ModCategory = 'sugar' | 'phosphate' | 'conjugate' | 'base';
53
+
54
+ export interface ModMeta {
55
+ /** Full user-facing name, used in tooltip + legend. */
56
+ name: string;
57
+ /** 3–8 char display label for chips that don't show a base letter (conjugates). */
58
+ short: string;
59
+ /** Canonical CSS color. */
60
+ color: string;
61
+ category: ModCategory;
62
+ }
63
+
64
+ /* ---------------------------------------------------------------- *
65
+ * Modification dictionary — flat lookups by HELM symbol.
66
+ *
67
+ * Canonical symbols are HELMCore (Pistoia / Bio package):
68
+ * sugars — `r`, `d`, `m`, `25r`, `fl2r`, `lna`, `moe`, ...
69
+ * phosphates — `p`, `sp`, ...
70
+ * bases — `A`, `C`, `G`, `U`, `T`
71
+ *
72
+ * Some vendor / Axolabs-style strings (`R`, `mR`, `fR`, `LR`, `dR`, `sP`)
73
+ * appear in legacy data and in older internal tools. The SYMBOL_ALIASES
74
+ * map normalizes them to canonical at lookup time, so the renderer
75
+ * accepts both. Tooltips / structure look-up use the canonical symbol
76
+ * (which is what the central Bio monomer library actually contains).
77
+ *
78
+ * Anything not in the dictionary falls back to a deterministic hash color.
79
+ * ---------------------------------------------------------------- */
80
+
81
+ export const SUGAR_MODS: Readonly<Record<string, ModMeta>> = Object.freeze({
82
+ // Canonical sugars — chip uses base-canonical color (see BASE_COLORS), not a sugar color
83
+ 'r': {name: 'Ribose', short: 'RNA', color: '#D0D0D0', category: 'sugar'},
84
+ 'd': {name: 'Deoxyribose', short: 'DNA', color: '#BDBDBD', category: 'sugar'},
85
+
86
+ // 2'-O-Methyl
87
+ 'm': {name: '2\'-O-Methyl', short: '2\'-OMe', color: '#4A90E2', category: 'sugar'},
88
+ '25r': {name: '2\'-O-Methyl (2,5)', short: '2\'-OMe', color: '#4A90E2', category: 'sugar'},
89
+
90
+ // 2'-Fluoro
91
+ 'fl2r': {name: '2\'-Fluoro', short: '2\'-F', color: '#50C878', category: 'sugar'},
92
+
93
+ // LNA / cEt / ENA — bridged
94
+ 'lna': {name: 'LNA', short: 'LNA', color: '#E89D3C', category: 'sugar'},
95
+ 'cet': {name: 'cEt', short: 'cEt', color: '#E07B30', category: 'sugar'},
96
+ 'ena': {name: 'ENA', short: 'ENA', color: '#D26C20', category: 'sugar'},
97
+
98
+ // 2'-MOE
99
+ 'moe': {name: '2\'-MOE', short: '2\'-MOE', color: '#66CDAA', category: 'sugar'},
100
+
101
+ // GNA / UNA / FANA / hexitol-NA
102
+ 'Rgna': {name: '(R)-GNA', short: 'GNA', color: '#C7A981', category: 'sugar'},
103
+ 'Sgna': {name: '(S)-GNA', short: 'GNA', color: '#C7A981', category: 'sugar'},
104
+ 'una': {name: 'UNA', short: 'UNA', color: '#ED68B8', category: 'sugar'},
105
+ 'fana': {name: 'FANA', short: 'FANA', color: '#76B947', category: 'sugar'},
106
+ 'hna': {name: 'HNA', short: 'HNA', color: '#A4D080', category: 'sugar'},
107
+ });
108
+
109
+ /** Vendor / legacy / Pistoia-style sugar symbols mapped to canonical HELMCore. */
110
+ export const SUGAR_ALIASES: Readonly<Record<string, string>> = Object.freeze({
111
+ 'R': 'r',
112
+ 'dR': 'd',
113
+ 'mR': 'm',
114
+ 'fR': 'fl2r',
115
+ 'LR': 'lna',
116
+ 'L': 'lna',
117
+ 'MOE': 'moe',
118
+ 'GNA': 'Sgna',
119
+ 'UNA': 'una',
120
+ });
121
+
122
+ export const PHOSPHATE_MODS: Readonly<Record<string, ModMeta>> = Object.freeze({
123
+ 'p': {name: 'Phosphate', short: 'PO', color: '#888888', category: 'phosphate'},
124
+ 'sp': {name: 'Phosphorothioate', short: 'PS', color: '#6B46C1', category: 'phosphate'},
125
+ 's2p': {name: 'Phosphorodithioate', short: 'PS₂', color: '#7E2BC4', category: 'phosphate'},
126
+ 'mp': {name: 'Methylphosphonate', short: 'MeP', color: '#9B5DE5', category: 'phosphate'},
127
+ });
128
+
129
+ export const PHOSPHATE_ALIASES: Readonly<Record<string, string>> = Object.freeze({
130
+ 'P': 'p',
131
+ 'sP': 'sp',
132
+ });
133
+
134
+ export const CONJUGATE_MODS: Readonly<Record<string, ModMeta>> = Object.freeze({
135
+ 'GalNAc': {name: 'GalNAc', short: 'GalNAc', color: '#E91E63', category: 'conjugate'},
136
+ 'L3': {name: 'GalNAc-L3 linker', short: 'L3', color: '#E91E63', category: 'conjugate'},
137
+ 'Chol': {name: 'Cholesterol', short: 'Chol', color: '#B57F50', category: 'conjugate'},
138
+ 'Bio': {name: 'Biotin', short: 'Bio', color: '#FFC107', category: 'conjugate'},
139
+ });
140
+
141
+ /** Pale per-base colors used when sugar is unmodified (R / dR).
142
+ * Modifications override these. */
143
+ export const BASE_COLORS: Readonly<Record<string, string>> = Object.freeze({
144
+ A: '#C1E7C5', // pale green
145
+ C: '#C5DCF0', // pale blue
146
+ G: '#E8DCA9', // pale tan
147
+ U: '#F0C5C5', // pale pink
148
+ T: '#F0C5C5',
149
+ });
150
+
151
+ export const FALLBACK_COLOR = '#BCBCBC';
152
+
153
+ /** Deterministic color for unknown HELM monomer symbols. Stable across cells. */
154
+ export function hashColor(symbol: string): string {
155
+ let h = 0;
156
+ for (let i = 0; i < symbol.length; i++)
157
+ h = ((h * 31) + symbol.charCodeAt(i)) | 0;
158
+ const hue = ((h >>> 0) % 360);
159
+ return `hsl(${hue}, 42%, 60%)`;
160
+ }
161
+
162
+ /** Canonicalize a sugar symbol via the alias map, then return the canonical form. */
163
+ export function canonicalSugarSymbol(sugar: string): string {
164
+ return SUGAR_ALIASES[sugar] ?? sugar;
165
+ }
166
+
167
+ /** Canonicalize a phosphate symbol via the alias map. */
168
+ export function canonicalPhosphateSymbol(phosphate: string): string {
169
+ return PHOSPHATE_ALIASES[phosphate] ?? phosphate;
170
+ }
171
+
172
+ /** Resolve a sugar HELM symbol to (color + meta). Accepts canonical or aliased input. */
173
+ export function resolveSugar(sugar: string, base: string | null): { color: string; meta: ModMeta } {
174
+ const canonical = canonicalSugarSymbol(sugar);
175
+ const known = SUGAR_MODS[canonical];
176
+ if (known) {
177
+ // For ribose / deoxyribose use the per-base canonical color rather than a flat gray
178
+ if ((canonical === 'r' || canonical === 'd') && base && BASE_COLORS[base])
179
+ return {color: BASE_COLORS[base], meta: known};
180
+ return {color: known.color, meta: known};
181
+ }
182
+ const c = hashColor(canonical);
183
+ return {color: c, meta: {name: canonical, short: canonical, color: c, category: 'sugar'}};
184
+ }
185
+
186
+ export function resolvePhosphate(phosphate: string): { color: string; meta: ModMeta } {
187
+ if (!phosphate)
188
+ return {color: '#888', meta: {name: '(none)', short: '', color: '#888', category: 'phosphate'}};
189
+ const canonical = canonicalPhosphateSymbol(phosphate);
190
+ const known = PHOSPHATE_MODS[canonical];
191
+ if (known)
192
+ return {color: known.color, meta: known};
193
+ const c = hashColor(canonical);
194
+ return {color: c, meta: {name: canonical, short: canonical, color: c, category: 'phosphate'}};
195
+ }
196
+
197
+ export function resolveConjugate(symbol: string): { color: string; meta: ModMeta } {
198
+ const known = CONJUGATE_MODS[symbol];
199
+ if (known)
200
+ return {color: known.color, meta: known};
201
+ return {
202
+ color: hashColor(symbol),
203
+ meta: {name: symbol, short: symbol.length > 6 ? symbol.slice(0, 6) : symbol, color: hashColor(symbol), category: 'conjugate'},
204
+ };
205
+ }
206
+
207
+ /** Pick black or white text for readability on a given background hex/rgb/hsl color. */
208
+ export function contrastTextColor(bg: string): string {
209
+ // Resolve any CSS color string to RGB through a temp DOM element
210
+ const probe = document.createElement('span');
211
+ probe.style.color = bg;
212
+ document.body.appendChild(probe);
213
+ const computed = getComputedStyle(probe).color;
214
+ document.body.removeChild(probe);
215
+ const m = computed.match(/\d+/g);
216
+ if (!m) return '#000';
217
+ const [r, g, b] = m.slice(0, 3).map(Number);
218
+ // perceptual luminance — black on light, white on dark
219
+ const lum = 0.299 * r + 0.587 * g + 0.114 * b;
220
+ return lum > 160 ? '#1a1a1a' : '#ffffff';
221
+ }
@@ -102,6 +102,13 @@ export namespace funcs {
102
102
  return await grok.functions.call('SequenceTranslator:PolyToolEnumerateChemTopMenu', {});
103
103
  }
104
104
 
105
+ /**
106
+ Enumerate cores and R-group lists into a molecule table (Zip or Cartesian)
107
+ */
108
+ export async function chemEnumerateReactionsTopMenu(): Promise<void> {
109
+ return await grok.functions.call('SequenceTranslator:ChemEnumerateReactionsTopMenu', {});
110
+ }
111
+
105
112
  export async function polyToolColumnChoice(df: DG.DataFrame , macroMolecule: DG.Column ): Promise<void> {
106
113
  return await grok.functions.call('SequenceTranslator:PolyToolColumnChoice', { df, macroMolecule });
107
114
  }
@@ -114,7 +121,7 @@ export namespace funcs {
114
121
  return await grok.functions.call('SequenceTranslator:PtEnumeratorHelmApp', {});
115
122
  }
116
123
 
117
- export async function ptEnumeratorChemApp(): Promise<void> {
124
+ export async function ptEnumeratorChemApp(): Promise<DG.View> {
118
125
  return await grok.functions.call('SequenceTranslator:PtEnumeratorChemApp', {});
119
126
  }
120
127
 
@@ -144,6 +151,41 @@ export namespace funcs {
144
151
  return await grok.functions.call('SequenceTranslator:GetPolyToolCombineDialog', {});
145
152
  }
146
153
 
154
+ /**
155
+ Renders OligoNucleotide (siRNA / ASO) duplex view in grid cells
156
+ */
157
+ export async function oligoNucleotideCellRenderer(): Promise<any> {
158
+ return await grok.functions.call('SequenceTranslator:OligoNucleotideCellRenderer', {});
159
+ }
160
+
161
+ /**
162
+ Modifications, lengths, conjugates and color legend for an OligoNucleotide cell
163
+ */
164
+ export async function oligoNucleotidePanel(value: any ): Promise<any> {
165
+ return await grok.functions.call('SequenceTranslator:OligoNucleotidePanel', { value });
166
+ }
167
+
168
+ /**
169
+ Sense and antisense full molecular structures rendered separately
170
+ */
171
+ export async function oligoNucleotideStructuresPanel(value: any ): Promise<any> {
172
+ return await grok.functions.call('SequenceTranslator:OligoNucleotideStructuresPanel', { value });
173
+ }
174
+
175
+ /**
176
+ Create a new column tagged as OligoNucleotide so HELM duplex cells render with the oligo view
177
+ */
178
+ export async function convertHelmToOligoNucleotide(table: DG.DataFrame , helmCol: DG.Column ): Promise<DG.Column> {
179
+ return await grok.functions.call('SequenceTranslator:ConvertHelmToOligoNucleotide', { table, helmCol });
180
+ }
181
+
182
+ /**
183
+ Combine separate sense + antisense HELM columns into one OligoNucleotide column
184
+ */
185
+ export async function combineSenseAntisenseToOligoNucleotide(table: DG.DataFrame , senseCol: DG.Column , antiCol: DG.Column ): Promise<DG.Column> {
186
+ return await grok.functions.call('SequenceTranslator:CombineSenseAntisenseToOligoNucleotide', { table, senseCol, antiCol });
187
+ }
188
+
147
189
  export async function applyNotationProviderForCyclized(col: DG.Column , separator: string ): Promise<void> {
148
190
  return await grok.functions.call('SequenceTranslator:ApplyNotationProviderForCyclized', { col, separator });
149
191
  }
@@ -13,9 +13,11 @@ import './tests/polytool-convert-tests';
13
13
  import './tests/polytool-unrule-tests';
14
14
  import './tests/polytool-enumerate-tests';
15
15
  import './tests/polytool-enumerate-breadth-tests';
16
+ import './tests/polytool-enumerate-chem-tests';
16
17
  import './tests/polytool-chain-parse-notation-tests';
17
18
  import './tests/polytool-chain-from-notation-tests';
18
19
  import './tests/toAtomicLevel-tests';
20
+ import './tests/oligo-renderer-tests';
19
21
 
20
22
  import {OligoToolkitTestPackage} from './tests/utils';
21
23
 
package/src/package.g.ts CHANGED
@@ -148,6 +148,13 @@ export async function polyToolEnumerateChemTopMenu() : Promise<void> {
148
148
  await PackageFunctions.polyToolEnumerateChemTopMenu();
149
149
  }
150
150
 
151
+ //name: chemEnumerateReactions
152
+ //description: Enumerate cores and R-group lists into a molecule table (Zip or Cartesian)
153
+ //top-menu: Chem | Transform | Reactions | Enumerate...
154
+ export async function chemEnumerateReactionsTopMenu() : Promise<void> {
155
+ await PackageFunctions.chemEnumerateReactionsTopMenu();
156
+ }
157
+
151
158
  //input: dataframe df { description: Input data table }
152
159
  //input: column macroMolecule
153
160
  export async function polyToolColumnChoice(df: DG.DataFrame, macroMolecule: DG.Column) : Promise<void> {
@@ -170,11 +177,12 @@ export async function ptEnumeratorHelmApp() : Promise<void> {
170
177
 
171
178
  //name: Chem Enumerator
172
179
  //tags: app
180
+ //output: view result
173
181
  //meta.icon: img/icons/structure.png
174
- //meta.browsePath: Peptides | PolyTool
182
+ //meta.browsePath: Chem | PolyTool
175
183
  //meta.role: app
176
- export async function ptEnumeratorChemApp() : Promise<void> {
177
- await PackageFunctions.ptEnumeratorChemApp();
184
+ export async function ptEnumeratorChemApp() {
185
+ return await PackageFunctions.ptEnumeratorChemApp();
178
186
  }
179
187
 
180
188
  //name: Polytool Helm Enumerator dialog
@@ -215,6 +223,51 @@ export async function getPolyToolCombineDialog() : Promise<void> {
215
223
  await PackageFunctions.getPolyToolCombineDialog();
216
224
  }
217
225
 
226
+ //description: Renders OligoNucleotide (siRNA / ASO) duplex view in grid cells
227
+ //tags: cellRenderer
228
+ //output: grid_cell_renderer result
229
+ //meta.cellType: OligoNucleotide
230
+ //meta.columnTags: quality=OligoNucleotide
231
+ //meta.role: cellRenderer
232
+ export function oligoNucleotideCellRenderer() : any {
233
+ return PackageFunctions.oligoNucleotideCellRenderer();
234
+ }
235
+
236
+ //name: Oligo-Nucleotide
237
+ //description: Modifications, lengths, conjugates and color legend for an OligoNucleotide cell
238
+ //tags: panel, widgets
239
+ //input: semantic_value value { semType: OligoNucleotide }
240
+ //output: widget result
241
+ export function oligoNucleotidePanel(value: DG.SemanticValue) : any {
242
+ return PackageFunctions.oligoNucleotidePanel(value);
243
+ }
244
+
245
+ //name: Oligo Structures
246
+ //description: Sense and antisense full molecular structures rendered separately
247
+ //tags: panel, widgets
248
+ //input: semantic_value value { semType: OligoNucleotide }
249
+ //output: widget result
250
+ export function oligoNucleotideStructuresPanel(value: DG.SemanticValue) : any {
251
+ return PackageFunctions.oligoNucleotideStructuresPanel(value);
252
+ }
253
+
254
+ //description: Create a new column tagged as OligoNucleotide so HELM duplex cells render with the oligo view
255
+ //input: dataframe table
256
+ //input: column helmCol { caption: HELM column; semType: Macromolecule }
257
+ //output: column result
258
+ export async function convertHelmToOligoNucleotide(table: DG.DataFrame, helmCol: DG.Column) : Promise<any> {
259
+ return await PackageFunctions.convertHelmToOligoNucleotide(table, helmCol);
260
+ }
261
+
262
+ //description: Combine separate sense + antisense HELM columns into one OligoNucleotide column
263
+ //input: dataframe table
264
+ //input: column senseCol { caption: Sense; semType: Macromolecule }
265
+ //input: column antiCol { caption: Antisense; semType: Macromolecule }
266
+ //output: column result
267
+ export async function combineSenseAntisenseToOligoNucleotide(table: DG.DataFrame, senseCol: DG.Column, antiCol: DG.Column) : Promise<any> {
268
+ return await PackageFunctions.combineSenseAntisenseToOligoNucleotide(table, senseCol, antiCol);
269
+ }
270
+
218
271
  //name: applyNotationProviderForHarmonizedSequence
219
272
  //input: column col
220
273
  //input: string separator