@cornerstonejs/tools 1.70.14 → 1.71.0

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 (65) hide show
  1. package/dist/cjs/index.d.ts +2 -2
  2. package/dist/cjs/index.js +3 -2
  3. package/dist/cjs/index.js.map +1 -1
  4. package/dist/cjs/synchronizers/synchronizers/createVOISynchronizer.js.map +1 -1
  5. package/dist/cjs/tools/OrientationMarkerTool.js +5 -1
  6. package/dist/cjs/tools/OrientationMarkerTool.js.map +1 -1
  7. package/dist/cjs/tools/SculptorTool/CircleSculptCursor.d.ts +20 -0
  8. package/dist/cjs/tools/SculptorTool/CircleSculptCursor.js +110 -0
  9. package/dist/cjs/tools/SculptorTool/CircleSculptCursor.js.map +1 -0
  10. package/dist/cjs/tools/SculptorTool.d.ts +41 -0
  11. package/dist/cjs/tools/SculptorTool.js +298 -0
  12. package/dist/cjs/tools/SculptorTool.js.map +1 -0
  13. package/dist/cjs/tools/distancePointToContour.d.ts +3 -0
  14. package/dist/cjs/tools/distancePointToContour.js +24 -0
  15. package/dist/cjs/tools/distancePointToContour.js.map +1 -0
  16. package/dist/cjs/tools/index.d.ts +2 -1
  17. package/dist/cjs/tools/index.js +3 -1
  18. package/dist/cjs/tools/index.js.map +1 -1
  19. package/dist/cjs/types/ISculptToolShape.d.ts +12 -0
  20. package/dist/cjs/types/ISculptToolShape.js +3 -0
  21. package/dist/cjs/types/ISculptToolShape.js.map +1 -0
  22. package/dist/cjs/types/index.d.ts +2 -1
  23. package/dist/esm/index.js +2 -2
  24. package/dist/esm/index.js.map +1 -1
  25. package/dist/esm/synchronizers/synchronizers/createVOISynchronizer.js.map +1 -1
  26. package/dist/esm/tools/OrientationMarkerTool.js +5 -1
  27. package/dist/esm/tools/OrientationMarkerTool.js.map +1 -1
  28. package/dist/esm/tools/SculptorTool/CircleSculptCursor.js +108 -0
  29. package/dist/esm/tools/SculptorTool/CircleSculptCursor.js.map +1 -0
  30. package/dist/esm/tools/SculptorTool.js +290 -0
  31. package/dist/esm/tools/SculptorTool.js.map +1 -0
  32. package/dist/esm/tools/distancePointToContour.js +19 -0
  33. package/dist/esm/tools/distancePointToContour.js.map +1 -0
  34. package/dist/esm/tools/index.js +2 -1
  35. package/dist/esm/tools/index.js.map +1 -1
  36. package/dist/esm/types/ISculptToolShape.js +2 -0
  37. package/dist/esm/types/ISculptToolShape.js.map +1 -0
  38. package/dist/types/index.d.ts +2 -2
  39. package/dist/types/index.d.ts.map +1 -1
  40. package/dist/types/synchronizers/synchronizers/createVOISynchronizer.d.ts.map +1 -1
  41. package/dist/types/tools/OrientationMarkerTool.d.ts.map +1 -1
  42. package/dist/types/tools/SculptorTool/CircleSculptCursor.d.ts +21 -0
  43. package/dist/types/tools/SculptorTool/CircleSculptCursor.d.ts.map +1 -0
  44. package/dist/types/tools/SculptorTool.d.ts +42 -0
  45. package/dist/types/tools/SculptorTool.d.ts.map +1 -0
  46. package/dist/types/tools/distancePointToContour.d.ts +4 -0
  47. package/dist/types/tools/distancePointToContour.d.ts.map +1 -0
  48. package/dist/types/tools/index.d.ts +2 -1
  49. package/dist/types/tools/index.d.ts.map +1 -1
  50. package/dist/types/types/ISculptToolShape.d.ts +13 -0
  51. package/dist/types/types/ISculptToolShape.d.ts.map +1 -0
  52. package/dist/types/types/index.d.ts +2 -1
  53. package/dist/types/types/index.d.ts.map +1 -1
  54. package/dist/umd/index.js +1 -1
  55. package/dist/umd/index.js.map +1 -1
  56. package/package.json +3 -3
  57. package/src/index.ts +2 -0
  58. package/src/synchronizers/synchronizers/createVOISynchronizer.ts +5 -2
  59. package/src/tools/OrientationMarkerTool.ts +5 -1
  60. package/src/tools/SculptorTool/CircleSculptCursor.ts +212 -0
  61. package/src/tools/SculptorTool.ts +604 -0
  62. package/src/tools/distancePointToContour.ts +35 -0
  63. package/src/tools/index.ts +2 -0
  64. package/src/types/ISculptToolShape.ts +63 -0
  65. package/src/types/index.ts +2 -0
