@cornerstonejs/core 4.17.5 → 4.18.1

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,99 @@
1
+ import type { mat4 } from 'gl-matrix';
2
+ import type { Point3, Point2, ICamera, ECGViewportInput, ECGWaveformData, ECGViewportProperties, ViewReferenceSpecifier } from '../types';
3
+ import { Transform } from './helpers/cpuFallback/rendering/transform';
4
+ import Viewport from './Viewport';
5
+ declare class ECGViewport extends Viewport {
6
+ readonly uid: string;
7
+ readonly renderingEngineId: string;
8
+ readonly canvasContext: CanvasRenderingContext2D;
9
+ private imageId;
10
+ private channels;
11
+ private waveformData;
12
+ private ecgWidth;
13
+ private ecgHeight;
14
+ private channelScale;
15
+ private ecgCamera;
16
+ constructor(props: ECGViewportInput);
17
+ static get useCustomRenderingPipeline(): boolean;
18
+ private addEventListeners;
19
+ private removeEventListeners;
20
+ private elementDisabledHandler;
21
+ setEcg(imageId: string): Promise<void>;
22
+ setChannelVisibility(index: number, visible: boolean): void;
23
+ getVisibleChannels(): {
24
+ name: string;
25
+ visible: boolean;
26
+ }[];
27
+ getWaveformData(): ECGWaveformData | null;
28
+ getContentDimensions(): {
29
+ width: number;
30
+ height: number;
31
+ };
32
+ private computeChannelScale;
33
+ private recalculateHeight;
34
+ private computeChannelLayouts;
35
+ setProperties(props: ECGViewportProperties): void;
36
+ getProperties: () => ECGViewportProperties;
37
+ resetProperties(): void;
38
+ setCamera(camera: ICamera): void;
39
+ getCamera(): ICamera;
40
+ resetCamera: () => boolean;
41
+ getFrameOfReferenceUID: () => string;
42
+ resize: () => void;
43
+ canvasToWorld: (canvasPos: Point2, destPos?: Point3) => Point3;
44
+ worldToCanvas: (worldPos: Point3) => Point2;
45
+ getPan(): Point2;
46
+ getRotation: () => number;
47
+ getNumberOfSlices: () => number;
48
+ getCurrentImageIdIndex: () => number;
49
+ getCurrentImageId: () => string | undefined;
50
+ getViewReferenceId(_specifier?: ViewReferenceSpecifier): string;
51
+ hasImageURI(imageURI: string): boolean;
52
+ isReferenceViewable(viewRef: {
53
+ FrameOfReferenceUID?: string;
54
+ }): boolean;
55
+ getSliceIndex: () => number;
56
+ getImageIds: () => string[];
57
+ scroll: () => void;
58
+ customRenderViewportToCanvas: () => void;
59
+ updateCameraClippingPlanesAndRange(): void;
60
+ private refreshRenderValues;
61
+ private getWorldToCanvasRatio;
62
+ protected getTransform(): Transform;
63
+ private renderFrame;
64
+ private drawGrid;
65
+ private drawTraces;
66
+ private drawLabels;
67
+ getImageData(): {
68
+ dimensions: number[];
69
+ spacing: number[];
70
+ origin: number[];
71
+ direction: number[];
72
+ imageData: {
73
+ getDirection: () => number[];
74
+ getDimensions: () => number[];
75
+ getRange: () => Point2;
76
+ getSpacing: () => number[];
77
+ worldToIndex: (point: Point3) => Point3;
78
+ indexToWorld: (point: Point3) => Point3;
79
+ };
80
+ hasPixelSpacing: boolean;
81
+ calibration: import("../types").IImageCalibration;
82
+ preScale: {
83
+ scaled: boolean;
84
+ };
85
+ metadata: {
86
+ Modality: string;
87
+ };
88
+ };
89
+ getSliceViewInfo(): {
90
+ width: number;
91
+ height: number;
92
+ sliceIndex: number;
93
+ slicePlane: number;
94
+ sliceToIndexMatrix: mat4;
95
+ indexToSliceMatrix: mat4;
96
+ };
97
+ getMiddleSliceData: () => never;
98
+ }
99
+ export default ECGViewport;
@@ -0,0 +1,548 @@
1
+ import { Events as EVENTS, MetadataModules } from '../enums';
2
+ import { Transform } from './helpers/cpuFallback/rendering/transform';
3
+ import triggerEvent from '../utilities/triggerEvent';
4
+ import Viewport from './Viewport';
5
+ import { getOrCreateCanvas } from './helpers';
6
+ import * as metaData from '../metaData';
7
+ const SECONDS_WIDTH = 150;
8
+ const CHANNEL_SPACING = 5;
9
+ const ECG_AMPLITUDE_INDEX_SIZE = 65536;
10
+ const COLOR_GRID_MAJOR = '#7f0000';
11
+ const COLOR_GRID_MINOR = '#3f0000';
12
+ const COLOR_BASELINE = '#7F4C00';
13
+ const COLOR_TRACE = '#ffffff';
14
+ const COLOR_LABEL = '#ffff00';
15
+ const COLOR_BACKGROUND = '#000000';
16
+ function computeMinMax(data) {
17
+ let min = 0;
18
+ let max = 0;
19
+ for (let i = 0; i < data.length; i++) {
20
+ if (data[i] < min) {
21
+ min = data[i];
22
+ }
23
+ if (data[i] > max) {
24
+ max = data[i];
25
+ }
26
+ }
27
+ return { min, max };
28
+ }
29
+ class ECGViewport extends Viewport {
30
+ constructor(props) {
31
+ super({
32
+ ...props,
33
+ canvas: props.canvas || getOrCreateCanvas(props.element),
34
+ });
35
+ this.imageId = null;
36
+ this.channels = [];
37
+ this.waveformData = null;
38
+ this.ecgWidth = 0;
39
+ this.ecgHeight = 0;
40
+ this.channelScale = 0;
41
+ this.ecgCamera = {
42
+ panWorld: [0, 0],
43
+ parallelScale: 1,
44
+ };
45
+ this.getProperties = () => {
46
+ return {
47
+ visibleChannels: this.channels
48
+ .map((ch, i) => (ch.visible ? i : -1))
49
+ .filter((i) => i >= 0),
50
+ };
51
+ };
52
+ this.resetCamera = () => {
53
+ this.refreshRenderValues();
54
+ this.canvasContext.fillRect(0, 0, this.canvas.width, this.canvas.height);
55
+ this.renderFrame();
56
+ return true;
57
+ };
58
+ this.getFrameOfReferenceUID = () => {
59
+ return `ecg-viewport-${this.id}`;
60
+ };
61
+ this.resize = () => {
62
+ const canvas = this.canvas;
63
+ const { clientWidth, clientHeight } = canvas;
64
+ if (canvas.width !== clientWidth || canvas.height !== clientHeight) {
65
+ canvas.width = clientWidth;
66
+ canvas.height = clientHeight;
67
+ }
68
+ if (this.waveformData) {
69
+ this.computeChannelScale();
70
+ this.recalculateHeight();
71
+ }
72
+ this.refreshRenderValues();
73
+ this.renderFrame();
74
+ };
75
+ this.canvasToWorld = (canvasPos, destPos = [0, 0, 0]) => {
76
+ if (!this.waveformData) {
77
+ destPos[0] = 0;
78
+ destPos[1] = 0;
79
+ destPos[2] = 0;
80
+ return destPos;
81
+ }
82
+ const scale = this.getWorldToCanvasRatio();
83
+ const pan = this.ecgCamera.panWorld;
84
+ const layouts = this.computeChannelLayouts();
85
+ const subCanvasPos = [
86
+ canvasPos[0] / scale - pan[0],
87
+ canvasPos[1] / scale - pan[1],
88
+ ];
89
+ let z = 0;
90
+ for (let i = 0; i < layouts.length; i++) {
91
+ const layout = layouts[i];
92
+ if (subCanvasPos[1] <= layout.yOffset) {
93
+ z = i;
94
+ break;
95
+ }
96
+ if (i === layouts.length - 1) {
97
+ z = i;
98
+ }
99
+ }
100
+ const x = Math.max(0, Math.min(this.waveformData.numberOfSamples - 1, (subCanvasPos[0] * this.waveformData.numberOfSamples) / this.ecgWidth));
101
+ const layout = layouts[z];
102
+ const y = (layout.baseline - subCanvasPos[1]) / this.channelScale;
103
+ destPos[0] = x;
104
+ destPos[1] = y;
105
+ destPos[2] = z;
106
+ return destPos;
107
+ };
108
+ this.worldToCanvas = (worldPos) => {
109
+ if (!this.waveformData) {
110
+ return [0, 0];
111
+ }
112
+ const scale = this.getWorldToCanvasRatio();
113
+ const pan = this.ecgCamera.panWorld;
114
+ const layouts = this.computeChannelLayouts();
115
+ const z = Math.round(worldPos[2]);
116
+ if (z < 0 || z >= layouts.length) {
117
+ return [0, 0];
118
+ }
119
+ const layout = layouts[z];
120
+ const canvasX = (worldPos[0] / this.waveformData.numberOfSamples) *
121
+ this.ecgWidth *
122
+ scale +
123
+ pan[0] * scale;
124
+ const canvasY = (layout.baseline - worldPos[1] * this.channelScale) * scale +
125
+ pan[1] * scale;
126
+ return [canvasX, canvasY];
127
+ };
128
+ this.getRotation = () => 0;
129
+ this.getNumberOfSlices = () => {
130
+ return 1;
131
+ };
132
+ this.getCurrentImageIdIndex = () => {
133
+ return 0;
134
+ };
135
+ this.getCurrentImageId = () => {
136
+ return this.imageId;
137
+ };
138
+ this.getSliceIndex = () => {
139
+ return 0;
140
+ };
141
+ this.getImageIds = () => {
142
+ return this.imageId ? [this.imageId] : [];
143
+ };
144
+ this.scroll = () => {
145
+ };
146
+ this.customRenderViewportToCanvas = () => {
147
+ this.renderFrame();
148
+ };
149
+ this.renderFrame = () => {
150
+ if (!this.waveformData) {
151
+ return;
152
+ }
153
+ const dpr = window.devicePixelRatio || 1;
154
+ const transform = this.getTransform();
155
+ const m = transform.getMatrix();
156
+ const ctx = this.canvasContext;
157
+ ctx.resetTransform();
158
+ ctx.fillStyle = COLOR_BACKGROUND;
159
+ ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
160
+ ctx.setTransform(m[0] / dpr, m[1] / dpr, m[2] / dpr, m[3] / dpr, m[4] / dpr, m[5] / dpr);
161
+ const layouts = this.computeChannelLayouts();
162
+ this.drawGrid(ctx);
163
+ this.drawTraces(ctx, layouts);
164
+ this.drawLabels(ctx, layouts);
165
+ ctx.resetTransform();
166
+ triggerEvent(this.element, EVENTS.IMAGE_RENDERED, {
167
+ element: this.element,
168
+ viewportId: this.id,
169
+ viewport: this,
170
+ renderingEngineId: this.renderingEngineId,
171
+ });
172
+ };
173
+ this.getMiddleSliceData = () => {
174
+ throw new Error('Method not implemented for ECG viewport.');
175
+ };
176
+ this.canvasContext = this.canvas.getContext('2d');
177
+ this.renderingEngineId = props.renderingEngineId;
178
+ this.element.setAttribute('data-viewport-uid', this.id);
179
+ this.element.setAttribute('data-rendering-engine-uid', this.renderingEngineId);
180
+ this.addEventListeners();
181
+ this.resize();
182
+ }
183
+ static get useCustomRenderingPipeline() {
184
+ return true;
185
+ }
186
+ addEventListeners() {
187
+ this.canvas.addEventListener(EVENTS.ELEMENT_DISABLED, this.elementDisabledHandler);
188
+ }
189
+ removeEventListeners() {
190
+ this.canvas.removeEventListener(EVENTS.ELEMENT_DISABLED, this.elementDisabledHandler);
191
+ }
192
+ elementDisabledHandler() {
193
+ this.removeEventListeners();
194
+ }
195
+ async setEcg(imageId) {
196
+ this.imageId = imageId;
197
+ const ecgModule = metaData.get(MetadataModules.ECG, imageId);
198
+ if (!ecgModule?.waveformData?.retrieveBulkData) {
199
+ throw new Error(`[ECGViewport] No ECG waveform data for imageId: ${imageId}`);
200
+ }
201
+ const { numberOfWaveformChannels: numberOfChannels, numberOfWaveformSamples: numberOfSamples, samplingFrequency, waveformBitsAllocated: bitsAllocated = 16, waveformSampleInterpretation: sampleInterpretation = 'SS', multiplexGroupLabel, channelDefinitionSequence: channelDefinitions = [], } = ecgModule;
202
+ const channelArrays = await ecgModule.waveformData.retrieveBulkData();
203
+ this.channels = [];
204
+ for (let i = 0; i < numberOfChannels; i++) {
205
+ const channelDef = channelDefinitions[i] || {};
206
+ const name = channelDef.channelSourceSequence?.codeMeaning ||
207
+ channelDef.ChannelSourceSequence?.CodeMeaning ||
208
+ `Channel ${i + 1}`;
209
+ const data = channelArrays[i] || new Int16Array(0);
210
+ const { min, max } = computeMinMax(data);
211
+ this.channels.push({ name, data, visible: true, min, max });
212
+ }
213
+ this.waveformData = {
214
+ channels: this.channels,
215
+ numberOfChannels,
216
+ numberOfSamples,
217
+ samplingFrequency,
218
+ bitsAllocated,
219
+ sampleInterpretation,
220
+ multiplexGroupLabel,
221
+ };
222
+ this.calibration = metaData.get(MetadataModules.CALIBRATION, imageId);
223
+ this.ecgWidth = Math.ceil((numberOfSamples * SECONDS_WIDTH) / samplingFrequency);
224
+ this.computeChannelScale();
225
+ this.recalculateHeight();
226
+ this.refreshRenderValues();
227
+ this.renderFrame();
228
+ }
229
+ setChannelVisibility(index, visible) {
230
+ if (index >= 0 && index < this.channels.length) {
231
+ this.channels[index].visible = visible;
232
+ this.computeChannelScale();
233
+ this.recalculateHeight();
234
+ this.refreshRenderValues();
235
+ this.renderFrame();
236
+ }
237
+ }
238
+ getVisibleChannels() {
239
+ return this.channels.map((ch) => ({
240
+ name: ch.name,
241
+ visible: ch.visible,
242
+ }));
243
+ }
244
+ getWaveformData() {
245
+ return this.waveformData;
246
+ }
247
+ getContentDimensions() {
248
+ return { width: this.ecgWidth, height: this.ecgHeight };
249
+ }
250
+ computeChannelScale() {
251
+ const visibleChannels = this.channels.filter((c) => c.visible && c.data.length > 0);
252
+ if (visibleChannels.length === 0 || this.ecgWidth === 0) {
253
+ this.channelScale = 0;
254
+ return;
255
+ }
256
+ let maxRange = 1;
257
+ for (const channel of visibleChannels) {
258
+ const range = channel.max - channel.min;
259
+ maxRange = Math.max(maxRange, range);
260
+ }
261
+ const canvasAspect = this.canvas.offsetHeight && this.canvas.offsetWidth
262
+ ? this.canvas.offsetHeight / this.canvas.offsetWidth
263
+ : 2 / 3;
264
+ const targetTotalHeight = this.ecgWidth * canvasAspect;
265
+ const totalSpacing = CHANNEL_SPACING * visibleChannels.length;
266
+ const heightPerChannel = (targetTotalHeight - totalSpacing) / visibleChannels.length;
267
+ this.channelScale = heightPerChannel / (maxRange * 1.25);
268
+ }
269
+ recalculateHeight() {
270
+ const scale = this.channelScale;
271
+ let totalHeight = 0;
272
+ for (const channel of this.channels) {
273
+ if (!channel.visible || channel.data.length === 0) {
274
+ continue;
275
+ }
276
+ const itemHeight = (channel.max - channel.min) * scale * 1.25;
277
+ totalHeight += itemHeight + CHANNEL_SPACING;
278
+ }
279
+ this.ecgHeight = totalHeight;
280
+ }
281
+ computeChannelLayouts() {
282
+ const scale = this.channelScale;
283
+ const layouts = [];
284
+ let yOffset = 0;
285
+ for (const channel of this.channels) {
286
+ if (!channel.visible || channel.data.length === 0) {
287
+ continue;
288
+ }
289
+ const itemHeight = (channel.max - channel.min) * scale * 1.25;
290
+ yOffset += itemHeight + CHANNEL_SPACING;
291
+ const baseline = yOffset + channel.min * scale;
292
+ layouts.push({ channel, itemHeight, yOffset, baseline });
293
+ }
294
+ return layouts;
295
+ }
296
+ setProperties(props) {
297
+ if (props.visibleChannels !== undefined) {
298
+ for (let i = 0; i < this.channels.length; i++) {
299
+ this.channels[i].visible = props.visibleChannels.includes(i);
300
+ }
301
+ this.computeChannelScale();
302
+ this.recalculateHeight();
303
+ this.refreshRenderValues();
304
+ this.renderFrame();
305
+ }
306
+ }
307
+ resetProperties() {
308
+ for (const channel of this.channels) {
309
+ channel.visible = true;
310
+ }
311
+ this.computeChannelScale();
312
+ this.recalculateHeight();
313
+ this.refreshRenderValues();
314
+ this.renderFrame();
315
+ }
316
+ setCamera(camera) {
317
+ const { parallelScale, focalPoint } = camera;
318
+ if (parallelScale) {
319
+ this.ecgCamera.parallelScale =
320
+ this.element.clientHeight / 2 / parallelScale;
321
+ }
322
+ if (focalPoint !== undefined) {
323
+ const focalPointCanvas = this.worldToCanvas(focalPoint);
324
+ const canvasCenter = [
325
+ this.element.clientWidth / 2,
326
+ this.element.clientHeight / 2,
327
+ ];
328
+ const panWorldDelta = [
329
+ (focalPointCanvas[0] - canvasCenter[0]) / this.ecgCamera.parallelScale,
330
+ (focalPointCanvas[1] - canvasCenter[1]) / this.ecgCamera.parallelScale,
331
+ ];
332
+ this.ecgCamera.panWorld = [
333
+ this.ecgCamera.panWorld[0] - panWorldDelta[0],
334
+ this.ecgCamera.panWorld[1] - panWorldDelta[1],
335
+ ];
336
+ }
337
+ this.canvasContext.fillStyle = COLOR_BACKGROUND;
338
+ this.canvasContext.fillRect(0, 0, this.canvas.width, this.canvas.height);
339
+ this.renderFrame();
340
+ }
341
+ getCamera() {
342
+ const { parallelScale } = this.ecgCamera;
343
+ const canvasCenter = [
344
+ this.element.clientWidth / 2,
345
+ this.element.clientHeight / 2,
346
+ ];
347
+ const canvasCenterWorld = this.canvasToWorld(canvasCenter);
348
+ return {
349
+ parallelProjection: true,
350
+ focalPoint: canvasCenterWorld,
351
+ position: [0, 0, 0],
352
+ viewUp: [0, -1, 0],
353
+ parallelScale: this.element.clientHeight / 2 / parallelScale,
354
+ viewPlaneNormal: [0, 0, 1],
355
+ };
356
+ }
357
+ getPan() {
358
+ const panWorld = this.ecgCamera.panWorld;
359
+ return [panWorld[0], panWorld[1]];
360
+ }
361
+ getViewReferenceId(_specifier) {
362
+ return `imageId:${this.imageId}`;
363
+ }
364
+ hasImageURI(imageURI) {
365
+ return this.imageId?.includes(imageURI) ?? false;
366
+ }
367
+ isReferenceViewable(viewRef) {
368
+ if (viewRef.FrameOfReferenceUID &&
369
+ viewRef.FrameOfReferenceUID !== this.getFrameOfReferenceUID()) {
370
+ return false;
371
+ }
372
+ return true;
373
+ }
374
+ updateCameraClippingPlanesAndRange() {
375
+ }
376
+ refreshRenderValues() {
377
+ if (!this.ecgWidth || !this.ecgHeight) {
378
+ return;
379
+ }
380
+ let worldToCanvasRatio = this.canvas.offsetWidth / this.ecgWidth;
381
+ if (this.ecgHeight * worldToCanvasRatio > this.canvas.offsetHeight) {
382
+ worldToCanvasRatio = this.canvas.offsetHeight / this.ecgHeight;
383
+ }
384
+ const drawWidth = Math.floor(this.ecgWidth * worldToCanvasRatio);
385
+ const drawHeight = Math.floor(this.ecgHeight * worldToCanvasRatio);
386
+ const xOffsetCanvas = (this.canvas.offsetWidth - drawWidth) / 2;
387
+ const yOffsetCanvas = (this.canvas.offsetHeight - drawHeight) / 2;
388
+ const xOffsetWorld = xOffsetCanvas / worldToCanvasRatio;
389
+ const yOffsetWorld = yOffsetCanvas / worldToCanvasRatio;
390
+ this.ecgCamera.panWorld = [xOffsetWorld, yOffsetWorld];
391
+ this.ecgCamera.parallelScale = worldToCanvasRatio;
392
+ }
393
+ getWorldToCanvasRatio() {
394
+ return this.ecgCamera.parallelScale;
395
+ }
396
+ getTransform() {
397
+ const panWorld = this.ecgCamera.panWorld;
398
+ const dpr = window.devicePixelRatio || 1;
399
+ const worldToCanvasRatio = this.getWorldToCanvasRatio();
400
+ const canvasToWorldRatio = 1.0 / worldToCanvasRatio;
401
+ const halfCanvas = [
402
+ this.canvas.offsetWidth / 2,
403
+ this.canvas.offsetHeight / 2,
404
+ ];
405
+ const halfCanvasWorldCoordinates = [
406
+ halfCanvas[0] * canvasToWorldRatio,
407
+ halfCanvas[1] * canvasToWorldRatio,
408
+ ];
409
+ const transform = new Transform();
410
+ transform.scale(dpr, dpr);
411
+ transform.translate(halfCanvas[0], halfCanvas[1]);
412
+ transform.scale(worldToCanvasRatio, worldToCanvasRatio);
413
+ transform.translate(panWorld[0], panWorld[1]);
414
+ transform.translate(-halfCanvasWorldCoordinates[0], -halfCanvasWorldCoordinates[1]);
415
+ return transform;
416
+ }
417
+ drawGrid(ctx) {
418
+ const scale = this.channelScale;
419
+ const pxWidth = this.ecgWidth;
420
+ const pxHeight = this.ecgHeight;
421
+ if (scale <= 0) {
422
+ return;
423
+ }
424
+ const MIN_LINE_SPACING = 8;
425
+ let hGridUnit = 100;
426
+ while (hGridUnit * scale < MIN_LINE_SPACING) {
427
+ hGridUnit *= 2;
428
+ }
429
+ const minorH = hGridUnit * scale;
430
+ const majorH = minorH * 5;
431
+ const minorV = SECONDS_WIDTH / 25;
432
+ const majorV = SECONDS_WIDTH / 5;
433
+ ctx.strokeStyle = COLOR_GRID_MINOR;
434
+ ctx.lineWidth = 0.5;
435
+ ctx.beginPath();
436
+ const hLines = Math.floor(pxHeight / minorH);
437
+ for (let h = 1; h <= hLines; h++) {
438
+ if (h % 5 !== 0) {
439
+ const y = h * minorH;
440
+ ctx.moveTo(0, y);
441
+ ctx.lineTo(pxWidth, y);
442
+ }
443
+ }
444
+ const vLines = Math.floor(pxWidth / minorV);
445
+ for (let v = 1; v <= vLines; v++) {
446
+ if (v % 5 !== 0) {
447
+ const x = v * minorV;
448
+ ctx.moveTo(x, 0);
449
+ ctx.lineTo(x, pxHeight);
450
+ }
451
+ }
452
+ ctx.stroke();
453
+ ctx.strokeStyle = COLOR_GRID_MAJOR;
454
+ ctx.lineWidth = 1;
455
+ ctx.beginPath();
456
+ const hMajorLines = Math.floor(pxHeight / majorH);
457
+ for (let h = 1; h <= hMajorLines; h++) {
458
+ const y = h * majorH;
459
+ ctx.moveTo(0, y);
460
+ ctx.lineTo(pxWidth, y);
461
+ }
462
+ const vMajorLines = Math.floor(pxWidth / majorV);
463
+ for (let v = 1; v <= vMajorLines; v++) {
464
+ const x = v * majorV;
465
+ ctx.moveTo(x, 0);
466
+ ctx.lineTo(x, pxHeight);
467
+ }
468
+ ctx.stroke();
469
+ }
470
+ drawTraces(ctx, layouts) {
471
+ const scale = this.channelScale;
472
+ const pxWidth = this.ecgWidth;
473
+ for (const { channel, baseline } of layouts) {
474
+ ctx.strokeStyle = COLOR_BASELINE;
475
+ ctx.lineWidth = 2;
476
+ ctx.beginPath();
477
+ ctx.moveTo(0, baseline);
478
+ ctx.lineTo(pxWidth, baseline);
479
+ ctx.stroke();
480
+ ctx.strokeStyle = COLOR_TRACE;
481
+ ctx.lineWidth = 1;
482
+ ctx.beginPath();
483
+ for (let i = 0; i < channel.data.length; i++) {
484
+ const x = (i * pxWidth) / channel.data.length;
485
+ const y = baseline - channel.data[i] * scale;
486
+ if (i === 0) {
487
+ ctx.moveTo(x, y);
488
+ }
489
+ else {
490
+ ctx.lineTo(x, y);
491
+ }
492
+ }
493
+ ctx.stroke();
494
+ }
495
+ }
496
+ drawLabels(ctx, layouts) {
497
+ const worldToCanvas = this.getWorldToCanvasRatio();
498
+ const fontSize = 14 / worldToCanvas;
499
+ for (const { channel, itemHeight, yOffset } of layouts) {
500
+ const labelY = yOffset - itemHeight + fontSize;
501
+ ctx.font = `${fontSize}px monospace`;
502
+ const textWidth = ctx.measureText(channel.name).width;
503
+ ctx.fillStyle = COLOR_BACKGROUND;
504
+ ctx.fillRect(5, labelY - fontSize, textWidth + 4, fontSize + 4);
505
+ ctx.fillStyle = COLOR_LABEL;
506
+ ctx.fillText(channel.name, 5, labelY);
507
+ }
508
+ }
509
+ getImageData() {
510
+ if (!this.waveformData) {
511
+ return null;
512
+ }
513
+ const nSamples = this.waveformData.numberOfSamples;
514
+ const nChannels = this.waveformData.numberOfChannels;
515
+ const dimensions = [nSamples, ECG_AMPLITUDE_INDEX_SIZE, nChannels];
516
+ const spacing = [1, 1, 1];
517
+ const origin = [0, 0, 0];
518
+ const direction = [1, 0, 0, 0, 1, 0, 0, 0, 1];
519
+ const amplitudeOffset = ECG_AMPLITUDE_INDEX_SIZE / 2;
520
+ const imageData = {
521
+ getDirection: () => direction,
522
+ getDimensions: () => dimensions,
523
+ getRange: () => [0, 1],
524
+ getSpacing: () => spacing,
525
+ worldToIndex: (point) => {
526
+ return [point[0], point[1] + amplitudeOffset, point[2]];
527
+ },
528
+ indexToWorld: (point) => {
529
+ return [point[0], point[1] - amplitudeOffset, point[2]];
530
+ },
531
+ };
532
+ return {
533
+ dimensions,
534
+ spacing,
535
+ origin,
536
+ direction,
537
+ imageData,
538
+ hasPixelSpacing: false,
539
+ calibration: this.calibration,
540
+ preScale: { scaled: false },
541
+ metadata: { Modality: 'ECG' },
542
+ };
543
+ }
544
+ getSliceViewInfo() {
545
+ throw new Error('Method not implemented for ECG viewport.');
546
+ }
547
+ }
548
+ export default ECGViewport;
@@ -4,6 +4,7 @@ import ViewportType from '../../enums/ViewportType';
4
4
  import VolumeViewport3D from '../VolumeViewport3D';
