@cornerstonejs/tools 5.4.17 → 5.5.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.
Files changed (51) hide show
  1. package/dist/esm/enums/Events.d.ts +7 -0
  2. package/dist/esm/enums/Events.js +7 -0
  3. package/dist/esm/index.d.ts +2 -2
  4. package/dist/esm/index.js +2 -2
  5. package/dist/esm/tools/SliceIntersectionTool.d.ts +150 -0
  6. package/dist/esm/tools/SliceIntersectionTool.js +1065 -0
  7. package/dist/esm/tools/WorldCrosshairTool.d.ts +118 -0
  8. package/dist/esm/tools/WorldCrosshairTool.js +749 -0
  9. package/dist/esm/tools/annotation/LivewireContourTool.d.ts +1 -3
  10. package/dist/esm/tools/annotation/LivewireContourTool.js +0 -18
  11. package/dist/esm/tools/annotation/RectangleROITool.js +8 -8
  12. package/dist/esm/tools/annotation/SplineROITool.d.ts +1 -3
  13. package/dist/esm/tools/annotation/SplineROITool.js +0 -18
  14. package/dist/esm/tools/annotation/UltrasoundDirectionalTool.js +3 -3
  15. package/dist/esm/tools/base/AnnotationTool.d.ts +4 -0
  16. package/dist/esm/tools/base/AnnotationTool.js +20 -0
  17. package/dist/esm/tools/index.d.ts +3 -1
  18. package/dist/esm/tools/index.js +3 -1
  19. package/dist/esm/tools/worldCrosshair/WorldCrosshairLines3D.d.ts +8 -0
  20. package/dist/esm/tools/worldCrosshair/WorldCrosshairLines3D.js +81 -0
  21. package/dist/esm/utilities/genericViewportToolHelpers.d.ts +1 -0
  22. package/dist/esm/utilities/genericViewportToolHelpers.js +31 -0
  23. package/dist/esm/utilities/index.d.ts +3 -1
  24. package/dist/esm/utilities/index.js +3 -1
  25. package/dist/esm/utilities/pickIntensityPointInSlab.d.ts +7 -0
  26. package/dist/esm/utilities/pickIntensityPointInSlab.js +106 -0
  27. package/dist/esm/utilities/spatial/areViewportsSpatiallyLinked.d.ts +3 -0
  28. package/dist/esm/utilities/spatial/areViewportsSpatiallyLinked.js +48 -0
  29. package/dist/esm/utilities/spatial/clipWorldLineToViewportCanvas.d.ts +3 -0
  30. package/dist/esm/utilities/spatial/clipWorldLineToViewportCanvas.js +58 -0
  31. package/dist/esm/utilities/spatial/distancePointToPlane.d.ts +3 -0
  32. package/dist/esm/utilities/spatial/distancePointToPlane.js +6 -0
  33. package/dist/esm/utilities/spatial/getDisplayedCanvasSize.d.ts +5 -0
  34. package/dist/esm/utilities/spatial/getDisplayedCanvasSize.js +12 -0
  35. package/dist/esm/utilities/spatial/getViewportPlane.d.ts +3 -0
  36. package/dist/esm/utilities/spatial/getViewportPlane.js +33 -0
  37. package/dist/esm/utilities/spatial/index.d.ts +11 -0
  38. package/dist/esm/utilities/spatial/index.js +10 -0
  39. package/dist/esm/utilities/spatial/intersectPlanes.d.ts +2 -0
  40. package/dist/esm/utilities/spatial/intersectPlanes.js +30 -0
  41. package/dist/esm/utilities/spatial/projectPointToPlane.d.ts +3 -0
  42. package/dist/esm/utilities/spatial/projectPointToPlane.js +11 -0
  43. package/dist/esm/utilities/spatial/rotateViewportAroundWorldPoint.d.ts +2 -0
  44. package/dist/esm/utilities/spatial/rotateViewportAroundWorldPoint.js +34 -0
  45. package/dist/esm/utilities/spatial/translateViewportAlongNormal.d.ts +2 -0
  46. package/dist/esm/utilities/spatial/translateViewportAlongNormal.js +36 -0
  47. package/dist/esm/utilities/spatial/types.d.ts +19 -0
  48. package/dist/esm/utilities/spatial/types.js +0 -0
  49. package/dist/esm/version.d.ts +1 -1
  50. package/dist/esm/version.js +1 -1
  51. package/package.json +4 -4
