copper3d 3.0.1 → 3.0.2

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 (98) hide show
  1. package/README.md +195 -28
  2. package/dist/Utils/segmentation/CommToolsData.d.ts +24 -9
  3. package/dist/Utils/segmentation/CommToolsData.js +65 -139
  4. package/dist/Utils/segmentation/CommToolsData.js.map +1 -1
  5. package/dist/Utils/segmentation/DragOperator.d.ts +10 -2
  6. package/dist/Utils/segmentation/DragOperator.js +14 -4
  7. package/dist/Utils/segmentation/DragOperator.js.map +1 -1
  8. package/dist/Utils/segmentation/DrawToolCore.d.ts +23 -16
  9. package/dist/Utils/segmentation/DrawToolCore.js +218 -401
  10. package/dist/Utils/segmentation/DrawToolCore.js.map +1 -1
  11. package/dist/Utils/segmentation/NrrdTools.d.ts +152 -10
  12. package/dist/Utils/segmentation/NrrdTools.js +599 -202
  13. package/dist/Utils/segmentation/NrrdTools.js.map +1 -1
  14. package/dist/Utils/segmentation/core/MaskVolume.d.ts +1 -1
  15. package/dist/Utils/segmentation/core/MaskVolume.js +24 -3
  16. package/dist/Utils/segmentation/core/MaskVolume.js.map +1 -1
  17. package/dist/Utils/segmentation/core/UndoManager.d.ts +1 -1
  18. package/dist/Utils/segmentation/core/UndoManager.js +1 -1
  19. package/dist/Utils/segmentation/core/index.d.ts +0 -2
  20. package/dist/Utils/segmentation/core/index.js +0 -1
  21. package/dist/Utils/segmentation/core/index.js.map +1 -1
  22. package/dist/Utils/segmentation/core/types.js +24 -24
  23. package/dist/Utils/segmentation/core/types.js.map +1 -1
  24. package/dist/Utils/segmentation/coreTools/GuiState.d.ts +22 -0
  25. package/dist/Utils/segmentation/coreTools/GuiState.js +48 -0
  26. package/dist/Utils/segmentation/coreTools/GuiState.js.map +1 -0
  27. package/dist/Utils/segmentation/coreTools/NrrdState.d.ts +25 -0
  28. package/dist/Utils/segmentation/coreTools/NrrdState.js +85 -0
  29. package/dist/Utils/segmentation/coreTools/NrrdState.js.map +1 -0
  30. package/dist/Utils/segmentation/coreTools/coreType.d.ts +88 -62
  31. package/dist/Utils/segmentation/coreTools/coreType.js.map +1 -1
  32. package/dist/Utils/segmentation/coreTools/divControlTools.js +1 -1
  33. package/dist/Utils/segmentation/coreTools/divControlTools.js.map +1 -1
  34. package/dist/Utils/segmentation/coreTools/gui.d.ts +12 -12
  35. package/dist/Utils/segmentation/coreTools/gui.js +81 -107
  36. package/dist/Utils/segmentation/coreTools/gui.js.map +1 -1
  37. package/dist/Utils/segmentation/eventRouter/EventRouter.d.ts +2 -1
  38. package/dist/Utils/segmentation/eventRouter/EventRouter.js +15 -10
  39. package/dist/Utils/segmentation/eventRouter/EventRouter.js.map +1 -1
  40. package/dist/Utils/segmentation/eventRouter/types.d.ts +3 -3
  41. package/dist/Utils/segmentation/tools/BaseTool.d.ts +8 -5
  42. package/dist/Utils/segmentation/tools/BaseTool.js +1 -2
  43. package/dist/Utils/segmentation/tools/BaseTool.js.map +1 -1
  44. package/dist/Utils/segmentation/tools/CrosshairTool.js +25 -25
  45. package/dist/Utils/segmentation/tools/CrosshairTool.js.map +1 -1
  46. package/dist/Utils/segmentation/tools/DragSliceTool.d.ts +2 -0
  47. package/dist/Utils/segmentation/tools/DragSliceTool.js +63 -53
  48. package/dist/Utils/segmentation/tools/DragSliceTool.js.map +1 -1
  49. package/dist/Utils/segmentation/tools/DrawingTool.d.ts +93 -0
  50. package/dist/Utils/segmentation/tools/DrawingTool.js +222 -0
  51. package/dist/Utils/segmentation/tools/DrawingTool.js.map +1 -0
  52. package/dist/Utils/segmentation/tools/EraserTool.js +8 -8
  53. package/dist/Utils/segmentation/tools/EraserTool.js.map +1 -1
  54. package/dist/Utils/segmentation/tools/ImageStoreHelper.d.ts +11 -28
  55. package/dist/Utils/segmentation/tools/ImageStoreHelper.js +39 -82
  56. package/dist/Utils/segmentation/tools/ImageStoreHelper.js.map +1 -1
  57. package/dist/Utils/segmentation/tools/PanTool.d.ts +53 -0
  58. package/dist/Utils/segmentation/tools/PanTool.js +90 -0
  59. package/dist/Utils/segmentation/tools/PanTool.js.map +1 -0
  60. package/dist/Utils/segmentation/tools/SphereTool.d.ts +208 -1
  61. package/dist/Utils/segmentation/tools/SphereTool.js +456 -45
  62. package/dist/Utils/segmentation/tools/SphereTool.js.map +1 -1
  63. package/dist/Utils/segmentation/tools/ZoomTool.js +3 -3
  64. package/dist/Utils/segmentation/tools/ZoomTool.js.map +1 -1
  65. package/dist/Utils/segmentation/tools/index.d.ts +6 -2
  66. package/dist/Utils/segmentation/tools/index.js +4 -1
  67. package/dist/Utils/segmentation/tools/index.js.map +1 -1
  68. package/dist/bundle.esm.js +2452 -1492
  69. package/dist/bundle.umd.js +2452 -1492
  70. package/dist/index.d.ts +6 -3
  71. package/dist/index.js +1 -1
  72. package/dist/index.js.map +1 -1
  73. package/dist/types/Utils/segmentation/CommToolsData.d.ts +24 -9
  74. package/dist/types/Utils/segmentation/DragOperator.d.ts +10 -2
  75. package/dist/types/Utils/segmentation/DrawToolCore.d.ts +23 -16
  76. package/dist/types/Utils/segmentation/NrrdTools.d.ts +152 -10
  77. package/dist/types/Utils/segmentation/core/MaskVolume.d.ts +1 -1
  78. package/dist/types/Utils/segmentation/core/UndoManager.d.ts +1 -1
  79. package/dist/types/Utils/segmentation/core/index.d.ts +0 -2
  80. package/dist/types/Utils/segmentation/coreTools/GuiState.d.ts +22 -0
  81. package/dist/types/Utils/segmentation/coreTools/NrrdState.d.ts +25 -0
  82. package/dist/types/Utils/segmentation/coreTools/coreType.d.ts +88 -62
  83. package/dist/types/Utils/segmentation/coreTools/gui.d.ts +12 -12
  84. package/dist/types/Utils/segmentation/eventRouter/EventRouter.d.ts +2 -1
  85. package/dist/types/Utils/segmentation/eventRouter/types.d.ts +3 -3
  86. package/dist/types/Utils/segmentation/tools/BaseTool.d.ts +8 -5
  87. package/dist/types/Utils/segmentation/tools/DragSliceTool.d.ts +2 -0
  88. package/dist/types/Utils/segmentation/tools/DrawingTool.d.ts +93 -0
  89. package/dist/types/Utils/segmentation/tools/ImageStoreHelper.d.ts +11 -28
  90. package/dist/types/Utils/segmentation/tools/PanTool.d.ts +53 -0
  91. package/dist/types/Utils/segmentation/tools/SphereTool.d.ts +208 -1
  92. package/dist/types/Utils/segmentation/tools/index.d.ts +6 -2
  93. package/dist/types/index.d.ts +6 -3
  94. package/package.json +1 -1
  95. package/dist/Utils/segmentation/core/MigrationUtils.d.ts +0 -97
  96. package/dist/Utils/segmentation/core/MigrationUtils.js +0 -163
  97. package/dist/Utils/segmentation/core/MigrationUtils.js.map +0 -1
  98. package/dist/types/Utils/segmentation/core/MigrationUtils.d.ts +0 -97