5
5
  import VideoViewport from '../VideoViewport';
6
6
  import WSIViewport from '../WSIViewport';
7
+ import ECGViewport from '../ECGViewport';
7
8
  const viewportTypeToViewportClass = {
8
9
  [ViewportType.ORTHOGRAPHIC]: VolumeViewport,
9
10
  [ViewportType.PERSPECTIVE]: VolumeViewport,
@@ -11,5 +12,6 @@ const viewportTypeToViewportClass = {
11
12
  [ViewportType.VOLUME_3D]: VolumeViewport3D,
12
13
  [ViewportType.VIDEO]: VideoViewport,
13
14
  [ViewportType.WHOLE_SLIDE]: WSIViewport,
15
+ [ViewportType.ECG]: ECGViewport,
14
16
  };
15
17
  export default viewportTypeToViewportClass;
@@ -18,6 +18,7 @@ declare enum MetadataModules {
18
18
  PET_SERIES = "petSeriesModule",
19
19
  SOP_COMMON = "sopCommonModule",
20
20
  ULTRASOUND_ENHANCED_REGION = "ultrasoundEnhancedRegionModule",
21
+ ECG = "ecgModule",
21
22
  VOI_LUT = "voiLutModule",
22
23
  FRAME_MODULE = "frameModule",
23
24
  WADO_WEB_CLIENT = "wadoWebClient",
@@ -19,6 +19,7 @@ var MetadataModules;
19
19
  MetadataModules["PET_SERIES"] = "petSeriesModule";
20
20
  MetadataModules["SOP_COMMON"] = "sopCommonModule";
21
21
  MetadataModules["ULTRASOUND_ENHANCED_REGION"] = "ultrasoundEnhancedRegionModule";
22
+ MetadataModules["ECG"] = "ecgModule";
22
23
  MetadataModules["VOI_LUT"] = "voiLutModule";
23
24
  MetadataModules["FRAME_MODULE"] = "frameModule";
24
25
  MetadataModules["WADO_WEB_CLIENT"] = "wadoWebClient";
@@ -4,6 +4,7 @@ declare enum ViewportType {
4
4
  PERSPECTIVE = "perspective",
5
5
  VOLUME_3D = "volume3d",
6
6
  VIDEO = "video",
7
- WHOLE_SLIDE = "wholeSlide"
7
+ WHOLE_SLIDE = "wholeSlide",
8
+ ECG = "ecg"
8
9
  }
9
10
  export default ViewportType;
@@ -6,5 +6,6 @@ var ViewportType;
6
6
  ViewportType["VOLUME_3D"] = "volume3d";
7
7
  ViewportType["VIDEO"] = "video";
8
8
  ViewportType["WHOLE_SLIDE"] = "wholeSlide";
9
+ ViewportType["ECG"] = "ecg";
9
10
  })(ViewportType || (ViewportType = {}));
