copper3d 2.2.3 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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 +113 -11
  6. package/dist/Utils/segmentation/CommToolsData.js +314 -59
  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 +56 -76
  12. package/dist/Utils/segmentation/DrawToolCore.js +383 -891
  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 +600 -234
  16. package/dist/Utils/segmentation/NrrdTools.js.map +1 -1
  17. package/dist/Utils/segmentation/core/MaskVolume.d.ts +444 -0
  18. package/dist/Utils/segmentation/core/MaskVolume.js +1117 -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 -39
  33. package/dist/Utils/segmentation/coreTools/coreType.js.map +1 -1
  34. package/dist/Utils/segmentation/coreTools/gui.d.ts +10 -17
  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 +145 -0
  38. package/dist/Utils/segmentation/eventRouter/EventRouter.js +375 -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 +28 -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 +62 -0
  62. package/dist/Utils/segmentation/tools/ImageStoreHelper.js +160 -0
  63. package/dist/Utils/segmentation/tools/ImageStoreHelper.js.map +1 -0
  64. package/dist/Utils/segmentation/tools/SphereTool.d.ts +36 -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 +60 -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 +4627 -1997
  74. package/dist/bundle.umd.js +4630 -1996
  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 +113 -11
  82. package/dist/types/Utils/segmentation/DragOperator.d.ts +13 -7
  83. package/dist/types/Utils/segmentation/DrawToolCore.d.ts +56 -76
  84. package/dist/types/Utils/segmentation/NrrdTools.d.ts +277 -15
  85. package/dist/types/Utils/segmentation/core/MaskVolume.d.ts +444 -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 -39
  91. package/dist/types/Utils/segmentation/coreTools/gui.d.ts +10 -17
  92. package/dist/types/Utils/segmentation/eventRouter/EventRouter.d.ts +145 -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 +28 -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 +62 -0
  101. package/dist/types/Utils/segmentation/tools/SphereTool.d.ts +36 -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,70 +683,14 @@ 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"
314
689
  * */
