Skip to content

Commit a44fd8b

Browse files
committed
refactor: replace label system with paint value mechanism and simplify voxel editing logic
1 parent 12edde4 commit a44fd8b

File tree

6 files changed

+104
-214
lines changed

6 files changed

+104
-214
lines changed

src/layer/vox/controls.ts

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,42 @@
11
import type { UserLayerConstructor } from "#src/layer/index.js";
22
import { LayerActionContext } from "#src/layer/index.js";
33
import type { UserLayerWithVoxelEditing } from "#src/layer/vox/index.js";
4+
import type { WatchableValueInterface } from "#src/trackable_value.js";
5+
import { observeWatchable } from "#src/trackable_value.js";
6+
import { unpackRGB } from "#src/util/color.js";
7+
import { RefCounted } from "#src/util/disposable.js";
8+
import { vec3 } from "#src/util/geom.js";
9+
import { NullarySignal } from "#src/util/signal.js";
410
import type { LayerControlDefinition } from "#src/widget/layer_control.js";
511
import { registerLayerControl } from "#src/widget/layer_control.js";
612
import { buttonLayerControl } from "#src/widget/layer_control_button.js";
713
import { checkboxLayerControl } from "#src/widget/layer_control_checkbox.js";
14+
import { colorLayerControl } from "#src/widget/layer_control_color.js";
815
import { enumLayerControl } from "#src/widget/layer_control_enum.js";
916
import { rangeLayerControl } from "#src/widget/layer_control_range.js";
1017

18+
class BigIntAsTrackableRGB extends RefCounted implements WatchableValueInterface<vec3> {
19+
changed = new NullarySignal();
20+
private tempColor = vec3.create();
21+
22+
constructor(public source: WatchableValueInterface<bigint>) {
23+
super();
24+
this.registerDisposer(source.changed.add(this.changed.dispatch));
25+
}
26+
27+
get value(): vec3 {
28+
const bigintValue = this.source.value;
29+
const [r, g, b] = unpackRGB(Number(bigintValue & 0xffffffn));
30+
vec3.set(this.tempColor, r, g, b);
31+
return this.tempColor;
32+
}
33+
34+
set value(newValue: vec3) {
35+
const rgb = newValue.map((c: number) => Math.round(c * 255));
36+
this.source.value = BigInt((rgb[0] << 16) | (rgb[1] << 8) | rgb[2]);
37+
}
38+
}
39+
1140
export const VOXEL_LAYER_CONTROLS: LayerControlDefinition<UserLayerWithVoxelEditing>[] =
1241
[
1342
{
@@ -57,12 +86,41 @@ export const VOXEL_LAYER_CONTROLS: LayerControlDefinition<UserLayerWithVoxelEdit
5786
}),
5887
},
5988
{
60-
label: "New label",
61-
toolJson: { type: "vox-new-label" },
89+
label: "Paint Color",
90+
toolJson: { type: "vox-paint-color" },
91+
...colorLayerControl((layer: UserLayerWithVoxelEditing) => new BigIntAsTrackableRGB(layer.paintValue)),
92+
},
93+
{
94+
label: "Paint Value",
95+
toolJson: { type: "vox-paint-value" },
96+
makeControl: (layer, context) => {
97+
const control = document.createElement("input");
98+
control.type = "text";
99+
control.title = "Specify segment ID or intensity value to paint";
100+
control.addEventListener("change", () => {
101+
try {
102+
layer.setVoxelPaintValue(BigInt(control.value));
103+
} catch {
104+
control.value = layer.paintValue.value.toString();
105+
}
106+
});
107+
context.registerDisposer(
108+
observeWatchable((value) => {
109+
control.value = value.toString();
110+
}, layer.paintValue),
111+
);
112+
control.value = layer.paintValue.value.toString();
113+
return { control, controlElement: control, parent: context };
114+
},
115+
activateTool: () => {},
116+
},
117+
{
118+
label: "New Random Value",
119+
toolJson: { type: "vox-random-value" },
62120
...buttonLayerControl({
63-
text: "New Label",
121+
text: "Random",
64122
onClick: (layer) =>
65-
layer.handleVoxAction("new-label", new LayerActionContext()),
123+
layer.handleVoxAction("randomize-paint-value", new LayerActionContext()),
66124
}),
67125
},
68126
];

src/layer/vox/index.ts

