copper3d 2.2.3 → 3.0.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 (111) hide show
  1. package/README.md +1056 -5
  2. package/dist/Scene/copperScene.d.ts +3 -0
  3. package/dist/Scene/copperScene.js +13 -0
  4. package/dist/Scene/copperScene.js.map +1 -1
  5. package/dist/Utils/segmentation/CommToolsData.d.ts +109 -6
  6. package/dist/Utils/segmentation/CommToolsData.js +314 -50
  7. package/dist/Utils/segmentation/CommToolsData.js.map +1 -1
  8. package/dist/Utils/segmentation/DragOperator.d.ts +13 -7
  9. package/dist/Utils/segmentation/DragOperator.js +51 -165
  10. package/dist/Utils/segmentation/DragOperator.js.map +1 -1
  11. package/dist/Utils/segmentation/DrawToolCore.d.ts +57 -76
  12. package/dist/Utils/segmentation/DrawToolCore.js +368 -878
  13. package/dist/Utils/segmentation/DrawToolCore.js.map +1 -1
  14. package/dist/Utils/segmentation/NrrdTools.d.ts +277 -15
  15. package/dist/Utils/segmentation/NrrdTools.js +594 -232
  16. package/dist/Utils/segmentation/NrrdTools.js.map +1 -1
  17. package/dist/Utils/segmentation/core/MaskVolume.d.ts +477 -0
  18. package/dist/Utils/segmentation/core/MaskVolume.js +1238 -0
  19. package/dist/Utils/segmentation/core/MaskVolume.js.map +1 -0
  20. package/dist/Utils/segmentation/core/MigrationUtils.d.ts +97 -0
  21. package/dist/Utils/segmentation/core/MigrationUtils.js +163 -0
  22. package/dist/Utils/segmentation/core/MigrationUtils.js.map +1 -0
  23. package/dist/Utils/segmentation/core/UndoManager.d.ts +47 -0
  24. package/dist/Utils/segmentation/core/UndoManager.js +86 -0
  25. package/dist/Utils/segmentation/core/UndoManager.js.map +1 -0
  26. package/dist/Utils/segmentation/core/index.d.ts +76 -0
  27. package/dist/Utils/segmentation/core/index.js +75 -0
  28. package/dist/Utils/segmentation/core/index.js.map +1 -0
  29. package/dist/Utils/segmentation/core/types.d.ts +106 -0
  30. package/dist/Utils/segmentation/core/types.js +98 -0
  31. package/dist/Utils/segmentation/core/types.js.map +1 -0
  32. package/dist/Utils/segmentation/coreTools/coreType.d.ts +55 -36
  33. package/dist/Utils/segmentation/coreTools/coreType.js.map +1 -1
  34. package/dist/Utils/segmentation/coreTools/gui.d.ts +6 -16
  35. package/dist/Utils/segmentation/coreTools/gui.js +23 -27
  36. package/dist/Utils/segmentation/coreTools/gui.js.map +1 -1
  37. package/dist/Utils/segmentation/eventRouter/EventRouter.d.ts +144 -0
  38. package/dist/Utils/segmentation/eventRouter/EventRouter.js +369 -0
  39. package/dist/Utils/segmentation/eventRouter/EventRouter.js.map +1 -0
  40. package/dist/Utils/segmentation/eventRouter/index.d.ts +7 -0
  41. package/dist/Utils/segmentation/eventRouter/index.js +7 -0
  42. package/dist/Utils/segmentation/eventRouter/index.js.map +1 -0
  43. package/dist/Utils/segmentation/eventRouter/types.d.ts +70 -0
  44. package/dist/Utils/segmentation/eventRouter/types.js +7 -0
  45. package/dist/Utils/segmentation/eventRouter/types.js.map +1 -0
  46. package/dist/Utils/segmentation/tools/BaseTool.d.ts +25 -0
  47. package/dist/Utils/segmentation/tools/BaseTool.js +18 -0
  48. package/dist/Utils/segmentation/tools/BaseTool.js.map +1 -0
  49. package/dist/Utils/segmentation/tools/ContrastTool.d.ts +26 -0
  50. package/dist/Utils/segmentation/tools/ContrastTool.js +118 -0
  51. package/dist/Utils/segmentation/tools/ContrastTool.js.map +1 -0
  52. package/dist/Utils/segmentation/tools/CrosshairTool.d.ts +30 -0
  53. package/dist/Utils/segmentation/tools/CrosshairTool.js +157 -0
  54. package/dist/Utils/segmentation/tools/CrosshairTool.js.map +1 -0
  55. package/dist/Utils/segmentation/tools/DragSliceTool.d.ts +44 -0
  56. package/dist/Utils/segmentation/tools/DragSliceTool.js +170 -0
  57. package/dist/Utils/segmentation/tools/DragSliceTool.js.map +1 -0
  58. package/dist/Utils/segmentation/tools/EraserTool.d.ts +16 -0
  59. package/dist/Utils/segmentation/tools/EraserTool.js +92 -0
  60. package/dist/Utils/segmentation/tools/EraserTool.js.map +1 -0
  61. package/dist/Utils/segmentation/tools/ImageStoreHelper.d.ts +65 -0
  62. package/dist/Utils/segmentation/tools/ImageStoreHelper.js +174 -0
  63. package/dist/Utils/segmentation/tools/ImageStoreHelper.js.map +1 -0
  64. package/dist/Utils/segmentation/tools/SphereTool.d.ts +38 -0
  65. package/dist/Utils/segmentation/tools/SphereTool.js +193 -0
  66. package/dist/Utils/segmentation/tools/SphereTool.js.map +1 -0
  67. package/dist/Utils/segmentation/tools/ZoomTool.d.ts +20 -0
  68. package/dist/Utils/segmentation/tools/ZoomTool.js +59 -0
  69. package/dist/Utils/segmentation/tools/ZoomTool.js.map +1 -0
  70. package/dist/Utils/segmentation/tools/index.d.ts +22 -0
  71. package/dist/Utils/segmentation/tools/index.js +23 -0
  72. package/dist/Utils/segmentation/tools/index.js.map +1 -0
  73. package/dist/bundle.esm.js +4740 -1979
  74. package/dist/bundle.umd.js +4743 -1978
  75. package/dist/index.d.ts +5 -3
  76. package/dist/index.js +4 -2
  77. package/dist/index.js.map +1 -1
  78. package/dist/types/Renderer/copperRenderer.d.ts +1 -1
  79. package/dist/types/Renderer/copperRendererOnDemond.d.ts +1 -1
  80. package/dist/types/Scene/copperScene.d.ts +3 -0
  81. package/dist/types/Utils/segmentation/CommToolsData.d.ts +109 -6
  82. package/dist/types/Utils/segmentation/DragOperator.d.ts +13 -7
  83. package/dist/types/Utils/segmentation/DrawToolCore.d.ts +57 -76
  84. package/dist/types/Utils/segmentation/NrrdTools.d.ts +277 -15
  85. package/dist/types/Utils/segmentation/core/MaskVolume.d.ts +477 -0
  86. package/dist/types/Utils/segmentation/core/MigrationUtils.d.ts +97 -0
  87. package/dist/types/Utils/segmentation/core/UndoManager.d.ts +47 -0
  88. package/dist/types/Utils/segmentation/core/index.d.ts +76 -0
  89. package/dist/types/Utils/segmentation/core/types.d.ts +106 -0
  90. package/dist/types/Utils/segmentation/coreTools/coreType.d.ts +55 -36
  91. package/dist/types/Utils/segmentation/coreTools/gui.d.ts +6 -16
  92. package/dist/types/Utils/segmentation/eventRouter/EventRouter.d.ts +144 -0
  93. package/dist/types/Utils/segmentation/eventRouter/index.d.ts +7 -0
  94. package/dist/types/Utils/segmentation/eventRouter/types.d.ts +70 -0
  95. package/dist/types/Utils/segmentation/tools/BaseTool.d.ts +25 -0
  96. package/dist/types/Utils/segmentation/tools/ContrastTool.d.ts +26 -0
  97. package/dist/types/Utils/segmentation/tools/CrosshairTool.d.ts +30 -0
  98. package/dist/types/Utils/segmentation/tools/DragSliceTool.d.ts +44 -0
  99. package/dist/types/Utils/segmentation/tools/EraserTool.d.ts +16 -0
  100. package/dist/types/Utils/segmentation/tools/ImageStoreHelper.d.ts +65 -0
  101. package/dist/types/Utils/segmentation/tools/SphereTool.d.ts +38 -0
  102. package/dist/types/Utils/segmentation/tools/ZoomTool.d.ts +20 -0
  103. package/dist/types/Utils/segmentation/tools/index.d.ts +22 -0
  104. package/dist/types/index.d.ts +5 -3
  105. package/dist/types/types/types.d.ts +3 -3
  106. package/dist/types/types.d.ts +3 -3
  107. package/package.json +1 -1
  108. package/dist/Utils/segmentation/coreTools/archive.d.ts +0 -1
  109. package/dist/Utils/segmentation/coreTools/archive.js +0 -23
  110. package/dist/Utils/segmentation/coreTools/archive.js.map +0 -1
  111. package/dist/types/Utils/segmentation/coreTools/archive.d.ts +0 -1
