@datagrok/helm 2.13.4 → 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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@datagrok/helm",
3
3
  "friendlyName": "Helm",
4
- "version": "2.13.4",
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.5",
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",
@@ -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);