Lines changed: 19 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,13 @@ import { TrackableBoolean } from "#src/trackable_boolean.js";
3737
import type { WatchableValueInterface } from "#src/trackable_value.js";
3838
import { TrackableValue, WatchableValue } from "#src/trackable_value.js";
3939
import type { UserLayerWithAnnotations } from "#src/ui/annotations.js";
40+
import { randomUint64 } from "#src/util/bigint.js";
4041
import { RefCounted } from "#src/util/disposable.js";
41-
import { verifyFiniteFloat, verifyInt } from "#src/util/json.js";
42-
import { NullarySignal } from "#src/util/signal.js";
42+
import { parseUint64, verifyFiniteFloat, verifyInt } from "#src/util/json.js";
4343
import { TrackableEnum } from "#src/util/trackable_enum.js";
4444
import { VoxelPreviewMultiscaleSource } from "#src/voxel_annotation/PreviewMultiscaleChunkSource.js";
4545
import type { VoxelEditControllerHost } from "#src/voxel_annotation/edit_controller.js";
4646
import { VoxelEditController } from "#src/voxel_annotation/edit_controller.js";
47-
import { LabelsManager } from "#src/voxel_annotation/labels.js";
4847

4948
export enum BrushShape {
5049
DISK = 0,
@@ -73,16 +72,9 @@ export class VoxelEditingContext
7372
//this.registerDisposer(optimisticRenderLayer);
7473
}
7574

76-
// VoxelEditControllerHost implementation
77-
get labelsManager(): LabelsManager {
78-
return this.hostLayer.voxLabelsManager!;
79-
}
8075
get rpc() {
8176
return this.hostLayer.manager.chunkManager.rpc!;
8277
}
83-
setDrawErrorMessage(message: string | undefined): void {
84-
this.hostLayer.setDrawErrorMessage(message);
85-
}
8678

8779
disposed() {
8880
this.controller.dispose();
@@ -141,23 +133,22 @@ export class VoxelEditingContext
141133
}
142134