@@ -0,0 +1,749 @@
1
+ import { vec3 } from 'gl-matrix';
2
+ import { getEnabledElement, getEnabledElementByIds, getEnabledElementByViewportId, eventTarget, triggerEvent, utilities as csUtils, Enums, } from '@cornerstonejs/core';
3
+ import { AnnotationTool } from './base/index.js';
4
+ import { getToolGroup } from '../store/ToolGroupManager/index.js';
5
+ import { state } from '../store/state.js';
6
+ import { Events } from '../enums/index.js';
7
+ import { addAnnotation, removeAnnotation, } from '../stateManagement/annotation/annotationState.js';
8
+ import { drawCircle as drawCircleSvg, drawLine as drawLineSvg, drawTextBox as drawTextBoxSvg, } from '../drawingSvg/index.js';
9
+ import { resetElementCursor, hideElementCursor, } from '../cursors/elementCursor.js';
10
+ import triggerAnnotationRenderForViewportIds from '../utilities/triggerAnnotationRenderForViewportIds.js';
11
+ import getViewportICamera from '../utilities/getViewportICamera.js';
12
+ import { navigatePlanarViewportToPoint } from '../utilities/genericViewportToolHelpers.js';
13
+ import { getViewportPlane, distancePointToPlane, projectPointToPlane, getDisplayedCanvasSize, } from '../utilities/spatial/index.js';
14
+ import pickIntensityPointInSlab from '../utilities/pickIntensityPointInSlab.js';
15
+ import { updateWorldCrosshairLines3D, removeWorldCrosshairLines3D, } from './worldCrosshair/WorldCrosshairLines3D.js';
16
+ const { ViewportType } = Enums;
17
+ const POINT_EPSILON = 1e-4;
18
+ const JUMP_EPSILON = 1e-6;
19
+ function isFinitePoint3(point) {
20
+ return (Array.isArray(point) &&
21
+ point.length === 3 &&
22
+ point.every((v) => Number.isFinite(v)));
23
+ }
24
+ function parseCssColorToRGB(color) {
25
+ if (typeof color === 'string') {
26
+ const rgbMatch = color.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
27
+ if (rgbMatch) {
28
+ return [
29
+ Number(rgbMatch[1]) / 255,
30
+ Number(rgbMatch[2]) / 255,
31
+ Number(rgbMatch[3]) / 255,
32
+ ];
33
+ }
34
+ const hexMatch = color.match(/^#([0-9a-f]{6})$/i);
35
+ if (hexMatch) {
36
+ const hex = parseInt(hexMatch[1], 16);
37
+ return [
38
+ ((hex >> 16) & 255) / 255,
39
+ ((hex >> 8) & 255) / 255,
40
+ (hex & 255) / 255,
41
+ ];
42
+ }
43
+ }
44
+ return [1, 1, 0];
45
+ }
46
+ class WorldCrosshairTool extends AnnotationTool {
47
+ constructor(toolProps = {}, defaultToolProps = {
48
+ supportedInteractionTypes: ['Mouse', 'Touch'],
49
+ configuration: {
50
+ pointColor: 'rgb(255, 255, 0)',
51
+ pointRadius: 4,
52
+ markerStyle: 'crosshair',
53
+ centerGapRadius: 12,
54
+ offSliceDisplay: 'projectedWithDistance',
55
+ offSliceToleranceMm: 0.5,
56
+ autoInitializeOnEnable: true,
57
+ clickToSet: false,
58
+ snapToSlabIntensity: true,
59
+ jumpOnSet: true,
60
+ jumpMode: 'sliceOnly',
61
+ preservePanZoom: true,
62
+ liveUpdateOnShiftMouseMove: true,
63
+ liveJumpOnShiftMouseMove: true,
64
+ showIn2D: true,
65
+ showIn3D: true,
66
+ threeDViewportIds: [],
67
+ threeDLineLengthMm: 100,
68
+ linkPolicy: 'frameOfReferenceUID',
69
+ explicitLinkedViewportIds: [],
70
+ },
71
+ }) {
72
+ super(toolProps, defaultToolProps);
73
+ this._state = {
74
+ worldPoint: null,
75
+ visible: true,
76
+ locked: false,
77
+ cursorWorldPoint: null,
78
+ };
79
+ this._annotation = null;
80
+ this._isDragging = false;
81
+ this._autoInitialized = false;
82
+ this.addNewAnnotation = (evt) => {
83
+ const eventDetail = evt.detail;
84
+ const { element, currentPoints } = eventDetail;
85
+ const enabledElement = getEnabledElement(element);
86
+ if (!enabledElement || this._state.locked) {
87
+ return this._getAnnotationOrDetached();
88
+ }
89
+ const nativeEvent = eventDetail.event;
90
+ if (!this.configuration.clickToSet && !nativeEvent?.shiftKey) {
91
+ return this._getAnnotationOrDetached();
92
+ }
93
+ const { viewport, renderingEngine, FrameOfReferenceUID } = enabledElement;
94
+ if (!this._isPlanarViewport(viewport)) {
95
+ return this._getAnnotationOrDetached();
96
+ }
97
+ this.setWorldPoint(this._resolveInteractionWorldPoint(viewport, currentPoints.world), {
98
+ sourceViewportId: viewport.id,
99
+ sourceRenderingEngineId: renderingEngine?.id,
100
+ frameOfReferenceUID: FrameOfReferenceUID,
101
+ });
102
+ evt.preventDefault();
103
+ hideElementCursor(element);
104
+ this._startDrag(element);
105
+ return this._getAnnotationOrDetached();
106
+ };
107
+ this.toolSelectedCallback = (evt, annotation) => {
108
+ const { element } = evt.detail;
109
+ if (this._state.locked) {
110
+ return;
111
+ }
112
+ annotation.highlighted = true;
113
+ hideElementCursor(element);
114
+ this._startDrag(element);
115
+ evt.preventDefault();
116
+ };
117
+ this.handleSelectedCallback = (evt, annotation, _handle, _interactionType = 'Mouse') => {
118
+ this.toolSelectedCallback(evt, annotation);
119
+ };
120
+ this.doubleClickCallback = (evt) => {
121
+ const { element, currentPoints } = evt.detail;
122
+ const enabledElement = getEnabledElement(element);
123
+ if (!enabledElement || !this._state.worldPoint) {
124
+ return;
125
+ }
126
+ const markerCanvas = this._getMarkerCanvasPosition(enabledElement.viewport);
127
+ if (!markerCanvas) {
128
+ return;
129
+ }
130
+ const distance = Math.hypot(markerCanvas[0] - currentPoints.canvas[0], markerCanvas[1] - currentPoints.canvas[1]);
131
+ if (distance > this.configuration.pointRadius + 6) {
132
+ return;
133
+ }
134
+ this.jumpLinkedViewportsToWorldPoint();
135
+ evt.preventDefault();
136
+ };
137
+ this.mouseMoveCallback = (evt, filteredToolAnnotations) => {
138
+ const { element, currentPoints } = evt.detail;
139
+ const nativeEvent = evt.detail.event;
140
+ let needsRedraw = false;
141
+ if (this.configuration.liveUpdateOnShiftMouseMove &&
142
+ nativeEvent?.shiftKey &&
143
+ !this._state.locked) {
144
+ const enabledElement = getEnabledElement(element);
145
+ const viewport = enabledElement?.viewport;
146
+ if (viewport && this._isPlanarViewport(viewport)) {
147
+ this._state.cursorWorldPoint = [
148
+ currentPoints.world[0],
149
+ currentPoints.world[1],
150
+ currentPoints.world[2],
151
+ ];
152
+ this.setWorldPoint(this._resolveInteractionWorldPoint(viewport, currentPoints.world), {
153
+ sourceViewportId: viewport.id,
154
+ sourceRenderingEngineId: enabledElement.renderingEngine?.id,
155
+ frameOfReferenceUID: enabledElement.FrameOfReferenceUID,
156
+ jump: this.configuration.liveJumpOnShiftMouseMove,
157
+ });
158
+ needsRedraw = true;
159
+ }
160
+ }
161
+ if (filteredToolAnnotations?.length) {
162
+ for (const annotation of filteredToolAnnotations) {
163
+ const near = this.isPointNearTool(element, annotation, currentPoints.canvas, 6, 'mouse');
164
+ if (near !== !!annotation.highlighted) {
165
+ annotation.highlighted = near;
166
+ needsRedraw = true;
167
+ }
168
+ }
169
+ }
170
+ return needsRedraw;
171
+ };
172
+ this.isPointNearTool = (element, _annotation, canvasCoords, proximity, _interactionType) => {
173
+ const enabledElement = getEnabledElement(element);
174
+ if (!enabledElement) {
175
+ return false;
176
+ }
177
+ const markerCanvas = this._getMarkerCanvasPosition(enabledElement.viewport);
178
+ if (!markerCanvas) {
179
+ return false;
180
+ }
181
+ const distance = Math.hypot(markerCanvas[0] - canvasCoords[0], markerCanvas[1] - canvasCoords[1]);
182
+ return distance <= this.configuration.pointRadius + proximity;
183
+ };
184
+ this.filterInteractableAnnotationsForElement = (element, annotations) => {
185
+ if (!annotations?.length || !this._state.worldPoint) {
186
+ return [];
187
+ }
188
+ const enabledElement = getEnabledElement(element);
189
+ if (!enabledElement ||
190
+ !this._getMarkerCanvasPosition(enabledElement.viewport)) {
191
+ return [];
192
+ }
193
+ return annotations.filter((annotation) => annotation.metadata.toolName === this.getToolName());
194
+ };
195
+ this.cancel = (element) => {
196
+ if (this._isDragging) {
197
+ this._isDragging = false;
198
+ this._deactivateModify(element);
199
+ resetElementCursor(element);
200
+ }
201
+ };
202
+ this._dragCallback = (evt) => {
203
+ const eventDetail = evt.detail;
204
+ const { element, currentPoints } = eventDetail;
205
+ const enabledElement = getEnabledElement(element);
206
+ if (!enabledElement || this._state.locked) {
207
+ return;
208
+ }
209
+ const { viewport, renderingEngine, FrameOfReferenceUID } = enabledElement;
210
+ if (!this._isPlanarViewport(viewport)) {
211
+ return;
212
+ }
213
+ this._state.cursorWorldPoint = [
214
+ currentPoints.world[0],
215
+ currentPoints.world[1],
216
+ currentPoints.world[2],
217
+ ];
218
+ this.setWorldPoint(this._resolveInteractionWorldPoint(viewport, currentPoints.world), {
219
+ sourceViewportId: viewport.id,
220
+ sourceRenderingEngineId: renderingEngine?.id,
221
+ frameOfReferenceUID: FrameOfReferenceUID,
222
+ });
223
+ };
224
+ this._endCallback = (evt) => {
225
+ const { element } = evt.detail;
226
+ this._isDragging = false;
227
+ this._deactivateModify(element);
228
+ resetElementCursor(element);
229
+ this._state.cursorWorldPoint = null;
230
+ this._renderLinkedViewports();
231
+ };
232
+ this.renderAnnotation = (enabledElement, svgDrawingHelper) => {
233
+ this._maybeAutoInitialize(enabledElement);
234
+ const { viewport } = enabledElement;
235
+ const { worldPoint, visible } = this._state;
236
+ const { showIn2D, pointColor, pointRadius, markerStyle, offSliceDisplay, offSliceToleranceMm, } = this.configuration;
237
+ if (!worldPoint || !visible || !showIn2D) {
238
+ return false;
239
+ }
240
+ if (!this._isPlanarViewport(viewport)) {
241
+ return false;
242
+ }
243
+ if (!this._isViewportLinked(viewport)) {
244
+ return false;
245
+ }
246
+ const plane = getViewportPlane(viewport);
247
+ if (!plane) {
248
+ return false;
249
+ }
250
+ const distanceMm = distancePointToPlane(worldPoint, plane);
251
+ const projectedPoint = projectPointToPlane(worldPoint, plane);
252
+ const canvasPoint = viewport.worldToCanvas(projectedPoint);
253
+ if (!canvasPoint || !canvasPoint.every((c) => Number.isFinite(c))) {
254
+ return false;
255
+ }
256
+ const toleranceMm = this._getEffectiveOffSliceTolerance(viewport, plane, offSliceToleranceMm);
257
+ const inSlice = Math.abs(distanceMm) <= toleranceMm;
258
+ if (!inSlice && offSliceDisplay === 'hide') {
259
+ return false;
260
+ }
261
+ const annotationUID = this._annotation?.annotationUID ?? `WorldCrosshair-${this.toolGroupId}`;
262
+ const highlighted = !!this._annotation?.highlighted;
263
+ const lineOptions = {
264
+ color: pointColor,
265
+ width: highlighted ? 2 : 1,
266
+ ...(inSlice ? {} : { lineDash: '3,3', strokeOpacity: 0.7 }),
267
+ };
268
+ if (markerStyle === 'point') {
269
+ drawCircleSvg(svgDrawingHelper, annotationUID, 'marker', canvasPoint, pointRadius, {
270
+ ...lineOptions,
271
+ fill: inSlice ? pointColor : 'transparent',
272
+ }, `${annotationUID}-marker`);
273
+ }
274
+ else {
275
+ const { clientWidth, clientHeight } = getDisplayedCanvasSize(viewport);
276
+ const gap = Math.max(this.configuration.centerGapRadius ?? 0, 0);
277
+ const [x, y] = canvasPoint;
278
+ const segments = [
279
+ ['marker-h1', [0, y], [x - gap, y]],
280
+ ['marker-h2', [x + gap, y], [clientWidth, y]],
281
+ ['marker-v1', [x, 0], [x, y - gap]],
282
+ ['marker-v2', [x, y + gap], [x, clientHeight]],
283
+ ];
284
+ segments.forEach(([uid, startPoint, endPoint]) => {
285
+ const length = Math.hypot(endPoint[0] - startPoint[0], endPoint[1] - startPoint[1]);
286
+ const towardsPositive = endPoint[0] >= startPoint[0] && endPoint[1] >= startPoint[1];
287
+ if (length < 1 || !towardsPositive) {
288
+ return;
289
+ }
290
+ drawLineSvg(svgDrawingHelper, annotationUID, uid, startPoint, endPoint, lineOptions, `${annotationUID}-${uid}`);
291
+ });
292
+ }
293
+ if (!inSlice && offSliceDisplay === 'projectedWithDistance') {
294
+ const sign = distanceMm >= 0 ? '+' : '-';
295
+ const label = `${sign}${Math.abs(distanceMm).toFixed(1)} mm`;
296
+ drawTextBoxSvg(svgDrawingHelper, annotationUID, 'marker-distance', [label], [canvasPoint[0] + pointRadius + 6, canvasPoint[1] + pointRadius + 6], {
297
+ color: pointColor,
298
+ });
299
+ }
300
+ return true;
301
+ };
302
+ }
303
+ setWorldPoint(worldPoint, options = {}) {
304
+ if (!isFinitePoint3(worldPoint)) {
305
+ return;
306
+ }
307
+ const newPoint = [
308
+ worldPoint[0],
309
+ worldPoint[1],
310
+ worldPoint[2],
311
+ ];
312
+ const previousPoint = this._state.worldPoint;
313
+ const changed = !previousPoint || vec3.distance(previousPoint, newPoint) > POINT_EPSILON;
314
+ if (options.sourceViewportId !== undefined) {
315
+ this._state.sourceViewportId = options.sourceViewportId;
316
+ }
317
+ if (options.sourceRenderingEngineId !== undefined) {
318
+ this._state.sourceRenderingEngineId = options.sourceRenderingEngineId;
319
+ }
320
+ const frameOfReferenceChanged = options.frameOfReferenceUID !== undefined &&
321
+ options.frameOfReferenceUID !== this._state.frameOfReferenceUID;
322
+ if (options.frameOfReferenceUID !== undefined) {
323
+ this._state.frameOfReferenceUID = options.frameOfReferenceUID;
324
+ }
325
+ if (changed) {
326
+ this._state.worldPoint = newPoint;
327
+ this._autoInitialized = true;
328
+ }
329
+ if (changed || frameOfReferenceChanged) {
330
+ this._syncAnnotation();
331
+ }
332
+ if (changed) {
333
+ if (!options.suppressEvents) {
334
+ triggerEvent(eventTarget, Events.WORLD_CROSSHAIR_POINT_CHANGED, this._getEventDetail());
335
+ }
336
+ }
337
+ const shouldJump = options.jump ?? this.configuration.jumpOnSet;
338
+ if (shouldJump) {
339
+ this._jumpLinkedViewports({ suppressEvents: true });
340
+ }
341
+ if (changed) {
342
+ this._update3DLines();
343
+ }
344
+ this._renderLinkedViewports();
345
+ }
346
+ getWorldPoint() {
347
+ const { worldPoint } = this._state;
348
+ return worldPoint ? [worldPoint[0], worldPoint[1], worldPoint[2]] : null;
349
+ }
350
+ clearWorldPoint(options = {}) {
351
+ if (!this._state.worldPoint) {
352
+ return;
353
+ }
354
+ this._state.worldPoint = null;
355
+ this._state.cursorWorldPoint = null;
356
+ this._syncAnnotation();
357
+ this._update3DLines();
358
+ if (!options.suppressEvents) {
359
+ triggerEvent(eventTarget, Events.WORLD_CROSSHAIR_POINT_CLEARED, this._getEventDetail());
360
+ }
361
+ this._renderLinkedViewports();
362
+ }
363
+ clearWorldCrosshair(options = {}) {
364
+ this.clearWorldPoint(options);
365
+ }
366
+ jumpLinkedViewportsToWorldPoint(options = {}) {
367
+ this._jumpLinkedViewports({ ...options, suppressEvents: false });
368
+ }
369
+ setVisibility(visible) {
370
+ if (this._state.visible === visible) {
371
+ return;
372
+ }
373
+ this._state.visible = visible;
374
+ this._update3DLines();
375
+ this._renderLinkedViewports();
376
+ }
377
+ setLocked(locked) {
378
+ this._state.locked = locked;
379
+ }
380
+ getState() {
381
+ return {
382
+ toolGroupId: this.toolGroupId,
383
+ ...this._state,
384
+ worldPoint: this.getWorldPoint(),
385
+ cursorWorldPoint: this._state.cursorWorldPoint
386
+ ? [...this._state.cursorWorldPoint]
387
+ : null,
388
+ };
389
+ }
390
+ onSetToolEnabled() {
391
+ this._syncAnnotation();
392
+ this._maybeAutoInitialize();
393
+ this._update3DLines();
394
+ this._renderLinkedViewports();
395
+ }
396
+ onSetToolActive() {
397
+ this.onSetToolEnabled();
398
+ }
399
+ onSetToolPassive() {
400
+ this.onSetToolEnabled();
401
+ }
402
+ onSetToolDisabled() {
403
+ this._isDragging = false;
404
+ this._autoInitialized = false;
405
+ state.isInteractingWithTool = false;
406
+ if (this._annotation?.annotationUID) {
407
+ removeAnnotation(this._annotation.annotationUID);
408
+ }
409
+ this._annotation = null;
410
+ this._get3DViewports().forEach((viewport) => removeWorldCrosshairLines3D(viewport, this._getMarkerUIDPrefix()));
411
+ this._renderLinkedViewports();
412
+ }
413
+ getHandleNearImagePoint(element, annotation, canvasCoords, proximity) {
414
+ if (this.isPointNearTool(element, annotation, canvasCoords, proximity, 'mouse')) {
415
+ return this._state.worldPoint;
416
+ }
417
+ }
418
+ _startDrag(element) {
419
+ this._isDragging = true;
420
+ this._activateModify(element);
421
+ }
422
+ _maybeAutoInitialize(enabledElement) {
423
+ if (this._autoInitialized ||
424
+ this._state.worldPoint ||
425
+ !this.configuration.autoInitializeOnEnable) {
426
+ return;
427
+ }
428
+ let viewport = enabledElement?.viewport;
429
+ let renderingEngineId = enabledElement?.renderingEngine?.id;
430
+ let frameOfReferenceUID = enabledElement?.FrameOfReferenceUID;
431
+ if (!viewport) {
432
+ for (const info of this._getViewportsInfo()) {
433
+ const candidate = getEnabledElementByIds(info.viewportId, info.renderingEngineId);
434
+ if (candidate && this._isPlanarViewport(candidate.viewport)) {
435
+ viewport = candidate.viewport;
436
+ renderingEngineId = candidate.renderingEngine?.id;
437
+ frameOfReferenceUID = candidate.FrameOfReferenceUID;
438
+ break;
439
+ }
440
+ }
441
+ }
442
+ if (!viewport || !this._isPlanarViewport(viewport)) {
443
+ return;
444
+ }
445
+ let focalPoint;
446
+ try {
447
+ ({ focalPoint } = getViewportICamera(viewport));
448
+ }
449
+ catch {
450
+ return;
451
+ }
452
+ if (!isFinitePoint3(focalPoint)) {
453
+ return;
454
+ }
455
+ this.setWorldPoint(focalPoint, {
456
+ sourceViewportId: viewport.id,
457
+ sourceRenderingEngineId: renderingEngineId,
458
+ frameOfReferenceUID: frameOfReferenceUID ?? viewport.getFrameOfReferenceUID?.(),
459
+ jump: false,
460
+ });
461
+ }
462
+ _jumpLinkedViewports(options = {}) {
463
+ const { worldPoint } = this._state;
464
+ if (!worldPoint) {
465
+ return;
466
+ }
467
+ const jumpMode = options.jumpMode ?? this.configuration.jumpMode;
468
+ const previousInteracting = state.isInteractingWithTool;
469
+ state.isInteractingWithTool = true;
470
+ try {
471
+ this._getLinkedViewports().forEach((viewport) => {
472
+ this._jumpViewportToPoint(viewport, worldPoint, jumpMode);
473
+ });
474
+ }
475
+ finally {
476
+ state.isInteractingWithTool = previousInteracting;
477
+ }
478
+ if (!options.suppressEvents) {
479
+ triggerEvent(eventTarget, Events.WORLD_CROSSHAIR_JUMPED_TO_POINT, this._getEventDetail());
480
+ }
481
+ }
482
+ _jumpViewportToPoint(viewport, worldPoint, jumpMode) {
483
+ if (!this._isPlanarViewport(viewport)) {
484
+ return;
485
+ }
486
+ let camera;
487
+ try {
488
+ camera = getViewportICamera(viewport);
489
+ }
490
+ catch {
491
+ return;
492
+ }
493
+ const { focalPoint, viewPlaneNormal } = camera;
494
+ if (!focalPoint || !viewPlaneNormal) {
495
+ return;
496
+ }
497
+ const delta = vec3.subtract(vec3.create(), worldPoint, focalPoint);
498
+ const normalDistance = vec3.dot(delta, viewPlaneNormal);
499
+ if (jumpMode === 'centered') {
500
+ if (!navigatePlanarViewportToPoint(viewport, worldPoint)) {
501
+ return;
502
+ }
503
+ viewport.setViewState({
504
+ anchorWorld: [worldPoint[0], worldPoint[1], worldPoint[2]],
505
+ anchorCanvas: [0.5, 0.5],
506
+ });
507
+ viewport.render();
508
+ return;
509
+ }
510
+ if (Math.abs(normalDistance) < JUMP_EPSILON) {
511
+ return;
512
+ }
513
+ const targetFocalPoint = vec3.scaleAndAdd(vec3.create(), focalPoint, viewPlaneNormal, normalDistance);
514
+ if (navigatePlanarViewportToPoint(viewport, [
515
+ targetFocalPoint[0],
516
+ targetFocalPoint[1],
517
+ targetFocalPoint[2],
518
+ ])) {
519
+ viewport.render();
520
+ }
521
+ }
522
+ _get3DViewports() {
523
+ const { threeDViewportIds } = this.configuration;
524
+ if (!threeDViewportIds?.length) {
525
+ return [];
526
+ }
527
+ return threeDViewportIds
528
+ .map((viewportId) => getEnabledElementByViewportId(viewportId)?.viewport)
529
+ .filter((viewport) => viewport &&
530
+ viewport.type === ViewportType.VOLUME_3D_NEXT &&
531
+ csUtils.isGenericViewport(viewport));
532
+ }
533
+ _update3DLines() {
534
+ const { showIn3D, threeDLineLengthMm, pointColor } = this.configuration;
535
+ const { worldPoint, visible } = this._state;
536
+ const uidPrefix = this._getMarkerUIDPrefix();
537
+ this._get3DViewports().forEach((viewport) => {
538
+ if (showIn3D && visible && worldPoint) {
539
+ updateWorldCrosshairLines3D(viewport, worldPoint, {
540
+ lineLengthMm: threeDLineLengthMm,
541
+ color: parseCssColorToRGB(pointColor),
542
+ uidPrefix,
543
+ });
544
+ }
545
+ else {
546
+ removeWorldCrosshairLines3D(viewport, uidPrefix);
547
+ }
548
+ });
549
+ }
550
+ _getMarkerUIDPrefix() {
551
+ return `WorldCrosshair-${this.toolGroupId}`;
552
+ }
553
+ _isPlanarViewport(viewport) {
554
+ return (viewport?.type === ViewportType.PLANAR_NEXT &&
555
+ csUtils.isGenericViewport(viewport));
556
+ }
557
+ _getViewportsInfo() {
558
+ return getToolGroup(this.toolGroupId)?.viewportsInfo ?? [];
559
+ }
560
+ _getLinkedViewports() {
561
+ const viewports = this._getViewportsInfo()
562
+ .map(({ viewportId, renderingEngineId }) => getEnabledElementByIds(viewportId, renderingEngineId)?.viewport)
563
+ .filter((viewport) => viewport && this._isPlanarViewport(viewport));
564
+ const { linkPolicy, explicitLinkedViewportIds } = this.configuration;
565
+ const { sourceViewportId, frameOfReferenceUID } = this._state;
566
+ if (linkPolicy === 'explicit') {
567
+ return viewports.filter((viewport) => viewport.id === sourceViewportId ||
568
+ explicitLinkedViewportIds?.includes(viewport.id));
569
+ }
570
+ if (linkPolicy === 'frameOfReferenceUID') {
571
+ if (!frameOfReferenceUID) {
572
+ return viewports;
573
+ }
574
+ return viewports.filter((viewport) => viewport.id === sourceViewportId ||
575
+ viewport.getFrameOfReferenceUID?.() === frameOfReferenceUID);
576
+ }
577
+ return viewports;
578
+ }
579
+ _isViewportLinked(viewport) {
580
+ if (!this._isPlanarViewport(viewport)) {
581
+ return false;
582
+ }
583
+ if (!this._getViewportsInfo().some(({ viewportId }) => viewportId === viewport.id)) {
584
+ return false;
585
+ }
586
+ const { linkPolicy, explicitLinkedViewportIds } = this.configuration;
587
+ const { sourceViewportId, frameOfReferenceUID } = this._state;
588
+ if (viewport.id === sourceViewportId) {
589
+ return true;
590
+ }
591
+ if (linkPolicy === 'explicit') {
592
+ return !!explicitLinkedViewportIds?.includes(viewport.id);
593
+ }
594
+ if (linkPolicy === 'frameOfReferenceUID') {
595
+ if (!frameOfReferenceUID) {
596
+ return true;
597
+ }
598
+ return viewport.getFrameOfReferenceUID?.() === frameOfReferenceUID;
599
+ }
600
+ return true;
601
+ }
602
+ _renderLinkedViewports() {
603
+ const viewportIds = this._getLinkedViewports().map((viewport) => viewport.id);
604
+ if (viewportIds.length) {
605
+ triggerAnnotationRenderForViewportIds(viewportIds);
606
+ }
607
+ }
608
+ _getMarkerCanvasPosition(viewport) {
609
+ const { worldPoint, visible } = this._state;
610
+ const { showIn2D, offSliceDisplay, offSliceToleranceMm } = this.configuration;
611
+ if (!worldPoint || !visible || !showIn2D) {
612
+ return null;
613
+ }
614
+ if (!this._isPlanarViewport(viewport)) {
615
+ return null;
616
+ }
617
+ if (!this._isViewportLinked(viewport)) {
618
+ return null;
619
+ }
620
+ const plane = getViewportPlane(viewport);
621
+ if (!plane) {
622
+ return null;
623
+ }
624
+ const distanceMm = distancePointToPlane(worldPoint, plane);
625
+ const toleranceMm = this._getEffectiveOffSliceTolerance(viewport, plane, offSliceToleranceMm);
626
+ if (Math.abs(distanceMm) > toleranceMm && offSliceDisplay === 'hide') {
627
+ return null;
628
+ }
629
+ const projected = projectPointToPlane(worldPoint, plane);
630
+ const canvasPoint = viewport.worldToCanvas(projected);
631
+ if (!canvasPoint || !canvasPoint.every((c) => Number.isFinite(c))) {
632
+ return null;
633
+ }
634
+ return canvasPoint;
635
+ }
636
+ _resolveInteractionWorldPoint(viewport, world) {
637
+ if (!this.configuration.snapToSlabIntensity) {
638
+ return world;
639
+ }
640
+ return pickIntensityPointInSlab(viewport, world) ?? world;
641
+ }
642
+ _getSourceSlabThicknessMm(viewport) {
643
+ if (!csUtils.viewportSupportsDisplaySetPresentation(viewport)) {
644
+ return 0;
645
+ }
646
+ const dataId = viewport.getSourceDataId();
647
+ if (!dataId) {
648
+ return 0;
649
+ }
650
+ const presentation = viewport.getDisplaySetPresentation(dataId);
651
+ const slabThickness = presentation?.slabThickness;
652
+ return Number.isFinite(slabThickness) && slabThickness > 0
653
+ ? slabThickness
654
+ : 0;
655
+ }
656
+ _getEffectiveOffSliceTolerance(viewport, plane, baseToleranceMm) {
657
+ const slabThickness = this._getSourceSlabThicknessMm(viewport);
658
+ const toleranceMm = Math.max(baseToleranceMm, slabThickness > 0 ? slabThickness / 2 + 1e-2 : 0);
659
+ let spacing;
660
+ try {
661
+ spacing = viewport.getImageData?.()
662
+ ?.spacing;
663
+ }
664
+ catch {
665
+ spacing = undefined;
666
+ }
667
+ if (!spacing || spacing.length !== 3) {
668
+ return toleranceMm;
669
+ }
670
+ const [nx, ny, nz] = plane.normal;
671
+ const spacingAlongNormal = Math.abs(nx) * spacing[0] +
672
+ Math.abs(ny) * spacing[1] +
673
+ Math.abs(nz) * spacing[2];
674
+ if (!Number.isFinite(spacingAlongNormal) || spacingAlongNormal <= 0) {
675
+ return toleranceMm;
676
+ }
677
+ return Math.max(toleranceMm, spacingAlongNormal / 2 + 1e-2);
678
+ }
679
+ _syncAnnotation() {
680
+ const { worldPoint, frameOfReferenceUID } = this._state;
681
+ if (!worldPoint) {
682
+ if (this._annotation?.annotationUID) {
683
+ removeAnnotation(this._annotation.annotationUID);
684
+ }
685
+ this._annotation = null;
686
+ return;
687
+ }
688
+ if (this._annotation &&
689
+ frameOfReferenceUID &&
690
+ this._annotation.metadata.FrameOfReferenceUID !== frameOfReferenceUID) {
691
+ removeAnnotation(this._annotation.annotationUID);
692
+ this._annotation = null;
693
+ }
694
+ if (this._annotation) {
695
+ this._annotation.data.handles.points = [
696
+ [worldPoint[0], worldPoint[1], worldPoint[2]],
697
+ ];
698
+ this._annotation.invalidated = true;
699
+ return;
700
+ }
701
+ if (!frameOfReferenceUID) {
702
+ return;
703
+ }
704
+ const annotation = {
705
+ highlighted: false,
706
+ invalidated: true,
707
+ metadata: {
708
+ toolName: this.getToolName(),
709
+ FrameOfReferenceUID: frameOfReferenceUID,
710
+ },
711
+ data: {
712
+ handles: {
713
+ points: [[worldPoint[0], worldPoint[1], worldPoint[2]]],
714
+ },
715
+ },
716
+ };
717
+ addAnnotation(annotation, frameOfReferenceUID);
718
+ this._annotation = annotation;
719
+ }
720
+ _getAnnotationOrDetached() {
721
+ if (this._annotation) {
722
+ return this._annotation;
723
+ }
724
+ return {
725
+ annotationUID: csUtils.uuidv4(),
726
+ highlighted: false,
727
+ invalidated: false,
728
+ metadata: {
729
+ toolName: this.getToolName(),
730
+ },
731
+ data: {
732
+ handles: { points: [] },
733
+ },
734
+ };
735
+ }
736
+ _getEventDetail() {
737
+ const { sourceViewportId, sourceRenderingEngineId, frameOfReferenceUID } = this._state;
738
+ return {
739
+ toolGroupId: this.toolGroupId,
740
+ worldPoint: this.getWorldPoint(),
741
+ sourceViewportId,
742
+ sourceRenderingEngineId,
743
+ frameOfReferenceUID,
744
+ };
745
+ }
746
+ }
747
+ WorldCrosshairTool.toolLabel = 'Reference Point';
748
+ WorldCrosshairTool.toolName = 'WorldCrosshair';
749
+ export default WorldCrosshairTool;