Skip to content

Commit 33bb42e

Browse files
authored
feat: Color legend in the layer-bar (#693)
1 parent 72efcde commit 33bb42e

File tree

8 files changed

+515
-32
lines changed

8 files changed

+515
-32
lines changed

src/layer/annotation/index.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,10 @@ import {
4848
TrackableBoolean,
4949
TrackableBooleanCheckbox,
5050
} from "#src/trackable_boolean.js";
51-
import { makeCachedLazyDerivedWatchableValue } from "#src/trackable_value.js";
51+
import {
52+
makeCachedLazyDerivedWatchableValue,
53+
observeWatchable,
54+
} from "#src/trackable_value.js";
5255
import type {
5356
AnnotationLayerView,
5457
MergedAnnotationStates,
@@ -731,8 +734,37 @@ export class AnnotationUserLayer extends Base {
731734
return x;
732735
}
733736

737+
observeLayerColor(callback: () => void) {
738+
const disposer = super.observeLayerColor(callback);
739+
const subDisposer = observeWatchable(
740+
callback,
741+
this.annotationDisplayState.color,
742+
);
743+
const shaderDisposer = observeWatchable(
744+
callback,
745+
this.annotationDisplayState.shader,
746+
);
747+
return () => {
748+
disposer();
749+
subDisposer();
750+
shaderDisposer();
751+
};
752+
}
753+
754+
get automaticLayerBarColors() {
755+
const shaderHasDefaultColor =
756+
this.annotationDisplayState.shader.value.includes("defaultColor");
757+
if (shaderHasDefaultColor && this.annotationDisplayState.color.value) {
758+
const [r, g, b] = this.annotationDisplayState.color.value;
759+
return [`rgb(${r * 255}, ${g * 255}, ${b * 255})`];
760+
}
761+
762+
return undefined;
763+
}
764+
734765
static type = "annotation";
735766
static typeAbbreviation = "ann";
767+
static supportsLayerBarColorSyncOption = true;
736768
}
737769

738770
function makeShaderCodeWidget(layer: AnnotationUserLayer) {

src/layer/index.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,13 +185,26 @@ export class UserLayer extends RefCounted {
185185
}
186186

187187
static supportsPickOption = false;
188+
static supportsLayerBarColorSyncOption = false;
188189

189190
pick = new TrackableBoolean(true, true);
190191

191192
selectionState: UserLayerSelectionState;
192193

193194
messages = new MessageList();
194195

196+
observeLayerColor(_: () => void): () => void {
197+
return () => {};
198+
}
199+
200+
get automaticLayerBarColors(): string[] | undefined {
201+
return [];
202+
}
203+
204+
get layerBarColors(): string[] | undefined {
205+
return this.automaticLayerBarColors;
206+
}
207+
195208
initializeSelectionState(state: this["selectionState"]) {
196209
state.generation = -1;
197210
state.localPositionValid = false;
@@ -740,6 +753,28 @@ export class ManagedUserLayer extends RefCounted {
740753
}
741754
}
742755

756+
get layerBarColors(): string[] | undefined {
757+
const userLayer = this.layer;
758+
return userLayer?.layerBarColors;
759+
}
760+
761+
observeLayerColor(callback: () => void): () => void {
762+
const userLayer = this.layer;
763+
if (userLayer !== null) {
764+
return userLayer.observeLayerColor(callback);
765+
}
766+
return () => {};
767+
}
768+
769+
get supportsLayerBarColorSyncOption() {
770+
const userLayer = this.layer;
771+
return (
772+
userLayer !== null &&
773+
(userLayer.constructor as typeof UserLayer)
774+
.supportsLayerBarColorSyncOption
775+
);
776+
}
777+
743778
/**
744779
* If layer is not null, tranfers ownership of a reference.
745780
*/

src/layer/segmentation/index.ts

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ import {
4545
RenderScaleHistogram,
4646
trackableRenderScaleTarget,
4747
} from "#src/render_scale_statistics.js";
48-
import { SegmentColorHash } from "#src/segment_color.js";
48+
import { getCssColor, SegmentColorHash } from "#src/segment_color.js";
4949
import type {
5050
SegmentationColorGroupState,
5151
SegmentationDisplayState,
@@ -54,6 +54,7 @@ import type {
5454
import {
5555
augmentSegmentId,
5656
bindSegmentListWidth,
57+
getBaseObjectColor,
5758
makeSegmentWidget,
5859
maybeAugmentSegmentId,
5960
registerCallbackWhenSegmentationDisplayStateChanged,
@@ -95,6 +96,7 @@ import {
9596
IndirectWatchableValue,
9697
makeCachedDerivedWatchableValue,
9798
makeCachedLazyDerivedWatchableValue,
99+
observeWatchable,
98100
registerNestedSync,
99101
TrackableValue,
100102
WatchableValue,
@@ -131,6 +133,8 @@ import { makeWatchableShaderError } from "#src/webgl/dynamic_shader.js";
131133
import type { DependentViewContext } from "#src/widget/dependent_view_widget.js";
132134
import { registerLayerShaderControlsTool } from "#src/widget/shader_controls.js";
133135

136+
const MAX_LAYER_BAR_UI_INDICATOR_COLORS = 6;
137+
134138
export class SegmentationUserLayerGroupState
135139
extends RefCounted
136140
implements SegmentationGroupState
@@ -539,6 +543,7 @@ class SegmentationUserLayerDisplayState implements SegmentationDisplayState {
539543
baseSegmentColoring = new TrackableBoolean(false, false);
540544
baseSegmentHighlighting = new TrackableBoolean(false, false);
541545
useTempSegmentStatedColors2d: SharedWatchableValue<boolean>;
546+
hasVolume = new TrackableBoolean(false, false);
542547

543548
filterBySegmentLabel: (id: bigint) => void;
544549

@@ -746,6 +751,7 @@ export class SegmentationUserLayer extends Base {
746751
const isGroupRoot =
747752
this.displayState.linkedSegmentationGroup.root.value === this;
748753
let updatedGraph: SegmentationGraphSource | undefined;
754+
let hasVolume = false;
749755
for (const loadedSubsource of subsources) {
750756
if (this.addStaticAnnotations(loadedSubsource)) continue;
751757
const { volume, mesh, segmentPropertyMap, segmentationGraph, local } =
@@ -758,6 +764,7 @@ export class SegmentationUserLayer extends Base {
758764
);
759765
continue;
760766
}
767+
hasVolume = true;
761768
loadedSubsource.activate(
762769
() =>
763770
loadedSubsource.addRenderLayer(
@@ -880,6 +887,7 @@ export class SegmentationUserLayer extends Base {
880887
updatedSegmentPropertyMaps,
881888
);
882889
this.displayState.originalSegmentationGroupState.graph.value = updatedGraph;
890+
this.displayState.hasVolume.value = hasVolume;
883891
}
884892

885893
getLegacyDataSourceSpecifications(
@@ -1287,9 +1295,90 @@ export class SegmentationUserLayer extends Base {
12871295
);
12881296
}
12891297

1298+
observeLayerColor(callback: () => void) {
1299+
const disposer = super.observeLayerColor(callback);
1300+
const defaultColorDisposer = observeWatchable(
1301+
callback,
1302+
this.displayState.segmentDefaultColor,
1303+
);
1304+
const visibleSegmentDisposer =
1305+
this.displayState.segmentationGroupState.value.visibleSegments.changed.add(
1306+
callback,
1307+
);
1308+
const colorHashChangeDisposer =
1309+
this.displayState.segmentationColorGroupState.value.segmentColorHash.changed.add(
1310+
callback,
1311+
);
1312+
const showAllByDefaultDisposer =
1313+
this.displayState.ignoreNullVisibleSet.changed.add(callback);
1314+
const hasVolumeDisposer = this.displayState.hasVolume.changed.add(callback);
1315+
return () => {
1316+
disposer();
1317+
defaultColorDisposer();
1318+
visibleSegmentDisposer();
1319+
colorHashChangeDisposer();
1320+
showAllByDefaultDisposer();
1321+
hasVolumeDisposer();
1322+
};
1323+
}
1324+
1325+
get automaticLayerBarColors() {
1326+
const { displayState } = this;
1327+
const visibleSegmentsSet =
1328+
displayState.segmentationGroupState.value.visibleSegments;
1329+
const fixedColor = displayState.segmentDefaultColor.value;
1330+
1331+
const noVisibleSegments = visibleSegmentsSet.size === 0;
1332+
const tooManyVisibleSegments =
1333+
visibleSegmentsSet.size > MAX_LAYER_BAR_UI_INDICATOR_COLORS;
1334+
const hasMappedColors =
1335+
displayState.segmentationColorGroupState.value.segmentStatedColors.size >
1336+
0;
1337+
const isFixedColorOnly = fixedColor !== undefined && !hasMappedColors;
1338+
const showAllByDefault = displayState.ignoreNullVisibleSet.value;
1339+
const hasVolume = displayState.hasVolume.value;
1340+
1341+
if (noVisibleSegments) {
1342+
if (!showAllByDefault || !hasVolume) return []; // No segments visible
1343+
if (isFixedColorOnly) return [getCssColor(fixedColor)];
1344+
return undefined; // Rainbow colors
1345+
}
1346+
if (isFixedColorOnly) {
1347+
return [getCssColor(fixedColor)]; // All segments show as one color
1348+
}
1349+
1350+
// Because manually mapped colors are not guaranteed to be unique,
1351+
// we need to actually check all the visible segments if
1352+
// manually mapped colors are used
1353+
if (!hasMappedColors && tooManyVisibleSegments) {
1354+
return undefined; // Too many segments to show
1355+
}
1356+
1357+
const visibleSegments = [...visibleSegmentsSet];
1358+
const colors = visibleSegments.map((id) => {
1359+
const color = getCssColor(getBaseObjectColor(displayState, id));
1360+
return { color, id };
1361+
});
1362+
1363+
// Sort the colors by their segment ID
1364+
// Otherwise, the order is random which is a bit confusing in the UI
1365+
colors.sort((a, b) => {
1366+
const aId = a.id;
1367+
const bId = b.id;
1368+
return aId < bId ? -1 : aId > bId ? 1 : 0;
1369+
});
1370+
1371+
const uniqueColors = [...new Set(colors.map((color) => color.color))];
1372+
if (uniqueColors.length > MAX_LAYER_BAR_UI_INDICATOR_COLORS) {
1373+
return undefined; // Too many colors to show
1374+
}
1375+
return uniqueColors;
1376+
}
1377+
12901378
static type = "segmentation";
12911379
static typeAbbreviation = "seg";
12921380
static supportsPickOption = true;
1381+
static supportsLayerBarColorSyncOption = true;
12931382
}
12941383

12951384
registerLayerControls(SegmentationUserLayer);

src/ui/layer_bar.css

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,15 +62,11 @@
6262
border-color: #daa520;
6363
}
6464

65-
.neuroglancer-layer-item[data-pick="true"] .neuroglancer-layer-item-label {
66-
background-color: #939;
67-
}
68-
6965
.neuroglancer-layer-item-label {
7066
display: inline-block;
7167
position: relative;
72-
background-color: #222;
73-
padding-right: 3px;
68+
padding: 0 1px;
69+
z-index: 1;
7470
}
7571

7672
.neuroglancer-layer-item-number {
@@ -134,6 +130,26 @@
134130
align-items: center;
135131
}
136132

133+
.neuroglancer-layer-item-label-wrapper {
134+
position: relative;
135+
overflow: hidden;
136+
margin: 0 2px;
137+
}
138+
139+
.neuroglancer-layer-item-label-color {
140+
position: absolute;
141+
top: 0px;
142+
left: 0px;
143+
width: 100%;
144+
height: 100%;
145+
z-index: 0;
146+
}
147+
148+
.neuroglancer-layer-item-label-color[data-color="multi"] {
149+
transform: scale(1.2);
150+
filter: blur(8px);
151+
}
152+
137153
.neuroglancer-layer-item-value {
138154
grid-row: 1;
139155
grid-column: 1;

0 commit comments

Comments
 (0)