@@ -2,20 +2,34 @@ import { setupGui } from "./coreTools/gui";
2
2
  import { autoFocusDiv } from "./coreTools/divControlTools";
3
3
  import { DragOperator } from "./DragOperator";
4
4
  import { DrawToolCore } from "./DrawToolCore";
5
+ import { MaskVolume, CHANNEL_HEX_COLORS, rgbaToHex, rgbaToCss } from "./core";
6
+ /**
7
+ * Core NRRD annotation tool for medical image segmentation.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * // Default 3 layers: layer1, layer2, layer3
12
+ * const tools = new NrrdTools(container);
13
+ *
14
+ * // Custom layers
15
+ * const tools = new NrrdTools(container, { layers: ["layer1", "layer2"] });
16
+ * ```
17
+ */
5
18
  export class NrrdTools extends DrawToolCore {
6
- constructor(container) {
7
- super(container);
19
+ constructor(container, options) {
20
+ super(container, options);
8
21
  this.initState = true;
22
+ this._sliceRAFId = null;
23
+ this._pendingSliceStep = 0;
9
24
  this.container = container;
10
- this.storedPaintImages = {
11
- label1: this.protectedData.maskData.paintImagesLabel1,
12
- label2: this.protectedData.maskData.paintImagesLabel2,
13
- label3: this.protectedData.maskData.paintImagesLabel3,
14
- };
15
25
  this.protectedData.previousDrawingImage =
16
26
  this.protectedData.ctxes.emptyCtx.createImageData(1, 1);
17
27
  this.init();
18
- this.dragOperator = new DragOperator(this.container, this.nrrd_states, this.gui_states, this.protectedData, this.drawingPrameters, this.setSyncsliceNum, this.setIsDrawFalse, this.flipDisplayImageByAxis, this.setEmptyCanvasSize, this.filterDrawedImage);
28
+ this.dragOperator = new DragOperator(this.container, this.nrrd_states, this.gui_states, this.protectedData, this.drawingPrameters, this.setSyncsliceNum.bind(this), this.setIsDrawFalse.bind(this), this.flipDisplayImageByAxis.bind(this), this.setEmptyCanvasSize.bind(this), this.getOrCreateSliceBuffer.bind(this), this.renderSliceToCanvas.bind(this));
29
+ // Inject EventRouter into DragOperator for centralized event handling
30
+ if (this.eventRouter) {
31
+ this.dragOperator.setEventRouter(this.eventRouter);
32
+ }
19
33
  }
20
34
  /**
21
35
  * core function for drag slices
@@ -24,13 +38,6 @@ export class NrrdTools extends DrawToolCore {
24
38
  drag(opts) {
25
39
  this.dragOperator.drag(opts);
26
40
  }
27
- // draw(opts?: IDrawOpts) {
28
- // this.drawOperator.draw(opts);
29
- // }
30
- // start() {
31
- // console.log(this.drawOperator.start);
32
- // return this.drawOperator.start;
33
- // }
34
41
  /**
35
42
  * Set the Draw Display Canvas base size
36
43
  * @param size number
@@ -86,26 +93,16 @@ export class NrrdTools extends DrawToolCore {
86
93
  setSyncsliceNum: this.setSyncsliceNum,
87
94
  resetLayerCanvas: this.resetLayerCanvas,
88
95
  redrawDisplayCanvas: this.redrawDisplayCanvas,
89
- reloadMaskToLabel: this.reloadMaskToLabel,
90
96
  flipDisplayImageByAxis: this.flipDisplayImageByAxis,
91
97
  filterDrawedImage: this.filterDrawedImage,
92
98
  setEmptyCanvasSize: this.setEmptyCanvasSize,
93
99
  storeAllImages: this.storeAllImages,
94
100
  drawImageOnEmptyImage: this.drawImageOnEmptyImage,
95
- checkSharedPlaceSlice: this.checkSharedPlaceSlice,
96
- replaceArray: this.replaceArray,
97
- findSliceInSharedPlace: this.findSliceInSharedPlace,
98
- sliceArrayH: this.sliceArrayH,
99
- sliceArrayV: this.sliceArrayV,
100
- storeImageToAxis: this.storeImageToAxis,
101
- replaceVerticalColPixels: this.replaceVerticalColPixels,
102
- replaceHorizontalRowPixels: this.replaceHorizontalRowPixels,
103
101
  storeEachLayerImage: this.storeEachLayerImage,
104
- storeImageToLabel: this.storeImageToLabel,
105
- getRestLabel: this.getRestLabel,
102
+ storeImageToLayer: this.storeImageToLayer,
103
+ getRestLayer: this.getRestLayer,
106
104
  setIsDrawFalse: this.setIsDrawFalse,
107
- initPaintImages: this.initPaintImages,
108
- createEmptyPaintImage: this.createEmptyPaintImage,
105
+ getVolumeForLayer: this.getVolumeForLayer.bind(this),
109
106
  };
110
107
  this.guiParameterSettings = setupGui(guiOptions);
111
108
  }
@@ -121,6 +118,386 @@ export class NrrdTools extends DrawToolCore {
121
118
  guiSetting: this.guiParameterSettings,
122
119
  };
123
120
  }
121
+ // ── Layer & Channel Management API ─────────────────────────────────────
122
+ /**
123
+ * Set the active layer and update fillColor/brushColor to match the active channel.
124
+ */
125
+ setActiveLayer(layerId) {
126
+ this.gui_states.layer = layerId;
127
+ this.syncBrushColor();
128
+ }
129
+ /**
130
+ * Set the active channel (1-8) and update fillColor/brushColor.
131
+ */
132
+ setActiveChannel(channel) {
133
+ this.gui_states.activeChannel = channel;
134
+ this.syncBrushColor();
135
+ }
136
+ /**
137
+ * Sync brush/fill color from the active layer's volume color map.
138
+ * Falls back to global CHANNEL_HEX_COLORS if volume not available.
139
+ */
140
+ syncBrushColor() {
141
+ const channel = this.gui_states.activeChannel || 1;
142
+ const layer = this.gui_states.layer;
143
+ const volume = this.protectedData.maskData.volumes[layer];
144
+ if (volume) {
145
+ const hex = rgbaToHex(volume.getChannelColor(channel));
146
+ this.gui_states.fillColor = hex;
147
+ this.gui_states.brushColor = hex;
148
+ }
149
+ else {
150
+ const hex = CHANNEL_HEX_COLORS[channel] || '#00ff00';
151
+ this.gui_states.fillColor = hex;
152
+ this.gui_states.brushColor = hex;
153
+ }
154
+ }
155
+ /**
156
+ * Get the currently active layer id.
157
+ */
158
+ getActiveLayer() {
159
+ return this.gui_states.layer;
160
+ }
161
+ /**
162
+ * Get the currently active channel value.
163
+ */
164
+ getActiveChannel() {
165
+ return this.gui_states.activeChannel;
166
+ }
167
+ /**
168
+ * Set visibility of a layer and re-render.
169
+ */
170
+ setLayerVisible(layerId, visible) {
171
+ this.gui_states.layerVisibility[layerId] = visible;
172
+ this.reloadMasksFromVolume();
173
+ }
174
+ /**
175
+ * Check if a layer is visible.
176
+ */
177
+ isLayerVisible(layerId) {
178
+ var _a;
179
+ return (_a = this.gui_states.layerVisibility[layerId]) !== null && _a !== void 0 ? _a : true;
180
+ }
181
+ /**
182
+ * Set visibility of a specific channel within a layer and re-render.
183
+ */
184
+ setChannelVisible(layerId, channel, visible) {
185
+ if (this.gui_states.channelVisibility[layerId]) {
186
+ this.gui_states.channelVisibility[layerId][channel] = visible;
187
+ }
188
+ this.reloadMasksFromVolume();
189
+ }
190
+ /**
191
+ * Check if a specific channel within a layer is visible.
192
+ */
193
+ isChannelVisible(layerId, channel) {
194
+ var _a, _b;
195
+ return (_b = (_a = this.gui_states.channelVisibility[layerId]) === null || _a === void 0 ? void 0 : _a[channel]) !== null && _b !== void 0 ? _b : true;
196
+ }
197
+ /**
198
+ * Get the full layer visibility map.
199
+ */
200
+ getLayerVisibility() {
201
+ return Object.assign({}, this.gui_states.layerVisibility);
202
+ }
203
+ /**
204
+ * Get the full channel visibility map.
205
+ */
206
+ getChannelVisibility() {
207
+ const result = {};
208
+ for (const layerId of this.nrrd_states.layers) {
209
+ result[layerId] = Object.assign({}, this.gui_states.channelVisibility[layerId]);
210
+ }
211
+ return result;
212
+ }
213
+ /**
214
+ * Check if a specific layer contains any non-zero mask data.
215
+ *
216
+ * This is useful for determining if a layer has actual annotations
217
+ * before saving or exporting to avoid processing empty layers.
218
+ *
219
+ * @param layerId - The layer to check ('layer1', 'layer2', or 'layer3')
220
+ * @returns True if the layer has any non-zero voxel data, false if empty
221
+ *
222
+ * @example
223
+ * ```ts
224
+ * if (nrrdTools.hasLayerData('layer1')) {
225
+ * await useSaveMasks(caseId, 'layer1');
226
+ * }
227
+ * ```
228
+ */
229
+ hasLayerData(layerId) {
230
+ const volume = this.protectedData.maskData.volumes[layerId];
231
+ if (!volume) {
232
+ return false;
233
+ }
234
+ return volume.hasData();
235
+ }
236
+ // ── Custom Channel Color API ────────────────────────────────────────────
237
+ /**
238
+ * Set a custom color for a specific channel in a specific layer.
239
+ * Only affects this layer; other layers remain unchanged.
240
+ *
241
+ * @param layerId Layer to customize (e.g. 'layer1', 'layer2')
242
+ * @param channel Channel label (1-8)
243
+ * @param color RGBAColor object { r, g, b, a } (each 0-255)
244
+ *
245
+ * @example
246
+ * ```ts
247
+ * // Make channel 2 in layer2 orange
248
+ * nrrdTools.setChannelColor('layer2', 2, { r: 255, g: 128, b: 0, a: 255 });
249
+ * ```
250
+ */
251
+ setChannelColor(layerId, channel, color) {
252
+ const volume = this.protectedData.maskData.volumes[layerId];
253
+ if (!volume) {
254
+ console.warn(`setChannelColor: unknown layer "${layerId}"`);
255
+ return;
256
+ }
257
+ volume.setChannelColor(channel, color);
258
+ if (layerId === this.gui_states.layer && channel === this.gui_states.activeChannel) {
259
+ this.syncBrushColor();
260
+ }
261
+ this.reloadMasksFromVolume();
262
+ this.nrrd_states.onChannelColorChanged(layerId, channel, color);
263
+ }
264
+ /**
265
+ * Get the current color for a specific channel in a specific layer.
266
+ *
267
+ * @param layerId Layer to query
268
+ * @param channel Channel label (1-8)
269
+ * @returns RGBAColor object (copy, safe to mutate)
270
+ */
271
+ getChannelColor(layerId, channel) {
272
+ const volume = this.protectedData.maskData.volumes[layerId];
273
+ if (!volume) {
274
+ return { r: 0, g: 255, b: 0, a: 255 };
275
+ }
276
+ return volume.getChannelColor(channel);
277
+ }
278
+ /**
279
+ * Get a hex color string for a channel in a layer (e.g. '#ff8000').
280
+ */
281
+ getChannelHexColor(layerId, channel) {
282
+ return rgbaToHex(this.getChannelColor(layerId, channel));
283
+ }
284
+ /**
285
+ * Get a CSS rgba() color string for a channel in a layer.
286
+ */
287
+ getChannelCssColor(layerId, channel) {
288
+ return rgbaToCss(this.getChannelColor(layerId, channel));
289
+ }
290
+ /**
291
+ * Batch-set multiple channel colors for a single layer (single re-render).
292
+ *
293
+ * @param layerId Layer to customize
294
+ * @param colorMap Partial map of channel -> RGBAColor
295
+ *
296
+ * @example
297
+ * ```ts
298
+ * nrrdTools.setChannelColors('layer2', {
299
+ * 2: { r: 255, g: 128, b: 0, a: 255 },
300
+ * 3: { r: 100, g: 200, b: 50, a: 255 },
301
+ * });
302
+ * ```
303
+ */
304
+ setChannelColors(layerId, colorMap) {
305
+ const volume = this.protectedData.maskData.volumes[layerId];
306
+ if (!volume) {
307
+ console.warn(`setChannelColors: unknown layer "${layerId}"`);
308
+ return;
309
+ }
310
+ for (const [ch, color] of Object.entries(colorMap)) {
311
+ volume.setChannelColor(Number(ch), color);
312
+ }
313
+ if (layerId === this.gui_states.layer) {
314
+ this.syncBrushColor();
315
+ }
316
+ this.reloadMasksFromVolume();
317
+ }
318
+ /**
319
+ * Set the same color for a specific channel across ALL layers (single re-render).
320
+ *
321
+ * @param channel Channel label (1-8)
322
+ * @param color RGBAColor object
323
+ */
324
+ setAllLayersChannelColor(channel, color) {
325
+ for (const layerId of this.nrrd_states.layers) {
326
+ const volume = this.protectedData.maskData.volumes[layerId];
327
+ if (volume) {
328
+ volume.setChannelColor(channel, color);
329
+ }
330
+ }
331
+ if (channel === this.gui_states.activeChannel) {
332
+ this.syncBrushColor();
333
+ }
334
+ this.reloadMasksFromVolume();
335
+ }
336
+ /**
337
+ * Reset channel colors to system defaults.
338
+ *
339
+ * @param layerId Optional. If omitted, resets all layers.
340
+ * @param channel Optional. If omitted, resets all channels in the layer(s).
341
+ *
342
+ * @example
343
+ * ```ts
344
+ * nrrdTools.resetChannelColors('layer2', 2); // Reset only ch2 in layer2
345
+ * nrrdTools.resetChannelColors('layer2'); // Reset all channels in layer2
346
+ * nrrdTools.resetChannelColors(); // Reset all layers
347
+ * ```
348
+ */
349
+ resetChannelColors(layerId, channel) {
350
+ const layers = layerId ? [layerId] : this.nrrd_states.layers;
351
+ for (const lid of layers) {
352
+ const volume = this.protectedData.maskData.volumes[lid];
353
+ if (volume) {
354
+ volume.resetChannelColors(channel);
355
+ }
356
+ }
357
+ this.syncBrushColor();
358
+ this.reloadMasksFromVolume();
359
+ }
360
+ // ── Keyboard & History API ──────────────────────────────────────────────
361
+ /**
362
+ * Programmatically trigger an undo operation.
363
+ *
364
+ * Equivalent to pressing Ctrl+Z. Reverts the most recent drawing stroke
365
+ * on the currently active layer and syncs the result to the backend via
366
+ * the `getMask` callback.
367
+ *
368
+ * @example
369
+ * ```ts
370
+ * undoBtn.addEventListener('click', () => nrrdTools.undo());
371
+ * ```
372
+ */
373
+ undo() {
374
+ this.undoLastPainting();
375
+ }
376
+ /**
377
+ * Programmatically trigger a redo operation.
378
+ *
379
+ * Equivalent to pressing Ctrl+Y. Re-applies the most recently undone
380
+ * drawing stroke on the currently active layer and syncs the result to
381
+ * the backend via the `getMask` callback.
382
+ *
383
+ * @example
384
+ * ```ts
385
+ * redoBtn.addEventListener('click', () => nrrdTools.redo());
386
+ * ```
387
+ */
388
+ redo() {
389
+ this.redoLastPainting();
390
+ }
391
+ /**
392
+ * Enter keyboard-configuration mode.
393
+ *
394
+ * While active, every keydown/keyup handler in DrawToolCore and DragOperator
395
+ * is suppressed so the user can press arbitrary keys in the settings dialog
396
+ * without accidentally triggering drawing, undo, or contrast shortcuts.
397
+ *
398
+ * Always pair with {@link exitKeyboardConfig} when the dialog closes.
399
+ *
400
+ * @example
401
+ * ```ts
402
+ * dialog.addEventListener('open', () => nrrdTools.enterKeyboardConfig());
403
+ * ```
404
+ */
405
+ enterKeyboardConfig() {
406
+ this._configKeyBoard = true;
407
+ }
408
+ /**
409
+ * Exit keyboard-configuration mode and resume normal shortcut handling.
410
+ *
411
+ * @example
412
+ * ```ts
413
+ * dialog.addEventListener('close', () => nrrdTools.exitKeyboardConfig());
414
+ * ```
415
+ */
416
+ exitKeyboardConfig() {
417
+ this._configKeyBoard = false;
418
+ }
419
+ /**
420
+ * Enable or disable the contrast window/level shortcut (Ctrl/Meta key).
421
+ *
422
+ * When disabled:
423
+ * - Holding Ctrl no longer enters contrast mode
424
+ * - If contrast mode is currently active it is immediately exited
425
+ * - All other Ctrl-based shortcuts (Ctrl+Z undo, Ctrl+Y redo) are
426
+ * unaffected because they are checked in the keydown handler before
427
+ * the contrast guard runs
428
+ *
429
+ * @param enabled - Pass `false` to disable, `true` to re-enable.
430
+ *
431
+ * @example
432
+ * ```ts
433
+ * // Disable contrast when sphere tool is active
434
+ * nrrdTools.setContrastShortcutEnabled(false);
435
+ *
436
+ * // Re-enable when returning to draw mode
437
+ * nrrdTools.setContrastShortcutEnabled(true);
438
+ * ```
439
+ */
440
+ setContrastShortcutEnabled(enabled) {
441
+ var _a;
442
+ (_a = this.eventRouter) === null || _a === void 0 ? void 0 : _a.setContrastEnabled(enabled);
443
+ }
444
+ /**
445
+ * Returns whether the contrast shortcut is currently enabled.
446
+ */
447
+ isContrastShortcutEnabled() {
448
+ var _a, _b;
449
+ return (_b = (_a = this.eventRouter) === null || _a === void 0 ? void 0 : _a.isContrastEnabled()) !== null && _b !== void 0 ? _b : true;
450
+ }
451
+ /**
452
+ * Update keyboard shortcut bindings.
453
+ *
454
+ * Synchronises both the internal class field (read by every keydown handler)
455
+ * and the EventRouter's internal copy (used for modifier-key mode tracking)
456
+ * so the two never drift apart. If `mouseWheel` is changed the wheel event
457
+ * listener is automatically re-bound via {@link updateMouseWheelEvent}.
458
+ *
459
+ * Only the fields you supply are updated; omitted fields keep their
460
+ * current values.
461
+ *
462
+ * @param settings - Partial keyboard settings to override.
463
+ *
464
+ * @example
465
+ * ```ts
466
+ * nrrdTools.setKeyboardSettings({
467
+ * undo: 'z',
468
+ * redo: 'y',
469
+ * crosshair: 'c',
470
+ * mouseWheel: 'Scroll:Slice',
471
+ * });
472
+ * ```
473
+ */
474
+ setKeyboardSettings(settings) {
475
+ var _a;
476
+ const mouseWheelChanged = settings.mouseWheel !== undefined
477
+ && settings.mouseWheel !== this._keyboardSettings.mouseWheel;
478
+ Object.assign(this._keyboardSettings, settings);
479
+ (_a = this.eventRouter) === null || _a === void 0 ? void 0 : _a.setKeyboardSettings(settings);
480
+ if (mouseWheelChanged) {
481
+ this.updateMouseWheelEvent();
482
+ }
483
+ }
484
+ /**
485
+ * Get a snapshot of the current keyboard shortcut bindings.
486
+ *
487
+ * Returns a shallow copy so callers cannot accidentally mutate internal
488
+ * state. To update settings use {@link setKeyboardSettings} instead.
489
+ *
490
+ * @returns Current keyboard settings.
491
+ *
492
+ * @example
493
+ * ```ts
494
+ * const { undo, redo } = nrrdTools.getKeyboardSettings();
495
+ * console.log(`Undo: Ctrl+${undo}, Redo: Ctrl+${redo}`);
496
+ * ```
497
+ */
498
+ getKeyboardSettings() {
499
+ return Object.assign({}, this._keyboardSettings);
500
+ }
124
501
  /**
125
502
  * A initialise function for nrrd_tools
126
503
  */
@@ -156,6 +533,16 @@ export class NrrdTools extends DrawToolCore {
156
533
  this.nrrd_states.ratios.y = randomSlice.x.volume.spacing[1];
157
534
  this.nrrd_states.ratios.z = randomSlice.x.volume.spacing[2];
158
535
  this.nrrd_states.dimensions = randomSlice.x.volume.dimensions;
536
+ // Phase 2 Day 9: Re-initialize MaskVolume with real NRRD dimensions.
537
+ // This replaces the 1×1×1 placeholders from CommToolsData constructor
538
+ // and "turns on" all Day 7/8 volume read/write paths.
539
+ // Invalidate reusable buffer from previous dataset
540
+ this.invalidateSliceBuffer();
541
+ const [vw, vh, vd] = this.nrrd_states.dimensions;
542
+ this.protectedData.maskData.volumes = this.nrrd_states.layers.reduce((acc, id) => {
543
+ acc[id] = new MaskVolume(vw, vh, vd, 1);
544
+ return acc;
545
+ }, {});
159
546
  this.nrrd_states.spaceOrigin = randomSlice.x.volume.header.space_origin.map((item) => {
160
547
  return item * 1;
161
548
  });
@@ -164,16 +551,13 @@ export class NrrdTools extends DrawToolCore {
164
551
  item.y.contrastOrder = index;
165
552
  item.z.contrastOrder = index;
166
553
  });
167
- this.nrrd_states.sharedPlace.x = this.getSharedPlace(this.nrrd_states.dimensions[0], this.nrrd_states.ratios.x);
168
- this.nrrd_states.sharedPlace.y = this.getSharedPlace(this.nrrd_states.dimensions[1], this.nrrd_states.ratios.y);
169
- this.nrrd_states.sharedPlace.z = this.getSharedPlace(this.nrrd_states.dimensions[2], this.nrrd_states.ratios.z);
170
- // init paintImages array
171
- this.initPaintImages(this.nrrd_states.dimensions);
554
+ // Phase 3: initPaintImages removed (MaskVolume initialized separately)
555
+ // this.initPaintImages(this.nrrd_states.dimensions);
172
556
  // init displayslices array, the axis default is "z"
173
557
  this.setDisplaySlicesBaseOnAxis();
174
558
  this.afterLoadSlice();
175
559
  }
176
- loadingMaskByLabel(masks, index, imageData) {
560
+ loadingMaskByLayer(masks, index, imageData) {
177
561
  let imageDataLable = this.protectedData.ctxes.emptyCtx.createImageData(this.nrrd_states.nrrd_x_pixel, this.nrrd_states.nrrd_y_pixel);
178
562
  this.setEmptyCanvasSize();
179
563
  for (let j = 0; j < masks[index].data.length; j++) {
@@ -182,6 +566,7 @@ export class NrrdTools extends DrawToolCore {
182
566
  }
183
567
  return imageDataLable;
184
568
  }
569
+ // need to remove
185
570
  setMasksData(masksData, loadingBar) {
186
571
  if (!!masksData) {
187
572
  this.nrrd_states.loadMaskJson = true;
@@ -191,27 +576,27 @@ export class NrrdTools extends DrawToolCore {
191
576
  progress.innerText = "Loading masks data......";
192
577
  }
193
578
  this.setEmptyCanvasSize();
194
- const len = masksData["label1"].length;
579
+ const len = masksData["layer1"].length;
195
580
  for (let i = 0; i < len; i++) {
196
581
  let imageData = this.protectedData.ctxes.emptyCtx.createImageData(this.nrrd_states.nrrd_x_pixel, this.nrrd_states.nrrd_y_pixel);
197
- let imageDataLabel1, imageDataLabel2, imageDataLabel3;
198
- if (masksData["label1"][i].data.length > 0) {
582
+ let imageDataLayer1, imageDataLayer2, imageDataLayer3;
583
+ if (masksData["layer1"][i].data.length > 0) {
199
584
  this.setEmptyCanvasSize();
200
- imageDataLabel1 = this.loadingMaskByLabel(masksData["label1"], i, imageData);
201
- this.protectedData.ctxes.emptyCtx.putImageData(imageDataLabel1, 0, 0);
202
- this.storeEachLayerImage(i, "label1");
585
+ imageDataLayer1 = this.loadingMaskByLayer(masksData["layer1"], i, imageData);
586
+ this.protectedData.ctxes.emptyCtx.putImageData(imageDataLayer1, 0, 0);
587
+ this.storeEachLayerImage(i, "layer1");
203
588
  }
204
- if (masksData["label2"][i].data.length > 0) {
589
+ if (masksData["layer2"][i].data.length > 0) {
205
590
  this.setEmptyCanvasSize();
206
- imageDataLabel2 = this.loadingMaskByLabel(masksData["label2"], i, imageData);
207
- this.protectedData.ctxes.emptyCtx.putImageData(imageDataLabel2, 0, 0);
208
- this.storeEachLayerImage(i, "label2");
591
+ imageDataLayer2 = this.loadingMaskByLayer(masksData["layer2"], i, imageData);
592
+ this.protectedData.ctxes.emptyCtx.putImageData(imageDataLayer2, 0, 0);
593
+ this.storeEachLayerImage(i, "layer2");
209
594
  }
210
- if (masksData["label3"][i].data.length > 0) {
595
+ if (masksData["layer3"][i].data.length > 0) {
211
596
  this.setEmptyCanvasSize();
212
- imageDataLabel3 = this.loadingMaskByLabel(masksData["label3"], i, imageData);
213
- this.protectedData.ctxes.emptyCtx.putImageData(imageDataLabel3, 0, 0);
214
- this.storeEachLayerImage(i, "label3");
597
+ imageDataLayer3 = this.loadingMaskByLayer(masksData["layer3"], i, imageData);
598
+ this.protectedData.ctxes.emptyCtx.putImageData(imageDataLayer3, 0, 0);
599
+ this.storeEachLayerImage(i, "layer3");
215
600
  }
216
601
  this.setEmptyCanvasSize();
217
602
  this.protectedData.ctxes.emptyCtx.putImageData(imageData, 0, 0);
@@ -224,6 +609,53 @@ export class NrrdTools extends DrawToolCore {
224
609
  }
225
610
  }
226
611
  }
612
+ /**
613
+ * Load raw voxel data into MaskVolume layers.
614
+ *
615
+ * Expects pre-extracted voxel bytes (e.g. from useNiftiVoxelData).
616
+ *
617
+ * @param layerVoxels Map of layer ID to raw voxel Uint8Array
618
+ * Keys should be 'layer1', 'layer2', 'layer3'
619
+ * @param loadingBar Optional loading bar UI
620
+ */
621
+ setMasksFromNIfTI(layerVoxels, loadingBar) {
622
+ if (!layerVoxels || layerVoxels.size === 0)
623
+ return;
624
+ if (loadingBar) {
625
+ loadingBar.loadingContainer.style.display = "flex";
626
+ loadingBar.progress.innerText = "Loading mask layers from NIfTI...";
627
+ }
628
+ try {
629
+ for (const [layerId, rawData] of layerVoxels) {
630
+ const volume = this.protectedData.maskData.volumes[layerId];
631
+ if (!volume) {
632
+ console.warn(`setMasksFromNIfTI: unknown layer "${layerId}", skipping`);
633
+ continue;
634
+ }
635
+ const expectedLen = volume.getRawData().length;
636
+ // Ensure we copy exactly the right number of bytes
637
+ if (rawData.length >= expectedLen) {
638
+ volume.setRawData(rawData.slice(0, expectedLen));
639
+ }
640
+ else {
641
+ const padded = new Uint8Array(expectedLen);
642
+ padded.set(rawData);
643
+ volume.setRawData(padded);
644
+ }
645
+ }
646
+ // Reload the current slice from MaskVolume to canvas
647
+ this.reloadMasksFromVolume();
648
+ this.gui_states.resetZoom();
649
+ }
650
+ catch (error) {
651
+ console.error("Error loading NIfTI masks:", error);
652
+ }
653
+ finally {
654
+ if (loadingBar) {
655
+ loadingBar.loadingContainer.style.display = "none";
656
+ }
657
+ }
658
+ }
227
659
  setShowInMainArea() {
228
660
  this.nrrd_states.showContrast = true;
229
661
  }
@@ -251,63 +683,6 @@ export class NrrdTools extends DrawToolCore {
251
683
  // mock mouse up
252
684
  this.drawCalSphereUp();
253
685
  }
254
- getSharedPlace(len, ratio) {
255
- let old = -1;
256
- let same = [];
257
- let temp = new Set();
258
- for (let i = 0; i < len; i++) {
259
- const index = Math.floor(i * ratio);
260
- if (index === old) {
261
- temp.add(i - 1);
262
- temp.add(i);
263
- }
264
- else {
265
- old = index;
266
- }
267
- }
268
- temp.forEach((value) => {
269
- same.push(value);
270
- });
271
- return same;
272
- }
273
- /**
274
- * init all painted images for store images
275
- * @param dimensions
276
- */
277
- initPaintImages(dimensions) {
278
- this.createEmptyPaintImage(dimensions, this.protectedData.maskData.paintImages);
279
- this.createEmptyPaintImage(dimensions, this.protectedData.maskData.paintImagesLabel1);
280
- this.createEmptyPaintImage(dimensions, this.protectedData.maskData.paintImagesLabel2);
281
- this.createEmptyPaintImage(dimensions, this.protectedData.maskData.paintImagesLabel3);
282
- }
283
- createEmptyPaintImage(dimensions, paintImages) {
284
- for (let i = 0; i < dimensions[0]; i++) {
285
- const markImage_x = this.protectedData.ctxes.emptyCtx.createImageData(this.nrrd_states.nrrd_z_pixel, this.nrrd_states.nrrd_y_pixel);
286
- const initMark_x = {
287
- index: i,
288
- image: markImage_x,
289
- };
290
- paintImages.x.push(initMark_x);
291
- }
292
- // for y slices' marks
293
- for (let i = 0; i < dimensions[1]; i++) {
294
- const markImage_y = this.protectedData.ctxes.emptyCtx.createImageData(this.nrrd_states.nrrd_x_pixel, this.nrrd_states.nrrd_z_pixel);
295
- const initMark_y = {
296
- index: i,
297
- image: markImage_y,
298
- };
299
- paintImages.y.push(initMark_y);
300
- }
301
- // for z slices' marks
302
- for (let i = 0; i < dimensions[2]; i++) {
303
- const markImage_z = this.protectedData.ctxes.emptyCtx.createImageData(this.nrrd_states.nrrd_x_pixel, this.nrrd_states.nrrd_y_pixel);
304
- const initMark_z = {
305
- index: i,
306
- image: markImage_z,
307
- };
308
- paintImages.z.push(initMark_z);
309
- }
310
- }
311
686
  /**
312
687
  * Switch all contrast slices' orientation
313
688
  * @param {string} aixs:"x" | "y" | "z"
@@ -443,19 +818,15 @@ export class NrrdTools extends DrawToolCore {
443
818
  // To effectively reduce the js memory garbage
444
819
  this.protectedData.allSlicesArray.length = 0;
445
820
  this.protectedData.displaySlices.length = 0;
446
- this.undoArray.length = 0;
447
- this.protectedData.maskData.paintImages.x.length = 0;
448
- this.protectedData.maskData.paintImages.y.length = 0;
449
- this.protectedData.maskData.paintImages.z.length = 0;
450
- this.protectedData.maskData.paintImagesLabel1.x.length = 0;
451
- this.protectedData.maskData.paintImagesLabel1.y.length = 0;
452
- this.protectedData.maskData.paintImagesLabel1.z.length = 0;
453
- this.protectedData.maskData.paintImagesLabel2.x.length = 0;
454
- this.protectedData.maskData.paintImagesLabel2.y.length = 0;
455
- this.protectedData.maskData.paintImagesLabel2.z.length = 0;
456
- this.protectedData.maskData.paintImagesLabel3.x.length = 0;
457
- this.protectedData.maskData.paintImagesLabel3.y.length = 0;
458
- this.protectedData.maskData.paintImagesLabel3.z.length = 0;
821
+ // Phase 6: Clear all undo/redo stacks
822
+ this.undoManager.clearAll();
823
+ // Phase 3: Reset MaskVolume storage to 1×1×1 placeholders
824
+ this.protectedData.maskData.volumes = this.nrrd_states.layers.reduce((acc, id) => {
825
+ acc[id] = new MaskVolume(1, 1, 1, 1);
826
+ return acc;
827
+ }, {});
828
+ // Invalidate reusable slice buffer
829
+ this.invalidateSliceBuffer();
459
830
  this.clearDictionary(this.protectedData.skipSlicesDic);
460
831
  // this.nrrd_states.previousPanelL = this.nrrd_states.previousPanelT = -99999;
461
832
  this.protectedData.canvases.displayCanvas.style.left =
@@ -483,10 +854,20 @@ export class NrrdTools extends DrawToolCore {
483
854
  }
484
855
  setSliceMoving(step) {
485
856
  if (this.protectedData.mainPreSlices) {
486
- this.protectedData.Is_Draw = true;
487
- this.setSyncsliceNum();
488
- this.dragOperator.updateIndex(step);
489
- this.setIsDrawFalse(1000);
857
+ // Accumulate steps so no keydown events are lost
858
+ this._pendingSliceStep += step;
859
+ // RAF throttle: render at most once per frame, but apply ALL accumulated steps
860
+ if (this._sliceRAFId !== null)
861
+ return;
862
+ this._sliceRAFId = requestAnimationFrame(() => {
863
+ this._sliceRAFId = null;
864
+ const totalStep = this._pendingSliceStep;
865
+ this._pendingSliceStep = 0;
866
+ this.protectedData.Is_Draw = true;
867
+ this.setSyncsliceNum();
868
+ this.dragOperator.updateIndex(totalStep);
869
+ this.setIsDrawFalse(1000);
870
+ });
490
871
  }
491
872
  }
492
873
  setMainAreaSize(factor) {
@@ -593,14 +974,16 @@ export class NrrdTools extends DrawToolCore {
593
974
  this.updateMaxIndex();
594
975
  // reset origin canvas and the nrrd_states origin Width/height
595
976
  // reset the current index
977
+ // (also calls resizePaintArea → reloads masks, resizes canvases)
596
978
  this.setOriginCanvasAndPre();
597
979
  // update the show number div on top area
598
980
  this.dragOperator.updateShowNumDiv(this.nrrd_states.contrastNum);
599
981
  // repaint all contrast images
600
982
  this.repraintCurrentContrastSlice();
601
- // resize the draw/drawOutLayer/display canvas size
602
- this.resizePaintArea(this.nrrd_states.sizeFoctor);
603
- this.resetPaintAreaUIPosition();
983
+ // Refresh display after contrast repaint (no need for full resizePaintArea
984
+ // since canvases were already resized in setOriginCanvasAndPre above)
985
+ this.redrawDisplayCanvas();
986
+ this.compositeAllLayers();
604
987
  }
605
988
  setMainPreSlice() {
606
989
  this.protectedData.mainPreSlices = this.protectedData.displaySlices[0];
@@ -636,12 +1019,8 @@ export class NrrdTools extends DrawToolCore {
636
1019
  this.nrrd_states.oldIndex =
637
1020
  this.protectedData.mainPreSlices.initIndex * this.nrrd_states.RSARatio;
638
1021
  this.nrrd_states.currentIndex = this.protectedData.mainPreSlices.initIndex;
639
- this.undoArray = [
640
- {
641
- sliceIndex: this.nrrd_states.currentIndex,
642
- layers: { label1: [], label2: [], label3: [] },
643
- },
644
- ];
1022
+ // Phase 6: Reset undo/redo stacks on new dataset load
1023
+ this.undoManager.clearAll();
645
1024
  // compute max index
646
1025
  this.updateMaxIndex();
647
1026
  this.dragOperator.updateShowNumDiv(this.nrrd_states.contrastNum);
@@ -661,13 +1040,9 @@ export class NrrdTools extends DrawToolCore {
661
1040
  this.protectedData.canvases.originCanvas.width;
662
1041
  this.nrrd_states.originHeight =
663
1042
  this.protectedData.canvases.originCanvas.height;
664
- // In html the width and height is pixels,
665
- // So the value must be int
666
- // Therefore, we must use Math.floor rather than using Math.ceil
667
- this.nrrd_states.changedWidth =
668
- Math.floor(this.nrrd_states.originWidth * Number(this.nrrd_states.sizeFoctor));
669
- this.nrrd_states.changedHeight =
670
- Math.floor(this.nrrd_states.originWidth * Number(this.nrrd_states.sizeFoctor));
1043
+ // Let resizePaintArea be the sole setter of changedWidth/changedHeight.
1044
+ // Setting them here would defeat the sizeChanged detection in resizePaintArea,
1045
+ // causing canvas elements to keep stale dimensions after axis switches.
671
1046
  this.resizePaintArea(this.nrrd_states.sizeFoctor);
672
1047
  this.resetPaintAreaUIPosition();
673
1048
  }
@@ -686,19 +1061,19 @@ export class NrrdTools extends DrawToolCore {
686
1061
  this.protectedData.mainAreaContainer.appendChild(loadingbar);
687
1062
  }
688
1063
  clearStoreImages() {
689
- this.protectedData.maskData.paintImages.x.length = 0;
690
- this.protectedData.maskData.paintImages.y.length = 0;
691
- this.protectedData.maskData.paintImages.z.length = 0;
692
- this.protectedData.maskData.paintImagesLabel1.x.length = 0;
693
- this.protectedData.maskData.paintImagesLabel1.y.length = 0;
694
- this.protectedData.maskData.paintImagesLabel1.z.length = 0;
695
- this.protectedData.maskData.paintImagesLabel2.x.length = 0;
696
- this.protectedData.maskData.paintImagesLabel2.y.length = 0;
697
- this.protectedData.maskData.paintImagesLabel2.z.length = 0;
698
- this.protectedData.maskData.paintImagesLabel3.x.length = 0;
699
- this.protectedData.maskData.paintImagesLabel3.y.length = 0;
700
- this.protectedData.maskData.paintImagesLabel3.z.length = 0;
701
- this.initPaintImages(this.nrrd_states.dimensions);
1064
+ // Phase 3 Task 3.1: Only clear the active layer's MaskVolume
1065
+ if (this.nrrd_states.dimensions.length === 3) {
1066
+ const [w, h, d] = this.nrrd_states.dimensions;
1067
+ const activeLayer = this.gui_states.layer;
1068
+ // Re-init only the active layer's MaskVolume
1069
+ this.protectedData.maskData.volumes[activeLayer] = new MaskVolume(w, h, d, 1);
1070
+ // Phase 6 Task 6.6: Clear undo/redo stack for this layer (volume too large to snapshot)
1071
+ this.undoManager.clearLayer(activeLayer);
1072
+ // Phase 3 Task 3.2: Notify external that this layer's volume was cleared
1073
+ this.nrrd_states.onClearLayerVolume(activeLayer);
1074
+ }
1075
+ // Invalidate reusable slice buffer
1076
+ this.invalidateSliceBuffer();
702
1077
  }
703
1078
  /**
704
1079
  * Reset the draw and display canvases layout after mouse pan.
@@ -728,12 +1103,9 @@ export class NrrdTools extends DrawToolCore {
728
1103
  resetLayerCanvas() {
729
1104
  this.protectedData.canvases.drawingCanvasLayerMaster.width =
730
1105
  this.protectedData.canvases.drawingCanvasLayerMaster.width;
731
- this.protectedData.canvases.drawingCanvasLayerOne.width =
732
- this.protectedData.canvases.drawingCanvasLayerOne.width;
733
- this.protectedData.canvases.drawingCanvasLayerTwo.width =
734
- this.protectedData.canvases.drawingCanvasLayerTwo.width;
735
- this.protectedData.canvases.drawingCanvasLayerThree.width =
736
- this.protectedData.canvases.drawingCanvasLayerThree.width;
1106
+ for (const [, target] of this.protectedData.layerTargets) {
1107
+ target.canvas.width = target.canvas.width;
1108
+ }
737
1109
  }
738
1110
  redrawMianPreOnDisplayCanvas() {
739
1111
  var _a;
@@ -755,93 +1127,83 @@ export class NrrdTools extends DrawToolCore {
755
1127
  * @param factor number
756
1128
  */
757
1129
  resizePaintArea(factor) {
758
- /**
759
- * clear canvas
760
- */
1130
+ const newWidth = Math.floor(this.nrrd_states.originWidth * factor);
1131
+ const newHeight = Math.floor(this.nrrd_states.originHeight * factor);
1132
+ const sizeChanged = newWidth !== this.nrrd_states.changedWidth ||
1133
+ newHeight !== this.nrrd_states.changedHeight;
1134
+ // Always clear display/drawing/origin canvases (needed for contrast updates)
761
1135
  this.protectedData.canvases.originCanvas.width =
762
1136
  this.protectedData.canvases.originCanvas.width;
763
1137
  this.protectedData.canvases.displayCanvas.width =
764
1138
  this.protectedData.canvases.displayCanvas.width;
765
1139
  this.protectedData.canvases.drawingCanvas.width =
766
1140
  this.protectedData.canvases.drawingCanvas.width;
767
- this.resetLayerCanvas();
768
- this.nrrd_states.changedWidth = Math.floor(this.nrrd_states.originWidth * factor);
769
- this.nrrd_states.changedHeight = Math.floor(this.nrrd_states.originHeight * factor);
770
- /**
771
- * resize canvas
772
- */
773
- this.protectedData.canvases.displayCanvas.width =
774
- this.nrrd_states.changedWidth;
775
- this.protectedData.canvases.displayCanvas.height =
776
- this.nrrd_states.changedHeight;
777
- this.protectedData.canvases.drawingCanvas.width =
778
- this.nrrd_states.changedWidth;
779
- this.protectedData.canvases.drawingCanvas.height =
780
- this.nrrd_states.changedHeight;
781
- this.protectedData.canvases.drawingCanvasLayerMaster.width =
782
- this.nrrd_states.changedWidth;
783
- this.protectedData.canvases.drawingCanvasLayerMaster.height =
784
- this.nrrd_states.changedHeight;
785
- this.protectedData.canvases.drawingCanvasLayerOne.width =
786
- this.nrrd_states.changedWidth;
787
- this.protectedData.canvases.drawingCanvasLayerOne.height =
788
- this.nrrd_states.changedHeight;
789
- this.protectedData.canvases.drawingCanvasLayerTwo.width =
790
- this.nrrd_states.changedWidth;
791
- this.protectedData.canvases.drawingCanvasLayerTwo.height =
792
- this.nrrd_states.changedHeight;
793
- this.protectedData.canvases.drawingCanvasLayerThree.width =
794
- this.nrrd_states.changedWidth;
795
- this.protectedData.canvases.drawingCanvasLayerThree.height =
796
- this.nrrd_states.changedHeight;
1141
+ if (sizeChanged) {
1142
+ // Only clear and resize layer canvases when size actually changes.
1143
+ // Skipping this avoids the expensive reloadMasksFromVolume() call
1144
+ // during contrast toggle (where size stays the same).
1145
+ this.resetLayerCanvas();
1146
+ this.nrrd_states.changedWidth = newWidth;
1147
+ this.nrrd_states.changedHeight = newHeight;
1148
+ this.protectedData.canvases.displayCanvas.width = newWidth;
1149
+ this.protectedData.canvases.displayCanvas.height = newHeight;
1150
+ this.protectedData.canvases.drawingCanvas.width = newWidth;
1151
+ this.protectedData.canvases.drawingCanvas.height = newHeight;
1152
+ this.protectedData.canvases.drawingCanvasLayerMaster.width = newWidth;
1153
+ this.protectedData.canvases.drawingCanvasLayerMaster.height = newHeight;
1154
+ for (const [, target] of this.protectedData.layerTargets) {
1155
+ target.canvas.width = newWidth;
1156
+ target.canvas.height = newHeight;
1157
+ }
1158
+ }
797
1159
  this.redrawDisplayCanvas();
798
- this.reloadMaskToLabel(this.protectedData.maskData.paintImages, this.protectedData.ctxes.drawingLayerMasterCtx);
799
- this.reloadMaskToLabel(this.protectedData.maskData.paintImagesLabel1, this.protectedData.ctxes.drawingLayerOneCtx);
800
- this.reloadMaskToLabel(this.protectedData.maskData.paintImagesLabel2, this.protectedData.ctxes.drawingLayerTwoCtx);
801
- // need to check here again: why use ctx two not three. now modify to three
802
- // this.reloadMaskToLabel(this.protectedData.maskData.paintImagesLabel3, this.protectedData.ctxes.drawingLayerTwoCtx);
803
- this.reloadMaskToLabel(this.protectedData.maskData.paintImagesLabel3, this.protectedData.ctxes.drawingLayerThreeCtx);
1160
+ if (sizeChanged) {
1161
+ // Phase 3: Reload masks from MaskVolume only when canvas size changed
1162
+ this.reloadMasksFromVolume();
1163
+ }
1164
+ else {
1165
+ // Size unchanged (e.g. contrast toggle): layer canvases still have
1166
+ // valid data, just recomposite to master for the start() render loop.
1167
+ this.compositeAllLayers();
1168
+ }
804
1169
  }
805
1170
  /**
806
- * Used to init the mask on each label and reload
807
- * @param paintImages
808
- * @param ctx
1171
+ * Phase 3: Reload all mask layers from MaskVolume using buffer reuse
1172
+ * Replaces the old reloadMaskToLayer approach
809
1173
  */
810
- reloadMaskToLabel(paintImages, ctx) {
811
- let paintedImage;
812
- switch (this.protectedData.axis) {
813
- case "x":
814
- if (paintImages.x.length > 0) {
815
- paintedImage = this.filterDrawedImage("x", this.nrrd_states.currentIndex, paintImages);
816
- }
817
- else {
818
- paintedImage = undefined;
819
- }
820
- break;
821
- case "y":
822
- if (paintImages.y.length > 0) {
823
- paintedImage = this.filterDrawedImage("y", this.nrrd_states.currentIndex, paintImages);
824
- }
825
- else {
826
- paintedImage = undefined;
827
- }
828
- break;
829
- case "z":
830
- if (paintImages.z.length > 0) {
831
- paintedImage = this.filterDrawedImage("z", this.nrrd_states.currentIndex, paintImages);
832
- }
833
- else {
834
- paintedImage = undefined;
835
- }
836
- break;
1174
+ reloadMasksFromVolume() {
1175
+ const axis = this.protectedData.axis;
1176
+ let sliceIndex = this.nrrd_states.currentIndex;
1177
+ // Clamp sliceIndex to valid range for current axis
1178
+ // (currentIndex may not be updated yet when switching axes)
1179
+ try {
1180
+ const vol = this.getVolumeForLayer(this.nrrd_states.layers[0]);
1181
+ const dims = vol.getDimensions();
1182
+ const maxSlice = axis === "x" ? dims.width : axis === "y" ? dims.height : dims.depth;
1183
+ if (sliceIndex >= maxSlice)
1184
+ sliceIndex = maxSlice - 1;
1185
+ if (sliceIndex < 0)
1186
+ sliceIndex = 0;
837
1187
  }
838
- if (paintedImage === null || paintedImage === void 0 ? void 0 : paintedImage.image) {
839
- // redraw the stored data to empty point 1
840
- this.setEmptyCanvasSize();
841
- this.protectedData.ctxes.emptyCtx.putImageData(paintedImage.image, 0, 0);
842
- ctx === null || ctx === void 0 ? void 0 : ctx.drawImage(this.protectedData.canvases.emptyCanvas, 0, 0, this.nrrd_states.changedWidth, this.nrrd_states.changedHeight);
1188
+ catch ( /* volume not ready */_a) { /* volume not ready */ }
1189
+ // Get a single reusable buffer shared across all layer renders
1190
+ const buffer = this.getOrCreateSliceBuffer(axis);
1191
+ if (!buffer)
1192
+ return;
1193
+ const w = this.nrrd_states.changedWidth;
1194
+ const h = this.nrrd_states.changedHeight;
1195
+ // Clear and render each layer using the shared buffer
1196
+ for (const layerId of this.nrrd_states.layers) {
1197
+ const target = this.protectedData.layerTargets.get(layerId);
1198
+ if (!target)
1199
+ continue;
1200
+ target.ctx.clearRect(0, 0, w, h);
1201
+ this.renderSliceToCanvas(layerId, axis, sliceIndex, buffer, target.ctx, w, h);
843
1202
  }
1203
+ // Composite all layers to master canvas
1204
+ this.compositeAllLayers();
844
1205
  }
1206
+ // compositeAllLayers() is inherited from CommToolsData
845
1207
  /**
846
1208
  * flip the canvas to a correct position.
847
1209
  * This is because the slice canvas from threejs is not in a correct 2D postion.
@@ -940,7 +1302,7 @@ export class NrrdTools extends DrawToolCore {
940
1302
  */
941
1303
  updateMouseWheelEvent() {
942
1304
  this.protectedData.canvases.drawingCanvas.removeEventListener("wheel", this.drawingPrameters.handleMouseZoomSliceWheel);
943
- switch (this.nrrd_states.keyboardSettings.mouseWheel) {
1305
+ switch (this._keyboardSettings.mouseWheel) {
944
1306
  case "Scroll:Zoom":
945
1307
  this.drawingPrameters.handleMouseZoomSliceWheel = this.configMouseZoomWheel();
946
1308
  break;