315
690
  setSliceOrientation(axisTo) {
691
+ var _a;
316
692
  let convetObj;
317
- if (this.nrrd_states.enableCursorChoose || this.gui_states.sphere) {
693
+ if (((_a = this.eventRouter) === null || _a === void 0 ? void 0 : _a.isCrosshairEnabled()) || this.gui_states.sphere) {
318
694
  if (this.protectedData.axis === "z") {
319
695
  this.cursorPage.z.index = this.nrrd_states.currentIndex;
320
696
  this.cursorPage.z.cursorPageX = this.nrrd_states.cursorPageX;
@@ -443,19 +819,15 @@ export class NrrdTools extends DrawToolCore {
443
819
  // To effectively reduce the js memory garbage
444
820
  this.protectedData.allSlicesArray.length = 0;
445
821
  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;
822
+ // Phase 6: Clear all undo/redo stacks
823
+ this.undoManager.clearAll();
824
+ // Phase 3: Reset MaskVolume storage to 1×1×1 placeholders
825
+ this.protectedData.maskData.volumes = this.nrrd_states.layers.reduce((acc, id) => {
826
+ acc[id] = new MaskVolume(1, 1, 1, 1);
827
+ return acc;
828
+ }, {});
829
+ // Invalidate reusable slice buffer
830
+ this.invalidateSliceBuffer();
459
831
  this.clearDictionary(this.protectedData.skipSlicesDic);
460
832
  // this.nrrd_states.previousPanelL = this.nrrd_states.previousPanelT = -99999;
461
833
  this.protectedData.canvases.displayCanvas.style.left =
@@ -483,10 +855,20 @@ export class NrrdTools extends DrawToolCore {
483
855
  }
484
856
  setSliceMoving(step) {
485
857
  if (this.protectedData.mainPreSlices) {
486
- this.protectedData.Is_Draw = true;
487
- this.setSyncsliceNum();
488
- this.dragOperator.updateIndex(step);
489
- this.setIsDrawFalse(1000);
858
+ // Accumulate steps so no keydown events are lost
859
+ this._pendingSliceStep += step;
860
+ // RAF throttle: render at most once per frame, but apply ALL accumulated steps
861
+ if (this._sliceRAFId !== null)
862
+ return;
863
+ this._sliceRAFId = requestAnimationFrame(() => {
864
+ this._sliceRAFId = null;
865
+ const totalStep = this._pendingSliceStep;
866
+ this._pendingSliceStep = 0;
867
+ this.protectedData.Is_Draw = true;
868
+ this.setSyncsliceNum();
869
+ this.dragOperator.updateIndex(totalStep);
870
+ this.setIsDrawFalse(1000);
871
+ });
490
872
  }
491
873
  }
492
874
  setMainAreaSize(factor) {
@@ -593,14 +975,16 @@ export class NrrdTools extends DrawToolCore {
593
975
  this.updateMaxIndex();
594
976
  // reset origin canvas and the nrrd_states origin Width/height
595
977
  // reset the current index
978
+ // (also calls resizePaintArea → reloads masks, resizes canvases)
596
979
  this.setOriginCanvasAndPre();
597
980
  // update the show number div on top area
598
981
  this.dragOperator.updateShowNumDiv(this.nrrd_states.contrastNum);
599
982
  // repaint all contrast images
600
983
  this.repraintCurrentContrastSlice();
601
- // resize the draw/drawOutLayer/display canvas size
602
- this.resizePaintArea(this.nrrd_states.sizeFoctor);
603
- this.resetPaintAreaUIPosition();
984
+ // Refresh display after contrast repaint (no need for full resizePaintArea
985
+ // since canvases were already resized in setOriginCanvasAndPre above)
986
+ this.redrawDisplayCanvas();
987
+ this.compositeAllLayers();
604
988
  }
605
989
  setMainPreSlice() {
606
990
  this.protectedData.mainPreSlices = this.protectedData.displaySlices[0];
@@ -636,12 +1020,8 @@ export class NrrdTools extends DrawToolCore {
636
1020
  this.nrrd_states.oldIndex =
637
1021
  this.protectedData.mainPreSlices.initIndex * this.nrrd_states.RSARatio;
638
1022
  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
- ];
1023
+ // Phase 6: Reset undo/redo stacks on new dataset load
1024
+ this.undoManager.clearAll();
645
1025
  // compute max index
646
1026
  this.updateMaxIndex();
647
1027
  this.dragOperator.updateShowNumDiv(this.nrrd_states.contrastNum);
@@ -661,13 +1041,9 @@ export class NrrdTools extends DrawToolCore {
661
1041
  this.protectedData.canvases.originCanvas.width;
662
1042
  this.nrrd_states.originHeight =
663
1043
  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));
1044
+ // Let resizePaintArea be the sole setter of changedWidth/changedHeight.
1045
+ // Setting them here would defeat the sizeChanged detection in resizePaintArea,
1046
+ // causing canvas elements to keep stale dimensions after axis switches.
671
1047
  this.resizePaintArea(this.nrrd_states.sizeFoctor);
672
1048
  this.resetPaintAreaUIPosition();
673
1049
  }
@@ -686,19 +1062,21 @@ export class NrrdTools extends DrawToolCore {
686
1062
  this.protectedData.mainAreaContainer.appendChild(loadingbar);
687
1063
  }
688
1064
  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);
1065
+ // Phase 3 Task 3.1: Only clear the active layer's MaskVolume
1066
+ if (this.nrrd_states.dimensions.length === 3) {
1067
+ const [w, h, d] = this.nrrd_states.dimensions;
1068
+ const activeLayer = this.gui_states.layer;
1069
+ // Re-init only the active layer's MaskVolume
1070
+ this.protectedData.maskData.volumes[activeLayer] = new MaskVolume(w, h, d, 1);
1071
+ // Phase 6 Task 6.6: Clear undo/redo stack for this layer (volume too large to snapshot)
1072
+ this.undoManager.clearLayer(activeLayer);
1073
+ // Phase 3 Task 3.2: Notify external that this layer's volume was cleared
1074
+ this.nrrd_states.onClearLayerVolume(activeLayer);
1075
+ }
1076
+ // Invalidate reusable slice buffer
1077
+ this.invalidateSliceBuffer();
1078
+ // Reload all layers to canvas (restores other layers' visuals)
1079
+ this.reloadMasksFromVolume();
702
1080
  }
703
1081
  /**
704
1082
  * Reset the draw and display canvases layout after mouse pan.
@@ -728,12 +1106,9 @@ export class NrrdTools extends DrawToolCore {
728
1106
  resetLayerCanvas() {
729
1107
  this.protectedData.canvases.drawingCanvasLayerMaster.width =
730
1108
  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;
1109
+ for (const [, target] of this.protectedData.layerTargets) {
1110
+ target.canvas.width = target.canvas.width;
1111
+ }
737
1112
  }
738
1113
  redrawMianPreOnDisplayCanvas() {
739
1114
  var _a;
@@ -755,93 +1130,83 @@ export class NrrdTools extends DrawToolCore {
755
1130
  * @param factor number
756
1131
  */
757
1132
  resizePaintArea(factor) {
758
- /**
759
- * clear canvas
760
- */
1133
+ const newWidth = Math.floor(this.nrrd_states.originWidth * factor);
1134
+ const newHeight = Math.floor(this.nrrd_states.originHeight * factor);
1135
+ const sizeChanged = newWidth !== this.nrrd_states.changedWidth ||
1136
+ newHeight !== this.nrrd_states.changedHeight;
1137
+ // Always clear display/drawing/origin canvases (needed for contrast updates)
761
1138
  this.protectedData.canvases.originCanvas.width =
762
1139
  this.protectedData.canvases.originCanvas.width;
763
1140
  this.protectedData.canvases.displayCanvas.width =
764
1141
  this.protectedData.canvases.displayCanvas.width;
765
1142
  this.protectedData.canvases.drawingCanvas.width =
766
1143
  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;
1144
+ if (sizeChanged) {
1145
+ // Only clear and resize layer canvases when size actually changes.
1146
+ // Skipping this avoids the expensive reloadMasksFromVolume() call
1147
+ // during contrast toggle (where size stays the same).
1148
+ this.resetLayerCanvas();
1149
+ this.nrrd_states.changedWidth = newWidth;
1150
+ this.nrrd_states.changedHeight = newHeight;
1151
+ this.protectedData.canvases.displayCanvas.width = newWidth;
1152
+ this.protectedData.canvases.displayCanvas.height = newHeight;
1153
+ this.protectedData.canvases.drawingCanvas.width = newWidth;
1154
+ this.protectedData.canvases.drawingCanvas.height = newHeight;
1155
+ this.protectedData.canvases.drawingCanvasLayerMaster.width = newWidth;
1156
+ this.protectedData.canvases.drawingCanvasLayerMaster.height = newHeight;
1157
+ for (const [, target] of this.protectedData.layerTargets) {
1158
+ target.canvas.width = newWidth;
1159
+ target.canvas.height = newHeight;
1160
+ }
1161
+ }
797
1162
  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);
1163
+ if (sizeChanged) {
1164
+ // Phase 3: Reload masks from MaskVolume only when canvas size changed
1165
+ this.reloadMasksFromVolume();
1166
+ }
1167
+ else {
1168
+ // Size unchanged (e.g. contrast toggle): layer canvases still have
1169
+ // valid data, just recomposite to master for the start() render loop.
1170
+ this.compositeAllLayers();
1171
+ }
804
1172
  }
805
1173
  /**
806
- * Used to init the mask on each label and reload
807
- * @param paintImages
808
- * @param ctx
1174
+ * Phase 3: Reload all mask layers from MaskVolume using buffer reuse
1175
+ * Replaces the old reloadMaskToLayer approach
809
1176
  */
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;
1177
+ reloadMasksFromVolume() {
1178
+ const axis = this.protectedData.axis;
1179
+ let sliceIndex = this.nrrd_states.currentIndex;
1180
+ // Clamp sliceIndex to valid range for current axis
1181
+ // (currentIndex may not be updated yet when switching axes)
1182
+ try {
1183
+ const vol = this.getVolumeForLayer(this.nrrd_states.layers[0]);
1184
+ const dims = vol.getDimensions();
1185
+ const maxSlice = axis === "x" ? dims.width : axis === "y" ? dims.height : dims.depth;
1186
+ if (sliceIndex >= maxSlice)
1187
+ sliceIndex = maxSlice - 1;
1188
+ if (sliceIndex < 0)
1189
+ sliceIndex = 0;
837
1190
  }
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);
1191
+ catch ( /* volume not ready */_a) { /* volume not ready */ }
1192
+ // Get a single reusable buffer shared across all layer renders
1193
+ const buffer = this.getOrCreateSliceBuffer(axis);
1194
+ if (!buffer)
1195
+ return;
1196
+ const w = this.nrrd_states.changedWidth;
1197
+ const h = this.nrrd_states.changedHeight;
1198
+ // Clear and render each layer using the shared buffer
1199
+ for (const layerId of this.nrrd_states.layers) {
1200
+ const target = this.protectedData.layerTargets.get(layerId);
1201
+ if (!target)
1202
+ continue;
1203
+ target.ctx.clearRect(0, 0, w, h);
1204
+ this.renderSliceToCanvas(layerId, axis, sliceIndex, buffer, target.ctx, w, h);
843
1205
  }
1206
+ // Composite all layers to master canvas
1207
+ this.compositeAllLayers();
844
1208
  }
1209
+ // compositeAllLayers() is inherited from CommToolsData
845
1210
  /**
846
1211
  * flip the canvas to a correct position.
847
1212
  * This is because the slice canvas from threejs is not in a correct 2D postion.
@@ -922,7 +1287,8 @@ export class NrrdTools extends DrawToolCore {
922
1287
  */
923
1288
  configMouseSliceWheel() {
924
1289
  const handleMouseZoomSliceWheelMove = (e) => {
925
- if (this.protectedData.Is_Shift_Pressed) {
1290
+ var _a;
1291
+ if ((_a = this.eventRouter) === null || _a === void 0 ? void 0 : _a.isShiftHeld()) {
926
1292
  return;
927
1293
  }
928
1294
  e.preventDefault();
@@ -940,7 +1306,7 @@ export class NrrdTools extends DrawToolCore {
940
1306
  */
941
1307
  updateMouseWheelEvent() {
942
1308
  this.protectedData.canvases.drawingCanvas.removeEventListener("wheel", this.drawingPrameters.handleMouseZoomSliceWheel);
943
- switch (this.nrrd_states.keyboardSettings.mouseWheel) {
1309
+ switch (this._keyboardSettings.mouseWheel) {
944
1310
  case "Scroll:Zoom":
945
1311
  this.drawingPrameters.handleMouseZoomSliceWheel = this.configMouseZoomWheel();
946
1312
  break;