@@ -0,0 +1,604 @@
1
+ import { getEnabledElement } from '@cornerstonejs/core';
2
+ import type { Types } from '@cornerstonejs/core';
3
+ import { BaseTool } from './base';
4
+ import { getAnnotations } from '../stateManagement';
5
+ import {
6
+ EventTypes,
7
+ PublicToolProps,
8
+ ToolProps,
9
+ SVGDrawingHelper,
10
+ ContourAnnotation,
11
+ } from '../types';
12
+ import { point } from '../utilities/math';
13
+ import { Events, ToolModes, AnnotationStyleStates } from '../enums';
14
+ import { ToolGroupManager } from '../store';
15
+ import { triggerAnnotationRenderForViewportIds } from '../utilities/triggerAnnotationRenderForViewportIds';
16
+ import {
17
+ hideElementCursor,
18
+ resetElementCursor,
19
+ } from '../cursors/elementCursor';
20
+ import { StyleSpecifier } from '../types/AnnotationStyle';
21
+ import { getStyleProperty } from '../stateManagement/annotation/config/helpers';
22
+ import { triggerAnnotationModified } from '../stateManagement/annotation/helpers/state';
23
+ import CircleSculptCursor from './SculptorTool/CircleSculptCursor';
24
+ import type { ISculptToolShape } from '../types/ISculptToolShape';
25
+ import { distancePointToContour } from './distancePointToContour';
26
+
27
+ export type SculptData = {
28
+ mousePoint: Types.Point3;
29
+ mouseCanvasPoint: Types.Point2;
30
+ points: Array<Types.Point3>;
31
+ maxSpacing: number;
32
+ element: HTMLDivElement;
33
+ };
34
+
35
+ type CommonData = {
36
+ activeAnnotationUID: string | null;
37
+ viewportIdsToRender: any[];
38
+ isEditingOpenContour: boolean;
39
+ canvasLocation: Types.Point2 | undefined;
40
+ };
41
+
42
+ /**
43
+ * This tool allows modifying the contour data for planar freehand by sculpting
44
+ * it externally using another shape to push the contour in one direction or the other.
45
+ */
46
+ class SculptorTool extends BaseTool {
47
+ static toolName: string;
48
+ registeredShapes = new Map();
49
+ private isActive = false;
50
+ private selectedShape: string;
51
+ private commonData: CommonData = {
52
+ activeAnnotationUID: null,
53
+ viewportIdsToRender: [],
54
+ isEditingOpenContour: false,
55
+ canvasLocation: undefined,
56
+ };
57
+ private sculptData?: SculptData;
58
+
59
+ constructor(
60
+ toolProps: PublicToolProps = {},
61
+ defaultToolProps: ToolProps = {
62
+ supportedInteractionTypes: ['Mouse', 'Touch'],
63
+ configuration: {
64
+ minSpacing: 1,
65
+ referencedToolNames: [
66
+ 'PlanarFreehandROI',
67
+ 'PlanarFreehandContourSegmentationTool',
68
+ ],
69
+ toolShape: 'circle',
70
+ referencedToolName: 'PlanarFreehandROI',
71
+ },
72
+ }
73
+ ) {
74
+ super(toolProps, defaultToolProps);
75
+ this.registerShapes(CircleSculptCursor.shapeName, CircleSculptCursor);
76
+ this.setToolShape(this.configuration.toolShape);
77
+ }
78
+
79
+ /**
80
+ * Register different tool shapes for sculptor tool
81
+ * @param shapeName name of shape
82
+ * @param shapeClass shape class
83
+ */
84
+ registerShapes<T extends ISculptToolShape>(
85
+ shapeName: string,
86
+ shapeClass: new () => T
87
+ ): void {
88
+ const shape = new shapeClass();
89
+ this.registeredShapes.set(shapeName, shape);
90
+ }
91
+
92
+ preMouseDownCallback = (evt: EventTypes.InteractionEventType): boolean => {
93
+ const eventData = evt.detail;
94
+ const element = eventData.element;
95
+
96
+ this.configureToolSize(evt);
97
+ this.selectFreehandTool(eventData);
98
+
99
+ if (this.commonData.activeAnnotationUID === null) {
100
+ return;
101
+ }
102
+
103
+ this.isActive = true;
104
+
105
+ hideElementCursor(element);
106
+ this.activateModify(element);
107
+ return true;
108
+ };
109
+
110
+ mouseMoveCallback = (evt: EventTypes.InteractionEventType): void => {
111
+ if (this.mode === ToolModes.Active) {
112
+ this.configureToolSize(evt);
113
+ this.updateCursor(evt);
114
+ } else {
115
+ this.commonData.canvasLocation = undefined;
116
+ }
117
+ };
118
+
119
+ /**
120
+ * Sculpts the freehand ROI with freehandSculpter tool, moving,
121
+ * adding and removing handles as necessary.
122
+ *
123
+ * @param eventData - Data object associated with the event.
124
+ * @param points - Array of points
125
+ */
126
+ protected sculpt(eventData: any, points: Array<Types.Point3>): void {
127
+ const config = this.configuration;
128
+ const element = eventData.element;
129
+
130
+ const enabledElement = getEnabledElement(element);
131
+ const { viewport } = enabledElement;
132
+ const cursorShape = this.registeredShapes.get(this.selectedShape);
133
+
134
+ this.sculptData = {
135
+ mousePoint: eventData.currentPoints.world,
136
+ mouseCanvasPoint: eventData.currentPoints.canvas,
137
+ points,
138
+ maxSpacing: cursorShape.getMaxSpacing(config.minSpacing),
139
+ element: element,
140
+ };
141
+
142
+ const pushedHandles = cursorShape.pushHandles(viewport, this.sculptData);
143
+
144
+ if (pushedHandles.first !== undefined) {
145
+ this.insertNewHandles(pushedHandles);
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Interpolates or fills in points between two points within a specified
151
+ * maximum spacing constraint.
152
+ */
153
+ protected interpolatePointsWithinMaxSpacing(
154
+ i: number,
155
+ points: Array<Types.Point3>,
156
+ indicesToInsertAfter: Array<number>,
157
+ maxSpacing: number
158
+ ): void {
159
+ const { element } = this.sculptData;
160
+ const enabledElement = getEnabledElement(element);
161
+ const { viewport } = enabledElement;
162
+ const nextHandleIndex = contourIndex(i + 1, points.length);
163
+
164
+ const currentCanvasPoint = viewport.worldToCanvas(points[i]);
165
+ const nextCanvasPoint = viewport.worldToCanvas(points[nextHandleIndex]);
166
+
167
+ const distanceToNextHandle = point.distanceToPoint(
168
+ currentCanvasPoint,
169
+ nextCanvasPoint
170
+ );
171
+
172
+ if (distanceToNextHandle > maxSpacing) {
173
+ indicesToInsertAfter.push(i);
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Updates cursor size
179
+ *
180
+ * @param evt - The event
181
+ */
182
+ private updateCursor(evt: EventTypes.InteractionEventType): void {
183
+ const eventData = evt.detail;
184
+ const element = eventData.element;
185
+
186
+ const enabledElement = getEnabledElement(element);
187
+ const { renderingEngine, viewport } = enabledElement;
188
+
189
+ this.commonData.viewportIdsToRender = [viewport.id];
190
+
191
+ const annotations = this.filterSculptableAnnotationsForElement(element);
192
+
193
+ if (!annotations?.length) {
194
+ return;
195
+ }
196
+
197
+ const activeAnnotation = annotations.find(
198
+ (annotation) =>
199
+ annotation.annotationUID === this.commonData.activeAnnotationUID
200
+ );
201
+
202
+ this.commonData.canvasLocation = eventData.currentPoints.canvas;
203
+
204
+ if (this.isActive) {
205
+ activeAnnotation.highlighted = true;
206
+ } else {
207
+ const cursorShape = this.registeredShapes.get(this.selectedShape);
208
+ const canvasCoords = eventData.currentPoints.canvas;
209
+ cursorShape.updateToolSize(canvasCoords, viewport, activeAnnotation);
210
+ }
211
+
212
+ triggerAnnotationRenderForViewportIds(
213
+ renderingEngine,
214
+ this.commonData.viewportIdsToRender
215
+ );
216
+ }
217
+
218
+ /**
219
+ * Returns sculptable freehand ROI annotations
220
+ *
221
+ * @param element - The viewport element
222
+ */
223
+ private filterSculptableAnnotationsForElement(
224
+ element: HTMLDivElement
225
+ ): ContourAnnotation[] {
226
+ const config = this.configuration;
227
+ const enabledElement = getEnabledElement(element);
228
+ const { renderingEngineId, viewportId } = enabledElement;
229
+ const sculptableAnnotations = [];
230
+
231
+ const toolGroup = ToolGroupManager.getToolGroupForViewport(
232
+ viewportId,
233
+ renderingEngineId
234
+ );
235
+
236
+ const toolInstance = toolGroup.getToolInstance(config.referencedToolName);
237
+
238
+ config.referencedToolNames.forEach((referencedToolName: string) => {
239
+ const annotations = getAnnotations(referencedToolName, element);
240
+ if (annotations) {
241
+ sculptableAnnotations.push(...annotations);
242
+ }
243
+ });
244
+
245
+ return toolInstance.filterInteractableAnnotationsForElement(
246
+ element,
247
+ sculptableAnnotations
248
+ );
249
+ }
250
+
251
+ /** Just pass the tool size interaction onto the internal tool size */
252
+ private configureToolSize(evt: EventTypes.InteractionEventType): void {
253
+ const cursorShape = this.registeredShapes.get(this.selectedShape);
254
+ cursorShape.configureToolSize(evt);
255
+ }
256
+
257
+ /**
258
+ * Inserts additional handles in sparsely sampled regions of the contour
259
+ */
260
+ private insertNewHandles(pushedHandles: {
261
+ first: number;
262
+ last: number | undefined;
263
+ }): void {
264
+ const indicesToInsertAfter = this.findNewHandleIndices(pushedHandles);
265
+ let newIndexModifier = 0;
266
+ for (let i = 0; i < indicesToInsertAfter?.length; i++) {
267
+ const insertIndex = indicesToInsertAfter[i] + 1 + newIndexModifier;
268
+
269
+ this.insertHandleRadially(insertIndex);
270
+ newIndexModifier++;
271
+ }
272
+ }
273
+
274
+ /**
275
+ * Returns an array of indicies that describe where new handles should be inserted
276
+ *
277
+ * @param pushedHandles - The first and last handles that were pushed.
278
+ */
279
+ private findNewHandleIndices(pushedHandles: {
280
+ first: number | undefined;
281
+ last: number | undefined;
282
+ }): Array<number> {
283
+ const { points, maxSpacing } = this.sculptData;
284
+ const indicesToInsertAfter = [];
285
+
286
+ for (let i = pushedHandles.first; i <= pushedHandles.last; i++) {
287
+ this.interpolatePointsWithinMaxSpacing(
288
+ i,
289
+ points,
290
+ indicesToInsertAfter,
291
+ maxSpacing
292
+ );
293
+ }
294
+
295
+ return indicesToInsertAfter;
296
+ }
297
+
298
+ /**
299
+ * Inserts a handle on the surface of the circle defined by toolSize and the mousePoint.
300
+ *
301
+ * @param insertIndex - The index to insert the new handle.
302
+ */
303
+ private insertHandleRadially(insertIndex: number): void {
304
+ const { points } = this.sculptData;
305
+
306
+ if (
307
+ insertIndex > points.length - 1 &&
308
+ this.commonData.isEditingOpenContour
309
+ ) {
310
+ return;
311
+ }
312
+
313
+ const cursorShape = this.registeredShapes.get(this.selectedShape);
314
+
315
+ const previousIndex = insertIndex - 1;
316
+ const nextIndex = contourIndex(insertIndex, points.length);
317
+ const insertPosition = cursorShape.getInsertPosition(
318
+ previousIndex,
319
+ nextIndex,
320
+ this.sculptData
321
+ );
322
+ const handleData = insertPosition;
323
+
324
+ points.splice(insertIndex, 0, handleData);
325
+ }
326
+
327
+ /**
328
+ * Select the freehand tool to be edited
329
+ *
330
+ * @param eventData - Data object associated with the event.
331
+ */
332
+ private selectFreehandTool(eventData: any): void {
333
+ const closestAnnotationUID =
334
+ this.getClosestFreehandToolOnElement(eventData);
335
+
336
+ if (closestAnnotationUID === undefined) {
337
+ return;
338
+ }
339
+
340
+ this.commonData.activeAnnotationUID = closestAnnotationUID;
341
+ }
342
+
343
+ /**
344
+ * Finds the nearest handle to the mouse cursor for all freehand
345
+ * data on the element.
346
+ *
347
+ * @param eventData - Data object associated with the event.
348
+ */
349
+ private getClosestFreehandToolOnElement(eventData: any): string {
350
+ const { element } = eventData;
351
+ const enabledElement = getEnabledElement(element);
352
+ const { viewport } = enabledElement;
353
+ const config = this.configuration;
354
+
355
+ const annotations = this.filterSculptableAnnotationsForElement(element);
356
+
357
+ if (!annotations?.length) {
358
+ return;
359
+ }
360
+
361
+ const canvasPoints = eventData.currentPoints.canvas;
362
+
363
+ const closest = {
364
+ distance: Infinity,
365
+ toolIndex: undefined,
366
+ annotationUID: undefined,
367
+ };
368
+
369
+ for (let i = 0; i < annotations?.length; i++) {
370
+ if (annotations[i].isLocked || !annotations[i].isVisible) {
371
+ continue;
372
+ }
373
+
374
+ const distanceFromTool = distancePointToContour(
375
+ viewport,
376
+ annotations[i],
377
+ canvasPoints
378
+ );
379
+
380
+ if (distanceFromTool === -1) {
381
+ continue;
382
+ }
383
+
384
+ if (distanceFromTool < closest.distance) {
385
+ closest.distance = distanceFromTool;
386
+ closest.toolIndex = i;
387
+ closest.annotationUID = annotations[i].annotationUID;
388
+ }
389
+ }
390
+
391
+ this.commonData.isEditingOpenContour =
392
+ !annotations[closest.toolIndex].data.contour.closed;
393
+
394
+ config.referencedToolName =
395
+ annotations[closest.toolIndex].metadata.toolName;
396
+
397
+ return closest.annotationUID;
398
+ }
399
+
400
+ /**
401
+ * Event handler for MOUSE_UP during the active loop.
402
+ *
403
+ * @param evt - The event
404
+ */
405
+ private endCallback = (
406
+ evt: EventTypes.MouseUpEventType | EventTypes.MouseClickEventType
407
+ ): void => {
408
+ const eventData = evt.detail;
409
+ const { element } = eventData;
410
+ const config = this.configuration;
411
+ const enabledElement = getEnabledElement(element);
412
+
413
+ this.isActive = false;
414
+ this.deactivateModify(element);
415
+ resetElementCursor(element);
416
+
417
+ const { renderingEngineId, viewportId } = enabledElement;
418
+
419
+ const toolGroup = ToolGroupManager.getToolGroupForViewport(
420
+ viewportId,
421
+ renderingEngineId
422
+ );
423
+
424
+ const toolInstance = toolGroup.getToolInstance(config.referencedToolName);
425
+
426
+ const annotations = this.filterSculptableAnnotationsForElement(element);
427
+
428
+ const activeAnnotation = annotations.find(
429
+ (annotation) =>
430
+ annotation.annotationUID === this.commonData.activeAnnotationUID
431
+ );
432
+
433
+ if (toolInstance.configuration.calculateStats) {
434
+ activeAnnotation.invalidated = true;
435
+ }
436
+
437
+ triggerAnnotationModified(activeAnnotation, element);
438
+ };
439
+
440
+ /**
441
+ * Event handler for MOUSE_DRAG during the active loop.
442
+ *
443
+ * @param evt - The event
444
+ */
445
+ private dragCallback = (evt: EventTypes.InteractionEventType): void => {
446
+ const eventData = evt.detail;
447
+ const element = eventData.element;
448
+
449
+ this.updateCursor(evt);
450
+
451
+ const annotations = this.filterSculptableAnnotationsForElement(element);
452
+ const activeAnnotation = annotations.find(
453
+ (annotation) =>
454
+ annotation.annotationUID === this.commonData.activeAnnotationUID
455
+ );
456
+
457
+ if (!annotations?.length || !this.isActive) {
458
+ return;
459
+ }
460
+
461
+ const points = activeAnnotation.data.contour.polyline;
462
+
463
+ this.sculpt(eventData, points);
464
+ };
465
+
466
+ /**
467
+ * Attaches event listeners to the element such that is is visible, modifiable, and new data can be created.
468
+ * @param element - - The viewport element to attach event listeners to.
469
+ */
470
+ protected activateModify(element: HTMLDivElement): void {
471
+ element.addEventListener(
472
+ Events.MOUSE_UP,
473
+ this.endCallback as EventListener
474
+ );
475
+ element.addEventListener(
476
+ Events.MOUSE_CLICK,
477
+ this.endCallback as EventListener
478
+ );
479
+ element.addEventListener(
480
+ Events.MOUSE_DRAG,
481
+ this.dragCallback as EventListener
482
+ );
483
+ element.addEventListener(
484
+ Events.TOUCH_TAP,
485
+ this.endCallback as EventListener
486
+ );
487
+ element.addEventListener(
488
+ Events.TOUCH_END,
489
+ this.endCallback as EventListener
490
+ );
491
+ element.addEventListener(
492
+ Events.TOUCH_DRAG,
493
+ this.dragCallback as EventListener
494
+ );
495
+ }
496
+
497
+ /**
498
+ * Removes event listeners from the element.
499
+ * @param element - The viewport element to remove event listeners from.
500
+ */
501
+ protected deactivateModify(element: HTMLDivElement): void {
502
+ element.removeEventListener(
503
+ Events.MOUSE_UP,
504
+ this.endCallback as EventListener
505
+ );
506
+ element.removeEventListener(
507
+ Events.MOUSE_CLICK,
508
+ this.endCallback as EventListener
509
+ );
510
+ element.removeEventListener(
511
+ Events.MOUSE_DRAG,
512
+ this.dragCallback as EventListener
513
+ );
514
+ element.removeEventListener(
515
+ Events.TOUCH_TAP,
516
+ this.endCallback as EventListener
517
+ );
518
+ element.removeEventListener(
519
+ Events.TOUCH_END,
520
+ this.endCallback as EventListener
521
+ );
522
+ element.removeEventListener(
523
+ Events.TOUCH_DRAG,
524
+ this.dragCallback as EventListener
525
+ );
526
+ }
527
+
528
+ /**
529
+ * Sets the tool shape to the specified tool
530
+ */
531
+ public setToolShape(toolShape: string): void {
532
+ this.selectedShape =
533
+ this.registeredShapes.get(toolShape) ?? CircleSculptCursor.shapeName;
534
+ }
535
+
536
+ /**
537
+ * Renders the cursor annotation on screen so that the user can choose the
538
+ * annotation size.
539
+ */
540
+ renderAnnotation(
541
+ enabledElement: Types.IEnabledElement,
542
+ svgDrawingHelper: SVGDrawingHelper
543
+ ): void {
544
+ const { viewport } = enabledElement;
545
+ const { element } = viewport;
546
+
547
+ const viewportIdsToRender = this.commonData.viewportIdsToRender;
548
+
549
+ if (
550
+ !this.commonData.canvasLocation ||
551
+ this.mode !== ToolModes.Active ||
552
+ !viewportIdsToRender.includes(viewport.id)
553
+ ) {
554
+ return;
555
+ }
556
+
557
+ const annotations = this.filterSculptableAnnotationsForElement(element);
558
+
559
+ if (!annotations?.length) {
560
+ return;
561
+ }
562
+
563
+ const styleSpecifier: StyleSpecifier = {
564
+ toolGroupId: this.toolGroupId,
565
+ toolName: this.getToolName(),
566
+ viewportId: enabledElement.viewport.id,
567
+ };
568
+
569
+ let color = getStyleProperty(
570
+ 'color',
571
+ styleSpecifier,
572
+ AnnotationStyleStates.Default,
573
+ this.mode
574
+ );
575
+
576
+ if (this.isActive) {
577
+ color = getStyleProperty(
578
+ 'color',
579
+ styleSpecifier,
580
+ AnnotationStyleStates.Highlighted,
581
+ this.mode
582
+ );
583
+ }
584
+
585
+ const cursorShape = this.registeredShapes.get(this.selectedShape);
586
+
587
+ cursorShape.renderShape(svgDrawingHelper, this.commonData.canvasLocation, {
588
+ color,
589
+ });
590
+ }
591
+ }
592
+
593
+ /**
594
+ * Function calculates the index of a contour given a position `i` and the length of the contour.
595
+ * It ensures that the resulting index is within the bounds of the contour by wrapping around if needed.
596
+ * This function is useful for obtaining neighboring indices or other related indices within the contour,
597
+ * such as for navigating or accessing elements in a circular or looped structure
598
+ */
599
+ export const contourIndex = (i: number, length: number): number => {
600
+ return (i + length) % length;
601
+ };
602
+
603
+ SculptorTool.toolName = 'SculptorTool';
604
+ export default SculptorTool;
@@ -0,0 +1,35 @@
1
+ import type { Types } from '@cornerstonejs/core';
2
+ import { ContourAnnotationData } from '../types';
3
+ import { point } from '../utilities/math';
4
+
5
+ /**
6
+ * Calculates the shortest distance from the provided point to any contour
7
+ * point in the given annotation.
8
+ */
9
+ export const distancePointToContour = (
10
+ viewport: Types.IViewport,
11
+ annotation: ContourAnnotationData,
12
+ coords: Types.Point2
13
+ ): number => {
14
+ if (!annotation?.data?.contour?.polyline?.length) {
15
+ return;
16
+ }
17
+ const { polyline } = annotation.data.contour;
18
+ const { length } = polyline;
19
+
20
+ let distance = Infinity;
21
+
22
+ for (let i = 0; i < length; i++) {
23
+ const canvasPoint = viewport.worldToCanvas(polyline[i]);
24
+ const distanceToPoint = point.distanceToPoint(canvasPoint, coords);
25
+
26
+ distance = Math.min(distance, distanceToPoint);
27
+ }
28
+
29
+ // If an error caused distance not to be calculated, return undefined.
30
+ if (distance === Infinity || isNaN(distance)) {
31
+ return;
32
+ }
33
+
34
+ return distance;
35
+ };
@@ -17,6 +17,7 @@ import SegmentationIntersectionTool from './SegmentationIntersectionTool';
17
17
  import ReferenceCursors from './ReferenceCursors';
18
18
  import ReferenceLines from './ReferenceLinesTool';
19
19
  import ScaleOverlayTool from './ScaleOverlayTool';
20
+ import SculptorTool from './SculptorTool';
20
21
 
21
22
  // Annotation tools
22
23
  import BidirectionalTool from './annotation/BidirectionalTool';
@@ -110,5 +111,6 @@ export {
110
111
  PaintFillTool,
111
112
  ScaleOverlayTool,
112
113
  OrientationMarkerTool,
114
+ SculptorTool,
113
115
  SegmentSelectTool,
114
116
  };
@@ -0,0 +1,63 @@
1
+ import type { Types } from '@cornerstonejs/core';
2
+ import type { SVGDrawingHelper, EventTypes, ContourAnnotation } from '.';
3
+ import type { PushedHandles } from '../tools/SculptorTool/CircleSculptCursor';
4
+ import type { SculptData } from '../tools/SculptorTool';
5
+
6
+ /**
7
+ * This interface defines a contract for implementing various shapes within the sculptor tool.
8
+ * Classes such as `CircleSculptCursor` adhere to this interface,
9
+ * providing specific implementations for sculptor tools to utilize the shape
10
+ * during sculpting operations.
11
+ */
12
+ export interface ISculptToolShape {
13
+ /**
14
+ * Used to render shapes supported for sculptor tool
15
+ *
16
+ * @param svgDrawingHelper - The svgDrawingHelper providing the context for drawing.
17
+ * @param canvasLocation - Current canvas location in canvas index coordinates
18
+ * @param options - Options for drawing shapes
19
+ */
20
+ renderShape(
21
+ svgDrawingHelper: SVGDrawingHelper,
22
+ canvasLocation: Types.Point2,
23
+ options
24
+ ): void;
25
+ /**
26
+ * Pushes the points radially away from the mouse if they are
27
+ * contained within the shape defined by the freehandSculpter tool
28
+ */
29
+ pushHandles(viewport: Types.IViewport, sculptData: SculptData): PushedHandles;
30
+
31
+ /**
32
+ * Function configures the tool size
33
+ */
34
+ configureToolSize(evt: EventTypes.InteractionEventType): void;
35
+
36
+ /**
37
+ * Updates the tool size
38
+ * @param canvasCoords - Current canvas points
39
+ */
40
+ updateToolSize(
41
+ canvasCoords: Types.Point2,
42
+ viewport: Types.IViewport,
43
+ activeAnnotation: ContourAnnotation
44
+ ): void;
45
+
46
+ /**
47
+ * Function returns max spacing between two handles
48
+ * @param minSpacing -
49
+ */
50
+ getMaxSpacing(minSpacing: number): number;
51
+
52
+ /**
53
+ * Function returns the the position to insert new handle
54
+ * @param previousIndex - Previous handle index
55
+ * @param nextIndex - Next handle index
56
+ * @param sculptData - Sculpt data
57
+ */
58
+ getInsertPosition(
59
+ previousIndex: number,
60
+ nextIndex: number,
61
+ sculptData: SculptData
62
+ ): Types.Point3;
63
+ }