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.
- package/README.md +1056 -5
- package/dist/Scene/copperScene.d.ts +3 -0
- package/dist/Scene/copperScene.js +13 -0
- package/dist/Scene/copperScene.js.map +1 -1
- package/dist/Utils/segmentation/CommToolsData.d.ts +109 -6
- package/dist/Utils/segmentation/CommToolsData.js +314 -50
- package/dist/Utils/segmentation/CommToolsData.js.map +1 -1
- package/dist/Utils/segmentation/DragOperator.d.ts +13 -7
- package/dist/Utils/segmentation/DragOperator.js +51 -165
- package/dist/Utils/segmentation/DragOperator.js.map +1 -1
- package/dist/Utils/segmentation/DrawToolCore.d.ts +57 -76
- package/dist/Utils/segmentation/DrawToolCore.js +368 -878
- package/dist/Utils/segmentation/DrawToolCore.js.map +1 -1
- package/dist/Utils/segmentation/NrrdTools.d.ts +277 -15
- package/dist/Utils/segmentation/NrrdTools.js +594 -232
- package/dist/Utils/segmentation/NrrdTools.js.map +1 -1
- package/dist/Utils/segmentation/core/MaskVolume.d.ts +477 -0
- package/dist/Utils/segmentation/core/MaskVolume.js +1238 -0
- package/dist/Utils/segmentation/core/MaskVolume.js.map +1 -0
- package/dist/Utils/segmentation/core/MigrationUtils.d.ts +97 -0
- package/dist/Utils/segmentation/core/MigrationUtils.js +163 -0
- package/dist/Utils/segmentation/core/MigrationUtils.js.map +1 -0
- package/dist/Utils/segmentation/core/UndoManager.d.ts +47 -0
- package/dist/Utils/segmentation/core/UndoManager.js +86 -0
- package/dist/Utils/segmentation/core/UndoManager.js.map +1 -0
- package/dist/Utils/segmentation/core/index.d.ts +76 -0
- package/dist/Utils/segmentation/core/index.js +75 -0
- package/dist/Utils/segmentation/core/index.js.map +1 -0
- package/dist/Utils/segmentation/core/types.d.ts +106 -0
- package/dist/Utils/segmentation/core/types.js +98 -0
- package/dist/Utils/segmentation/core/types.js.map +1 -0
- package/dist/Utils/segmentation/coreTools/coreType.d.ts +55 -36
- package/dist/Utils/segmentation/coreTools/coreType.js.map +1 -1
- package/dist/Utils/segmentation/coreTools/gui.d.ts +6 -16
- package/dist/Utils/segmentation/coreTools/gui.js +23 -27
- package/dist/Utils/segmentation/coreTools/gui.js.map +1 -1
- package/dist/Utils/segmentation/eventRouter/EventRouter.d.ts +144 -0
- package/dist/Utils/segmentation/eventRouter/EventRouter.js +369 -0
- package/dist/Utils/segmentation/eventRouter/EventRouter.js.map +1 -0
- package/dist/Utils/segmentation/eventRouter/index.d.ts +7 -0
- package/dist/Utils/segmentation/eventRouter/index.js +7 -0
- package/dist/Utils/segmentation/eventRouter/index.js.map +1 -0
- package/dist/Utils/segmentation/eventRouter/types.d.ts +70 -0
- package/dist/Utils/segmentation/eventRouter/types.js +7 -0
- package/dist/Utils/segmentation/eventRouter/types.js.map +1 -0
- package/dist/Utils/segmentation/tools/BaseTool.d.ts +25 -0
- package/dist/Utils/segmentation/tools/BaseTool.js +18 -0
- package/dist/Utils/segmentation/tools/BaseTool.js.map +1 -0
- package/dist/Utils/segmentation/tools/ContrastTool.d.ts +26 -0
- package/dist/Utils/segmentation/tools/ContrastTool.js +118 -0
- package/dist/Utils/segmentation/tools/ContrastTool.js.map +1 -0
- package/dist/Utils/segmentation/tools/CrosshairTool.d.ts +30 -0
- package/dist/Utils/segmentation/tools/CrosshairTool.js +157 -0
- package/dist/Utils/segmentation/tools/CrosshairTool.js.map +1 -0
- package/dist/Utils/segmentation/tools/DragSliceTool.d.ts +44 -0
- package/dist/Utils/segmentation/tools/DragSliceTool.js +170 -0
- package/dist/Utils/segmentation/tools/DragSliceTool.js.map +1 -0
- package/dist/Utils/segmentation/tools/EraserTool.d.ts +16 -0
- package/dist/Utils/segmentation/tools/EraserTool.js +92 -0
- package/dist/Utils/segmentation/tools/EraserTool.js.map +1 -0
- package/dist/Utils/segmentation/tools/ImageStoreHelper.d.ts +65 -0
- package/dist/Utils/segmentation/tools/ImageStoreHelper.js +174 -0
- package/dist/Utils/segmentation/tools/ImageStoreHelper.js.map +1 -0
- package/dist/Utils/segmentation/tools/SphereTool.d.ts +38 -0
- package/dist/Utils/segmentation/tools/SphereTool.js +193 -0
- package/dist/Utils/segmentation/tools/SphereTool.js.map +1 -0
- package/dist/Utils/segmentation/tools/ZoomTool.d.ts +20 -0
- package/dist/Utils/segmentation/tools/ZoomTool.js +59 -0
- package/dist/Utils/segmentation/tools/ZoomTool.js.map +1 -0
- package/dist/Utils/segmentation/tools/index.d.ts +22 -0
- package/dist/Utils/segmentation/tools/index.js +23 -0
- package/dist/Utils/segmentation/tools/index.js.map +1 -0
- package/dist/bundle.esm.js +4740 -1979
- package/dist/bundle.umd.js +4743 -1978
- package/dist/index.d.ts +5 -3
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/types/Renderer/copperRenderer.d.ts +1 -1
- package/dist/types/Renderer/copperRendererOnDemond.d.ts +1 -1
- package/dist/types/Scene/copperScene.d.ts +3 -0
- package/dist/types/Utils/segmentation/CommToolsData.d.ts +109 -6
- package/dist/types/Utils/segmentation/DragOperator.d.ts +13 -7
- package/dist/types/Utils/segmentation/DrawToolCore.d.ts +57 -76
- package/dist/types/Utils/segmentation/NrrdTools.d.ts +277 -15
- package/dist/types/Utils/segmentation/core/MaskVolume.d.ts +477 -0
- package/dist/types/Utils/segmentation/core/MigrationUtils.d.ts +97 -0
- package/dist/types/Utils/segmentation/core/UndoManager.d.ts +47 -0
- package/dist/types/Utils/segmentation/core/index.d.ts +76 -0
- package/dist/types/Utils/segmentation/core/types.d.ts +106 -0
- package/dist/types/Utils/segmentation/coreTools/coreType.d.ts +55 -36
- package/dist/types/Utils/segmentation/coreTools/gui.d.ts +6 -16
- package/dist/types/Utils/segmentation/eventRouter/EventRouter.d.ts +144 -0
- package/dist/types/Utils/segmentation/eventRouter/index.d.ts +7 -0
- package/dist/types/Utils/segmentation/eventRouter/types.d.ts +70 -0
- package/dist/types/Utils/segmentation/tools/BaseTool.d.ts +25 -0
- package/dist/types/Utils/segmentation/tools/ContrastTool.d.ts +26 -0
- package/dist/types/Utils/segmentation/tools/CrosshairTool.d.ts +30 -0
- package/dist/types/Utils/segmentation/tools/DragSliceTool.d.ts +44 -0
- package/dist/types/Utils/segmentation/tools/EraserTool.d.ts +16 -0
- package/dist/types/Utils/segmentation/tools/ImageStoreHelper.d.ts +65 -0
- package/dist/types/Utils/segmentation/tools/SphereTool.d.ts +38 -0
- package/dist/types/Utils/segmentation/tools/ZoomTool.d.ts +20 -0
- package/dist/types/Utils/segmentation/tools/index.d.ts +22 -0
- package/dist/types/index.d.ts +5 -3
- package/dist/types/types/types.d.ts +3 -3
- package/dist/types/types.d.ts +3 -3
- package/package.json +1 -1
- package/dist/Utils/segmentation/coreTools/archive.d.ts +0 -1
- package/dist/Utils/segmentation/coreTools/archive.js +0 -23
- package/dist/Utils/segmentation/coreTools/archive.js.map +0 -1
- 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.
|
|
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
|
-
|
|
105
|
-
|
|
102
|
+
storeImageToLayer: this.storeImageToLayer,
|
|
103
|
+
getRestLayer: this.getRestLayer,
|
|
106
104
|
setIsDrawFalse: this.setIsDrawFalse,
|
|
107
|
-
|
|
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
|
-
|
|
168
|
-
|
|
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
|
-
|
|
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["
|
|
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
|
|
198
|
-
if (masksData["
|
|
582
|
+
let imageDataLayer1, imageDataLayer2, imageDataLayer3;
|
|
583
|
+
if (masksData["layer1"][i].data.length > 0) {
|
|
199
584
|
this.setEmptyCanvasSize();
|
|
200
|
-
|
|
201
|
-
this.protectedData.ctxes.emptyCtx.putImageData(
|
|
202
|
-
this.storeEachLayerImage(i, "
|
|
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["
|
|
589
|
+
if (masksData["layer2"][i].data.length > 0) {
|
|
205
590
|
this.setEmptyCanvasSize();
|
|
206
|
-
|
|
207
|
-
this.protectedData.ctxes.emptyCtx.putImageData(
|
|
208
|
-
this.storeEachLayerImage(i, "
|
|
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["
|
|
595
|
+
if (masksData["layer3"][i].data.length > 0) {
|
|
211
596
|
this.setEmptyCanvasSize();
|
|
212
|
-
|
|
213
|
-
this.protectedData.ctxes.emptyCtx.putImageData(
|
|
214
|
-
this.storeEachLayerImage(i, "
|
|
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
|
-
|
|
447
|
-
this.
|
|
448
|
-
|
|
449
|
-
this.protectedData.maskData.
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
this.
|
|
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
|
-
|
|
487
|
-
this.
|
|
488
|
-
|
|
489
|
-
this.
|
|
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
|
-
//
|
|
602
|
-
|
|
603
|
-
this.
|
|
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
|
-
|
|
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
|
-
//
|
|
665
|
-
//
|
|
666
|
-
//
|
|
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
|
-
|
|
690
|
-
this.
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
this.
|
|
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.
|
|
732
|
-
|
|
733
|
-
|
|
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
|
-
|
|
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
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
this.
|
|
775
|
-
|
|
776
|
-
this.
|
|
777
|
-
|
|
778
|
-
this.
|
|
779
|
-
|
|
780
|
-
this.
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
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
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
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
|
-
*
|
|
807
|
-
*
|
|
808
|
-
* @param ctx
|
|
1171
|
+
* Phase 3: Reload all mask layers from MaskVolume using buffer reuse
|
|
1172
|
+
* Replaces the old reloadMaskToLayer approach
|
|
809
1173
|
*/
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
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
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
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.
|
|
1305
|
+
switch (this._keyboardSettings.mouseWheel) {
|
|
944
1306
|
case "Scroll:Zoom":
|
|
945
1307
|
this.drawingPrameters.handleMouseZoomSliceWheel = this.configMouseZoomWheel();
|
|
946
1308
|
break;
|