143135
export declare abstract class UserLayerWithVoxelEditing extends UserLayer {
144-
voxLabelsManager?: LabelsManager;
145-
labelsChanged: NullarySignal;
146136
isEditable: WatchableValue<boolean>;
147-
onDrawMessageChanged?: () => void;
148-
voxDrawErrorMessage: string | undefined;
149137

150138
voxBrushRadius: TrackableValue<number>;
151139
voxEraseMode: TrackableBoolean;
152140
voxBrushShape: TrackableEnum<BrushShape>;
153141
voxFloodMaxVoxels: TrackableValue<number>;
142+
paintValue: TrackableValue<bigint>;
154143

155144
editingContexts: Map<LoadedDataSubsource, VoxelEditingContext>;
156145

157146
abstract _createVoxelRenderLayer(
158147
source: MultiscaleVolumeChunkSource,
159148
transform: WatchableValueInterface<RenderLayerTransformOrError>,
160149
): ImageRenderLayer | SegmentationRenderLayer;
150+
abstract getVoxelPaintValue(erase: boolean): bigint;
151+
abstract setVoxelPaintValue(value: bigint): void;
161152

162153
initializeVoxelEditingForSubsource(
163154
loadedSubsource: LoadedDataSubsource,
@@ -168,7 +159,6 @@ export declare abstract class UserLayerWithVoxelEditing extends UserLayer {
168159
): void;
169160

170161
getIdentitySliceViewSourceOptions(): SliceViewSourceOptions;
171-
setDrawErrorMessage(message: string | undefined): void;
172162
handleVoxAction(action: string, context: LayerActionContext): void;
173163
}
174164

@@ -177,23 +167,15 @@ export function UserLayerWithVoxelEditingMixin<
177167
>(Base: TBase) {
178168
abstract class C extends Base implements UserLayerWithVoxelEditing {
179169
editingContexts = new Map<LoadedDataSubsource, VoxelEditingContext>();
180-
voxLabelsManager?: LabelsManager;
181-
labelsChanged = new NullarySignal();
182170
isEditable = new WatchableValue<boolean>(false);
171+
paintValue = new TrackableValue<bigint>(1n, (x) => parseUint64(x));
183172

184173
// Brush properties
185174
voxBrushRadius = new TrackableValue<number>(3, verifyInt);
186175
voxEraseMode = new TrackableBoolean(false);
187176
voxBrushShape = new TrackableEnum(BrushShape, BrushShape.DISK);
188177
voxFloodMaxVoxels = new TrackableValue<number>(10000, verifyFiniteFloat);
189178

190-
voxDrawErrorMessage: string | undefined = undefined;
191-
onDrawMessageChanged?: () => void;
192-
setDrawErrorMessage(message: string | undefined): void {
193-
this.voxDrawErrorMessage = message;
194-
this.onDrawMessageChanged?.();
195-
}
196-
197179
constructor(...args: any[]) {
198180
super(...args);
199181
this.registerDisposer(() => {
@@ -206,13 +188,23 @@ export function UserLayerWithVoxelEditingMixin<
206188
this.voxEraseMode.changed.add(this.specificationChanged.dispatch);
207189
this.voxBrushShape.changed.add(this.specificationChanged.dispatch);
208190
this.voxFloodMaxVoxels.changed.add(this.specificationChanged.dispatch);
191+
this.paintValue.changed.add(this.specificationChanged.dispatch);
209192
this.tabs.add("Draw", {
210193
label: "Draw",
211194
order: 20,
212195
getter: () => new VoxToolTab(this),
213196
});
214197
}
215198

199+
getVoxelPaintValue(erase: boolean): bigint {
200+
if (erase) return 0n;
201+
return this.paintValue.value;
202+
}
203+
setVoxelPaintValue(value: bigint) {
204+
this.paintValue.value = value;
205+
}
206+
207+
216208
abstract _createVoxelRenderLayer(
217209
source: MultiscaleVolumeChunkSource,
218210
transform: WatchableValueInterface<RenderLayerTransformOrError>,
@@ -226,16 +218,6 @@ export function UserLayerWithVoxelEditingMixin<
226218

227219
const primarySource = loadedSubsource.subsourceEntry.subsource
228220
.volume as MultiscaleVolumeChunkSource;
229-
const baseSpec = primarySource.getSources(
230-
this.getIdentitySliceViewSourceOptions(),
231-
)[0][0]!.chunkSource.spec;
232-
233-
if (this.voxLabelsManager === undefined) {
234-
this.voxLabelsManager = new LabelsManager(
235-
baseSpec.dataType,
236-
this.labelsChanged.dispatch,
237-
);
238-
}
239221

240222
const previewSource = new VoxelPreviewMultiscaleSource(
241223
this.manager.chunkManager,
@@ -289,8 +271,7 @@ export function UserLayerWithVoxelEditingMixin<
289271
};
290272
}
291273

292-
handleVoxAction(action: string, context: LayerActionContext): void {
293-
super.handleAction(action, context);
274+
handleVoxAction(action: string, _context: LayerActionContext): void {
294275
const firstContext = this.editingContexts.values().next().value;
295276
if (!firstContext) return;
296277
const controller = firstContext.controller;
@@ -301,8 +282,8 @@ export function UserLayerWithVoxelEditingMixin<
301282
case "redo":
302283
controller.redo();
303284
break;
304-
case "new-label":
305-
this.voxLabelsManager?.createNewLabel();
285+
case "randomize-paint-value":
286+
this.paintValue.value = randomUint64();
306287
break;
307288
}
308289
}

src/layer/vox/tabs/tools.ts

Lines changed: 0 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -23,27 +23,11 @@ import {
2323
BRUSH_TOOL_ID,
2424
FLOODFILL_TOOL_ID,
2525
} from "#src/ui/voxel_annotations.js";
26-
import { DataType } from "#src/util/data_type.js";
2726
import type { VoxelEditController } from "#src/voxel_annotation/edit_controller.js";
28-
import type { LabelsManager } from "#src/voxel_annotation/labels.js";
2927
import { DependentViewWidget } from "#src/widget/dependent_view_widget.js";
3028
import { addLayerControlToOptionsTab } from "#src/widget/layer_control.js";
3129
import { Tab } from "#src/widget/tab_view.js";
3230

33-
function formatUnsignedId(id: bigint, dataType: DataType): string {
34-
if (id >= 0n) {
35-
return id.toString();
36-
}
37-
// Handle two's complement representation for negative BigInts.
38-
if (dataType === DataType.UINT32) {
39-
return ((1n << 32n) + id).toString();
40-
}
41-
if (dataType === DataType.UINT64) {
42-
return ((1n << 64n) + id).toString();
43-
}
44-
return id.toString();
45-
}
46-
4731
export class VoxToolTab extends Tab {
4832
constructor(public layer: UserLayerWithVoxelEditing) {
4933
super();
@@ -135,115 +119,6 @@ export class VoxToolTab extends Tab {
135119
toolbox.appendChild(controlElement);
136120
}
137121

138-
const labelsSection = document.createElement("div");
139-
labelsSection.style.display = "flex";
140-
labelsSection.style.flexDirection = "column";
141-
labelsSection.style.gap = "6px";
142-
labelsSection.style.marginTop = "8px";
143-
144-
const labelsTitle = document.createElement("div");
145-
labelsTitle.textContent = "Labels";
146-
labelsTitle.style.fontWeight = "600";
147-
148-
const labelsWidget = this.registerDisposer(
149-
new DependentViewWidget(
150-
{
151-
changed: this.layer.labelsChanged,
152-
get value() {
153-
return layer.voxLabelsManager;
154-
},
155-
},
156-
(labelsManager: LabelsManager | undefined, parent) => {
157-
if (labelsManager === undefined) return;
158-
159-
const list = document.createElement("div");
160-
list.className = "neuroglancer-vox-labels";
161-
list.style.display = "flex";
162-
list.style.flexDirection = "column";
163-
list.style.gap = "4px";
164-
list.style.maxHeight = "180px";
165-
list.style.overflowY = "auto";
166-
167-
for (const label of labelsManager.labels) {
168-
const row = document.createElement("div");
169-
row.className = "neuroglancer-vox-label-row";
170-
row.style.display = "grid";
171-
row.style.gridTemplateColumns = "16px 1fr";
172-
row.style.alignItems = "center";
173-
row.style.gap = "8px";
174-
175-
const swatch = document.createElement("div");
176-
swatch.style.width = "16px";
177-
swatch.style.height = "16px";
178-
swatch.style.borderRadius = "3px";
179-
swatch.style.border = "1px solid rgba(0,0,0,0.2)";
180-
swatch.style.background = labelsManager.colorForValue(label);
181-
182-
const text = document.createElement("div");
183-
text.textContent = formatUnsignedId(label, labelsManager.dataType);
184-
text.style.fontFamily = "monospace";
185-
text.style.whiteSpace = "nowrap";
186-
text.style.overflow = "hidden";
187-
text.style.textOverflow = "ellipsis";
188-
189-
row.appendChild(swatch);
190-
row.appendChild(text);
191-
192-
if (label === labelsManager.selectedLabelId) {
193-
row.style.background = "rgba(100,150,255,0.15)";
194-
row.style.outline = "1px solid rgba(100,150,255,0.6)";
195-
}
196-
row.style.cursor = "pointer";
197-
row.style.padding = "2px 4px";
198-
row.style.borderRadius = "4px";
199-
row.addEventListener("click", () => {
200-
labelsManager.selectVoxLabel(label);
201-
});
202-
203-
list.appendChild(row);
204-
}
205-
206-
if (labelsManager.labelsError) {
207-
const errorDiv = document.createElement("div");
208-
errorDiv.className = "neuroglancer-vox-labels-error";
209-
errorDiv.style.color = "#b00020";
210-
errorDiv.style.fontSize = "12px";
211-
errorDiv.style.whiteSpace = "pre-wrap";
212-
errorDiv.textContent = labelsManager.labelsError;
213-
parent.appendChild(errorDiv);
214-
}
215-
216-
parent.appendChild(list);
217-
},
218-
this.visibility,
219-
),
220-
);
221-
222-
labelsSection.appendChild(labelsTitle);
223-
labelsSection.appendChild(labelsWidget.element);
224-
toolbox.appendChild(labelsSection);
225-
226-
const drawErrorContainer = document.createElement("div");
227-
drawErrorContainer.className = "neuroglancer-vox-draw-error";
228-
drawErrorContainer.style.color = "#b00020";
229-
drawErrorContainer.style.fontSize = "12px";
230-
drawErrorContainer.style.whiteSpace = "pre-wrap";
231-
drawErrorContainer.style.marginTop = "8px";
232-
drawErrorContainer.style.display = "none";
233-
toolbox.appendChild(drawErrorContainer);
234-
235-
this.layer.onDrawMessageChanged = () => {
236-
const msg = this.layer.voxDrawErrorMessage;
237-
if (msg && msg.length > 0) {
238-
drawErrorContainer.textContent = msg;
239-
drawErrorContainer.style.display = "block";
240-
} else {
241-
drawErrorContainer.textContent = "";
242-
drawErrorContainer.style.display = "none";
243-
}
244-
};
245-
this.layer.onDrawMessageChanged();
246-
247122
element.appendChild(toolbox);
248123
}
249124
}

0 commit comments

Comments
 (0)