@@ -1,6 +1,43 @@
1
1
  /**
2
2
  * SphereTool - Sphere drawing and calculator sphere operations
3
3
  *
4
+ * Provides 3D sphere placement across multiple slice views.
5
+ * Supports 4 sphere types: tumour, skin, nipple, ribcage.
6
+ * Each type maps to a specific layer and channel for future MaskVolume integration.
7
+ *
8
+ * ## Interaction Flow
9
+ * 1. Sphere mode activated (gui_states.sphere = true)
10
+ * - Draw mode (Shift key) is disabled
11
+ * - Crosshair toggle is disabled
12
+ * 2. Left-click down: record sphere center (mouseX, mouseY, sliceIndex)
13
+ * - Remove zoom/slice wheel event
14
+ * - Bind sphere wheel event
15
+ * - Draw preview circle on sphereCanvas
16
+ * 3. While holding left-click, scroll wheel adjusts sphereRadius [1, 50]
17
+ * 4. Left-click up:
18
+ * - Fire getSphere / getCalculateSpherePositions callbacks
19
+ * - (Plan B) Draw 3D sphere across x/y/z axes into sphereMaskVolume
20
+ * - Remove sphere wheel event
21
+ * - Restore zoom/slice wheel event
22
+ *
23
+ * ## Channel Mapping
24
+ * Each sphere type defaults to layer1 with a specific channel:
25
+ * - tumour → layer1, channel 1
26
+ * - ribcage → layer1, channel 3
27
+ * - skin → layer1, channel 4
28
+ * - nipple → layer1, channel 5
29
+ *
30
+ * ## SphereMaskVolume
31
+ * Sphere 3D data is stored in a dedicated MaskVolume (nrrd_states.sphereMaskVolume)
32
+ * separate from the layer draw mask volumes, to avoid polluting layer1's draw data.
33
+ * This volume is created in setAllSlices() and cleared in reset().
34
+ *
35
+ * ## Future: Writing to Layer MaskVolume
36
+ * Currently sphere data does NOT write to layer1's MaskVolume.
37
+ * The channel mapping and interfaces are reserved for future integration.
38
+ * When enabled, use SPHERE_CHANNEL_MAP to determine the target layer & channel,
39
+ * then call volume.setVoxel() or similar to persist sphere data.
40
+ *
4
41
  * Extracted from DrawToolCore.ts:
5
42
  * - drawSphere / drawSphereCore / clearSphereCanvas
6
43
  * - drawSphereOnEachViews / drawCalculatorSphereOnEachViews
@@ -9,6 +46,39 @@
9
46
  * - getSpherePosition / clearSpherePrintStoreImages
10
47
  */
11
48
  import { BaseTool } from "./BaseTool";