10
11
  export default ViewportType;
@@ -11,6 +11,7 @@ import BaseVolumeViewport from './RenderingEngine/BaseVolumeViewport';
11
11
  import StackViewport from './RenderingEngine/StackViewport';
12
12
  import VideoViewport from './RenderingEngine/VideoViewport';
13
13
  import WSIViewport from './RenderingEngine/WSIViewport';
14
+ import ECGViewport from './RenderingEngine/ECGViewport';
14
15
  import Viewport from './RenderingEngine/Viewport';
15
16
  import eventTarget from './eventTarget';
16
17
  import { version } from './version';
@@ -39,4 +40,4 @@ import { cornerstoneMeshLoader } from './loaders/cornerstoneMeshLoader';
39
40
  import { setVolumesForViewports, addVolumesToViewports, addImageSlicesToViewports } from './RenderingEngine/helpers';
40
41
  export * from './loaders/decimatedVolumeLoader';
41
42
  export type { Types, IRetrieveConfiguration, RetrieveOptions, RetrieveStage, ImageLoadListener, IImagesLoader, };
42
- export { init, isCornerstoneInitialized, peerImport, resetInitialization, getConfiguration, setConfiguration, getWebWorkerManager, canRenderFloatTextures, Enums, CONSTANTS, Events as EVENTS, Settings, BaseVolumeViewport, VolumeViewport, VolumeViewport3D, Viewport, StackViewport, VideoViewport, WSIViewport, RenderingEngine, BaseRenderingEngine, TiledRenderingEngine, ContextPoolRenderingEngine, ImageVolume, Surface, getRenderingEngine, getRenderingEngines, getEnabledElement, getEnabledElementByIds, getEnabledElements, getEnabledElementByViewportId, createVolumeActor, createVolumeMapper, cache, eventTarget, triggerEvent, imageLoader, registerImageLoader, volumeLoader, metaData, utilities, setVolumesForViewports, addVolumesToViewports, addImageSlicesToViewports, imageLoadPoolManager as requestPoolManager, imageRetrievalPoolManager, imageLoadPoolManager, getShouldUseCPURendering, setUseCPURendering, setPreferSizeOverAccuracy, resetUseCPURendering, geometryLoader, cornerstoneMeshLoader, ProgressiveRetrieveImages, decimatedVolumeLoader, cornerstoneStreamingImageVolumeLoader, cornerstoneStreamingDynamicImageVolumeLoader, StreamingDynamicImageVolume, StreamingImageVolume, convertMapperToNotSharedMapper, version, };
43
+ export { init, isCornerstoneInitialized, peerImport, resetInitialization, getConfiguration, setConfiguration, getWebWorkerManager, canRenderFloatTextures, Enums, CONSTANTS, Events as EVENTS, Settings, BaseVolumeViewport, VolumeViewport, VolumeViewport3D, Viewport, StackViewport, VideoViewport, WSIViewport, ECGViewport, RenderingEngine, BaseRenderingEngine, TiledRenderingEngine, ContextPoolRenderingEngine, ImageVolume, Surface, getRenderingEngine, getRenderingEngines, getEnabledElement, getEnabledElementByIds, getEnabledElements, getEnabledElementByViewportId, createVolumeActor, createVolumeMapper, cache, eventTarget, triggerEvent, imageLoader, registerImageLoader, volumeLoader, metaData, utilities, setVolumesForViewports, addVolumesToViewports, addImageSlicesToViewports, imageLoadPoolManager as requestPoolManager, imageRetrievalPoolManager, imageLoadPoolManager, getShouldUseCPURendering, setUseCPURendering, setPreferSizeOverAccuracy, resetUseCPURendering, geometryLoader, cornerstoneMeshLoader, ProgressiveRetrieveImages, decimatedVolumeLoader, cornerstoneStreamingImageVolumeLoader, cornerstoneStreamingDynamicImageVolumeLoader, StreamingDynamicImageVolume, StreamingImageVolume, convertMapperToNotSharedMapper, version, };
package/dist/esm/index.js CHANGED
@@ -11,6 +11,7 @@ import BaseVolumeViewport from './RenderingEngine/BaseVolumeViewport';
11
11
  import StackViewport from './RenderingEngine/StackViewport';
