Skip to content

Commit b306e07

Browse files
committed
refactor(voxel-annotations): remove obsolete color controls and limite the value to the valid range for the data type.
1 parent bef93e3 commit b306e07

File tree

3 files changed

+47
-97
lines changed

3 files changed

+47
-97
lines changed

src/layer/vox/controls.ts

Lines changed: 1 addition & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -17,45 +17,14 @@
1717
import type { UserLayerConstructor } from "#src/layer/index.js";
1818
import { LayerActionContext } from "#src/layer/index.js";
1919
import type { UserLayerWithVoxelEditing } from "#src/layer/vox/index.js";
20-
import type { WatchableValueInterface } from "#src/trackable_value.js";
2120
import { observeWatchable } from "#src/trackable_value.js";
22-
import { unpackRGB } from "#src/util/color.js";
23-
import { RefCounted } from "#src/util/disposable.js";
24-
import { vec3 } from "#src/util/geom.js";
25-
import { NullarySignal } from "#src/util/signal.js";
2621
import type { LayerControlDefinition } from "#src/widget/layer_control.js";
2722
import { registerLayerControl } from "#src/widget/layer_control.js";
2823
import { buttonLayerControl } from "#src/widget/layer_control_button.js";
2924
import { checkboxLayerControl } from "#src/widget/layer_control_checkbox.js";
30-
import { colorLayerControl } from "#src/widget/layer_control_color.js";
3125
import { enumLayerControl } from "#src/widget/layer_control_enum.js";
3226
import { rangeLayerControl } from "#src/widget/layer_control_range.js";
3327