49
+ import { CHANNEL_HEX_COLORS } from "../core";
50
+ /**
51
+ * Default layer and channel for each sphere type.
52
+ *
53
+ * This mapping is used to:
54
+ * 1. Determine the color for sphere preview rendering
55
+ * 2. (Future) Write sphere data into the corresponding layer's MaskVolume channel
56
+ *
57
+ * @example
58
+ * ```ts
59
+ * const { layer, channel } = SPHERE_CHANNEL_MAP['tumour'];
60
+ * // layer = 'layer1', channel = 1
61
+ * ```
62
+ */
63
+ export const SPHERE_CHANNEL_MAP = {
64
+ tumour: { layer: 'layer1', channel: 1 },
65
+ nipple: { layer: 'layer1', channel: 2 },
66
+ ribcage: { layer: 'layer1', channel: 3 },
67
+ skin: { layer: 'layer1', channel: 4 },
68
+ };
69
+ // SPHERE_COLORS was removed to enforce dynamic color lookups.
70
+ /**
71
+ * Label values for sphere types stored in sphereMaskVolume.
72
+ * Regular sphere mode uses SPHERE_LABEL (1).
73
+ * Calculator mode uses type-specific labels (1-4).
74
+ */
75
+ export const SPHERE_LABELS = {
76
+ default: 1,
77
+ tumour: 1,
78
+ nipple: 2,
79
+ ribcage: 3,
80
+ skin: 4,
81
+ };
12
82
  export class SphereTool extends BaseTool {
13
83
  constructor(ctx, callbacks) {
14
84
  super(ctx);
@@ -17,25 +87,84 @@ export class SphereTool extends BaseTool {
17
87
  setCallbacks(callbacks) {
18
88
  this.callbacks = callbacks;
19
89
  }
90
+ // ===== Sphere Type Helpers =====
91
+ /**
92
+ * Get the channel number for a sphere type.
93
+ *
94
+ * @param type - Sphere type ('tumour', 'skin', 'ribcage', 'nipple')
95
+ * @returns Channel number (1-8) mapped to this sphere type
96
+ *
97
+ * @example
98
+ * ```ts
99
+ * sphereTool.getChannelForSphereType('tumour'); // → 1
100
+ * sphereTool.getChannelForSphereType('skin'); // → 4
101
+ * ```
102
+ */
103
+ getChannelForSphereType(type) {
104
+ return SPHERE_CHANNEL_MAP[type].channel;
105
+ }
106
+ /**
107
+ * Get the default layer for a sphere type.
108
+ *
109
+ * @param type - Sphere type
110
+ * @returns Layer ID (e.g. 'layer1')
111
+ */
112
+ getLayerForSphereType(type) {
113
+ return SPHERE_CHANNEL_MAP[type].layer;
114
+ }
115
+ /**
116
+ * Get the preview color for a sphere type.
117
+ *
118
+ * If a volume is available for the target layer, uses the volume's
119
+ * custom color map (respects per-layer color customization).
120
+ * Otherwise falls back to SPHERE_COLORS (derived from CHANNEL_HEX_COLORS).
121
+ *
122
+ * @param type - Sphere type
123
+ * @returns CSS color string (hex)
124
+ */
125
+ getColorForSphereType(type) {
126
+ const { layer, channel } = SPHERE_CHANNEL_MAP[type];
127
+ const volumes = this.ctx.protectedData.maskData.volumes;
128
+ const volume = volumes[layer];
129
+ if (volume) {
130
+ // Use per-layer custom color if available
131
+ const rgba = volume.getChannelColor(channel);
132
+ // Convert RGBA to hex
133
+ const r = rgba.r.toString(16).padStart(2, '0');
134
+ const g = rgba.g.toString(16).padStart(2, '0');
135
+ const b = rgba.b.toString(16).padStart(2, '0');
136
+ return `#${r}${g}${b}`;
137
+ }
138
+ return CHANNEL_HEX_COLORS[channel];
139
+ }
20
140
  // ===== Sphere Canvas Size =====
21
141
  setSphereCanvasSize(axis) {
22
142
  const nrrd = this.ctx.nrrd_states;
23
143
  switch (axis !== null && axis !== void 0 ? axis : this.ctx.protectedData.axis) {
24
144
  case "x":
25
- this.ctx.protectedData.canvases.drawingSphereCanvas.width = nrrd.nrrd_z_mm;
26
- this.ctx.protectedData.canvases.drawingSphereCanvas.height = nrrd.nrrd_y_mm;
145
+ this.ctx.protectedData.canvases.drawingSphereCanvas.width = nrrd.image.nrrd_z_mm;
146
+ this.ctx.protectedData.canvases.drawingSphereCanvas.height = nrrd.image.nrrd_y_mm;
27
147
  break;
28
148
  case "y":
29
- this.ctx.protectedData.canvases.drawingSphereCanvas.width = nrrd.nrrd_x_mm;
30
- this.ctx.protectedData.canvases.drawingSphereCanvas.height = nrrd.nrrd_z_mm;
149
+ this.ctx.protectedData.canvases.drawingSphereCanvas.width = nrrd.image.nrrd_x_mm;
150
+ this.ctx.protectedData.canvases.drawingSphereCanvas.height = nrrd.image.nrrd_z_mm;
31
151
  break;
32
152
  case "z":
33
- this.ctx.protectedData.canvases.drawingSphereCanvas.width = nrrd.nrrd_x_mm;
34
- this.ctx.protectedData.canvases.drawingSphereCanvas.height = nrrd.nrrd_y_mm;
153
+ this.ctx.protectedData.canvases.drawingSphereCanvas.width = nrrd.image.nrrd_x_mm;
154
+ this.ctx.protectedData.canvases.drawingSphereCanvas.height = nrrd.image.nrrd_y_mm;
35
155
  break;
36
156
  }
37
157
  }
38
158
  // ===== Core Sphere Drawing =====
159
+ /**
160
+ * Draw a filled circle on the given context.
161
+ *
162
+ * @param ctx - Canvas 2D context
163
+ * @param x - Center X coordinate
164
+ * @param y - Center Y coordinate
165
+ * @param radius - Circle radius in pixels
166
+ * @param color - Fill color (CSS string)
167
+ */
39
168
  drawSphereCore(ctx, x, y, radius, color) {
40
169
  ctx.beginPath();
41
170
  ctx.arc(x, y, radius, 0, 2 * Math.PI);
@@ -57,50 +186,84 @@ export class SphereTool extends BaseTool {
57
186
  this.ctx.protectedData.ctxes.drawingSphereCtx,
58
187
  ];
59
188
  }
189
+ /**
190
+ * Draw a sphere preview circle on the sphere canvas using the current
191
+ * gui_states.fillColor. Called during sphere mode pointer-down.
192
+ *
193
+ * NOTE: Does NOT composite to master canvas — the start() render loop
194
+ * draws the sphere canvas directly to drawingCtx for proper layering.
195
+ */
60
196
  drawSphere(mouseX, mouseY, radius) {
61
- const [canvas, ctx] = this.clearSphereCanvas();
62
- this.drawSphereCore(ctx, mouseX, mouseY, radius, this.ctx.gui_states.fillColor);
63
- this.ctx.protectedData.ctxes.drawingLayerMasterCtx.drawImage(canvas, 0, 0, this.ctx.nrrd_states.changedWidth, this.ctx.nrrd_states.changedHeight);
197
+ const [, ctx] = this.clearSphereCanvas();
198
+ const color = this.getColorForSphereType(this.ctx.gui_states.mode.activeSphereType);
199
+ drawSphereCore(ctx, mouseX, mouseY, radius, color);
64
200
  }
65
201
  // ===== Sphere Wheel =====
202
+ /**
203
+ * Returns a wheel event handler that adjusts sphere radius while the user
204
+ * holds left-click in sphere mode.
205
+ *
206
+ * Radius is clamped to [1, 50].
207
+ */
66
208
  configMouseSphereWheel() {
67
209
  return (e) => {
68
210
  e.preventDefault();
69
211
  if (e.deltaY < 0) {
70
- this.ctx.nrrd_states.sphereRadius += 1;
212
+ this.ctx.nrrd_states.sphere.sphereRadius += 1;
71
213
  }
72
214
  else {
73
- this.ctx.nrrd_states.sphereRadius -= 1;
215
+ this.ctx.nrrd_states.sphere.sphereRadius -= 1;
74
216
  }
75
- this.ctx.nrrd_states.sphereRadius = Math.max(1, Math.min(this.ctx.nrrd_states.sphereRadius, 50));
76
- const mouseX = this.ctx.nrrd_states.sphereOrigin[this.ctx.protectedData.axis][0];
77
- const mouseY = this.ctx.nrrd_states.sphereOrigin[this.ctx.protectedData.axis][1];
78
- this.drawSphere(mouseX, mouseY, this.ctx.nrrd_states.sphereRadius);
217
+ this.ctx.nrrd_states.sphere.sphereRadius = Math.max(1, Math.min(this.ctx.nrrd_states.sphere.sphereRadius, 50));
218
+ const mouseX = this.ctx.nrrd_states.sphere.sphereOrigin[this.ctx.protectedData.axis][0];
219
+ const mouseY = this.ctx.nrrd_states.sphere.sphereOrigin[this.ctx.protectedData.axis][1];
220
+ this.drawSphere(mouseX, mouseY, this.ctx.nrrd_states.sphere.sphereRadius);
79
221
  };
80
222
  }
81
223
  // ===== Store Sphere Images =====
82
- storeSphereImages(index, axis) {
83
- this.callbacks.setEmptyCanvasSize(axis);
84
- this.callbacks.drawImageOnEmptyImage(this.ctx.protectedData.canvases.drawingSphereCanvas);
85
- const imageData = this.ctx.protectedData.ctxes.emptyCtx.getImageData(0, 0, this.ctx.protectedData.canvases.emptyCanvas.width, this.ctx.protectedData.canvases.emptyCanvas.height);
86
- this.callbacks.storeImageToAxis(index, imageData, axis);
224
+ /**
225
+ * Store sphere slice image into SphereMaskVolume.
226
+ *
227
+ * Currently a no-op sphere slice data is rendered as overlay only.
228
+ *
229
+ * TODO: Future — write sphere circle data into nrrd_states.sphereMaskVolume
230
+ * at the specified slice index and axis. Use SPHERE_CHANNEL_MAP to determine
231
+ * the target channel when integrating with layer MaskVolume.
232
+ */
233
+ storeSphereImages(_index, _axis) {
234
+ // No-op: sphere slice storage is rendered as overlay.
235
+ // Future: write to nrrd_states.sphereMaskVolume here.
87
236
  }
88
237
  // ===== Multi-View Sphere =====
238
+ /**
239
+ * Draw a sphere cross-section at a given decay distance from center,
240
+ * for a specific axis view.
241
+ *
242
+ * Called for each decay value [0..sphereRadius] for all 3 axes,
243
+ * creating the 3D sphere effect.
244
+ *
245
+ * The sphere circle radius at each slice = sphereRadius - decay (linear decay).
246
+ *
247
+ * @param decay - Distance from sphere center slice
248
+ * @param axis - Axis to render on ('x', 'y', 'z')
249
+ */
89
250
  drawSphereOnEachViews(decay, axis) {
90
251
  this.setSphereCanvasSize(axis);
91
- const mouseX = this.ctx.nrrd_states.sphereOrigin[axis][0];
92
- const mouseY = this.ctx.nrrd_states.sphereOrigin[axis][1];
93
- const originIndex = this.ctx.nrrd_states.sphereOrigin[axis][2];
252
+ const mouseX = this.ctx.nrrd_states.sphere.sphereOrigin[axis][0];
253
+ const mouseY = this.ctx.nrrd_states.sphere.sphereOrigin[axis][1];
254
+ const originIndex = this.ctx.nrrd_states.sphere.sphereOrigin[axis][2];
94
255
  const preIndex = originIndex - decay;
95
256
  const nextIndex = originIndex + decay;
96
257
  const ctx = this.ctx.protectedData.ctxes.drawingSphereCtx;
97
258
  const canvas = this.ctx.protectedData.canvases.drawingSphereCanvas;
259
+ // Use the dynamic type color instead of the static fillColor
260
+ const color = this.getColorForSphereType(this.ctx.gui_states.mode.activeSphereType);
98
261
  if (preIndex === nextIndex) {
99
- this.drawSphereCore(ctx, mouseX, mouseY, this.ctx.nrrd_states.sphereRadius, this.ctx.gui_states.fillColor);
262
+ this.drawSphereCore(ctx, mouseX, mouseY, this.ctx.nrrd_states.sphere.sphereRadius, color);
100
263
  this.storeSphereImages(preIndex, axis);
101
264
  }
102
265
  else {
103
- this.drawSphereCore(ctx, mouseX, mouseY, this.ctx.nrrd_states.sphereRadius - decay, this.ctx.gui_states.fillColor);
266
+ this.drawSphereCore(ctx, mouseX, mouseY, this.ctx.nrrd_states.sphere.sphereRadius - decay, color);
104
267
  this.callbacks.drawImageOnEmptyImage(canvas);
105
268
  this.storeSphereImages(preIndex, axis);
106
269
  this.storeSphereImages(nextIndex, axis);
@@ -115,22 +278,31 @@ export class SphereTool extends BaseTool {
115
278
  z: position[axis][2],
116
279
  };
117
280
  }
281
+ /**
282
+ * Draw all 4 calculator sphere types on each view axis.
283
+ *
284
+ * Groups sphere positions by their slice index so that spheres on the
285
+ * same slice are drawn together before storing.
286
+ *
287
+ * Uses SPHERE_COLORS for each sphere type instead of nrrd_states.*Color.
288
+ */
118
289
  drawCalculatorSphereOnEachViews(axis) {
119
290
  this.setSphereCanvasSize(axis);
120
291
  const ctx = this.ctx.protectedData.ctxes.drawingSphereCtx;
121
292
  const canvas = this.ctx.protectedData.canvases.drawingSphereCanvas;
122
293
  const nrrd = this.ctx.nrrd_states;
123
- const tumourPosition = nrrd.tumourSphereOrigin
124
- ? Object.assign(this.getSpherePosition(nrrd.tumourSphereOrigin, axis), { color: nrrd.tumourColor })
294
+ // Build position list with dynamic sphere type colors
295
+ const tumourPosition = nrrd.sphere.tumourSphereOrigin
296
+ ? Object.assign(this.getSpherePosition(nrrd.sphere.tumourSphereOrigin, axis), { color: this.getColorForSphereType('tumour') })
125
297
  : null;
126
- const skinPosition = nrrd.skinSphereOrigin
127
- ? Object.assign(this.getSpherePosition(nrrd.skinSphereOrigin, axis), { color: nrrd.skinColor })
298
+ const skinPosition = nrrd.sphere.skinSphereOrigin
299
+ ? Object.assign(this.getSpherePosition(nrrd.sphere.skinSphereOrigin, axis), { color: this.getColorForSphereType('skin') })
128
300
  : null;
129
- const ribcagePosition = nrrd.ribSphereOrigin
130
- ? Object.assign(this.getSpherePosition(nrrd.ribSphereOrigin, axis), { color: nrrd.ribcageColor })
301
+ const ribcagePosition = nrrd.sphere.ribSphereOrigin
302
+ ? Object.assign(this.getSpherePosition(nrrd.sphere.ribSphereOrigin, axis), { color: this.getColorForSphereType('ribcage') })
131
303
  : null;
132
- const nipplePosition = nrrd.nippleSphereOrigin
133
- ? Object.assign(this.getSpherePosition(nrrd.nippleSphereOrigin, axis), { color: nrrd.nippleColor })
304
+ const nipplePosition = nrrd.sphere.nippleSphereOrigin
305
+ ? Object.assign(this.getSpherePosition(nrrd.sphere.nippleSphereOrigin, axis), { color: this.getColorForSphereType('nipple') })
134
306
  : null;
135
307
  const positionGroup = [];
136
308
  if (tumourPosition)
@@ -162,32 +334,271 @@ export class SphereTool extends BaseTool {
162
334
  });
163
335
  rePositionGroup.forEach((group) => {
164
336
  group.forEach((p) => {
165
- this.drawSphereCore(ctx, p.x, p.y, nrrd.sphereRadius, p.color);
337
+ this.drawSphereCore(ctx, p.x, p.y, nrrd.sphere.sphereRadius, p.color);
166
338
  });
167
339
  this.storeSphereImages(group[0].z, axis);
168
340
  ctx.clearRect(0, 0, canvas.width, canvas.height);
169
341
  });
170
342
  }
343
+ /**
344
+ * Draw all placed calculator spheres on the current slice.
345
+ *
346
+ * Called every frame to show existing sphere markers on the current view.
347
+ * Uses SPHERE_COLORS for consistent type-based coloring.
348
+ *
349
+ * @param radius - Sphere radius to draw
350
+ */
171
351
  drawCalculatorSphere(radius) {
172
- const [canvas, ctx] = this.clearSphereCanvas();
352
+ const [, ctx] = this.clearSphereCanvas();
173
353
  const nrrd = this.ctx.nrrd_states;
174
354
  const axis = this.ctx.protectedData.axis;
175
- if (nrrd.tumourSphereOrigin && nrrd.tumourSphereOrigin[axis][2] === nrrd.currentIndex) {
176
- this.drawSphereCore(ctx, nrrd.tumourSphereOrigin[axis][0], nrrd.tumourSphereOrigin[axis][1], radius, nrrd.tumourColor);
355
+ if (nrrd.sphere.tumourSphereOrigin && nrrd.sphere.tumourSphereOrigin[axis][2] === nrrd.view.currentSliceIndex) {
356
+ this.drawSphereCore(ctx, nrrd.sphere.tumourSphereOrigin[axis][0], nrrd.sphere.tumourSphereOrigin[axis][1], radius, this.getColorForSphereType('tumour'));
177
357
  }
178
- if (nrrd.skinSphereOrigin && nrrd.skinSphereOrigin[axis][2] === nrrd.currentIndex) {
179
- this.drawSphereCore(ctx, nrrd.skinSphereOrigin[axis][0], nrrd.skinSphereOrigin[axis][1], radius, nrrd.skinColor);
358
+ if (nrrd.sphere.skinSphereOrigin && nrrd.sphere.skinSphereOrigin[axis][2] === nrrd.view.currentSliceIndex) {
359
+ this.drawSphereCore(ctx, nrrd.sphere.skinSphereOrigin[axis][0], nrrd.sphere.skinSphereOrigin[axis][1], radius, this.getColorForSphereType('skin'));
180
360
  }
181
- if (nrrd.ribSphereOrigin && nrrd.ribSphereOrigin[axis][2] === nrrd.currentIndex) {
182
- this.drawSphereCore(ctx, nrrd.ribSphereOrigin[axis][0], nrrd.ribSphereOrigin[axis][1], radius, nrrd.ribcageColor);
361
+ if (nrrd.sphere.ribSphereOrigin && nrrd.sphere.ribSphereOrigin[axis][2] === nrrd.view.currentSliceIndex) {
362
+ this.drawSphereCore(ctx, nrrd.sphere.ribSphereOrigin[axis][0], nrrd.sphere.ribSphereOrigin[axis][1], radius, this.getColorForSphereType('ribcage'));
183
363
  }
184
- if (nrrd.nippleSphereOrigin && nrrd.nippleSphereOrigin[axis][2] === nrrd.currentIndex) {
185
- this.drawSphereCore(ctx, nrrd.nippleSphereOrigin[axis][0], nrrd.nippleSphereOrigin[axis][1], radius, nrrd.nippleColor);
364
+ if (nrrd.sphere.nippleSphereOrigin && nrrd.sphere.nippleSphereOrigin[axis][2] === nrrd.view.currentSliceIndex) {
365
+ this.drawSphereCore(ctx, nrrd.sphere.nippleSphereOrigin[axis][0], nrrd.sphere.nippleSphereOrigin[axis][1], radius, this.getColorForSphereType('nipple'));
186
366
  }
187
- this.ctx.protectedData.ctxes.drawingLayerMasterCtx.drawImage(canvas, 0, 0, nrrd.changedWidth, nrrd.changedHeight);
367
+ // NOTE: Does NOT composite to master canvas the start() render loop
368
+ // draws the sphere canvas directly to drawingCtx for proper layering.
188
369
  }
370
+ /**
371
+ * Clear sphere overlay images.
372
+ *
373
+ * No-op in current implementation — sphere data is rendered as overlay
374
+ * and stored in the dedicated sphereMaskVolume (not in layer volumes).
375
+ */
189
376
  clearSpherePrintStoreImages() {
190
- // No-op: sphere images are no longer stored in Phase 3 volumetric model
377
+ // No-op: sphere images are stored in sphereMaskVolume, not layer volumes.
378
+ // The sphereMaskVolume is cleared in NrrdTools.reset() when switching cases.
191
379
  }
380
+ // ===== 3D Sphere Volume Write =====
381
+ /**
382
+ * Convert canvas (mm-space) coordinates to 3D voxel coordinates.
383
+ *
384
+ * @param canvasX - X position in canvas mm-space
385
+ * @param canvasY - Y position in canvas mm-space
386
+ * @param sliceIndex - Slice index along the viewing axis
387
+ * @param axis - Viewing axis where the sphere was placed
388
+ * @returns 3D voxel coordinates { x, y, z }
389
+ */
390
+ canvasToVoxelCenter(canvasX, canvasY, sliceIndex, axis) {
391
+ const nrrd = this.ctx.nrrd_states;
392
+ switch (axis) {
393
+ case 'z':
394
+ return {
395
+ x: canvasX * nrrd.image.nrrd_x_pixel / nrrd.image.nrrd_x_mm,
396
+ y: canvasY * nrrd.image.nrrd_y_pixel / nrrd.image.nrrd_y_mm,
397
+ z: sliceIndex,
398
+ };
399
+ case 'y':
400
+ return {
401
+ x: canvasX * nrrd.image.nrrd_x_pixel / nrrd.image.nrrd_x_mm,
402
+ y: sliceIndex,
403
+ z: (nrrd.image.nrrd_z_mm - canvasY) * nrrd.image.nrrd_z_pixel / nrrd.image.nrrd_z_mm,
404
+ };
405
+ case 'x':
406
+ return {
407
+ x: sliceIndex,
408
+ y: canvasY * nrrd.image.nrrd_y_pixel / nrrd.image.nrrd_y_mm,
409
+ z: canvasX * nrrd.image.nrrd_z_pixel / nrrd.image.nrrd_z_mm,
410
+ };
411
+ }
412
+ }
413
+ /**
414
+ * Write a 3D solid sphere to sphereMaskVolume.
415
+ *
416
+ * Converts the sphere center from canvas mm-space to voxel coordinates,
417
+ * then iterates over a bounding box and sets voxels within the sphere.
418
+ * The sphere is an ellipsoid in voxel space to appear circular in
419
+ * physical (mm) space on all axis views.
420
+ *
421
+ * @param label - Label value to write (1-4, default 1)
422
+ */
423
+ write3DSphereToVolume(label = SPHERE_LABELS.default) {
424
+ const vol = this.ctx.nrrd_states.sphere.sphereMaskVolume;
425
+ if (!vol)
426
+ return;
427
+ const nrrd = this.ctx.nrrd_states;
428
+ const axis = this.ctx.protectedData.axis;
429
+ const dims = vol.getDimensions();
430
+ const origin = nrrd.sphere.sphereOrigin[axis];
431
+ const center = this.canvasToVoxelCenter(origin[0], origin[1], origin[2], axis);
432
+ const radius = nrrd.sphere.sphereRadius;
433
+ // Convert mm radius to voxels in each direction
434
+ const rx = radius * nrrd.image.nrrd_x_pixel / nrrd.image.nrrd_x_mm;
435
+ const ry = radius * nrrd.image.nrrd_y_pixel / nrrd.image.nrrd_y_mm;
436
+ const rz = radius * nrrd.image.nrrd_z_pixel / nrrd.image.nrrd_z_mm;
437
+ // Bounding box clamped to volume
438
+ const minX = Math.max(0, Math.floor(center.x - rx));
439
+ const maxX = Math.min(dims.width - 1, Math.ceil(center.x + rx));
440
+ const minY = Math.max(0, Math.floor(center.y - ry));
441
+ const maxY = Math.min(dims.height - 1, Math.ceil(center.y + ry));
442
+ const minZ = Math.max(0, Math.floor(center.z - rz));
443
+ const maxZ = Math.min(dims.depth - 1, Math.ceil(center.z + rz));
444
+ for (let z = minZ; z <= maxZ; z++) {
445
+ for (let y = minY; y <= maxY; y++) {
446
+ for (let x = minX; x <= maxX; x++) {
447
+ const dx = rx > 0 ? (x - center.x) / rx : 0;
448
+ const dy = ry > 0 ? (y - center.y) / ry : 0;
449
+ const dz = rz > 0 ? (z - center.z) / rz : 0;
450
+ if (dx * dx + dy * dy + dz * dz <= 1.0) {
451
+ vol.setVoxel(x, y, z, label);
452
+ }
453
+ }
454
+ }
455
+ }
456
+ }
457
+ /**
458
+ * Write a calculator sphere type to sphereMaskVolume.
459
+ * Uses the stored origin for the specified sphere type.
460
+ *
461
+ * @param type - Sphere type ('tumour', 'skin', 'nipple', 'ribcage')
462
+ */
463
+ writeCalculatorSphereToVolume(type) {
464
+ const nrrd = this.ctx.nrrd_states;
465
+ const vol = nrrd.sphere.sphereMaskVolume;
466
+ if (!vol)
467
+ return;
468
+ let origin = null;
469
+ switch (type) {
470
+ case 'tumour':
471
+ origin = nrrd.sphere.tumourSphereOrigin;
472
+ break;
473
+ case 'skin':
474
+ origin = nrrd.sphere.skinSphereOrigin;
475
+ break;
476
+ case 'nipple':
477
+ origin = nrrd.sphere.nippleSphereOrigin;
478
+ break;
479
+ case 'ribcage':
480
+ origin = nrrd.sphere.ribSphereOrigin;
481
+ break;
482
+ }
483
+ if (!origin)
484
+ return;
485
+ const dims = vol.getDimensions();
486
+ const radius = nrrd.sphere.sphereRadius;
487
+ // Use z-axis representation for consistent voxel mapping
488
+ const cx = origin.z[0] * nrrd.image.nrrd_x_pixel / nrrd.image.nrrd_x_mm;
489
+ const cy = origin.z[1] * nrrd.image.nrrd_y_pixel / nrrd.image.nrrd_y_mm;
490
+ const cz = origin.z[2];
491
+ const rx = radius * nrrd.image.nrrd_x_pixel / nrrd.image.nrrd_x_mm;
492
+ const ry = radius * nrrd.image.nrrd_y_pixel / nrrd.image.nrrd_y_mm;
493
+ const rz = radius * nrrd.image.nrrd_z_pixel / nrrd.image.nrrd_z_mm;
494
+ const label = SPHERE_LABELS[type];
495
+ const minX = Math.max(0, Math.floor(cx - rx));
496
+ const maxX = Math.min(dims.width - 1, Math.ceil(cx + rx));
497
+ const minY = Math.max(0, Math.floor(cy - ry));
498
+ const maxY = Math.min(dims.height - 1, Math.ceil(cy + ry));
499
+ const minZ = Math.max(0, Math.floor(cz - rz));
500
+ const maxZ = Math.min(dims.depth - 1, Math.ceil(cz + rz));
501
+ for (let z = minZ; z <= maxZ; z++) {
502
+ for (let y = minY; y <= maxY; y++) {
503
+ for (let x = minX; x <= maxX; x++) {
504
+ const dx = rx > 0 ? (x - cx) / rx : 0;
505
+ const dy = ry > 0 ? (y - cy) / ry : 0;
506
+ const dz = rz > 0 ? (z - cz) / rz : 0;
507
+ if (dx * dx + dy * dy + dz * dz <= 1.0) {
508
+ vol.setVoxel(x, y, z, label);
509
+ }
510
+ }
511
+ }
512
+ }
513
+ }
514
+ /**
515
+ * Write all placed calculator spheres to the volume.
516
+ * Clears the volume first, then writes each placed sphere type.
517
+ */
518
+ writeAllCalculatorSpheresToVolume() {
519
+ const nrrd = this.ctx.nrrd_states;
520
+ const vol = nrrd.sphere.sphereMaskVolume;
521
+ if (!vol)
522
+ return;
523
+ vol.clear();
524
+ if (nrrd.sphere.tumourSphereOrigin)
525
+ this.writeCalculatorSphereToVolume('tumour');
526
+ if (nrrd.sphere.skinSphereOrigin)
527
+ this.writeCalculatorSphereToVolume('skin');
528
+ if (nrrd.sphere.nippleSphereOrigin)
529
+ this.writeCalculatorSphereToVolume('nipple');
530
+ if (nrrd.sphere.ribSphereOrigin)
531
+ this.writeCalculatorSphereToVolume('ribcage');
532
+ }
533
+ // ===== Sphere Overlay Rendering from Volume =====
534
+ /**
535
+ * Render the current slice of sphereMaskVolume to drawingSphereCanvas.
536
+ *
537
+ * Called after any operation that changes the view (slice, axis, zoom)
538
+ * to keep the sphere overlay visible. Uses the same render pipeline as
539
+ * layer masks (emptyCanvas → sphere canvas at display scale).
540
+ */
541
+ refreshSphereCanvas() {
542
+ const vol = this.ctx.nrrd_states.sphere.sphereMaskVolume;
543
+ const sphereCtx = this.ctx.protectedData.ctxes.drawingSphereCtx;
544
+ const sphereCanvas = this.ctx.protectedData.canvases.drawingSphereCanvas;
545
+ const nrrd = this.ctx.nrrd_states;
546
+ const axis = this.ctx.protectedData.axis;
547
+ // Set sphere canvas to origin (mm) dimensions — matches preview approach
548
+ this.setSphereCanvasSize(axis);
549
+ if (!vol)
550
+ return;
551
+ const sliceIndex = nrrd.view.currentSliceIndex;
552
+ try {
553
+ const dims = vol.getDimensions();
554
+ const maxSlice = axis === 'x' ? dims.width : axis === 'y' ? dims.height : dims.depth;
555
+ if (sliceIndex < 0 || sliceIndex >= maxSlice)
556
+ return;
557
+ // Get slice dimensions (voxel space)
558
+ const [sliceW, sliceH] = axis === 'z' ? [dims.width, dims.height]
559
+ : axis === 'y' ? [dims.width, dims.depth]
560
+ : [dims.depth, dims.height];
561
+ const imageData = new ImageData(sliceW, sliceH);
562
+ vol.renderLabelSliceInto(sliceIndex, axis, imageData, undefined, 1.0);
563
+ // Quick check if any data on this slice
564
+ let hasContent = false;
565
+ for (let i = 3; i < imageData.data.length; i += 4) {
566
+ if (imageData.data[i] > 0) {
567
+ hasContent = true;
568
+ break;
569
+ }
570
+ }
571
+ if (!hasContent)
572
+ return;
573
+ // Draw via emptyCanvas (same pattern as renderSliceToCanvas)
574
+ this.callbacks.setEmptyCanvasSize(axis);
575
+ this.ctx.protectedData.ctxes.emptyCtx.putImageData(imageData, 0, 0);
576
+ sphereCtx.imageSmoothingEnabled = true;
577
+ // Coronal axis ('y') needs vertical flip to match display coordinate system
578
+ if (axis === 'y') {
579
+ sphereCtx.save();
580
+ sphereCtx.scale(1, -1);
581
+ sphereCtx.translate(0, -sphereCanvas.height);
582
+ }
583
+ sphereCtx.drawImage(this.ctx.protectedData.canvases.emptyCanvas, 0, 0, sphereCanvas.width, sphereCanvas.height);
584
+ if (axis === 'y') {
585
+ sphereCtx.restore();
586
+ }
587
+ }
588
+ catch (_a) {
589
+ // Volume not ready or slice out of bounds
590
+ }
591
+ }
592
+ }
593
+ /**
594
+ * Standalone helper — draw a filled circle.
595
+ * Used internally and by drawSphere() for preview rendering.
596
+ */
597
+ function drawSphereCore(ctx, x, y, radius, color) {
598
+ ctx.beginPath();
599
+ ctx.arc(x, y, radius, 0, 2 * Math.PI);
600
+ ctx.fillStyle = color;
601
+ ctx.fill();
602
+ ctx.closePath();
192
603
  }
193
604
  //# sourceMappingURL=SphereTool.js.map