12
12
  import VideoViewport from './RenderingEngine/VideoViewport';
13
13
  import WSIViewport from './RenderingEngine/WSIViewport';
14
+ import ECGViewport from './RenderingEngine/ECGViewport';
14
15
  import Viewport from './RenderingEngine/Viewport';
15
16
  import eventTarget from './eventTarget';
16
17
  import { version } from './version';
@@ -36,4 +37,4 @@ import { cornerstoneStreamingDynamicImageVolumeLoader } from './loaders/cornerst
36
37
  import { cornerstoneMeshLoader } from './loaders/cornerstoneMeshLoader';
37
38
  import { setVolumesForViewports, addVolumesToViewports, addImageSlicesToViewports, } from './RenderingEngine/helpers';
38
39
  export * from './loaders/decimatedVolumeLoader';
39
- export { init, isCornerstoneInitialized, peerImport, resetInitialization, getConfiguration, setConfiguration, getWebWorkerManager, canRenderFloatTextures, Enums, CONSTANTS, Events as EVENTS, Settings, BaseVolumeViewport, VolumeViewport, VolumeViewport3D, Viewport, StackViewport, VideoViewport, WSIViewport, RenderingEngine, BaseRenderingEngine, TiledRenderingEngine, ContextPoolRenderingEngine, ImageVolume, Surface, getRenderingEngine, getRenderingEngines, getEnabledElement, getEnabledElementByIds, getEnabledElements, getEnabledElementByViewportId, createVolumeActor, createVolumeMapper, cache, eventTarget, triggerEvent, imageLoader, registerImageLoader, volumeLoader, metaData, utilities, setVolumesForViewports, addVolumesToViewports, addImageSlicesToViewports, imageLoadPoolManager as requestPoolManager, imageRetrievalPoolManager, imageLoadPoolManager, getShouldUseCPURendering, setUseCPURendering, setPreferSizeOverAccuracy, resetUseCPURendering, geometryLoader, cornerstoneMeshLoader, ProgressiveRetrieveImages, decimatedVolumeLoader, cornerstoneStreamingImageVolumeLoader, cornerstoneStreamingDynamicImageVolumeLoader, StreamingDynamicImageVolume, StreamingImageVolume, convertMapperToNotSharedMapper, version, };
40
+ export { init, isCornerstoneInitialized, peerImport, resetInitialization, getConfiguration, setConfiguration, getWebWorkerManager, canRenderFloatTextures, Enums, CONSTANTS, Events as EVENTS, Settings, BaseVolumeViewport, VolumeViewport, VolumeViewport3D, Viewport, StackViewport, VideoViewport, WSIViewport, ECGViewport, RenderingEngine, BaseRenderingEngine, TiledRenderingEngine, ContextPoolRenderingEngine, ImageVolume, Surface, getRenderingEngine, getRenderingEngines, getEnabledElement, getEnabledElementByIds, getEnabledElements, getEnabledElementByViewportId, createVolumeActor, createVolumeMapper, cache, eventTarget, triggerEvent, imageLoader, registerImageLoader, volumeLoader, metaData, utilities, setVolumesForViewports, addVolumesToViewports, addImageSlicesToViewports, imageLoadPoolManager as requestPoolManager, imageRetrievalPoolManager, imageLoadPoolManager, getShouldUseCPURendering, setUseCPURendering, setPreferSizeOverAccuracy, resetUseCPURendering, geometryLoader, cornerstoneMeshLoader, ProgressiveRetrieveImages, decimatedVolumeLoader, cornerstoneStreamingImageVolumeLoader, cornerstoneStreamingDynamicImageVolumeLoader, StreamingDynamicImageVolume, StreamingImageVolume, convertMapperToNotSharedMapper, version, };
@@ -0,0 +1,5 @@
1
+ import type { ViewportProperties } from './ViewportProperties';
2
+ type ECGViewportProperties = ViewportProperties & {
3
+ visibleChannels?: number[];
4
+ };
5
+ export type { ECGViewportProperties as default };
File without changes
@@ -0,0 +1,34 @@
1
+ import type { ViewportType } from '../enums';
2
+ import type Point2 from './Point2';
3
+ export interface InternalECGCamera {
4
+ panWorld?: Point2;
5
+ parallelScale?: number;
6
+ }
7
+ export interface ECGViewportInput {
8
+ id: string;
9
+ renderingEngineId: string;
10
+ type: ViewportType;
11
+ element: HTMLDivElement;
12
+ sx: number;
13
+ sy: number;
14
+ sWidth: number;
15
+ sHeight: number;
16
+ defaultOptions: unknown;
17
+ canvas: HTMLCanvasElement;
18
+ }
19
+ export interface ECGChannel {
20
+ name: string;
21
+ data: Int16Array;
22
+ visible: boolean;
23
+ min: number;
24
+ max: number;
25
+ }
26
+ export interface ECGWaveformData {
27
+ channels: ECGChannel[];
28
+ numberOfChannels: number;
29
+ numberOfSamples: number;
30
+ samplingFrequency: number;
31
+ bitsAllocated: number;
32
+ sampleInterpretation: string;
33
+ multiplexGroupLabel: string;
34
+ }
File without changes
@@ -0,0 +1,2 @@
1
+ import type ECGViewport from '../RenderingEngine/ECGViewport';
2
+ export type IECGViewport = ECGViewport;
File without changes
@@ -86,7 +86,10 @@ import type { Color, ColorLUT } from './Color';
86
86
  import type VideoViewportProperties from './VideoViewportProperties';
