@datagrok/helm 2.13.3 → 2.13.5
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 +13 -0
- package/CLAUDE.md +0 -13
- 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 +2 -2
- package/src/package.g.ts +5 -0
- package/src/package.ts +5 -1
- package/src/utils/helm-grid-cell-renderer.ts +108 -0
- package/test-console-output-1.log +105 -96
- package/test-record-1.mp4 +0 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@datagrok/helm",
|
|
3
3
|
"friendlyName": "Helm",
|
|
4
|
-
"version": "2.13.
|
|
4
|
+
"version": "2.13.5",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Maria Dolotova",
|
|
7
7
|
"email": "mdolotova@datagrok.ai"
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"css/helm.css"
|
|
17
17
|
],
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@datagrok-libraries/bio": "^5.63.
|
|
19
|
+
"@datagrok-libraries/bio": "^5.63.7",
|
|
20
20
|
"@datagrok-libraries/chem-meta": "^1.2.9",
|
|
21
21
|
"@datagrok-libraries/helm-web-editor": "^1.1.16",
|
|
22
22
|
"@datagrok-libraries/utils": "^4.6.9",
|
package/src/package.g.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {PackageFunctions} from './package';
|
|
2
2
|
import * as DG from 'datagrok-api/dg';
|
|
3
3
|
|
|
4
|
+
//tags: init
|
|
4
5
|
//meta.role: init
|
|
5
6
|
export async function initHelm() : Promise<void> {
|
|
6
7
|
await PackageFunctions.initHelm();
|
|
@@ -12,6 +13,7 @@ export async function getHelmService() : Promise<any> {
|
|
|
12
13
|
return await PackageFunctions.getHelmService();
|
|
13
14
|
}
|
|
14
15
|
|
|
16
|
+
//tags: cellRenderer
|
|
15
17
|
//output: grid_cell_renderer result
|
|
16
18
|
//meta.columnTags: quality=Macromolecule, units=helm
|
|
17
19
|
//meta.cellType: helm
|
|
@@ -21,6 +23,7 @@ export function helmCellRenderer() : any {
|
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
//description: Macromolecule
|
|
26
|
+
//tags: cellEditor
|
|
24
27
|
//input: grid_cell cell
|
|
25
28
|
//meta.columnTags: quality=Macromolecule, units=helm
|
|
26
29
|
//meta.role: cellEditor
|
|
@@ -37,6 +40,7 @@ export function openEditor(mol: DG.SemanticValue) : void {
|
|
|
37
40
|
}
|
|
38
41
|
|
|
39
42
|
//name: Properties
|
|
43
|
+
//tags: panel, widgets, bio
|
|
40
44
|
//input: semantic_value sequence { semType: Macromolecule }
|
|
41
45
|
//output: widget result
|
|
42
46
|
//meta.role: widgets,panel
|
|
@@ -51,6 +55,7 @@ export function getMolfiles(col: DG.Column<any>) : any {
|
|
|
51
55
|
return PackageFunctions.getMolfiles(col);
|
|
52
56
|
}
|
|
53
57
|
|
|
58
|
+
//tags: valueEditor
|
|
54
59
|
//input: string name { optional: true }
|
|
55
60
|
//input: object options { optional: true }
|
|
56
61
|
//output: object result
|
package/src/package.ts
CHANGED
|
@@ -125,7 +125,7 @@ function checkMonomersAndOpenWebEditor(cell: DG.GridCell, value?: string, units?
|
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
export class PackageFunctions {
|
|
128
|
-
@grok.decorators.init()
|
|
128
|
+
@grok.decorators.init({tags: ['init']})
|
|
129
129
|
static async initHelm(): Promise<void> {
|
|
130
130
|
if (initHelmPromise === null)
|
|
131
131
|
initHelmPromise = initHelmInt();
|
|
@@ -147,6 +147,7 @@ export class PackageFunctions {
|
|
|
147
147
|
'cellType': 'helm',
|
|
148
148
|
'role': 'cellRenderer'
|
|
149
149
|
},
|
|
150
|
+
'tags': ['cellRenderer'],
|
|
150
151
|
'outputs': [{name: 'result', type: 'grid_cell_renderer'}]
|
|
151
152
|
})
|
|
152
153
|
static helmCellRenderer(): DG.GridCellRenderer {
|
|
@@ -162,6 +163,7 @@ export class PackageFunctions {
|
|
|
162
163
|
'columnTags': 'quality=Macromolecule, units=helm',
|
|
163
164
|
'role': 'cellEditor'
|
|
164
165
|
},
|
|
166
|
+
'tags': ['cellEditor'],
|
|
165
167
|
'description': 'Macromolecule'
|
|
166
168
|
})
|
|
167
169
|
static editMoleculeCell(
|
|
@@ -197,6 +199,7 @@ export class PackageFunctions {
|
|
|
197
199
|
@grok.decorators.panel({
|
|
198
200
|
'name': 'Properties',
|
|
199
201
|
'meta': {role: 'widgets', domain: 'bio'},
|
|
202
|
+
'tags': ['panel', 'widgets', 'bio']
|
|
200
203
|
})
|
|
201
204
|
static propertiesWidget(
|
|
202
205
|
@grok.decorators.param({'options': {'semType': 'Macromolecule'}}) sequence: DG.SemanticValue): DG.Widget {
|
|
@@ -219,6 +222,7 @@ export class PackageFunctions {
|
|
|
219
222
|
'semType': 'Macromolecule',
|
|
220
223
|
'role': 'valueEditor'
|
|
221
224
|
},
|
|
225
|
+
'tags': ['valueEditor'],
|
|
222
226
|
'outputs': [{'type': 'object', 'name': 'result'}]
|
|
223
227
|
})
|
|
224
228
|
static helmInput(
|
|
@@ -17,6 +17,11 @@ import {IMonomerLibBase} from '@datagrok-libraries/bio/src/types/monomer-library
|
|
|
17
17
|
import {execMonomerHoverLinks} from '@datagrok-libraries/bio/src/monomer-works/monomer-hover';
|
|
18
18
|
import {MmcrTemps} from '@datagrok-libraries/bio/src/utils/cell-renderer-consts';
|
|
19
19
|
import {getHelmHelper, IHelmHelper} from '@datagrok-libraries/bio/src/helm/helm-helper';
|
|
20
|
+
import {
|
|
21
|
+
DEFAULT_MACROMOLECULE_HIGHLIGHT_FILL, DEFAULT_MACROMOLECULE_HIGHLIGHT_STROKE,
|
|
22
|
+
MACROMOLECULE_HIGHLIGHT_EVENT_ID, MACROMOLECULE_HIGHLIGHT_TEMP,
|
|
23
|
+
MacromoleculeHighlightEntry, MacromoleculeHighlightEventArgs, macromoleculeHighlightColorToCss,
|
|
24
|
+
} from '@datagrok-libraries/bio/src/utils/macromolecule-highlight';
|
|
20
25
|
|
|
21
26
|
import {getHoveredMonomerFromEditorMol, getSeqMonomerFromHelmAtom} from './get-hovered';
|
|
22
27
|
|
|
@@ -58,10 +63,41 @@ export class HelmGridCellRendererBack extends CellRendererBackAsyncBase<HelmProp
|
|
|
58
63
|
this.invalidateGrid();
|
|
59
64
|
}));
|
|
60
65
|
|
|
66
|
+
this.subs.push(grok.events.onCustomEvent(MACROMOLECULE_HIGHLIGHT_EVENT_ID)
|
|
67
|
+
.subscribe((args: MacromoleculeHighlightEventArgs) => this.handleHighlightEvent(args)));
|
|
68
|
+
|
|
61
69
|
this.dirty = true;
|
|
62
70
|
this.invalidateGrid();
|
|
63
71
|
}
|
|
64
72
|
|
|
73
|
+
private handleHighlightEvent(args: MacromoleculeHighlightEventArgs): void {
|
|
74
|
+
if (!args || !this.tableCol || !this.tableCol.dataFrame) return;
|
|
75
|
+
if (args.columnName !== this.tableCol.name) return;
|
|
76
|
+
const df = this.tableCol.dataFrame;
|
|
77
|
+
if (args.tableId && df.id !== args.tableId) return;
|
|
78
|
+
if (args.tableName && df.name !== args.tableName) return;
|
|
79
|
+
|
|
80
|
+
const map = (this.tableCol.temp[MACROMOLECULE_HIGHLIGHT_TEMP] ??=
|
|
81
|
+
new Map<number, MacromoleculeHighlightEntry>()) as Map<number, MacromoleculeHighlightEntry>;
|
|
82
|
+
if (args.monomers == null || args.monomers.length === 0)
|
|
83
|
+
map.delete(args.rowIdx);
|
|
84
|
+
else {
|
|
85
|
+
map.set(args.rowIdx, {
|
|
86
|
+
monomers: args.monomers.slice(),
|
|
87
|
+
fillColor: args.fillColor,
|
|
88
|
+
strokeColor: args.strokeColor,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
this.invalidateGrid();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private getHighlightForRow(rowIdx: number | null): MacromoleculeHighlightEntry | null {
|
|
95
|
+
if (rowIdx == null) return null;
|
|
96
|
+
const map = this.tableCol.temp[MACROMOLECULE_HIGHLIGHT_TEMP] as
|
|
97
|
+
Map<number, MacromoleculeHighlightEntry> | undefined;
|
|
98
|
+
return map?.get(rowIdx) ?? null;
|
|
99
|
+
}
|
|
100
|
+
|
|
65
101
|
protected getMonomerLib(): IMonomerLibBase {
|
|
66
102
|
return this.tableCol.temp[MmcrTemps.overriddenLibrary] ?? this.sysMonomerLib;
|
|
67
103
|
}
|
|
@@ -153,12 +189,84 @@ export class HelmGridCellRendererBack extends CellRendererBackAsyncBase<HelmProp
|
|
|
153
189
|
aux.dBox.x, aux.dBox.y, aux.dBox.width, aux.dBox.height,
|
|
154
190
|
cellDBox.x, cellDBox.y, cellDBox.width, cellDBox.height);
|
|
155
191
|
|
|
192
|
+
this.drawHighlightOverlay(fitCtx, aux, cellDBox, gridCell.tableRowIndex);
|
|
193
|
+
|
|
156
194
|
const fitCanvasData = fitCtx.getImageData(0, 0, fitCanvasWidth, fitCanvasHeight);
|
|
157
195
|
this.renderOnGrid(gridCtx, gridCellBounds, gridCell, fitCanvasData);
|
|
158
196
|
|
|
159
197
|
return aux.cBox.width != cellWidth || aux.cBox.height != cellHeight; // request rendering
|
|
160
198
|
}
|
|
161
199
|
|
|
200
|
+
// Overlays a translucent ring around each highlighted monomer on top of the already-drawn
|
|
201
|
+
// helm image. Highlight data comes from `tableCol.temp[MACROMOLECULE_HIGHLIGHT_TEMP]` for the given
|
|
202
|
+
// row. The ring is sized from the median bond length so it sits just outside the monomer glyph.
|
|
203
|
+
private drawHighlightOverlay(
|
|
204
|
+
ctx: CanvasRenderingContext2D, aux: HelmAux, cellDBox: DG.Rect, rowIdx: number | null,
|
|
205
|
+
): void {
|
|
206
|
+
const entry = this.getHighlightForRow(rowIdx);
|
|
207
|
+
if (!entry || !entry.monomers || entry.monomers.length === 0) return;
|
|
208
|
+
const atoms = aux.mol?.atoms;
|
|
209
|
+
if (!atoms || atoms.length === 0) return;
|
|
210
|
+
if (aux.bBox.width <= 0 || aux.bBox.height <= 0) return;
|
|
211
|
+
|
|
212
|
+
const sx = cellDBox.width / aux.bBox.width;
|
|
213
|
+
const sy = cellDBox.height / aux.bBox.height;
|
|
214
|
+
const sAvg = (sx + sy) / 2;
|
|
215
|
+
|
|
216
|
+
// Estimate monomer glyph radius in SVG space from bonds, fall back to atom-density heuristic.
|
|
217
|
+
const svgMonomerR = this.estimateMonomerRadiusSvg(aux);
|
|
218
|
+
// Draw ring just outside the glyph: ~1.05× glyph radius.
|
|
219
|
+
const markerR = Math.max(6, svgMonomerR * sAvg * 0.7);
|
|
220
|
+
const lineWidth = Math.max(1.5, markerR * 0.35);
|
|
221
|
+
|
|
222
|
+
const fillCss = macromoleculeHighlightColorToCss(entry.fillColor ?? DEFAULT_MACROMOLECULE_HIGHLIGHT_FILL, 0.2);
|
|
223
|
+
const strokeCss = macromoleculeHighlightColorToCss(entry.strokeColor ?? DEFAULT_MACROMOLECULE_HIGHLIGHT_STROKE);
|
|
224
|
+
|
|
225
|
+
ctx.save();
|
|
226
|
+
try {
|
|
227
|
+
ctx.lineWidth = lineWidth;
|
|
228
|
+
ctx.strokeStyle = strokeCss;
|
|
229
|
+
ctx.fillStyle = fillCss;
|
|
230
|
+
for (const idx of entry.monomers) {
|
|
231
|
+
const a = atoms[idx];
|
|
232
|
+
if (!a || !a.p) continue;
|
|
233
|
+
const fx = cellDBox.x + (a.p.x - aux.bBox.x) * sx;
|
|
234
|
+
const fy = cellDBox.y + (a.p.y - aux.bBox.y) * sy;
|
|
235
|
+
// Stroke ring with translucent fill — ring sits outside monomer, interior tint is subtle.
|
|
236
|
+
ctx.beginPath();
|
|
237
|
+
ctx.arc(fx, fy, markerR, 0, Math.PI * 2);
|
|
238
|
+
ctx.fill();
|
|
239
|
+
ctx.stroke();
|
|
240
|
+
}
|
|
241
|
+
} finally {
|
|
242
|
+
ctx.restore();
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Best-effort monomer glyph radius in SVG (bBox) coordinates. Uses the median bond length when
|
|
247
|
+
// bonds are available (monomer glyph width ≈ bond length), otherwise falls back to an area-
|
|
248
|
+
// based density estimate from bBox / atom count.
|
|
249
|
+
private estimateMonomerRadiusSvg(aux: HelmAux): number {
|
|
250
|
+
const bonds = (aux.mol as any)?.bonds as Array<{a1?: {p?: {x: number, y: number}}, a2?: {p?: {x: number, y: number}}}> | undefined;
|
|
251
|
+
const lens: number[] = [];
|
|
252
|
+
if (bonds && bonds.length > 0) {
|
|
253
|
+
for (const b of bonds) {
|
|
254
|
+
const p1 = b?.a1?.p; const p2 = b?.a2?.p;
|
|
255
|
+
if (!p1 || !p2) continue;
|
|
256
|
+
const d = Math.hypot(p1.x - p2.x, p1.y - p2.y);
|
|
257
|
+
if (d > 0) lens.push(d);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (lens.length > 0) {
|
|
261
|
+
lens.sort((x, y) => x - y);
|
|
262
|
+
const median = lens[Math.floor(lens.length / 2)];
|
|
263
|
+
return median * 0.55; // glyph radius ≈ half the bond length
|
|
264
|
+
}
|
|
265
|
+
const n = Math.max(1, aux.mol?.atoms?.length ?? 1);
|
|
266
|
+
const density = Math.sqrt((aux.bBox.width * aux.bBox.height) / n);
|
|
267
|
+
return density * 0.45;
|
|
268
|
+
}
|
|
269
|
+
|
|
162
270
|
onMouseMove(gridCell: DG.GridCell, e: MouseEvent): void {
|
|
163
271
|
if (!gridCell.cell?.value || !this._auxList || !!e.buttons) return;
|
|
164
272
|
const aux = this._auxList.get(gridCell.cell.value);
|