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.
- 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 +113 -11
- package/dist/Utils/segmentation/CommToolsData.js +314 -59
- 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 +56 -76
- package/dist/Utils/segmentation/DrawToolCore.js +383 -891
- package/dist/Utils/segmentation/DrawToolCore.js.map +1 -1
- package/dist/Utils/segmentation/NrrdTools.d.ts +277 -15
- package/dist/Utils/segmentation/NrrdTools.js +600 -234
- package/dist/Utils/segmentation/NrrdTools.js.map +1 -1
- package/dist/Utils/segmentation/core/MaskVolume.d.ts +444 -0
- package/dist/Utils/segmentation/core/MaskVolume.js +1117 -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 -39
- package/dist/Utils/segmentation/coreTools/coreType.js.map +1 -1
- package/dist/Utils/segmentation/coreTools/gui.d.ts +10 -17
- 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 +145 -0
- package/dist/Utils/segmentation/eventRouter/EventRouter.js +375 -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 +28 -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 +62 -0
- package/dist/Utils/segmentation/tools/ImageStoreHelper.js +160 -0
- package/dist/Utils/segmentation/tools/ImageStoreHelper.js.map +1 -0
- package/dist/Utils/segmentation/tools/SphereTool.d.ts +36 -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 +60 -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 +4627 -1997
- package/dist/bundle.umd.js +4630 -1996
- 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 +113 -11
- package/dist/types/Utils/segmentation/DragOperator.d.ts +13 -7
- package/dist/types/Utils/segmentation/DrawToolCore.d.ts +56 -76
- package/dist/types/Utils/segmentation/NrrdTools.d.ts +277 -15
- package/dist/types/Utils/segmentation/core/MaskVolume.d.ts +444 -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 -39
- package/dist/types/Utils/segmentation/coreTools/gui.d.ts +10 -17
- package/dist/types/Utils/segmentation/eventRouter/EventRouter.d.ts +145 -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 +28 -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 +62 -0
- package/dist/types/Utils/segmentation/tools/SphereTool.d.ts +36 -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,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.
|
|
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
|
-
|
|
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;
|
|
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
|
-
|
|
487
|
-
this.
|
|
488
|
-
|
|
489
|
-
this.
|
|
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
|
-
//
|
|
602
|
-
|
|
603
|
-
this.
|
|
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
|
-
|
|
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
|
-
//
|
|
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));
|
|
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
|
-
|
|
690
|
-
this.
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
this.
|
|
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.
|
|
732
|
-
|
|
733
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
|
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
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
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
|
-
*
|
|
807
|
-
*
|
|
808
|
-
* @param ctx
|
|
1174
|
+
* Phase 3: Reload all mask layers from MaskVolume using buffer reuse
|
|
1175
|
+
* Replaces the old reloadMaskToLayer approach
|
|
809
1176
|
*/
|
|
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;
|
|
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
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
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
|
-
|
|
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.
|
|
1309
|
+
switch (this._keyboardSettings.mouseWheel) {
|
|
944
1310
|
case "Scroll:Zoom":
|
|
945
1311
|
this.drawingPrameters.handleMouseZoomSliceWheel = this.configMouseZoomWheel();
|
|
946
1312
|
break;
|