87
87
  import type WSIViewportProperties from './WSIViewportProperties';
88
88
  import type { IVideoViewport } from './IVideoViewport';
89
+ import type { IECGViewport } from './IECGViewport';
89
90
  import type { InternalVideoCamera, VideoViewportInput } from './VideoViewportTypes';
91
+ import type { InternalECGCamera, ECGViewportInput, ECGChannel, ECGWaveformData } from './ECGViewportTypes';
92
+ import type ECGViewportProperties from './ECGViewportProperties';
90
93
  import type { ISurface } from './ISurface';
91
94
  import type BoundsIJK from './BoundsIJK';
92
95
  import type { ImageVolumeProps } from './ImageVolumeProps';
@@ -105,4 +108,4 @@ import type { RenderingEngineModeType } from './RenderingEngineMode';
105
108
  import type { VtkOffscreenMultiRenderWindow } from './VtkOffscreenMultiRenderWindow';
106
109
  export type * from './MetadataModuleTypes';
107
110
  export type * from './InstanceTypes';
108
- export type { Cornerstone3DConfig, IBaseStreamingImageVolume, ICamera, IStackViewport, IVideoViewport, IWSIViewport, IVolumeViewport, IEnabledElement, ICache, IVolume, IViewportId, IImageVolume, ImageVolumeProps, IDynamicImageVolume, IRenderingEngine, ScalingParameters, PTScaling, IPointsManager, PolyDataPointConfiguration, Scaling, IStreamingImageVolume, IImage, IImageData, IImageCalibration, CPUIImageData, CPUImageData, EventTypes, ImageLoaderFn, VolumeLoaderFn, IRegisterImageLoader, IStreamingVolumeProperties, IViewport, ViewReference, DataSetOptions as ImageSetOptions, ViewPresentation, ViewPresentationSelector, ReferenceCompatibleOptions, ViewReferenceSpecifier, StackViewportProperties, VolumeViewportProperties, ViewportProperties, PublicViewportInput, VolumeActor, Actor, ActorEntry, ImageActor, ICanvasActor, IImageLoadObject, IVolumeLoadObject, IVolumeInput, VolumeInputCallback, IStackInput, StackInputCallback, ViewportPreset, Metadata, OrientationVectors, AABB2, AABB3, Point2, Point3, PointsXYZ, Point4, Mat3, Plane, ViewportInputOptions, VideoViewportProperties, WSIViewportProperties, VOIRange, VOI, DisplayArea, FlipDirection, ICachedImage, ICachedVolume, CPUFallbackEnabledElement, CPUFallbackViewport, CPUFallbackTransform, CPUFallbackColormapData, CPUFallbackViewportDisplayedArea, CPUFallbackColormapsData, CPUFallbackColormap, TransformMatrix2D, CPUFallbackLookupTable, CPUFallbackLUT, CPUFallbackRenderingTools, CustomEventType, ActorSliceRange, ImageSliceData, IGeometry, IGeometryLoadObject, ICachedGeometry, PublicContourSetData, ContourSetData, ContourData, IContourSet, IContour, PublicSurfaceData, SurfaceData, ISurface, PublicMeshData, MeshData, IMesh, RGB, ColormapPublic, ColormapRegistration, PixelDataTypedArray, PixelDataTypedArrayString, ImagePixelModule, ImagePlaneModule, AffineMatrix, ImageLoadListener, InternalVideoCamera, VideoViewportInput, BoundsIJK, BoundsLPS, Color, ColorLUT, VolumeProps, IImageFrame, LocalVolumeOptions, IVoxelManager, IRLEVoxelMap, RLERun, ViewportInput, ImageLoadRequests, IBaseVolumeViewport, GeometryLoaderFn, ScrollOptions, JumpToSliceOptions, Memo, HistoryMemo, VoxelManager, RLEVoxelMap, RenderingEngineModeType, VtkOffscreenMultiRenderWindow, };
111
+ export type { Cornerstone3DConfig, IBaseStreamingImageVolume, ICamera, IStackViewport, IVideoViewport, IECGViewport, IWSIViewport, IVolumeViewport, IEnabledElement, ICache, IVolume, IViewportId, IImageVolume, ImageVolumeProps, IDynamicImageVolume, IRenderingEngine, ScalingParameters, PTScaling, IPointsManager, PolyDataPointConfiguration, Scaling, IStreamingImageVolume, IImage, IImageData, IImageCalibration, CPUIImageData, CPUImageData, EventTypes, ImageLoaderFn, VolumeLoaderFn, IRegisterImageLoader, IStreamingVolumeProperties, IViewport, ViewReference, DataSetOptions as ImageSetOptions, ViewPresentation, ViewPresentationSelector, ReferenceCompatibleOptions, ViewReferenceSpecifier, StackViewportProperties, VolumeViewportProperties, ViewportProperties, PublicViewportInput, VolumeActor, Actor, ActorEntry, ImageActor, ICanvasActor, IImageLoadObject, IVolumeLoadObject, IVolumeInput, VolumeInputCallback, IStackInput, StackInputCallback, ViewportPreset, Metadata, OrientationVectors, AABB2, AABB3, Point2, Point3, PointsXYZ, Point4, Mat3, Plane, ViewportInputOptions, VideoViewportProperties, WSIViewportProperties, VOIRange, VOI, DisplayArea, FlipDirection, ICachedImage, ICachedVolume, CPUFallbackEnabledElement, CPUFallbackViewport, CPUFallbackTransform, CPUFallbackColormapData, CPUFallbackViewportDisplayedArea, CPUFallbackColormapsData, CPUFallbackColormap, TransformMatrix2D, CPUFallbackLookupTable, CPUFallbackLUT, CPUFallbackRenderingTools, CustomEventType, ActorSliceRange, ImageSliceData, IGeometry, IGeometryLoadObject, ICachedGeometry, PublicContourSetData, ContourSetData, ContourData, IContourSet, IContour, PublicSurfaceData, SurfaceData, ISurface, PublicMeshData, MeshData, IMesh, RGB, ColormapPublic, ColormapRegistration, PixelDataTypedArray, PixelDataTypedArrayString, ImagePixelModule, ImagePlaneModule, AffineMatrix, ImageLoadListener, InternalVideoCamera, VideoViewportInput, InternalECGCamera, ECGViewportInput, ECGChannel, ECGWaveformData, ECGViewportProperties, BoundsIJK, BoundsLPS, Color, ColorLUT, VolumeProps, IImageFrame, LocalVolumeOptions, IVoxelManager, IRLEVoxelMap, RLERun, ViewportInput, ImageLoadRequests, IBaseVolumeViewport, GeometryLoaderFn, ScrollOptions, JumpToSliceOptions, Memo, HistoryMemo, VoxelManager, RLEVoxelMap, RenderingEngineModeType, VtkOffscreenMultiRenderWindow, };
@@ -1 +1 @@
1
- export declare const version = "4.17.5";
1
+ export declare const version = "4.18.1";
@@ -1 +1 @@
1
- export const version = '4.17.5';
1
+ export const version = '4.18.1';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cornerstonejs/core",
3
- "version": "4.17.5",
3
+ "version": "4.18.1",
4
4
  "description": "Cornerstone3D Core",
5
5
  "module": "./dist/esm/index.js",
6
6
  "types": "./dist/esm/index.d.ts",
@@ -97,5 +97,5 @@
97
97
  "type": "individual",
98
98
  "url": "https://ohif.org/donate"
99
99
  },
100
- "gitHead": "d42f5ee3747c6521ef3af5e9d1faf69c98157396"
100
+ "gitHead": "1fc5684032269b1b1e3e3733cd891d48c27303f7"
101
101
  }