34-
class BigIntAsTrackableRGB
35-
extends RefCounted
36-
implements WatchableValueInterface<vec3>
37-
{
38-
changed = new NullarySignal();
39-
private tempColor = vec3.create();
40-
41-
constructor(public source: WatchableValueInterface<bigint>) {
42-
super();
43-
this.registerDisposer(source.changed.add(this.changed.dispatch));
44-
}
45-
46-
get value(): vec3 {
47-
const bigintValue = this.source.value;
48-
const [r, g, b] = unpackRGB(Number(bigintValue & 0xffffffn));
49-
vec3.set(this.tempColor, r, g, b);
50-
return this.tempColor;
51-
}
52-
53-
set value(newValue: vec3) {
54-
const rgb = newValue.map((c: number) => Math.round(c * 255));
55-
this.source.value = BigInt((rgb[0] << 16) | (rgb[1] << 8) | rgb[2]);
56-
}
57-
}
58-
5928
export const VOXEL_LAYER_CONTROLS: LayerControlDefinition<UserLayerWithVoxelEditing>[] =
6029
[
6130
{
@@ -104,14 +73,6 @@ export const VOXEL_LAYER_CONTROLS: LayerControlDefinition<UserLayerWithVoxelEdit
10473
layer.handleVoxAction("redo", new LayerActionContext()),
10574
}),
10675
},
107-
{
108-
label: "Paint Color",
109-
toolJson: { type: "vox-paint-color" },
110-
...colorLayerControl(
111-
(layer: UserLayerWithVoxelEditing) =>
112-
new BigIntAsTrackableRGB(layer.paintValue),
113-
),
114-
},
11576
{
11677
label: "Paint Value",
11778
toolJson: { type: "vox-paint-value" },
@@ -121,7 +82,7 @@ export const VOXEL_LAYER_CONTROLS: LayerControlDefinition<UserLayerWithVoxelEdit
12182
control.title = "Specify segment ID or intensity value to paint";
12283
control.addEventListener("change", () => {
12384
try {
124-
layer.setVoxelPaintValue(BigInt(control.value));
85+
layer.setVoxelPaintValue(control.value);
12586
} catch {
12687
control.value = layer.paintValue.value.toString();
12788
}

src/layer/vox/index.ts

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
getChunkTransformParameters,
3131
} from "#src/render_coordinate_transform.js";
3232
import type { SliceViewSourceOptions } from "#src/sliceview/base.js";
33+
import { DataType } from "#src/sliceview/base.js";
3334
import type { MultiscaleVolumeChunkSource } from "#src/sliceview/volume/frontend.js";
3435
import type { ImageRenderLayer } from "#src/sliceview/volume/image_renderlayer.js";
3536
import type { SegmentationRenderLayer } from "#src/sliceview/volume/segmentation_renderlayer.js";
@@ -62,6 +63,16 @@ const BRUSH_SHAPE_JSON_KEY = "brushShape";
6263
const FLOOD_FILL_MAX_VOXELS_JSON_KEY = "floodFillMaxVoxels";
6364
const PAINT_VALUE_JSON_KEY = "paintValue";
6465

66+
const DATA_TYPE_BIT_INFO = {
67+
[DataType.UINT8]: { bits: 8, signed: false },
68+
[DataType.INT8]: { bits: 8, signed: true },
69+
[DataType.UINT16]: { bits: 16, signed: false },
70+
[DataType.INT16]: { bits: 16, signed: true },
71+
[DataType.UINT32]: { bits: 32, signed: false },
72+
[DataType.INT32]: { bits: 32, signed: true },
73+
[DataType.UINT64]: { bits: 64, signed: false },
74+
};
75+
6576
export class VoxelEditingContext
6677
extends RefCounted
6778
implements VoxelEditControllerHost
@@ -189,7 +200,7 @@ export declare abstract class UserLayerWithVoxelEditing extends UserLayer {
189200
transform: WatchableValueInterface<RenderLayerTransformOrError>,
190201
): ImageRenderLayer | SegmentationRenderLayer;
191202
abstract getVoxelPaintValue(erase: boolean): bigint;
192-
abstract setVoxelPaintValue(value: bigint): void;
203+
abstract setVoxelPaintValue(value: any): bigint;
193204

194205
initializeVoxelEditingForSubsource(
195206
loadedSubsource: LoadedDataSubsource,
@@ -276,10 +287,41 @@ export function UserLayerWithVoxelEditingMixin<
276287
if (erase) return 0n;
277288
return this.paintValue.value;
278289
}
279-
setVoxelPaintValue(value: bigint) {
280-
this.paintValue.value = value;
290+
291+
setVoxelPaintValue(x: any) {
292+
const editContext = this.editingContexts.values().next().value;
293+
const dataType = editContext.primarySource.dataType;
294+
let value: bigint;
295+
296+
if (dataType === DataType.FLOAT32) {
297+
const floatValue = parseFloat(String(x));
298+
value = BigInt(Math.round(floatValue));
299+
} else {
300+
value = BigInt(x);
301+
}
302+
303+
const info = DATA_TYPE_BIT_INFO[dataType as keyof typeof DATA_TYPE_BIT_INFO];
304+
if (!info) {
305+
this.paintValue.value = value;
306+
return value;
307+
}
308+
309+
const { bits, signed } = info;
310+
const mask = (1n << BigInt(bits)) - 1n;
311+
let truncated = value & mask;
312+
313+
if (signed) {
314+
const signBit = 1n << BigInt(bits - 1);
315+
if ((truncated & signBit) !== 0n) {
316+
truncated -= (1n << BigInt(bits));
317+
}
318+
}
319+
320+
this.paintValue.value = truncated;
321+
return truncated;
281322
}
282323

324+
283325
abstract _createVoxelRenderLayer(
284326
source: MultiscaleVolumeChunkSource,
285327
transform: WatchableValueInterface<RenderLayerTransformOrError>,
@@ -359,7 +401,7 @@ export function UserLayerWithVoxelEditingMixin<
359401
controller.redo();
360402
break;
361403
case "randomize-paint-value":
362-
this.paintValue.value = randomUint64();
404+
this.setVoxelPaintValue(randomUint64());
363405
break;
364406
}
365407
}

src/voxel_annotation/TODOs.md

Lines changed: 0 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -17,56 +17,3 @@
1717

1818
- design a dataset creation feature
1919
- adapt the brush size to the zoom level linearly
20-
21-
## Merging vox layer into seg and img layers
22-
23-
The proposed architecture integrates voxel editing directly into the existing Image and Segmentation layers by leveraging their inherent capabilities, rather than introducing a separate, simplified render layer. The core of the design is a new UserLayerWithVoxelEditingMixin which equips a host UserLayer with an editing controller and an associated in-memory VolumeChunkSource for optimistic previews. When a user paints, the edits are applied locally to this in-memory source. A second instance of the layer's primary, feature-rich RenderLayer class is then used to draw these edits as an overlay. This ensures the live preview is rendered with the exact same user-defined shaders and settings as the base data for perfect visual fidelity, while also elegantly handling the performance issue of editing compressed chunks by operating on an uncompressed in-memory source. This architecture reuses existing components, simplifies the overall codebase by eliminating the need for a separate VoxelAnnotationRenderLayer, and cleanly separates the concerns of displaying committed data versus previewing transient edits.
24-
25-
```mermaid
26-
sequenceDiagram
27-
participant User
28-
participant Tool as VoxelBrushTool
29-
participant ControllerFE as VoxelEditController (FE)
30-
participant EditSourceFE as OverlayChunkSource (FE)
31-
participant BaseSourceFE as VolumeChunkSource (FE)
32-
participant ControllerBE as VoxelEditController (BE)
33-
participant BaseSourceBE as VolumeChunkSource (BE)
34-
35-
User->>Tool: Mouse Down/Drag
36-
Tool->>ControllerFE: paintBrushWithShape(mouse, ...)
37-
ControllerFE->>ControllerFE: Calculates affected voxels and chunks
38-
39-
ControllerFE->>EditSourceFE: applyLocalEdits(chunkKeys, ...)
40-
activate EditSourceFE
41-
EditSourceFE->>EditSourceFE: Modifies its own in-memory chunk data
42-
note over EditSourceFE: This chunk's texture is re-uploaded to the GPU
43-
deactivate EditSourceFE
44-
45-
ControllerFE->>ControllerBE: commitEdits(edits, ...) [RPC]
46-
47-
activate ControllerBE
48-
ControllerBE->>ControllerBE: Debounces and batches edits
49-
ControllerBE->>BaseSourceBE: applyEdits(chunkKeys, ...)
50-
activate BaseSourceBE
51-
BaseSourceBE-->>ControllerBE: Returns VoxelChange (for undo stack)
52-
deactivate BaseSourceBE
53-
ControllerBE->>ControllerFE: callChunkReload(chunkKeys) [RPC]
54-
activate ControllerFE
55-
ControllerFE->>BaseSourceFE: invalidateChunks(chunkKeys)
56-
note over BaseSourceFE: BaseSourceFE re-fetches chunk with the now-permanent edit.
57-
ControllerFE->>EditSourceFE: clearOptimisticChunk(chunkKeys)
58-
deactivate ControllerFE
59-
60-
ControllerBE->>ControllerBE: Pushes change to Undo Stack & enqueues for downsampling
61-
deactivate ControllerBE
62-
63-
loop Downsampling & Reload Cascade
64-
ControllerBE->>ControllerBE: downsampleStep(chunkKeys)
65-
ControllerBE->>ControllerFE: callChunkReload(chunkKeys) [RPC]
66-
activate ControllerFE
67-
ControllerFE->>BaseSourceFE: invalidateChunks(chunkKeys)
68-
note over BaseSourceFE: BaseSourceFE re-fetches chunk with the now-permanent edit.
69-
ControllerFE->>EditSourceFE: clearOptimisticChunk(chunkKeys)
70-
deactivate ControllerFE
71-
end
72-
```

0 commit comments

Comments
 (0)