From d4a25f6a7e2cfc16522cd3f5fe9f04a28a46ae5e Mon Sep 17 00:00:00 2001 From: Sean Martin Date: Fri, 1 May 2026 12:44:57 +0200 Subject: [PATCH 01/13] refactor: remove custom functions for spatial skeleton json management --- src/layer/segmentation/index.ts | 36 ++--- .../spatial_skeleton_serialization.spec.ts | 136 ------------------ .../spatial_skeleton_serialization.ts | 110 -------------- 3 files changed, 15 insertions(+), 267 deletions(-) delete mode 100644 src/layer/segmentation/spatial_skeleton_serialization.spec.ts delete mode 100644 src/layer/segmentation/spatial_skeleton_serialization.ts diff --git a/src/layer/segmentation/index.ts b/src/layer/segmentation/index.ts index c7980e21a..5cba578ba 100644 --- a/src/layer/segmentation/index.ts +++ b/src/layer/segmentation/index.ts @@ -58,7 +58,6 @@ import { executeSpatialSkeletonNodeTrueEndUpdate, } from "#src/layer/segmentation/spatial_skeleton_commands.js"; import { showSpatialSkeletonActionError } from "#src/layer/segmentation/spatial_skeleton_errors.js"; -import { appendSpatialSkeletonSerializationState } from "#src/layer/segmentation/spatial_skeleton_serialization.js"; import { MeshLayer, MeshSource, @@ -2465,26 +2464,21 @@ export class SegmentationUserLayer extends Base { this.displayState.spatialSkeletonNodeQuery.toJSON(); x[json_keys.SPATIAL_SKELETON_NODE_FILTER_JSON_KEY] = this.displayState.spatialSkeletonNodeFilter.toJSON(); - appendSpatialSkeletonSerializationState( - x, - { - hiddenObjectAlpha: this.displayState.hiddenObjectAlpha, - skeletonLod: this.displayState.skeletonLod, - spatialSkeletonGridResolutionTarget2d: - this.displayState.spatialSkeletonGridResolutionTarget2d, - spatialSkeletonGridResolutionTarget3d: - this.displayState.spatialSkeletonGridResolutionTarget3d, - spatialSkeletonGridResolutionRelative2d: - this.displayState.spatialSkeletonGridResolutionRelative2d, - spatialSkeletonGridResolutionRelative3d: - this.displayState.spatialSkeletonGridResolutionRelative3d, - spatialSkeletonGridLevel2d: - this.displayState.spatialSkeletonGridLevel2d, - spatialSkeletonGridLevel3d: - this.displayState.spatialSkeletonGridLevel3d, - }, - this.hasSpatiallyIndexedSkeletonsLayer.value, - ); + x[json_keys.HIDDEN_OPACITY_3D_JSON_KEY] = + this.displayState.hiddenObjectAlpha.toJSON(); + x[json_keys.SKELETON_LOD_JSON_KEY] = this.displayState.skeletonLod.toJSON(); + x[json_keys.SPATIAL_SKELETON_GRID_LEVEL_2D_JSON_KEY] = + this.displayState.spatialSkeletonGridLevel2d.toJSON(); + x[json_keys.SPATIAL_SKELETON_GRID_LEVEL_3D_JSON_KEY] = + this.displayState.spatialSkeletonGridLevel3d.toJSON(); + x[json_keys.SPATIAL_SKELETON_GRID_RESOLUTION_TARGET_2D_JSON_KEY] = + this.displayState.spatialSkeletonGridResolutionTarget2d.toJSON(); + x[json_keys.SPATIAL_SKELETON_GRID_RESOLUTION_TARGET_3D_JSON_KEY] = + this.displayState.spatialSkeletonGridResolutionTarget3d.toJSON(); + x[json_keys.SPATIAL_SKELETON_GRID_RESOLUTION_RELATIVE_2D_JSON_KEY] = + this.displayState.spatialSkeletonGridResolutionRelative2d.toJSON(); + x[json_keys.SPATIAL_SKELETON_GRID_RESOLUTION_RELATIVE_3D_JSON_KEY] = + this.displayState.spatialSkeletonGridResolutionRelative3d.toJSON(); x[json_keys.HOVER_HIGHLIGHT_JSON_KEY] = this.displayState.hoverHighlight.toJSON(); x[json_keys.BASE_SEGMENT_COLORING_JSON_KEY] = diff --git a/src/layer/segmentation/spatial_skeleton_serialization.spec.ts b/src/layer/segmentation/spatial_skeleton_serialization.spec.ts deleted file mode 100644 index 9b91739d4..000000000 --- a/src/layer/segmentation/spatial_skeleton_serialization.spec.ts +++ /dev/null @@ -1,136 +0,0 @@ -/** - * @license - * Copyright 2026 Google Inc. - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { describe, expect, it } from "vitest"; - -import * as json_keys from "#src/layer/segmentation/json_keys.js"; -import { appendSpatialSkeletonSerializationState } from "#src/layer/segmentation/spatial_skeleton_serialization.js"; -import { trackableAlphaValue } from "#src/trackable_alpha.js"; -import { TrackableBoolean } from "#src/trackable_boolean.js"; -import { trackableFiniteFloat } from "#src/trackable_finite_float.js"; -import { TrackableValue } from "#src/trackable_value.js"; -import { - verifyFiniteNonNegativeFloat, - verifyNonnegativeInt, -} from "#src/util/json.js"; - -function makeTrackables() { - return { - hiddenObjectAlpha: trackableAlphaValue(0.5), - skeletonLod: trackableFiniteFloat(0), - spatialSkeletonGridResolutionTarget2d: new TrackableValue( - 1, - verifyFiniteNonNegativeFloat, - 1, - ), - spatialSkeletonGridResolutionTarget3d: new TrackableValue( - 1, - verifyFiniteNonNegativeFloat, - 1, - ), - spatialSkeletonGridResolutionRelative2d: new TrackableBoolean(false, false), - spatialSkeletonGridResolutionRelative3d: new TrackableBoolean(false, false), - spatialSkeletonGridLevel2d: new TrackableValue( - 0, - verifyNonnegativeInt, - 0, - ), - spatialSkeletonGridLevel3d: new TrackableValue( - 0, - verifyNonnegativeInt, - 0, - ), - }; -} - -describe("appendSpatialSkeletonSerializationState", () => { - it("does not emit spatial skeleton keys when round-tripping a legacy spec", () => { - const legacySpec: Record = {}; - const trackables = makeTrackables(); - trackables.hiddenObjectAlpha.restoreState( - legacySpec[json_keys.HIDDEN_OPACITY_3D_JSON_KEY], - ); - trackables.skeletonLod.restoreState( - legacySpec[json_keys.SKELETON_LOD_JSON_KEY], - ); - trackables.spatialSkeletonGridResolutionTarget2d.restoreState( - legacySpec[json_keys.SPATIAL_SKELETON_GRID_RESOLUTION_TARGET_2D_JSON_KEY], - ); - trackables.spatialSkeletonGridResolutionTarget3d.restoreState( - legacySpec[json_keys.SPATIAL_SKELETON_GRID_RESOLUTION_TARGET_3D_JSON_KEY], - ); - trackables.spatialSkeletonGridResolutionRelative2d.restoreState( - legacySpec[ - json_keys.SPATIAL_SKELETON_GRID_RESOLUTION_RELATIVE_2D_JSON_KEY - ], - ); - trackables.spatialSkeletonGridResolutionRelative3d.restoreState( - legacySpec[ - json_keys.SPATIAL_SKELETON_GRID_RESOLUTION_RELATIVE_3D_JSON_KEY - ], - ); - trackables.spatialSkeletonGridLevel2d.restoreState( - legacySpec[json_keys.SPATIAL_SKELETON_GRID_LEVEL_2D_JSON_KEY], - ); - trackables.spatialSkeletonGridLevel3d.restoreState( - legacySpec[json_keys.SPATIAL_SKELETON_GRID_LEVEL_3D_JSON_KEY], - ); - - const serialized: Record = {}; - appendSpatialSkeletonSerializationState( - serialized, - trackables, - /* includeDefaults= */ false, - ); - expect(serialized).toEqual({}); - }); - - it("emits non-default values for non-spatial layers", () => { - const trackables = makeTrackables(); - trackables.skeletonLod.value = 0.35; - - const serialized: Record = {}; - appendSpatialSkeletonSerializationState( - serialized, - trackables, - /* includeDefaults= */ false, - ); - expect(serialized).toEqual({ - [json_keys.SKELETON_LOD_JSON_KEY]: 0.35, - }); - }); - - it("emits defaults for spatially indexed skeleton layers", () => { - const trackables = makeTrackables(); - - const serialized: Record = {}; - appendSpatialSkeletonSerializationState( - serialized, - trackables, - /* includeDefaults= */ true, - ); - expect(serialized).toEqual({ - [json_keys.HIDDEN_OPACITY_3D_JSON_KEY]: 0.5, - [json_keys.SKELETON_LOD_JSON_KEY]: 0, - [json_keys.SPATIAL_SKELETON_GRID_RESOLUTION_TARGET_2D_JSON_KEY]: 1, - [json_keys.SPATIAL_SKELETON_GRID_RESOLUTION_TARGET_3D_JSON_KEY]: 1, - [json_keys.SPATIAL_SKELETON_GRID_RESOLUTION_RELATIVE_2D_JSON_KEY]: false, - [json_keys.SPATIAL_SKELETON_GRID_RESOLUTION_RELATIVE_3D_JSON_KEY]: false, - [json_keys.SPATIAL_SKELETON_GRID_LEVEL_2D_JSON_KEY]: 0, - [json_keys.SPATIAL_SKELETON_GRID_LEVEL_3D_JSON_KEY]: 0, - }); - }); -}); diff --git a/src/layer/segmentation/spatial_skeleton_serialization.ts b/src/layer/segmentation/spatial_skeleton_serialization.ts deleted file mode 100644 index 8a7e9658b..000000000 --- a/src/layer/segmentation/spatial_skeleton_serialization.ts +++ /dev/null @@ -1,110 +0,0 @@ -/** - * @license - * Copyright 2026 Google Inc. - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as json_keys from "#src/layer/segmentation/json_keys.js"; - -export interface JsonSerializableTrackable { - toJSON(): any; - value: T; -} - -export interface SpatialSkeletonSerializationTrackables { - hiddenObjectAlpha: JsonSerializableTrackable; - skeletonLod: JsonSerializableTrackable; - spatialSkeletonGridResolutionTarget2d: JsonSerializableTrackable; - spatialSkeletonGridResolutionTarget3d: JsonSerializableTrackable; - spatialSkeletonGridResolutionRelative2d: JsonSerializableTrackable; - spatialSkeletonGridResolutionRelative3d: JsonSerializableTrackable; - spatialSkeletonGridLevel2d: JsonSerializableTrackable; - spatialSkeletonGridLevel3d: JsonSerializableTrackable; -} - -function getSerializedTrackableValue( - trackable: JsonSerializableTrackable, - includeDefaults: boolean, -) { - const value = trackable.toJSON(); - if (value !== undefined) return value; - if (!includeDefaults) return undefined; - return trackable.value; -} - -function setSerializedTrackable( - target: Record, - key: string, - trackable: JsonSerializableTrackable, - includeDefaults: boolean, -) { - const value = getSerializedTrackableValue(trackable, includeDefaults); - if (value !== undefined) { - target[key] = value; - } -} - -export function appendSpatialSkeletonSerializationState( - target: Record, - trackables: SpatialSkeletonSerializationTrackables, - includeDefaults: boolean, -) { - setSerializedTrackable( - target, - json_keys.HIDDEN_OPACITY_3D_JSON_KEY, - trackables.hiddenObjectAlpha, - includeDefaults, - ); - setSerializedTrackable( - target, - json_keys.SKELETON_LOD_JSON_KEY, - trackables.skeletonLod, - includeDefaults, - ); - setSerializedTrackable( - target, - json_keys.SPATIAL_SKELETON_GRID_RESOLUTION_TARGET_2D_JSON_KEY, - trackables.spatialSkeletonGridResolutionTarget2d, - includeDefaults, - ); - setSerializedTrackable( - target, - json_keys.SPATIAL_SKELETON_GRID_RESOLUTION_TARGET_3D_JSON_KEY, - trackables.spatialSkeletonGridResolutionTarget3d, - includeDefaults, - ); - setSerializedTrackable( - target, - json_keys.SPATIAL_SKELETON_GRID_RESOLUTION_RELATIVE_2D_JSON_KEY, - trackables.spatialSkeletonGridResolutionRelative2d, - includeDefaults, - ); - setSerializedTrackable( - target, - json_keys.SPATIAL_SKELETON_GRID_RESOLUTION_RELATIVE_3D_JSON_KEY, - trackables.spatialSkeletonGridResolutionRelative3d, - includeDefaults, - ); - setSerializedTrackable( - target, - json_keys.SPATIAL_SKELETON_GRID_LEVEL_2D_JSON_KEY, - trackables.spatialSkeletonGridLevel2d, - includeDefaults, - ); - setSerializedTrackable( - target, - json_keys.SPATIAL_SKELETON_GRID_LEVEL_3D_JSON_KEY, - trackables.spatialSkeletonGridLevel3d, - includeDefaults, - ); -} From 0dc8ee2d4b27681286b93c1702ecde2103ae4e30 Mon Sep 17 00:00:00 2001 From: Sean Martin Date: Fri, 1 May 2026 13:28:56 +0200 Subject: [PATCH 02/13] wip: remove unwanted parts of state will need to update the logic after this, but this moves to the version of the state where the user controlled value is the only thing stored in the state, and everything else needed for the computation is derived from this single control this will require updates in segmentation/index.ts further and then in skeleton/frontend.ts as some of the old things there won't be used --- src/layer/segmentation/index.ts | 40 +++--------------------- src/layer/segmentation/json_keys.ts | 20 +++--------- src/layer/segmentation/layer_controls.ts | 4 +-- 3 files changed, 11 insertions(+), 53 deletions(-) diff --git a/src/layer/segmentation/index.ts b/src/layer/segmentation/index.ts index 5cba578ba..b3e25fcab 100644 --- a/src/layer/segmentation/index.ts +++ b/src/layer/segmentation/index.ts @@ -2359,9 +2359,6 @@ export class SegmentationUserLayer extends Base { this.displayState.hiddenObjectAlpha.restoreState( specification[json_keys.HIDDEN_OPACITY_3D_JSON_KEY], ); - this.displayState.skeletonLod.restoreState( - specification[json_keys.SKELETON_LOD_JSON_KEY], - ); this.displayState.spatialSkeletonNodeQuery.restoreState( specification[json_keys.SPATIAL_SKELETON_NODE_QUERY_JSON_KEY], ); @@ -2371,31 +2368,11 @@ export class SegmentationUserLayer extends Base { (value) => this.displayState.spatialSkeletonNodeFilter.restoreState(value), ); - this.displayState.spatialSkeletonGridResolutionRelative2d.restoreState( - specification[ - json_keys.SPATIAL_SKELETON_GRID_RESOLUTION_RELATIVE_2D_JSON_KEY - ], - ); - this.displayState.spatialSkeletonGridResolutionRelative3d.restoreState( - specification[ - json_keys.SPATIAL_SKELETON_GRID_RESOLUTION_RELATIVE_3D_JSON_KEY - ], - ); - this.displayState.applySpatialSkeletonGridResolutionTarget2dFromSpec( - specification[ - json_keys.SPATIAL_SKELETON_GRID_RESOLUTION_TARGET_2D_JSON_KEY - ], - ); - this.displayState.applySpatialSkeletonGridResolutionTarget3dFromSpec( - specification[ - json_keys.SPATIAL_SKELETON_GRID_RESOLUTION_TARGET_3D_JSON_KEY - ], - ); this.displayState.applySpatialSkeletonGridLevel2dFromSpec( - specification[json_keys.SPATIAL_SKELETON_GRID_LEVEL_2D_JSON_KEY], + specification[json_keys.SKELETON_CROSS_SECTION_RENDER_SCALE_JSON_KEY], ); this.displayState.applySpatialSkeletonGridLevel3dFromSpec( - specification[json_keys.SPATIAL_SKELETON_GRID_LEVEL_3D_JSON_KEY], + specification[json_keys.SKELETON_PERSPECTIVE_RENDER_SCALE_JSON_KEY], ); this.displayState.baseSegmentColoring.restoreState( specification[json_keys.BASE_SEGMENT_COLORING_JSON_KEY], @@ -2466,19 +2443,10 @@ export class SegmentationUserLayer extends Base { this.displayState.spatialSkeletonNodeFilter.toJSON(); x[json_keys.HIDDEN_OPACITY_3D_JSON_KEY] = this.displayState.hiddenObjectAlpha.toJSON(); - x[json_keys.SKELETON_LOD_JSON_KEY] = this.displayState.skeletonLod.toJSON(); - x[json_keys.SPATIAL_SKELETON_GRID_LEVEL_2D_JSON_KEY] = - this.displayState.spatialSkeletonGridLevel2d.toJSON(); - x[json_keys.SPATIAL_SKELETON_GRID_LEVEL_3D_JSON_KEY] = - this.displayState.spatialSkeletonGridLevel3d.toJSON(); - x[json_keys.SPATIAL_SKELETON_GRID_RESOLUTION_TARGET_2D_JSON_KEY] = + x[json_keys.SKELETON_CROSS_SECTION_RENDER_SCALE_JSON_KEY] = this.displayState.spatialSkeletonGridResolutionTarget2d.toJSON(); - x[json_keys.SPATIAL_SKELETON_GRID_RESOLUTION_TARGET_3D_JSON_KEY] = + x[json_keys.SKELETON_PERSPECTIVE_RENDER_SCALE_JSON_KEY] = this.displayState.spatialSkeletonGridResolutionTarget3d.toJSON(); - x[json_keys.SPATIAL_SKELETON_GRID_RESOLUTION_RELATIVE_2D_JSON_KEY] = - this.displayState.spatialSkeletonGridResolutionRelative2d.toJSON(); - x[json_keys.SPATIAL_SKELETON_GRID_RESOLUTION_RELATIVE_3D_JSON_KEY] = - this.displayState.spatialSkeletonGridResolutionRelative3d.toJSON(); x[json_keys.HOVER_HIGHLIGHT_JSON_KEY] = this.displayState.hoverHighlight.toJSON(); x[json_keys.BASE_SEGMENT_COLORING_JSON_KEY] = diff --git a/src/layer/segmentation/json_keys.ts b/src/layer/segmentation/json_keys.ts index 4b1a53469..b1464ab1e 100644 --- a/src/layer/segmentation/json_keys.ts +++ b/src/layer/segmentation/json_keys.ts @@ -2,19 +2,10 @@ export const SELECTED_ALPHA_JSON_KEY = "selectedAlpha"; export const NOT_SELECTED_ALPHA_JSON_KEY = "notSelectedAlpha"; export const OBJECT_ALPHA_JSON_KEY = "objectAlpha"; export const HIDDEN_OPACITY_3D_JSON_KEY = "hiddenObjectAlpha"; -export const SKELETON_LOD_JSON_KEY = "skeletonLod"; -export const SPATIAL_SKELETON_GRID_LEVEL_2D_JSON_KEY = - "spatialSkeletonGridLevel2d"; -export const SPATIAL_SKELETON_GRID_LEVEL_3D_JSON_KEY = - "spatialSkeletonGridLevel3d"; -export const SPATIAL_SKELETON_GRID_RESOLUTION_TARGET_2D_JSON_KEY = - "spatialSkeletonGridResolutionTarget2d"; -export const SPATIAL_SKELETON_GRID_RESOLUTION_TARGET_3D_JSON_KEY = - "spatialSkeletonGridResolutionTarget3d"; -export const SPATIAL_SKELETON_GRID_RESOLUTION_RELATIVE_2D_JSON_KEY = - "spatialSkeletonGridResolutionRelative2d"; -export const SPATIAL_SKELETON_GRID_RESOLUTION_RELATIVE_3D_JSON_KEY = - "spatialSkeletonGridResolutionRelative3d"; +export const SKELETON_CROSS_SECTION_RENDER_SCALE_JSON_KEY = + "skeletonCrossSectionRenderScale"; +export const SKELETON_PERSPECTIVE_RENDER_SCALE_JSON_KEY = + "skeletonPerspectiveRenderScale"; export const SATURATION_JSON_KEY = "saturation"; export const HOVER_HIGHLIGHT_JSON_KEY = "hoverHighlight"; export const HIDE_SEGMENT_ZERO_JSON_KEY = "hideSegmentZero"; @@ -32,8 +23,7 @@ export const SKELETON_RENDERING_JSON_KEY = "skeletonRendering"; export const SKELETON_SHADER_JSON_KEY = "skeletonShader"; export const SKELETON_CODE_VISIBLE_KEY = "codeVisible"; export const SEGMENT_QUERY_JSON_KEY = "segmentQuery"; -export const SPATIAL_SKELETON_NODE_QUERY_JSON_KEY = - "spatialSkeletonNodeQuery"; +export const SPATIAL_SKELETON_NODE_QUERY_JSON_KEY = "spatialSkeletonNodeQuery"; export const SPATIAL_SKELETON_NODE_FILTER_JSON_KEY = "spatialSkeletonNodeFilter"; export const MESH_SILHOUETTE_RENDERING_JSON_KEY = "meshSilhouetteRendering"; diff --git a/src/layer/segmentation/layer_controls.ts b/src/layer/segmentation/layer_controls.ts index 0fc723db8..85c6989ec 100644 --- a/src/layer/segmentation/layer_controls.ts +++ b/src/layer/segmentation/layer_controls.ts @@ -73,7 +73,7 @@ export const LAYER_CONTROLS: LayerControlDefinition[] = [ }, { label: "Resolution (skeleton grid 2D)", - toolJson: json_keys.SPATIAL_SKELETON_GRID_RESOLUTION_TARGET_2D_JSON_KEY, + toolJson: json_keys.SKELETON_CROSS_SECTION_RENDER_SCALE_JSON_KEY, isValid: (layer) => makeCachedDerivedWatchableValue( (levels, hasSpatialSkeletons) => @@ -97,7 +97,7 @@ export const LAYER_CONTROLS: LayerControlDefinition[] = [ }, { label: "Resolution (skeleton grid 3D)", - toolJson: json_keys.SPATIAL_SKELETON_GRID_RESOLUTION_TARGET_3D_JSON_KEY, + toolJson: json_keys.SKELETON_PERSPECTIVE_RENDER_SCALE_JSON_KEY, isValid: (layer) => makeCachedDerivedWatchableValue( (levels, hasSpatialSkeletons) => From 68b2194a6043124df5c36219ccc4de9bcfb6f757 Mon Sep 17 00:00:00 2001 From: Sean Martin Date: Fri, 1 May 2026 15:18:37 +0200 Subject: [PATCH 03/13] refactor: remove code related to old state management --- src/layer/segmentation/index.ts | 261 ++--------------------- src/layer/segmentation/layer_controls.ts | 8 - src/skeleton/frontend.ts | 236 ++++++++------------ src/widget/render_scale_widget.ts | 60 +----- 4 files changed, 110 insertions(+), 455 deletions(-) diff --git a/src/layer/segmentation/index.ts b/src/layer/segmentation/index.ts index b3e25fcab..8eb7e6f6e 100644 --- a/src/layer/segmentation/index.ts +++ b/src/layer/segmentation/index.ts @@ -596,22 +596,6 @@ function buildSpatialSkeletonGridLevels( })); } -function findClosestSpatialSkeletonGridLevel( - levels: SpatialSkeletonGridLevel[], - lod: number, -): number { - let bestIndex = 0; - let bestDistance = Number.POSITIVE_INFINITY; - for (let i = 0; i < levels.length; ++i) { - const distance = Math.abs(levels[i].lod - lod); - if (distance < bestDistance) { - bestDistance = distance; - bestIndex = i; - } - } - return bestIndex; -} - function findClosestSpatialSkeletonGridLevelBySpacing( levels: SpatialSkeletonGridLevel[], spacing: number, @@ -823,72 +807,6 @@ class SegmentationUserLayerDisplayState implements SegmentationDisplayState { this.spatialSkeletonGridResolutionTarget3dExplicit = true; this.applySpatialSkeletonGridResolutionTarget("3d"); }); - this.spatialSkeletonGridResolutionRelative2d.changed.add(() => { - const nextRelative = this.spatialSkeletonGridResolutionRelative2d.value; - if (nextRelative !== this.lastSpatialSkeletonGridResolutionRelative2d) { - const pixelSize = Math.max( - this.spatialSkeletonGridPixelSize2d.value, - 1e-6, - ); - const currentTarget = this.spatialSkeletonGridResolutionTarget2d.value; - const adjustedTarget = nextRelative - ? currentTarget / pixelSize - : currentTarget * pixelSize; - this.suppressSpatialSkeletonGridResolutionTarget2d = true; - this.spatialSkeletonGridResolutionTarget2d.value = adjustedTarget; - this.suppressSpatialSkeletonGridResolutionTarget2d = false; - this.spatialSkeletonGridResolutionTarget2dExplicit = true; - this.lastSpatialSkeletonGridResolutionRelative2d = nextRelative; - } - this.applySpatialSkeletonGridResolutionTarget("2d"); - }); - this.spatialSkeletonGridResolutionRelative3d.changed.add(() => { - const nextRelative = this.spatialSkeletonGridResolutionRelative3d.value; - if (nextRelative !== this.lastSpatialSkeletonGridResolutionRelative3d) { - const pixelSize = Math.max( - this.spatialSkeletonGridPixelSize3d.value, - 1e-6, - ); - const currentTarget = this.spatialSkeletonGridResolutionTarget3d.value; - const adjustedTarget = nextRelative - ? currentTarget / pixelSize - : currentTarget * pixelSize; - this.suppressSpatialSkeletonGridResolutionTarget3d = true; - this.spatialSkeletonGridResolutionTarget3d.value = adjustedTarget; - this.suppressSpatialSkeletonGridResolutionTarget3d = false; - this.spatialSkeletonGridResolutionTarget3dExplicit = true; - this.lastSpatialSkeletonGridResolutionRelative3d = nextRelative; - } - this.applySpatialSkeletonGridResolutionTarget("3d"); - }); - this.spatialSkeletonGridPixelSize2d.changed.add(() => { - if (this.spatialSkeletonGridResolutionRelative2d.value) { - this.applySpatialSkeletonGridResolutionTarget("2d"); - } - }); - this.spatialSkeletonGridPixelSize3d.changed.add(() => { - if (this.spatialSkeletonGridResolutionRelative3d.value) { - this.applySpatialSkeletonGridResolutionTarget("3d"); - } - }); - this.spatialSkeletonGridLevel2d.changed.add(() => { - if (this.suppressSpatialSkeletonGridLevel2d) return; - if (this.spatialSkeletonGridLevels.value.length === 0) return; - this.setSpatialSkeletonGridLevel( - "2d", - this.spatialSkeletonGridLevel2d.value, - true, - ); - }); - this.spatialSkeletonGridLevel3d.changed.add(() => { - if (this.suppressSpatialSkeletonGridLevel3d) return; - if (this.spatialSkeletonGridLevels.value.length === 0) return; - this.setSpatialSkeletonGridLevel( - "3d", - this.spatialSkeletonGridLevel3d.value, - true, - ); - }); } segmentSelectionState = new SegmentSelectionState(); @@ -927,10 +845,6 @@ class SegmentationUserLayerDisplayState implements SegmentationDisplayState { verifyFiniteNonNegativeFloat, 1, ); - spatialSkeletonGridResolutionRelative2d = new TrackableBoolean(false, false); - spatialSkeletonGridResolutionRelative3d = new TrackableBoolean(false, false); - spatialSkeletonGridPixelSize2d = new WatchableValue(1); - spatialSkeletonGridPixelSize3d = new WatchableValue(1); spatialSkeletonGridChunkStats2d = new WatchableValue({ presentCount: 0, totalCount: 0, @@ -949,14 +863,8 @@ class SegmentationUserLayerDisplayState implements SegmentationDisplayState { ); private spatialSkeletonGridResolutionTarget2dExplicit = false; private spatialSkeletonGridResolutionTarget3dExplicit = false; - private spatialSkeletonGridLevel2dExplicit = false; - private spatialSkeletonGridLevel3dExplicit = false; - private suppressSpatialSkeletonGridLevel2d = false; - private suppressSpatialSkeletonGridLevel3d = false; private suppressSpatialSkeletonGridResolutionTarget2d = false; private suppressSpatialSkeletonGridResolutionTarget3d = false; - private lastSpatialSkeletonGridResolutionRelative2d = false; - private lastSpatialSkeletonGridResolutionRelative3d = false; ignoreNullVisibleSet = new TrackableBoolean(true, true); skeletonRenderingOptions = new SkeletonRenderingOptions(); shaderError = makeWatchableShaderError(); @@ -1011,40 +919,23 @@ class SegmentationUserLayerDisplayState implements SegmentationDisplayState { const target3dIndex = this.spatialSkeletonGridResolutionTarget3dExplicit ? findClosestSpatialSkeletonGridLevelBySpacing( levels, - this.getSpatialSkeletonGridTargetSpacing("3d"), + this.spatialSkeletonGridResolutionTarget3d.value, ) - : this.spatialSkeletonGridLevel3dExplicit - ? this.spatialSkeletonGridLevel3d.value - : findClosestSpatialSkeletonGridLevel(levels, this.skeletonLod.value); - const resolved3dIndex = this.setSpatialSkeletonGridLevel( - "3d", - target3dIndex, - this.spatialSkeletonGridResolutionTarget3dExplicit || - this.spatialSkeletonGridLevel3dExplicit, - ); + : 0; + const resolved3dIndex = this.setSpatialSkeletonGridLevel("3d", target3dIndex); const target2dIndex = this.spatialSkeletonGridResolutionTarget2dExplicit ? findClosestSpatialSkeletonGridLevelBySpacing( levels, - this.getSpatialSkeletonGridTargetSpacing("2d"), + this.spatialSkeletonGridResolutionTarget2d.value, ) - : this.spatialSkeletonGridLevel2dExplicit - ? this.spatialSkeletonGridLevel2d.value - : resolved3dIndex; - this.setSpatialSkeletonGridLevel( - "2d", - target2dIndex, - this.spatialSkeletonGridResolutionTarget2dExplicit || - this.spatialSkeletonGridLevel2dExplicit, - ); + : resolved3dIndex; + this.setSpatialSkeletonGridLevel("2d", target2dIndex); if (!this.spatialSkeletonGridResolutionTarget3dExplicit) { const spacing = getSpatialSkeletonGridSpacing( levels[Math.min(resolved3dIndex, levels.length - 1)].size, ); - const targetValue = this.spatialSkeletonGridResolutionRelative3d.value - ? spacing / Math.max(this.spatialSkeletonGridPixelSize3d.value, 1e-6) - : spacing; this.suppressSpatialSkeletonGridResolutionTarget3d = true; - this.spatialSkeletonGridResolutionTarget3d.value = targetValue; + this.spatialSkeletonGridResolutionTarget3d.value = spacing; this.suppressSpatialSkeletonGridResolutionTarget3d = false; } if (!this.spatialSkeletonGridResolutionTarget2dExplicit) { @@ -1052,86 +943,13 @@ class SegmentationUserLayerDisplayState implements SegmentationDisplayState { Math.max(target2dIndex, 0), levels.length - 1, ); - const spacing = getSpatialSkeletonGridSpacing( - levels[resolved2dIndex].size, - ); - const targetValue = this.spatialSkeletonGridResolutionRelative2d.value - ? spacing / Math.max(this.spatialSkeletonGridPixelSize2d.value, 1e-6) - : spacing; + const spacing = getSpatialSkeletonGridSpacing(levels[resolved2dIndex].size); this.suppressSpatialSkeletonGridResolutionTarget2d = true; - this.spatialSkeletonGridResolutionTarget2d.value = targetValue; + this.spatialSkeletonGridResolutionTarget2d.value = spacing; this.suppressSpatialSkeletonGridResolutionTarget2d = false; } } - applySpatialSkeletonGridLevel2dFromSpec(value: any) { - if ( - value !== undefined && - !this.spatialSkeletonGridResolutionTarget2dExplicit - ) { - this.spatialSkeletonGridLevel2d.restoreState(value); - this.spatialSkeletonGridLevel2dExplicit = true; - if (this.spatialSkeletonGridLevels.value.length > 0) { - this.setSpatialSkeletonGridLevel( - "2d", - this.spatialSkeletonGridLevel2d.value, - true, - ); - if (!this.spatialSkeletonGridResolutionTarget2dExplicit) { - const spacing = getSpatialSkeletonGridSpacing( - this.spatialSkeletonGridLevels.value[ - Math.min( - this.spatialSkeletonGridLevel2d.value, - this.spatialSkeletonGridLevels.value.length - 1, - ) - ].size, - ); - const targetValue = this.spatialSkeletonGridResolutionRelative2d.value - ? spacing / - Math.max(this.spatialSkeletonGridPixelSize2d.value, 1e-6) - : spacing; - this.suppressSpatialSkeletonGridResolutionTarget2d = true; - this.spatialSkeletonGridResolutionTarget2d.value = targetValue; - this.suppressSpatialSkeletonGridResolutionTarget2d = false; - } - } - } - } - - applySpatialSkeletonGridLevel3dFromSpec(value: any) { - if ( - value !== undefined && - !this.spatialSkeletonGridResolutionTarget3dExplicit - ) { - this.spatialSkeletonGridLevel3d.restoreState(value); - this.spatialSkeletonGridLevel3dExplicit = true; - if (this.spatialSkeletonGridLevels.value.length > 0) { - this.setSpatialSkeletonGridLevel( - "3d", - this.spatialSkeletonGridLevel3d.value, - true, - ); - if (!this.spatialSkeletonGridResolutionTarget3dExplicit) { - const spacing = getSpatialSkeletonGridSpacing( - this.spatialSkeletonGridLevels.value[ - Math.min( - this.spatialSkeletonGridLevel3d.value, - this.spatialSkeletonGridLevels.value.length - 1, - ) - ].size, - ); - const targetValue = this.spatialSkeletonGridResolutionRelative3d.value - ? spacing / - Math.max(this.spatialSkeletonGridPixelSize3d.value, 1e-6) - : spacing; - this.suppressSpatialSkeletonGridResolutionTarget3d = true; - this.spatialSkeletonGridResolutionTarget3d.value = targetValue; - this.suppressSpatialSkeletonGridResolutionTarget3d = false; - } - } - } - } - applySpatialSkeletonGridResolutionTarget2dFromSpec(value: any) { if (value !== undefined) { this.suppressSpatialSkeletonGridResolutionTarget2d = true; @@ -1156,60 +974,30 @@ class SegmentationUserLayerDisplayState implements SegmentationDisplayState { } } - private getSpatialSkeletonGridTargetSpacing(kind: "2d" | "3d") { - const target = - kind === "2d" - ? this.spatialSkeletonGridResolutionTarget2d.value - : this.spatialSkeletonGridResolutionTarget3d.value; - const isRelative = - kind === "2d" - ? this.spatialSkeletonGridResolutionRelative2d.value - : this.spatialSkeletonGridResolutionRelative3d.value; - const pixelSize = - kind === "2d" - ? this.spatialSkeletonGridPixelSize2d.value - : this.spatialSkeletonGridPixelSize3d.value; - return isRelative ? target * pixelSize : target; - } - private applySpatialSkeletonGridResolutionTarget(kind: "2d" | "3d") { const levels = this.spatialSkeletonGridLevels.value; if (levels.length === 0) return; - const targetSpacing = this.getSpatialSkeletonGridTargetSpacing(kind); - const index = findClosestSpatialSkeletonGridLevelBySpacing( - levels, - targetSpacing, - ); - const markExplicit = + const target = kind === "2d" - ? this.spatialSkeletonGridResolutionTarget2dExplicit - : this.spatialSkeletonGridResolutionTarget3dExplicit; - this.setSpatialSkeletonGridLevel(kind, index, markExplicit); + ? this.spatialSkeletonGridResolutionTarget2d.value + : this.spatialSkeletonGridResolutionTarget3d.value; + const index = findClosestSpatialSkeletonGridLevelBySpacing(levels, target); + this.setSpatialSkeletonGridLevel(kind, index); } - private setSpatialSkeletonGridLevel( - kind: "2d" | "3d", - index: number, - markExplicit: boolean, - ) { + private setSpatialSkeletonGridLevel(kind: "2d" | "3d", index: number) { const levels = this.spatialSkeletonGridLevels.value; if (levels.length === 0) return 0; const clampedIndex = Math.min(Math.max(index, 0), levels.length - 1); if (kind === "2d") { - if (markExplicit) this.spatialSkeletonGridLevel2dExplicit = true; - this.suppressSpatialSkeletonGridLevel2d = true; this.spatialSkeletonGridLevel2d.value = clampedIndex; - this.suppressSpatialSkeletonGridLevel2d = false; const nextLod = levels[clampedIndex].lod; if (this.spatialSkeletonLod2d.value !== nextLod) { this.spatialSkeletonLod2d.value = nextLod; } return clampedIndex; } - if (markExplicit) this.spatialSkeletonGridLevel3dExplicit = true; - this.suppressSpatialSkeletonGridLevel3d = true; this.spatialSkeletonGridLevel3d.value = clampedIndex; - this.suppressSpatialSkeletonGridLevel3d = false; const nextLod = levels[clampedIndex].lod; if (this.skeletonLod.value !== nextLod) { this.skeletonLod.value = nextLod; @@ -1615,9 +1403,6 @@ export class SegmentationUserLayer extends Base { this.displayState.hiddenObjectAlpha.changed.add( this.specificationChanged.dispatch, ); - this.displayState.skeletonLod.changed.add( - this.specificationChanged.dispatch, - ); this.displayState.spatialSkeletonNodeQuery.changed.add( this.specificationChanged.dispatch, ); @@ -1630,18 +1415,6 @@ export class SegmentationUserLayer extends Base { this.displayState.spatialSkeletonGridResolutionTarget3d.changed.add( this.specificationChanged.dispatch, ); - this.displayState.spatialSkeletonGridResolutionRelative2d.changed.add( - this.specificationChanged.dispatch, - ); - this.displayState.spatialSkeletonGridResolutionRelative3d.changed.add( - this.specificationChanged.dispatch, - ); - this.displayState.spatialSkeletonGridLevel2d.changed.add( - this.specificationChanged.dispatch, - ); - this.displayState.spatialSkeletonGridLevel3d.changed.add( - this.specificationChanged.dispatch, - ); this.displayState.hoverHighlight.changed.add( this.specificationChanged.dispatch, ); @@ -2368,10 +2141,10 @@ export class SegmentationUserLayer extends Base { (value) => this.displayState.spatialSkeletonNodeFilter.restoreState(value), ); - this.displayState.applySpatialSkeletonGridLevel2dFromSpec( + this.displayState.applySpatialSkeletonGridResolutionTarget2dFromSpec( specification[json_keys.SKELETON_CROSS_SECTION_RENDER_SCALE_JSON_KEY], ); - this.displayState.applySpatialSkeletonGridLevel3dFromSpec( + this.displayState.applySpatialSkeletonGridResolutionTarget3dFromSpec( specification[json_keys.SKELETON_PERSPECTIVE_RENDER_SCALE_JSON_KEY], ); this.displayState.baseSegmentColoring.restoreState( diff --git a/src/layer/segmentation/layer_controls.ts b/src/layer/segmentation/layer_controls.ts index 85c6989ec..478f07e00 100644 --- a/src/layer/segmentation/layer_controls.ts +++ b/src/layer/segmentation/layer_controls.ts @@ -88,11 +88,7 @@ export const LAYER_CONTROLS: LayerControlDefinition[] = [ ...spatialSkeletonGridRenderScaleLayerControl((layer) => ({ histogram: layer.displayState.spatialSkeletonGridRenderScaleHistogram2d, target: layer.displayState.spatialSkeletonGridResolutionTarget2d, - relative: layer.displayState.spatialSkeletonGridResolutionRelative2d, - pixelSize: layer.displayState.spatialSkeletonGridPixelSize2d, chunkStats: layer.displayState.spatialSkeletonGridChunkStats2d, - relativeTooltip: - "Interpret the 2D skeleton grid resolution target as relative to zoom", })), }, { @@ -112,11 +108,7 @@ export const LAYER_CONTROLS: LayerControlDefinition[] = [ ...spatialSkeletonGridRenderScaleLayerControl((layer) => ({ histogram: layer.displayState.spatialSkeletonGridRenderScaleHistogram3d, target: layer.displayState.spatialSkeletonGridResolutionTarget3d, - relative: layer.displayState.spatialSkeletonGridResolutionRelative3d, - pixelSize: layer.displayState.spatialSkeletonGridPixelSize3d, chunkStats: layer.displayState.spatialSkeletonGridChunkStats3d, - relativeTooltip: - "Interpret the 3D skeleton grid resolution target as relative to zoom", })), }, { diff --git a/src/skeleton/frontend.ts b/src/skeleton/frontend.ts index 93f64dd69..4183ea44b 100644 --- a/src/skeleton/frontend.ts +++ b/src/skeleton/frontend.ts @@ -1706,21 +1706,19 @@ export class MultiscaleSliceViewSpatiallyIndexedSkeletonLayer extends SliceViewR public multiscaleSource: MultiscaleSpatiallyIndexedSkeletonSource, public displayState: SegmentationDisplayState, ) { - const renderScaleTarget = (displayState as any) - .renderScaleTarget as WatchableValueInterface; - const gridLevel2d = (displayState as any).spatialSkeletonGridLevel2d as - | WatchableValueInterface - | undefined; + const spatialDisplayState = displayState as SegmentationDisplayState & + SpatialSkeletonDisplayState; + const anyDisplayState = displayState as any; + const renderScaleTarget = anyDisplayState.renderScaleTarget as WatchableValueInterface; + const gridLevel2d = spatialDisplayState.spatialSkeletonGridLevel2d; super(chunkManager, multiscaleSource, { - transform: (displayState as any).transform, - localPosition: (displayState as any).localPosition, + transform: anyDisplayState.transform, + localPosition: anyDisplayState.localPosition, renderScaleTarget, visibleSourcesInvalidation: gridLevel2d === undefined ? [] : [gridLevel2d], }); - this.renderOptions = ( - displayState as any - ).skeletonRenderingOptions.params2d; + this.renderOptions = anyDisplayState.skeletonRenderingOptions.params2d; this.registerDisposer( this.renderOptions.mode.changed.add(this.redrawNeeded.dispatch), ); @@ -1728,7 +1726,7 @@ export class MultiscaleSliceViewSpatiallyIndexedSkeletonLayer extends SliceViewR this.renderOptions.lineWidth.changed.add(this.redrawNeeded.dispatch), ); const rpc = this.chunkManager.rpc!; - const lod2d = (displayState as any).spatialSkeletonLod2d; + const lod2d = spatialDisplayState.spatialSkeletonLod2d; if (gridLevel2d !== undefined && lod2d !== undefined) { this.rpcTransfer = { ...this.rpcTransfer, @@ -1770,10 +1768,10 @@ export class MultiscaleSliceViewSpatiallyIndexedSkeletonLayer extends SliceViewR const { presentCount, totalCount } = mergeVisibleSpatialChunkKeyCounts( this.visibleChunkKeysBySliceView.values(), ); + const displayState = this.displayState as SegmentationDisplayState & + SpatialSkeletonDisplayState; updateSpatialSkeletonGridChunkStatsWatchable( - (this.displayState as any).spatialSkeletonGridChunkStats2d as - | WatchableValue - | undefined, + displayState.spatialSkeletonGridChunkStats2d, presentCount, totalCount, ); @@ -1809,15 +1807,14 @@ export class MultiscaleSliceViewSpatiallyIndexedSkeletonLayer extends SliceViewR } draw(renderContext: SliceViewRenderContext) { - const displayState = this.displayState as any; - const lodValue = displayState.spatialSkeletonLod2d?.value as - | number - | undefined; + const displayState = this.displayState as SegmentationDisplayState & + SpatialSkeletonDisplayState; + const lodValue = displayState.spatialSkeletonLod2d?.value; const sliceView = renderContext.sliceView; this.registerChunkStatsSliceView( sliceView as RefCounted & { rpcId: number }, ); - if (displayState.objectAlpha?.value <= 0.0 || lodValue === undefined) { + if ((displayState as any).objectAlpha?.value <= 0.0 || lodValue === undefined) { this.clearVisibleChunkKeysForSliceView(sliceView.rpcId); return; } @@ -1892,8 +1889,6 @@ function updateSpatialSkeletonGridRenderScaleHistogram( levels: | Array<{ size: { x: number; y: number; z: number }; lod: number }> | undefined, - relative: boolean, - pixelSize: number, ) { histogram.begin(frameNumber); if (lod === undefined || transformedSources.length === 0) { @@ -1904,7 +1899,6 @@ function updateSpatialSkeletonGridRenderScaleHistogram( if (scales.length === 0) { return; } - const safePixelSize = Math.max(pixelSize, 1e-6); for (const tsource of scales) { const gridIndex = (tsource.source as any).parameters?.gridIndex as | number @@ -1934,13 +1928,12 @@ function updateSpatialSkeletonGridRenderScaleHistogram( }, ); const spacing = getSpatialSkeletonGridSpacing(tsource, levels, gridIndex); - const renderScale = relative ? spacing / safePixelSize : spacing; const total = presentCount + missingCount; if (total > 0) { - histogram.add(spacing, renderScale, presentCount, missingCount); + histogram.add(spacing, spacing, presentCount, missingCount); } else { // Keep all grid rows visible in the histogram even when currently empty. - histogram.add(spacing, renderScale, 0, 1, true); + histogram.add(spacing, spacing, 0, 1, true); } } } @@ -2071,6 +2064,26 @@ function collectPlaneIntersectingSpatialChunkKeysBySource( return visibleChunkKeysBySource; } +interface SpatialSkeletonDisplayState { + spatialSkeletonGridLevel2d?: WatchableValueInterface; + spatialSkeletonGridLevel3d?: WatchableValueInterface; + skeletonLod?: WatchableValueInterface; + spatialSkeletonGridChunkStats2d?: WatchableValue<{ + presentCount: number; + totalCount: number; + }>; + spatialSkeletonGridChunkStats3d?: WatchableValue<{ + presentCount: number; + totalCount: number; + }>; + spatialSkeletonLod2d?: WatchableValueInterface; + spatialSkeletonGridLevels?: WatchableValueInterface< + Array<{ size: { x: number; y: number; z: number }; lod: number }> + >; + spatialSkeletonGridRenderScaleHistogram2d?: RenderScaleHistogram; + spatialSkeletonGridRenderScaleHistogram3d?: RenderScaleHistogram; +} + export class SpatiallyIndexedSkeletonLayer extends RefCounted implements SkeletonLayerInterface @@ -2508,12 +2521,16 @@ export class SpatiallyIndexedSkeletonLayer this.displayState.transform, ), ); + const spatialDisplayState = displayState as SkeletonLayerDisplayState & + SpatialSkeletonDisplayState; this.gridLevel = options.gridLevel ?? - (displayState as any).spatialSkeletonGridLevel3d ?? + spatialDisplayState.spatialSkeletonGridLevel3d ?? new WatchableValue(0); this.lod = - options.lod ?? (displayState as any).skeletonLod ?? new WatchableValue(0); + options.lod ?? + spatialDisplayState.skeletonLod ?? + new WatchableValue(0); this.selectedNodeId = options.selectedNodeId; this.pendingNodePositionVersion = options.pendingNodePositionVersion; this.getPendingNodePositionOverride = options.getPendingNodePosition; @@ -2623,15 +2640,11 @@ export class SpatiallyIndexedSkeletonLayer ); } registerNumericRedrawWatchable(this.gridLevel); - registerNumericRedrawWatchable( - (displayState as any).spatialSkeletonGridLevel2d, - ); - registerNumericRedrawWatchable( - (displayState as any).spatialSkeletonGridLevel3d, - ); + registerNumericRedrawWatchable(spatialDisplayState.spatialSkeletonGridLevel2d); + registerNumericRedrawWatchable(spatialDisplayState.spatialSkeletonGridLevel3d); registerNumericRedrawWatchable(this.lod); - registerNumericRedrawWatchable((displayState as any).spatialSkeletonLod2d); - registerNumericRedrawWatchable((displayState as any).skeletonLod); + registerNumericRedrawWatchable(spatialDisplayState.spatialSkeletonLod2d); + registerNumericRedrawWatchable(spatialDisplayState.skeletonLod); if (displayState.hiddenObjectAlpha) { this.registerDisposer( displayState.hiddenObjectAlpha.changed.add(() => requestRedraw()), @@ -2717,20 +2730,19 @@ export class SpatiallyIndexedSkeletonLayer } private getGridLevelForView(view: SpatiallyIndexedSkeletonView) { - const displayState = this.displayState as any; - return ( - view === "2d" - ? displayState.spatialSkeletonGridLevel2d?.value - : displayState.spatialSkeletonGridLevel3d?.value - ) as number | undefined; + const displayState = this.displayState as SkeletonLayerDisplayState & + SpatialSkeletonDisplayState; + return view === "2d" + ? displayState.spatialSkeletonGridLevel2d?.value + : displayState.spatialSkeletonGridLevel3d?.value; } private getGridChunkStatsWatchable(view: SpatiallyIndexedSkeletonView) { - return ( - view === "2d" - ? (this.displayState as any).spatialSkeletonGridChunkStats2d - : (this.displayState as any).spatialSkeletonGridChunkStats3d - ) as WatchableValue | undefined; + const displayState = this.displayState as SkeletonLayerDisplayState & + SpatialSkeletonDisplayState; + return view === "2d" + ? displayState.spatialSkeletonGridChunkStats2d + : displayState.spatialSkeletonGridChunkStats3d; } private updateSelectedChunkStatsForView(view: SpatiallyIndexedSkeletonView) { @@ -3446,12 +3458,11 @@ export class PerspectiveViewSpatiallyIndexedSkeletonLayer extends PerspectiveVie this.registerDisposer( renderOptions.lineWidth.changed.add(this.redrawNeeded.dispatch), ); - const histogram = (base.displayState as any) - .spatialSkeletonGridRenderScaleHistogram3d as - | RenderScaleHistogram - | undefined; - if (histogram !== undefined) { - this.registerDisposer(histogram.visibility.add(this.visibility)); + const spatialDisplayState = base.displayState as SkeletonLayerDisplayState & + SpatialSkeletonDisplayState; + const histogram3d = spatialDisplayState.spatialSkeletonGridRenderScaleHistogram3d; + if (histogram3d !== undefined) { + this.registerDisposer(histogram3d.visibility.add(this.visibility)); } } @@ -3640,36 +3651,12 @@ export class PerspectiveViewSpatiallyIndexedSkeletonLayer extends PerspectiveVie ThreeDimensionalRenderLayerAttachmentState >, ) { - const pixelSizeWatchable = (this.base.displayState as any) - .spatialSkeletonGridPixelSize3d as - | WatchableValueInterface - | undefined; - if (pixelSizeWatchable !== undefined) { - const voxelPhysicalScales = - renderContext.projectionParameters.displayDimensionRenderInfo - ?.voxelPhysicalScales; - if (voxelPhysicalScales !== undefined) { - const { invViewMatrix } = renderContext.projectionParameters; - let computedPixelSize = 0; - for (let i = 0; i < 3; ++i) { - const s = voxelPhysicalScales[i]; - const x = invViewMatrix[i]; - computedPixelSize += (s * x) ** 2; - } - const pixelSize = Math.sqrt(computedPixelSize); - if ( - Number.isFinite(pixelSize) && - pixelSizeWatchable.value !== pixelSize - ) { - pixelSizeWatchable.value = pixelSize; - } - } - } if (!renderContext.emitColor && renderContext.alreadyEmittedPickID) { return; } - const displayState = this.base.displayState as any; - const lodValue = displayState.skeletonLod?.value as number | undefined; + const displayState = this.base.displayState as SkeletonLayerDisplayState & + SpatialSkeletonDisplayState; + const lodValue = displayState.skeletonLod?.value; this.base.updateVisibleChunksForView( "3d", this.transformedSources, @@ -3677,18 +3664,11 @@ export class PerspectiveViewSpatiallyIndexedSkeletonLayer extends PerspectiveVie lodValue, attachment.view.rpcId, ); - const levels = displayState.spatialSkeletonGridLevels?.value as - | Array<{ size: { x: number; y: number; z: number }; lod: number }> - | undefined; - const histogram = displayState.spatialSkeletonGridRenderScaleHistogram3d as - | RenderScaleHistogram - | undefined; + const levels = displayState.spatialSkeletonGridLevels?.value; + const histogram = displayState.spatialSkeletonGridRenderScaleHistogram3d; if (histogram !== undefined) { const frameNumber = this.base.chunkManager.chunkQueueManager.frameNumberCounter.frameNumber; - const relative = - displayState.spatialSkeletonGridResolutionRelative3d?.value === true; - const pixelSize = Math.max(pixelSizeWatchable?.value ?? 1, 1e-6); updateSpatialSkeletonGridRenderScaleHistogram( histogram, frameNumber, @@ -3697,8 +3677,6 @@ export class PerspectiveViewSpatiallyIndexedSkeletonLayer extends PerspectiveVie this.base.localPosition.value, lodValue, levels, - relative, - pixelSize, ); } this.base.draw( @@ -3710,9 +3688,7 @@ export class PerspectiveViewSpatiallyIndexedSkeletonLayer extends PerspectiveVie attachment, { view: "3d", - gridLevel: displayState.spatialSkeletonGridLevel3d?.value as - | number - | undefined, + gridLevel: displayState.spatialSkeletonGridLevel3d?.value, lod: lodValue, }, ); @@ -3725,8 +3701,9 @@ export class PerspectiveViewSpatiallyIndexedSkeletonLayer extends PerspectiveVie ThreeDimensionalRenderLayerAttachmentState >, ) { - const displayState = this.base.displayState as any; - const lodValue = displayState.skeletonLod?.value as number | undefined; + const displayState = this.base.displayState as SkeletonLayerDisplayState & + SpatialSkeletonDisplayState; + const lodValue = displayState.skeletonLod?.value; return this.base.isReady( this.transformedSources, renderContext.projectionParameters, @@ -3783,10 +3760,9 @@ export class SliceViewSpatiallyIndexedSkeletonLayer extends SliceViewRenderLayer } draw(renderContext: SliceViewRenderContext) { - const displayState = this.base.displayState as any; - const lodValue = displayState.spatialSkeletonLod2d?.value as - | number - | undefined; + const displayState = this.base.displayState as SkeletonLayerDisplayState & + SpatialSkeletonDisplayState; + const lodValue = displayState.spatialSkeletonLod2d?.value; const sliceView = renderContext.sliceView; const sliceViewId = sliceView.rpcId; if (!this.trackedChunkStatsSliceViews.has(sliceViewId)) { @@ -3796,7 +3772,7 @@ export class SliceViewSpatiallyIndexedSkeletonLayer extends SliceViewRenderLayer this.base.clearVisibleChunkKeysForRenderedView("2d", sliceViewId); }); } - if (displayState.objectAlpha?.value <= 0.0 || lodValue === undefined) { + if ((displayState as any).objectAlpha?.value <= 0.0 || lodValue === undefined) { this.base.clearVisibleChunkKeysForRenderedView("2d", sliceViewId); return; } @@ -3836,22 +3812,21 @@ export class SliceViewPanelSpatiallyIndexedSkeletonLayer extends SliceViewPanelR this.registerDisposer( renderOptions.lineWidth.changed.add(this.redrawNeeded.dispatch), ); - const gridLevel2d = (base.displayState as any).spatialSkeletonGridLevel2d; + const spatialDisplayState2d = base.displayState as SkeletonLayerDisplayState & + SpatialSkeletonDisplayState; + const gridLevel2d = spatialDisplayState2d.spatialSkeletonGridLevel2d; if (gridLevel2d?.changed) { this.registerDisposer( gridLevel2d.changed.add(this.redrawNeeded.dispatch), ); } - const lod2d = (base.displayState as any).spatialSkeletonLod2d; + const lod2d = spatialDisplayState2d.spatialSkeletonLod2d; if (lod2d?.changed) { this.registerDisposer(lod2d.changed.add(this.redrawNeeded.dispatch)); } - const histogram = (base.displayState as any) - .spatialSkeletonGridRenderScaleHistogram2d as - | RenderScaleHistogram - | undefined; - if (histogram !== undefined) { - this.registerDisposer(histogram.visibility.add(this.visibility)); + const histogram2d = spatialDisplayState2d.spatialSkeletonGridRenderScaleHistogram2d; + if (histogram2d !== undefined) { + this.registerDisposer(histogram2d.visibility.add(this.visibility)); } this.registerDisposer(base.redrawNeeded.add(this.redrawNeeded.dispatch)); } @@ -4008,42 +3983,20 @@ export class SliceViewPanelSpatiallyIndexedSkeletonLayer extends SliceViewPanelR ThreeDimensionalRenderLayerAttachmentState >, ) { - const pixelSizeWatchable = (this.base.displayState as any) - .spatialSkeletonGridPixelSize2d as - | WatchableValueInterface - | undefined; - if (pixelSizeWatchable !== undefined) { - const pixelSize = - renderContext.sliceView.projectionParameters.value.pixelSize; - if ( - Number.isFinite(pixelSize) && - pixelSizeWatchable.value !== pixelSize - ) { - pixelSizeWatchable.value = pixelSize; - } - } - const displayState = this.base.displayState as any; - const lodValue = displayState.spatialSkeletonLod2d?.value as - | number - | undefined; + const displayState = this.base.displayState as SkeletonLayerDisplayState & + SpatialSkeletonDisplayState; + const lodValue = displayState.spatialSkeletonLod2d?.value; this.base.updateVisibleChunksForView( "2d", this.transformedSources, renderContext.sliceView.projectionParameters.value, lodValue, ); - const levels = displayState.spatialSkeletonGridLevels?.value as - | Array<{ size: { x: number; y: number; z: number }; lod: number }> - | undefined; - const histogram = displayState.spatialSkeletonGridRenderScaleHistogram2d as - | RenderScaleHistogram - | undefined; + const levels = displayState.spatialSkeletonGridLevels?.value; + const histogram = displayState.spatialSkeletonGridRenderScaleHistogram2d; if (histogram !== undefined) { const frameNumber = this.base.chunkManager.chunkQueueManager.frameNumberCounter.frameNumber; - const relative = - displayState.spatialSkeletonGridResolutionRelative2d?.value === true; - const pixelSize = Math.max(pixelSizeWatchable?.value ?? 1, 1e-6); updateSpatialSkeletonGridRenderScaleHistogram( histogram, frameNumber, @@ -4052,8 +4005,6 @@ export class SliceViewPanelSpatiallyIndexedSkeletonLayer extends SliceViewPanelR this.base.localPosition.value, lodValue, levels, - relative, - pixelSize, ); } this.base.draw( @@ -4065,9 +4016,7 @@ export class SliceViewPanelSpatiallyIndexedSkeletonLayer extends SliceViewPanelR attachment, { view: "2d", - gridLevel: displayState.spatialSkeletonGridLevel2d?.value as - | number - | undefined, + gridLevel: displayState.spatialSkeletonGridLevel2d?.value, lod: lodValue, }, ); @@ -4080,10 +4029,9 @@ export class SliceViewPanelSpatiallyIndexedSkeletonLayer extends SliceViewPanelR ThreeDimensionalRenderLayerAttachmentState >, ) { - const displayState = this.base.displayState as any; - const lodValue = displayState.spatialSkeletonLod2d?.value as - | number - | undefined; + const displayState = this.base.displayState as SkeletonLayerDisplayState & + SpatialSkeletonDisplayState; + const lodValue = displayState.spatialSkeletonLod2d?.value; return this.base.isReady( this.transformedSources, renderContext.projectionParameters, diff --git a/src/widget/render_scale_widget.ts b/src/widget/render_scale_widget.ts index 1efa63ecb..e7942dac4 100644 --- a/src/widget/render_scale_widget.ts +++ b/src/widget/render_scale_widget.ts @@ -26,7 +26,6 @@ import { renderScaleHistogramBinSize, renderScaleHistogramOrigin, } from "#src/render_scale_statistics.js"; -import { TrackableBooleanCheckbox } from "#src/trackable_boolean.js"; import type { TrackableValueInterface, WatchableValueInterface, @@ -72,14 +71,10 @@ export interface RenderScaleWidgetOptions { export interface SpatialSkeletonGridRenderScaleWidgetOptions { histogram: RenderScaleHistogram; target: TrackableValueInterface; - relative: WatchableValueInterface; - pixelSize: WatchableValueInterface; chunkStats?: WatchableValueInterface<{ presentCount: number; totalCount: number; }>; - relativeLabel?: string; - relativeTooltip?: string; } export class RenderScaleWidget extends RefCounted { @@ -429,7 +424,6 @@ export class VolumeRenderingRenderScaleWidget extends RenderScaleWidget { export class SpatialSkeletonGridRenderScaleWidget extends RenderScaleWidget { protected unitOfTarget = "nm"; - private relative?: WatchableValueInterface; private chunkStats?: WatchableValueInterface<{ presentCount: number; totalCount: number; @@ -444,60 +438,21 @@ export class SpatialSkeletonGridRenderScaleWidget extends RenderScaleWidget { histogram: RenderScaleHistogram, target: TrackableValueInterface, options: { - relative?: WatchableValueInterface; - pixelSize?: WatchableValueInterface; chunkStats?: WatchableValueInterface<{ presentCount: number; totalCount: number; }>; - relativeLabel?: string; - relativeTooltip?: string; } = {}, ) { super(histogram, target); this.element.classList.add("neuroglancer-render-scale-widget-grid"); this.syncScaleConfig(); - this.relative = options.relative; this.chunkStats = options.chunkStats; if (options.chunkStats !== undefined) { this.registerDisposer( options.chunkStats.changed.add(() => this.updateView()), ); } - if (options.relative !== undefined) { - const relativeTooltip = - options.relativeTooltip ?? - "Interpret the skeleton grid resolution target as relative to zoom"; - this.label.classList.add("neuroglancer-render-scale-widget-relative"); - this.label.title = relativeTooltip; - const relativeCheckbox = this.registerDisposer( - new TrackableBooleanCheckbox(options.relative, { - enabledTitle: relativeTooltip, - disabledTitle: relativeTooltip, - }), - ); - relativeCheckbox.element.classList.add( - "neuroglancer-render-scale-widget-relative-checkbox", - ); - this.label.appendChild(relativeCheckbox.element); - const relativeLabel = document.createElement("span"); - relativeLabel.textContent = options.relativeLabel ?? "Rel"; - this.label.appendChild(relativeLabel); - this.registerDisposer( - options.relative.changed.add(() => this.updateView()), - ); - if (options.pixelSize !== undefined) { - this.registerDisposer( - options.pixelSize.changed.add(() => this.updateView()), - ); - } - this.registerEventListener(this.element, "click", (event: MouseEvent) => { - if (event.target === relativeCheckbox.element) { - return; - } - event.preventDefault(); - }); - } this.legendRenderScale.title = "Target skeleton grid spacing"; } @@ -519,7 +474,6 @@ export class SpatialSkeletonGridRenderScaleWidget extends RenderScaleWidget { updateView() { this.syncScaleConfig(); - this.unitOfTarget = this.relative?.value === true ? "px" : "nm"; super.updateView(); } } @@ -576,22 +530,10 @@ export function spatialSkeletonGridRenderScaleLayerControl< ): LayerControlFactory { return { makeControl: (layer, context) => { - const { - histogram, - target, - relative, - pixelSize, - chunkStats, - relativeLabel, - relativeTooltip, - } = getter(layer); + const { histogram, target, chunkStats } = getter(layer); const control = context.registerDisposer( new SpatialSkeletonGridRenderScaleWidget(histogram, target, { - relative, - pixelSize, chunkStats, - relativeLabel, - relativeTooltip, }), ); return { control, controlElement: control.element }; From be339cb6af1e069ed751f14f1e2ab0fad0732ecc Mon Sep 17 00:00:00 2001 From: Sean Martin Date: Fri, 1 May 2026 15:39:19 +0200 Subject: [PATCH 04/13] refactor: remove customisation from skeleton grid render scale the old was adding custom css and behaviour but we can rely on the base instead for simplicity --- src/layer/segmentation/layer_controls.ts | 12 ++- src/widget/render_scale_widget.css | 55 -------------- src/widget/render_scale_widget.ts | 96 +----------------------- 3 files changed, 7 insertions(+), 156 deletions(-) diff --git a/src/layer/segmentation/layer_controls.ts b/src/layer/segmentation/layer_controls.ts index 478f07e00..228825364 100644 --- a/src/layer/segmentation/layer_controls.ts +++ b/src/layer/segmentation/layer_controls.ts @@ -8,7 +8,7 @@ import { enumLayerControl } from "#src/widget/layer_control_enum.js"; import { rangeLayerControl } from "#src/widget/layer_control_range.js"; import { renderScaleLayerControl, - spatialSkeletonGridRenderScaleLayerControl, + SpatialSkeletonGridRenderScaleWidget, } from "#src/widget/render_scale_widget.js"; import { colorSeedLayerControl, @@ -85,11 +85,10 @@ export const LAYER_CONTROLS: LayerControlDefinition[] = [ ), title: "Select the grid size level for spatially indexed skeletons in 2D views", - ...spatialSkeletonGridRenderScaleLayerControl((layer) => ({ + ...renderScaleLayerControl((layer) => ({ histogram: layer.displayState.spatialSkeletonGridRenderScaleHistogram2d, target: layer.displayState.spatialSkeletonGridResolutionTarget2d, - chunkStats: layer.displayState.spatialSkeletonGridChunkStats2d, - })), + }), SpatialSkeletonGridRenderScaleWidget), }, { label: "Resolution (skeleton grid 3D)", @@ -105,11 +104,10 @@ export const LAYER_CONTROLS: LayerControlDefinition[] = [ ), title: "Select the grid size level for spatially indexed skeletons in 3D views", - ...spatialSkeletonGridRenderScaleLayerControl((layer) => ({ + ...renderScaleLayerControl((layer) => ({ histogram: layer.displayState.spatialSkeletonGridRenderScaleHistogram3d, target: layer.displayState.spatialSkeletonGridResolutionTarget3d, - chunkStats: layer.displayState.spatialSkeletonGridChunkStats3d, - })), + }), SpatialSkeletonGridRenderScaleWidget), }, { label: "Opacity (3d)", diff --git a/src/widget/render_scale_widget.css b/src/widget/render_scale_widget.css index b0028b86b..6c1132d7c 100644 --- a/src/widget/render_scale_widget.css +++ b/src/widget/render_scale_widget.css @@ -42,58 +42,3 @@ .neuroglancer-render-scale-widget-legend > div { height: 12px; } - -.neuroglancer-render-scale-widget-grid - .neuroglancer-render-scale-widget-legend { - width: 100%; - min-width: 0; - flex: 1 1 100%; - display: flex; - column-gap: 8px; - text-align: left; -} - -.neuroglancer-render-scale-widget-grid - .neuroglancer-render-scale-widget-legend - > div { - white-space: nowrap; - flex: 1 1 0; - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; -} - -.neuroglancer-render-scale-widget-grid { - align-items: center; - flex-wrap: wrap; - column-gap: 4px; -} - -.neuroglancer-render-scale-widget-grid > canvas { - order: 3; - width: 100%; - flex: 1 1 100%; -} - -.neuroglancer-render-scale-widget-grid - .neuroglancer-render-scale-widget-prompt { - order: 1; -} - -.neuroglancer-render-scale-widget-grid - .neuroglancer-render-scale-widget-legend { - order: 2; - font-size: 10px; -} - -.neuroglancer-render-scale-widget-relative { - display: inline-flex; - align-items: center; - gap: 4px; - font-size: 10px; - margin-right: 4px; -} - -.neuroglancer-render-scale-widget-relative-checkbox { - margin: 0; -} diff --git a/src/widget/render_scale_widget.ts b/src/widget/render_scale_widget.ts index e7942dac4..38a770ee4 100644 --- a/src/widget/render_scale_widget.ts +++ b/src/widget/render_scale_widget.ts @@ -26,10 +26,7 @@ import { renderScaleHistogramBinSize, renderScaleHistogramOrigin, } from "#src/render_scale_statistics.js"; -import type { - TrackableValueInterface, - WatchableValueInterface, -} from "#src/trackable_value.js"; +import type { TrackableValueInterface } from "#src/trackable_value.js"; import { WatchableValue } from "#src/trackable_value.js"; import { serializeColor } from "#src/util/color.js"; import { hsvToRgb } from "#src/util/colorspace.js"; @@ -68,15 +65,6 @@ export interface RenderScaleWidgetOptions { target: TrackableValueInterface; } -export interface SpatialSkeletonGridRenderScaleWidgetOptions { - histogram: RenderScaleHistogram; - target: TrackableValueInterface; - chunkStats?: WatchableValueInterface<{ - presentCount: number; - totalCount: number; - }>; -} - export class RenderScaleWidget extends RefCounted { label = document.createElement("div"); element = document.createElement("div"); @@ -424,56 +412,10 @@ export class VolumeRenderingRenderScaleWidget extends RenderScaleWidget { export class SpatialSkeletonGridRenderScaleWidget extends RenderScaleWidget { protected unitOfTarget = "nm"; - private chunkStats?: WatchableValueInterface<{ - presentCount: number; - totalCount: number; - }>; - private syncScaleConfig() { + updateView() { this.logScaleOrigin = this.histogram.logScaleOrigin; this.logScaleBinSize = this.histogram.logScaleBinSize; - } - - constructor( - histogram: RenderScaleHistogram, - target: TrackableValueInterface, - options: { - chunkStats?: WatchableValueInterface<{ - presentCount: number; - totalCount: number; - }>; - } = {}, - ) { - super(histogram, target); - this.element.classList.add("neuroglancer-render-scale-widget-grid"); - this.syncScaleConfig(); - this.chunkStats = options.chunkStats; - if (options.chunkStats !== undefined) { - this.registerDisposer( - options.chunkStats.changed.add(() => this.updateView()), - ); - } - this.legendRenderScale.title = "Target skeleton grid spacing"; - } - - adjustViaWheel(event: WheelEvent) { - this.syncScaleConfig(); - super.adjustViaWheel(event); - } - - protected getLegendChunkCounts( - totalPresent: number, - totalNotPresent: number, - ) { - const chunkStats = this.chunkStats?.value; - if (chunkStats !== undefined) { - return chunkStats; - } - return super.getLegendChunkCounts(totalPresent, totalNotPresent); - } - - updateView() { - this.syncScaleConfig(); super.updateView(); } } @@ -522,37 +464,3 @@ export function renderScaleLayerControl< }, }; } - -export function spatialSkeletonGridRenderScaleLayerControl< - LayerType extends UserLayer, ->( - getter: (layer: LayerType) => SpatialSkeletonGridRenderScaleWidgetOptions, -): LayerControlFactory { - return { - makeControl: (layer, context) => { - const { histogram, target, chunkStats } = getter(layer); - const control = context.registerDisposer( - new SpatialSkeletonGridRenderScaleWidget(histogram, target, { - chunkStats, - }), - ); - return { control, controlElement: control.element }; - }, - activateTool: (activation, control) => { - activation.bindInputEventMap(TOOL_INPUT_EVENT_MAP); - activation.bindAction( - "adjust-via-wheel", - (event: ActionEvent) => { - event.stopPropagation(); - event.preventDefault(); - control.adjustViaWheel(event.detail); - }, - ); - activation.bindAction("reset", (event: ActionEvent) => { - event.stopPropagation(); - event.preventDefault(); - control.reset(); - }); - }, - }; -} From 659008e022153cce77add196c85a56aa372aaeb0 Mon Sep 17 00:00:00 2001 From: Sean Martin Date: Fri, 1 May 2026 16:31:52 +0200 Subject: [PATCH 05/13] refactor: remove chunk stats code on simplifing the render scale widget, I believe there is a good chunk of dead code. I don't think we need this as it should be handled separately. --- src/layer/segmentation/index.ts | 47 ++--- src/skeleton/frontend.ts | 325 +------------------------------- 2 files changed, 16 insertions(+), 356 deletions(-) diff --git a/src/layer/segmentation/index.ts b/src/layer/segmentation/index.ts index 8eb7e6f6e..bfb6c9dff 100644 --- a/src/layer/segmentation/index.ts +++ b/src/layer/segmentation/index.ts @@ -800,12 +800,24 @@ class SegmentationUserLayerDisplayState implements SegmentationDisplayState { this.spatialSkeletonGridResolutionTarget2d.changed.add(() => { if (this.suppressSpatialSkeletonGridResolutionTarget2d) return; this.spatialSkeletonGridResolutionTarget2dExplicit = true; - this.applySpatialSkeletonGridResolutionTarget("2d"); + const levels = this.spatialSkeletonGridLevels.value; + if (levels.length > 0) { + this.setSpatialSkeletonGridLevel( + "2d", + findClosestSpatialSkeletonGridLevelBySpacing(levels, this.spatialSkeletonGridResolutionTarget2d.value), + ); + } }); this.spatialSkeletonGridResolutionTarget3d.changed.add(() => { if (this.suppressSpatialSkeletonGridResolutionTarget3d) return; this.spatialSkeletonGridResolutionTarget3dExplicit = true; - this.applySpatialSkeletonGridResolutionTarget("3d"); + const levels = this.spatialSkeletonGridLevels.value; + if (levels.length > 0) { + this.setSpatialSkeletonGridLevel( + "3d", + findClosestSpatialSkeletonGridLevelBySpacing(levels, this.spatialSkeletonGridResolutionTarget3d.value), + ); + } }); } @@ -845,14 +857,6 @@ class SegmentationUserLayerDisplayState implements SegmentationDisplayState { verifyFiniteNonNegativeFloat, 1, ); - spatialSkeletonGridChunkStats2d = new WatchableValue({ - presentCount: 0, - totalCount: 0, - }); - spatialSkeletonGridChunkStats3d = new WatchableValue({ - presentCount: 0, - totalCount: 0, - }); spatialSkeletonGridRenderScaleHistogram2d = new RenderScaleHistogram(); spatialSkeletonGridRenderScaleHistogram3d = new RenderScaleHistogram(); spatialSkeletonLod2d = new WatchableValue(0); @@ -952,39 +956,16 @@ class SegmentationUserLayerDisplayState implements SegmentationDisplayState { applySpatialSkeletonGridResolutionTarget2dFromSpec(value: any) { if (value !== undefined) { - this.suppressSpatialSkeletonGridResolutionTarget2d = true; this.spatialSkeletonGridResolutionTarget2d.restoreState(value); - this.suppressSpatialSkeletonGridResolutionTarget2d = false; - this.spatialSkeletonGridResolutionTarget2dExplicit = true; - if (this.spatialSkeletonGridLevels.value.length > 0) { - this.applySpatialSkeletonGridResolutionTarget("2d"); - } } } applySpatialSkeletonGridResolutionTarget3dFromSpec(value: any) { if (value !== undefined) { - this.suppressSpatialSkeletonGridResolutionTarget3d = true; this.spatialSkeletonGridResolutionTarget3d.restoreState(value); - this.suppressSpatialSkeletonGridResolutionTarget3d = false; - this.spatialSkeletonGridResolutionTarget3dExplicit = true; - if (this.spatialSkeletonGridLevels.value.length > 0) { - this.applySpatialSkeletonGridResolutionTarget("3d"); - } } } - private applySpatialSkeletonGridResolutionTarget(kind: "2d" | "3d") { - const levels = this.spatialSkeletonGridLevels.value; - if (levels.length === 0) return; - const target = - kind === "2d" - ? this.spatialSkeletonGridResolutionTarget2d.value - : this.spatialSkeletonGridResolutionTarget3d.value; - const index = findClosestSpatialSkeletonGridLevelBySpacing(levels, target); - this.setSpatialSkeletonGridLevel(kind, index); - } - private setSpatialSkeletonGridLevel(kind: "2d" | "3d", index: number) { const levels = this.spatialSkeletonGridLevels.value; if (levels.length === 0) return 0; diff --git a/src/skeleton/frontend.ts b/src/skeleton/frontend.ts index 4183ea44b..5bb9161f1 100644 --- a/src/skeleton/frontend.ts +++ b/src/skeleton/frontend.ts @@ -92,8 +92,6 @@ import { } from "#src/skeleton/source_selection.js"; import { spatiallyIndexedSkeletonTextureAttributeSpecs } from "#src/skeleton/spatial_attribute_layout.js"; import { - forEachPlaneIntersectingVolumetricChunk, - getNormalizedChunkLayout, forEachVisibleVolumetricChunk, type SliceViewBase, type SliceViewChunkSpecification, @@ -1695,11 +1693,6 @@ export abstract class MultiscaleSpatiallyIndexedSkeletonSource extends Multiscal export class MultiscaleSliceViewSpatiallyIndexedSkeletonLayer extends SliceViewRenderLayer { private renderOptions: ViewSpecificSkeletonRenderingOptions; - private visibleChunkKeysBySliceView = new Map< - number, - VisibleSpatialChunkKeysBySource - >(); - private trackedChunkStatsSliceViews = new Set(); RPC_TYPE_ID = SPATIALLY_INDEXED_SKELETON_SLICEVIEW_RENDER_LAYER_RPC_ID; constructor( public chunkManager: ChunkManager, @@ -1764,71 +1757,13 @@ export class MultiscaleSliceViewSpatiallyIndexedSkeletonLayer extends SliceViewR ); } - private updateChunkStatsWatchable() { - const { presentCount, totalCount } = mergeVisibleSpatialChunkKeyCounts( - this.visibleChunkKeysBySliceView.values(), - ); - const displayState = this.displayState as SegmentationDisplayState & - SpatialSkeletonDisplayState; - updateSpatialSkeletonGridChunkStatsWatchable( - displayState.spatialSkeletonGridChunkStats2d, - presentCount, - totalCount, - ); - } - - private setVisibleChunkKeysForSliceView( - sliceViewId: number, - visibleChunkKeysBySource: VisibleSpatialChunkKeysBySource, - ) { - this.visibleChunkKeysBySliceView.set(sliceViewId, visibleChunkKeysBySource); - this.updateChunkStatsWatchable(); - } - - private clearVisibleChunkKeysForSliceView(sliceViewId: number) { - if (!this.visibleChunkKeysBySliceView.delete(sliceViewId)) { - return; - } - this.updateChunkStatsWatchable(); - } - - private registerChunkStatsSliceView( - sliceView: RefCounted & { rpcId: number }, - ) { - const { rpcId } = sliceView; - if (this.trackedChunkStatsSliceViews.has(rpcId)) { - return; - } - this.trackedChunkStatsSliceViews.add(rpcId); - sliceView.registerDisposer(() => { - this.trackedChunkStatsSliceViews.delete(rpcId); - this.clearVisibleChunkKeysForSliceView(rpcId); - }); - } - - draw(renderContext: SliceViewRenderContext) { + draw(_renderContext: SliceViewRenderContext) { const displayState = this.displayState as SegmentationDisplayState & SpatialSkeletonDisplayState; const lodValue = displayState.spatialSkeletonLod2d?.value; - const sliceView = renderContext.sliceView; - this.registerChunkStatsSliceView( - sliceView as RefCounted & { rpcId: number }, - ); if ((displayState as any).objectAlpha?.value <= 0.0 || lodValue === undefined) { - this.clearVisibleChunkKeysForSliceView(sliceView.rpcId); return; } - const visibleSources = - renderContext.sliceView.visibleLayers.get(this)?.visibleSources ?? []; - this.setVisibleChunkKeysForSliceView( - sliceView.rpcId, - collectPlaneIntersectingSpatialChunkKeysBySource( - visibleSources, - renderContext.projectionParameters, - this.localPosition.value, - lodValue, - ), - ); } } @@ -1943,139 +1878,11 @@ type VisibleSpatialChunksBySource = Map< readonly SpatiallyIndexedSkeletonChunk[] >; -interface VisibleSpatialChunkKeys { - presentChunkKeys: Set; - totalChunkKeys: Set; -} - -type VisibleSpatialChunkKeysBySource = Map; - -interface SpatialSkeletonGridChunkStats { - presentCount: number; - totalCount: number; -} - -function getOrCreateVisibleSpatialChunkKeys( - visibleChunkKeysBySource: VisibleSpatialChunkKeysBySource, - sourceId: string, -) { - let visibleChunkKeys = visibleChunkKeysBySource.get(sourceId); - if (visibleChunkKeys === undefined) { - visibleChunkKeys = { - presentChunkKeys: new Set(), - totalChunkKeys: new Set(), - }; - visibleChunkKeysBySource.set(sourceId, visibleChunkKeys); - } - return visibleChunkKeys; -} - -function updateSpatialSkeletonGridChunkStatsWatchable( - watchable: WatchableValue | undefined, - presentCount: number, - totalCount: number, -) { - if (watchable === undefined) return; - const prev = watchable.value; - if (prev.presentCount === presentCount && prev.totalCount === totalCount) { - return; - } - watchable.value = { presentCount, totalCount }; -} - -function mergeVisibleSpatialChunkKeyCounts( - visibleChunkKeysByView: Iterable, - selectedSourceIds?: Iterable, -) { - const presentChunkKeys = new Set(); - const totalChunkKeys = new Set(); - const selectedSourceIdSet = - selectedSourceIds === undefined ? undefined : new Set(selectedSourceIds); - for (const visibleChunkKeysBySource of visibleChunkKeysByView) { - for (const [sourceId, visibleChunkKeys] of visibleChunkKeysBySource) { - if ( - selectedSourceIdSet !== undefined && - !selectedSourceIdSet.has(sourceId) - ) { - continue; - } - for (const chunkKey of visibleChunkKeys.presentChunkKeys) { - presentChunkKeys.add(`${sourceId}:${chunkKey}`); - } - for (const chunkKey of visibleChunkKeys.totalChunkKeys) { - totalChunkKeys.add(`${sourceId}:${chunkKey}`); - } - } - } - return { - presentCount: presentChunkKeys.size, - totalCount: totalChunkKeys.size, - }; -} - -function collectPlaneIntersectingSpatialChunkKeysBySource( - transformedSources: readonly TransformedSource[], - projectionParameters: any, - localPosition: Float32Array, - lod: number | undefined, -) { - const visibleChunkKeysBySource: VisibleSpatialChunkKeysBySource = new Map(); - if (lod === undefined || transformedSources.length === 0) { - return visibleChunkKeysBySource; - } - const lodSuffix = `:${lod}`; - const seenChunkKeysBySource = new Map>(); - for (const tsource of transformedSources) { - const sourceId = getObjectId(tsource.source); - const visibleChunkKeys = getOrCreateVisibleSpatialChunkKeys( - visibleChunkKeysBySource, - sourceId, - ); - let seenChunkKeys = seenChunkKeysBySource.get(sourceId); - if (seenChunkKeys === undefined) { - seenChunkKeys = new Set(); - seenChunkKeysBySource.set(sourceId, seenChunkKeys); - } - const chunkLayout = getNormalizedChunkLayout( - projectionParameters, - tsource.chunkLayout, - ); - forEachPlaneIntersectingVolumetricChunk( - projectionParameters, - localPosition, - tsource, - chunkLayout, - (positionInChunks) => { - const chunkKey = `${positionInChunks.join()}${lodSuffix}`; - if (seenChunkKeys!.has(chunkKey)) { - return; - } - seenChunkKeys!.add(chunkKey); - visibleChunkKeys.totalChunkKeys.add(chunkKey); - const chunk = ( - tsource.source as SpatiallyIndexedSkeletonSource - ).chunks.get(chunkKey) as SpatiallyIndexedSkeletonChunk | undefined; - if (chunk?.state === ChunkState.GPU_MEMORY) { - visibleChunkKeys.presentChunkKeys.add(chunkKey); - } - }, - ); - } - return visibleChunkKeysBySource; -} interface SpatialSkeletonDisplayState { spatialSkeletonGridLevel2d?: WatchableValueInterface; spatialSkeletonGridLevel3d?: WatchableValueInterface; skeletonLod?: WatchableValueInterface; - spatialSkeletonGridChunkStats2d?: WatchableValue<{ - presentCount: number; - totalCount: number; - }>; - spatialSkeletonGridChunkStats3d?: WatchableValue<{ - presentCount: number; - totalCount: number; - }>; spatialSkeletonLod2d?: WatchableValueInterface; spatialSkeletonGridLevels?: WatchableValueInterface< Array<{ size: { x: number; y: number; z: number }; lod: number }> @@ -2119,10 +1926,6 @@ export class SpatiallyIndexedSkeletonLayer SpatiallyIndexedSkeletonView, VisibleSpatialChunksBySource >(); - private visibleChunkKeysByRenderedView = new Map< - SpatiallyIndexedSkeletonView, - Map - >(); gridLevel: WatchableValueInterface; lod: WatchableValueInterface; private selectedNodeId: @@ -2729,82 +2532,6 @@ export class SpatiallyIndexedSkeletonLayer return this.visibleChunksByView.get(view); } - private getGridLevelForView(view: SpatiallyIndexedSkeletonView) { - const displayState = this.displayState as SkeletonLayerDisplayState & - SpatialSkeletonDisplayState; - return view === "2d" - ? displayState.spatialSkeletonGridLevel2d?.value - : displayState.spatialSkeletonGridLevel3d?.value; - } - - private getGridChunkStatsWatchable(view: SpatiallyIndexedSkeletonView) { - const displayState = this.displayState as SkeletonLayerDisplayState & - SpatialSkeletonDisplayState; - return view === "2d" - ? displayState.spatialSkeletonGridChunkStats2d - : displayState.spatialSkeletonGridChunkStats3d; - } - - private updateSelectedChunkStatsForView(view: SpatiallyIndexedSkeletonView) { - const visibleChunkKeysByRenderedView = - this.visibleChunkKeysByRenderedView.get(view); - const selectedSourceIds = new Set(); - for (const sourceEntry of this.selectSourcesForViewAndGrid( - view, - this.getGridLevelForView(view), - )) { - selectedSourceIds.add(getObjectId(sourceEntry.chunkSource)); - } - const { presentCount, totalCount } = mergeVisibleSpatialChunkKeyCounts( - visibleChunkKeysByRenderedView?.values() ?? [], - selectedSourceIds, - ); - updateSpatialSkeletonGridChunkStatsWatchable( - this.getGridChunkStatsWatchable(view), - presentCount, - totalCount, - ); - } - - setVisibleChunkKeysForRenderedView( - view: SpatiallyIndexedSkeletonView, - renderedViewId: number, - visibleChunkKeysBySource: VisibleSpatialChunkKeysBySource, - ) { - let visibleChunkKeysByRenderedView = - this.visibleChunkKeysByRenderedView.get(view); - if (visibleChunkKeysByRenderedView === undefined) { - visibleChunkKeysByRenderedView = new Map(); - this.visibleChunkKeysByRenderedView.set( - view, - visibleChunkKeysByRenderedView, - ); - } - visibleChunkKeysByRenderedView.set( - renderedViewId, - visibleChunkKeysBySource, - ); - this.updateSelectedChunkStatsForView(view); - } - - clearVisibleChunkKeysForRenderedView( - view: SpatiallyIndexedSkeletonView, - renderedViewId: number, - ) { - const visibleChunkKeysByRenderedView = - this.visibleChunkKeysByRenderedView.get(view); - if (visibleChunkKeysByRenderedView === undefined) { - return; - } - if (!visibleChunkKeysByRenderedView.delete(renderedViewId)) { - return; - } - if (visibleChunkKeysByRenderedView.size === 0) { - this.visibleChunkKeysByRenderedView.delete(view); - } - this.updateSelectedChunkStatsForView(view); - } - private *iterateCandidateChunks( selectedSources: readonly SpatiallyIndexedSkeletonSourceEntry[], targetLod: number | undefined, @@ -2929,18 +2656,13 @@ export class SpatiallyIndexedSkeletonLayer transformedSources: readonly TransformedSource[][], projectionParameters: any, lod: number | undefined, - renderedViewId?: number, ) { if (lod === undefined) { this.visibleChunksByView.delete(view); - if (renderedViewId !== undefined) { - this.clearVisibleChunkKeysForRenderedView(view, renderedViewId); - } return; } const lodSuffix = `:${lod}`; const chunksBySource: VisibleSpatialChunksBySource = new Map(); - const visibleChunkKeysBySource: VisibleSpatialChunkKeysBySource = new Map(); const seenChunkKeysBySource = new Map>(); for (const scales of transformedSources) { for (const tsource of scales) { @@ -2950,10 +2672,6 @@ export class SpatiallyIndexedSkeletonLayer visibleChunks = []; chunksBySource.set(sourceId, visibleChunks); } - const visibleChunkKeys = getOrCreateVisibleSpatialChunkKeys( - visibleChunkKeysBySource, - sourceId, - ); let seenChunkKeys = seenChunkKeysBySource.get(sourceId); if (seenChunkKeys === undefined) { seenChunkKeys = new Set(); @@ -2969,7 +2687,6 @@ export class SpatiallyIndexedSkeletonLayer return; } seenChunkKeys!.add(chunkKey); - visibleChunkKeys.totalChunkKeys.add(chunkKey); const chunkSource = tsource.source as SpatiallyIndexedSkeletonSource; const chunk = chunkSource.chunks.get(chunkKey) as @@ -2978,20 +2695,12 @@ export class SpatiallyIndexedSkeletonLayer if (chunk?.state !== ChunkState.GPU_MEMORY) { return; } - visibleChunkKeys.presentChunkKeys.add(chunkKey); (visibleChunks as SpatiallyIndexedSkeletonChunk[]).push(chunk); }, ); } } this.visibleChunksByView.set(view, chunksBySource); - if (renderedViewId !== undefined) { - this.setVisibleChunkKeysForRenderedView( - view, - renderedViewId, - visibleChunkKeysBySource, - ); - } } private areVisibleChunksReady( @@ -3473,12 +3182,6 @@ export class PerspectiveViewSpatiallyIndexedSkeletonLayer extends PerspectiveVie >, ) { super.attach(attachment); - attachment.registerDisposer(() => - this.base.clearVisibleChunkKeysForRenderedView( - "3d", - attachment.view.rpcId, - ), - ); // Manually add layer to backend const backend = this.backend; @@ -3662,7 +3365,6 @@ export class PerspectiveViewSpatiallyIndexedSkeletonLayer extends PerspectiveVie this.transformedSources, renderContext.projectionParameters, lodValue, - attachment.view.rpcId, ); const levels = displayState.spatialSkeletonGridLevels?.value; const histogram = displayState.spatialSkeletonGridRenderScaleHistogram3d; @@ -3714,7 +3416,6 @@ export class PerspectiveViewSpatiallyIndexedSkeletonLayer extends PerspectiveVie export class SliceViewSpatiallyIndexedSkeletonLayer extends SliceViewRenderLayer { private renderOptions: ViewSpecificSkeletonRenderingOptions; - private trackedChunkStatsSliceViews = new Set(); constructor(public base: SpatiallyIndexedSkeletonLayer) { super( base.chunkManager, @@ -3759,35 +3460,13 @@ export class SliceViewSpatiallyIndexedSkeletonLayer extends SliceViewRenderLayer return undefined; } - draw(renderContext: SliceViewRenderContext) { + draw(_renderContext: SliceViewRenderContext) { const displayState = this.base.displayState as SkeletonLayerDisplayState & SpatialSkeletonDisplayState; const lodValue = displayState.spatialSkeletonLod2d?.value; - const sliceView = renderContext.sliceView; - const sliceViewId = sliceView.rpcId; - if (!this.trackedChunkStatsSliceViews.has(sliceViewId)) { - this.trackedChunkStatsSliceViews.add(sliceViewId); - sliceView.registerDisposer(() => { - this.trackedChunkStatsSliceViews.delete(sliceViewId); - this.base.clearVisibleChunkKeysForRenderedView("2d", sliceViewId); - }); - } if ((displayState as any).objectAlpha?.value <= 0.0 || lodValue === undefined) { - this.base.clearVisibleChunkKeysForRenderedView("2d", sliceViewId); return; } - const visibleSources = - renderContext.sliceView.visibleLayers.get(this)?.visibleSources ?? []; - this.base.setVisibleChunkKeysForRenderedView( - "2d", - sliceViewId, - collectPlaneIntersectingSpatialChunkKeysBySource( - visibleSources, - renderContext.projectionParameters, - this.base.localPosition.value, - lodValue, - ), - ); } } From c36911de08296ece3f4c09cb6c91eade23240164 Mon Sep 17 00:00:00 2001 From: Sean Martin Date: Mon, 4 May 2026 18:19:54 +0200 Subject: [PATCH 06/13] refactor: stub draw methods --- src/skeleton/frontend.ts | 37 ++++++++++++------------------------- 1 file changed, 12 insertions(+), 25 deletions(-) diff --git a/src/skeleton/frontend.ts b/src/skeleton/frontend.ts index 9c698efb7..f801139ee 100644 --- a/src/skeleton/frontend.ts +++ b/src/skeleton/frontend.ts @@ -1703,7 +1703,8 @@ export class MultiscaleSliceViewSpatiallyIndexedSkeletonLayer extends SliceViewR const spatialDisplayState = displayState as SegmentationDisplayState & SpatialSkeletonDisplayState; const anyDisplayState = displayState as any; - const renderScaleTarget = anyDisplayState.renderScaleTarget as WatchableValueInterface; + const renderScaleTarget = + anyDisplayState.renderScaleTarget as WatchableValueInterface; const gridLevel2d = spatialDisplayState.spatialSkeletonGridLevel2d; super(chunkManager, multiscaleSource, { transform: anyDisplayState.transform, @@ -1758,14 +1759,7 @@ export class MultiscaleSliceViewSpatiallyIndexedSkeletonLayer extends SliceViewR ); } - draw(_renderContext: SliceViewRenderContext) { - const displayState = this.displayState as SegmentationDisplayState & - SpatialSkeletonDisplayState; - const lodValue = displayState.spatialSkeletonLod2d?.value; - if ((displayState as any).objectAlpha?.value <= 0.0 || lodValue === undefined) { - return; - } - } + draw(_renderContext: SliceViewRenderContext) {} } type SpatiallyIndexedSkeletonSourceEntry = @@ -1879,7 +1873,6 @@ type VisibleSpatialChunksBySource = Map< readonly SpatiallyIndexedSkeletonChunk[] >; - interface SpatialSkeletonDisplayState { spatialSkeletonGridLevel2d?: WatchableValueInterface; spatialSkeletonGridLevel3d?: WatchableValueInterface; @@ -2328,9 +2321,7 @@ export class SpatiallyIndexedSkeletonLayer spatialDisplayState.spatialSkeletonGridLevel3d ?? new WatchableValue(0); this.lod = - options.lod ?? - spatialDisplayState.skeletonLod ?? - new WatchableValue(0); + options.lod ?? spatialDisplayState.skeletonLod ?? new WatchableValue(0); this.selectedNodeId = options.selectedNodeId; this.pendingNodePositionVersion = options.pendingNodePositionVersion; this.getPendingNodePositionOverride = options.getPendingNodePosition; @@ -3116,7 +3107,8 @@ export class PerspectiveViewSpatiallyIndexedSkeletonLayer extends PerspectiveVie ); const spatialDisplayState = base.displayState as SkeletonLayerDisplayState & SpatialSkeletonDisplayState; - const histogram3d = spatialDisplayState.spatialSkeletonGridRenderScaleHistogram3d; + const histogram3d = + spatialDisplayState.spatialSkeletonGridRenderScaleHistogram3d; if (histogram3d !== undefined) { this.registerDisposer(histogram3d.visibility.add(this.visibility)); } @@ -3412,14 +3404,7 @@ export class SliceViewSpatiallyIndexedSkeletonLayer extends SliceViewRenderLayer return undefined; } - draw(_renderContext: SliceViewRenderContext) { - const displayState = this.base.displayState as SkeletonLayerDisplayState & - SpatialSkeletonDisplayState; - const lodValue = displayState.spatialSkeletonLod2d?.value; - if ((displayState as any).objectAlpha?.value <= 0.0 || lodValue === undefined) { - return; - } - } + draw(_renderContext: SliceViewRenderContext) {} } export class SliceViewPanelSpatiallyIndexedSkeletonLayer extends SliceViewPanelRenderLayer { @@ -3443,8 +3428,9 @@ export class SliceViewPanelSpatiallyIndexedSkeletonLayer extends SliceViewPanelR this.registerDisposer( renderOptions.lineWidth.changed.add(this.redrawNeeded.dispatch), ); - const spatialDisplayState2d = base.displayState as SkeletonLayerDisplayState & - SpatialSkeletonDisplayState; + const spatialDisplayState2d = + base.displayState as SkeletonLayerDisplayState & + SpatialSkeletonDisplayState; const gridLevel2d = spatialDisplayState2d.spatialSkeletonGridLevel2d; if (gridLevel2d?.changed) { this.registerDisposer( @@ -3455,7 +3441,8 @@ export class SliceViewPanelSpatiallyIndexedSkeletonLayer extends SliceViewPanelR if (lod2d?.changed) { this.registerDisposer(lod2d.changed.add(this.redrawNeeded.dispatch)); } - const histogram2d = spatialDisplayState2d.spatialSkeletonGridRenderScaleHistogram2d; + const histogram2d = + spatialDisplayState2d.spatialSkeletonGridRenderScaleHistogram2d; if (histogram2d !== undefined) { this.registerDisposer(histogram2d.visibility.add(this.visibility)); } From 6a08e2fbd513a3747555fa59e9551a8bdfbbc1b5 Mon Sep 17 00:00:00 2001 From: Sean Martin Date: Mon, 4 May 2026 18:34:25 +0200 Subject: [PATCH 07/13] refactor: remove uneeded guards --- src/layer/segmentation/index.ts | 54 ++++++++++----------------------- 1 file changed, 16 insertions(+), 38 deletions(-) diff --git a/src/layer/segmentation/index.ts b/src/layer/segmentation/index.ts index bfb6c9dff..4e21f2f33 100644 --- a/src/layer/segmentation/index.ts +++ b/src/layer/segmentation/index.ts @@ -798,8 +798,6 @@ class SegmentationUserLayerDisplayState implements SegmentationDisplayState { ); this.spatialSkeletonGridResolutionTarget2d.changed.add(() => { - if (this.suppressSpatialSkeletonGridResolutionTarget2d) return; - this.spatialSkeletonGridResolutionTarget2dExplicit = true; const levels = this.spatialSkeletonGridLevels.value; if (levels.length > 0) { this.setSpatialSkeletonGridLevel( @@ -809,8 +807,6 @@ class SegmentationUserLayerDisplayState implements SegmentationDisplayState { } }); this.spatialSkeletonGridResolutionTarget3d.changed.add(() => { - if (this.suppressSpatialSkeletonGridResolutionTarget3d) return; - this.spatialSkeletonGridResolutionTarget3dExplicit = true; const levels = this.spatialSkeletonGridLevels.value; if (levels.length > 0) { this.setSpatialSkeletonGridLevel( @@ -865,10 +861,6 @@ class SegmentationUserLayerDisplayState implements SegmentationDisplayState { SpatialSkeletonNodeFilterType, SpatialSkeletonNodeFilterType.NONE, ); - private spatialSkeletonGridResolutionTarget2dExplicit = false; - private spatialSkeletonGridResolutionTarget3dExplicit = false; - private suppressSpatialSkeletonGridResolutionTarget2d = false; - private suppressSpatialSkeletonGridResolutionTarget3d = false; ignoreNullVisibleSet = new TrackableBoolean(true, true); skeletonRenderingOptions = new SkeletonRenderingOptions(); shaderError = makeWatchableShaderError(); @@ -920,38 +912,24 @@ class SegmentationUserLayerDisplayState implements SegmentationDisplayState { } this.spatialSkeletonGridLevels.value = levels; if (levels.length === 0) return; - const target3dIndex = this.spatialSkeletonGridResolutionTarget3dExplicit - ? findClosestSpatialSkeletonGridLevelBySpacing( - levels, - this.spatialSkeletonGridResolutionTarget3d.value, - ) - : 0; + const target3dIndex = findClosestSpatialSkeletonGridLevelBySpacing( + levels, + this.spatialSkeletonGridResolutionTarget3d.value, + ); const resolved3dIndex = this.setSpatialSkeletonGridLevel("3d", target3dIndex); - const target2dIndex = this.spatialSkeletonGridResolutionTarget2dExplicit - ? findClosestSpatialSkeletonGridLevelBySpacing( - levels, - this.spatialSkeletonGridResolutionTarget2d.value, - ) - : resolved3dIndex; + const target2dIndex = findClosestSpatialSkeletonGridLevelBySpacing( + levels, + this.spatialSkeletonGridResolutionTarget2d.value, + ); this.setSpatialSkeletonGridLevel("2d", target2dIndex); - if (!this.spatialSkeletonGridResolutionTarget3dExplicit) { - const spacing = getSpatialSkeletonGridSpacing( - levels[Math.min(resolved3dIndex, levels.length - 1)].size, - ); - this.suppressSpatialSkeletonGridResolutionTarget3d = true; - this.spatialSkeletonGridResolutionTarget3d.value = spacing; - this.suppressSpatialSkeletonGridResolutionTarget3d = false; - } - if (!this.spatialSkeletonGridResolutionTarget2dExplicit) { - const resolved2dIndex = Math.min( - Math.max(target2dIndex, 0), - levels.length - 1, - ); - const spacing = getSpatialSkeletonGridSpacing(levels[resolved2dIndex].size); - this.suppressSpatialSkeletonGridResolutionTarget2d = true; - this.spatialSkeletonGridResolutionTarget2d.value = spacing; - this.suppressSpatialSkeletonGridResolutionTarget2d = false; - } + const spacing3d = getSpatialSkeletonGridSpacing( + levels[Math.min(resolved3dIndex, levels.length - 1)].size, + ); + this.spatialSkeletonGridResolutionTarget3d.value = spacing3d; + const resolved2dIndex = Math.min(Math.max(target2dIndex, 0), levels.length - 1); + this.spatialSkeletonGridResolutionTarget2d.value = getSpatialSkeletonGridSpacing( + levels[resolved2dIndex].size, + ); } applySpatialSkeletonGridResolutionTarget2dFromSpec(value: any) { From 915315a20f665783f627865628f9202590fb5f7d Mon Sep 17 00:00:00 2001 From: Sean Martin Date: Mon, 4 May 2026 18:46:56 +0200 Subject: [PATCH 08/13] refactor: remove custom state fn --- src/layer/segmentation/index.ts | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/src/layer/segmentation/index.ts b/src/layer/segmentation/index.ts index 4e21f2f33..3e2010376 100644 --- a/src/layer/segmentation/index.ts +++ b/src/layer/segmentation/index.ts @@ -932,18 +932,6 @@ class SegmentationUserLayerDisplayState implements SegmentationDisplayState { ); } - applySpatialSkeletonGridResolutionTarget2dFromSpec(value: any) { - if (value !== undefined) { - this.spatialSkeletonGridResolutionTarget2d.restoreState(value); - } - } - - applySpatialSkeletonGridResolutionTarget3dFromSpec(value: any) { - if (value !== undefined) { - this.spatialSkeletonGridResolutionTarget3d.restoreState(value); - } - } - private setSpatialSkeletonGridLevel(kind: "2d" | "3d", index: number) { const levels = this.spatialSkeletonGridLevels.value; if (levels.length === 0) return 0; @@ -2100,12 +2088,8 @@ export class SegmentationUserLayer extends Base { (value) => this.displayState.spatialSkeletonNodeFilter.restoreState(value), ); - this.displayState.applySpatialSkeletonGridResolutionTarget2dFromSpec( - specification[json_keys.SKELETON_CROSS_SECTION_RENDER_SCALE_JSON_KEY], - ); - this.displayState.applySpatialSkeletonGridResolutionTarget3dFromSpec( - specification[json_keys.SKELETON_PERSPECTIVE_RENDER_SCALE_JSON_KEY], - ); + this.displayState.spatialSkeletonGridResolutionTarget2d.restoreState(specification[json_keys.SKELETON_CROSS_SECTION_RENDER_SCALE_JSON_KEY]); + this.displayState.spatialSkeletonGridResolutionTarget3d.restoreState(specification[json_keys.SKELETON_PERSPECTIVE_RENDER_SCALE_JSON_KEY]); this.displayState.baseSegmentColoring.restoreState( specification[json_keys.BASE_SEGMENT_COLORING_JSON_KEY], ); From 6b1e3419fb8bb8eb5af6e0a604328ac4fb813d2c Mon Sep 17 00:00:00 2001 From: Sean Martin Date: Mon, 4 May 2026 19:07:34 +0200 Subject: [PATCH 09/13] fix: remove resolving to nearest grid --- src/layer/segmentation/index.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/layer/segmentation/index.ts b/src/layer/segmentation/index.ts index 3e2010376..9311d453e 100644 --- a/src/layer/segmentation/index.ts +++ b/src/layer/segmentation/index.ts @@ -916,20 +916,12 @@ class SegmentationUserLayerDisplayState implements SegmentationDisplayState { levels, this.spatialSkeletonGridResolutionTarget3d.value, ); - const resolved3dIndex = this.setSpatialSkeletonGridLevel("3d", target3dIndex); + this.setSpatialSkeletonGridLevel("3d", target3dIndex); const target2dIndex = findClosestSpatialSkeletonGridLevelBySpacing( levels, this.spatialSkeletonGridResolutionTarget2d.value, ); this.setSpatialSkeletonGridLevel("2d", target2dIndex); - const spacing3d = getSpatialSkeletonGridSpacing( - levels[Math.min(resolved3dIndex, levels.length - 1)].size, - ); - this.spatialSkeletonGridResolutionTarget3d.value = spacing3d; - const resolved2dIndex = Math.min(Math.max(target2dIndex, 0), levels.length - 1); - this.spatialSkeletonGridResolutionTarget2d.value = getSpatialSkeletonGridSpacing( - levels[resolved2dIndex].size, - ); } private setSpatialSkeletonGridLevel(kind: "2d" | "3d", index: number) { From 17d92f37bf30e89a52ea81e083ed1b738b1550ba Mon Sep 17 00:00:00 2001 From: Sean Martin Date: Mon, 4 May 2026 20:12:01 +0200 Subject: [PATCH 10/13] fix: update tests for removed code --- src/layer/segmentation/index.spec.ts | 10 ---- src/skeleton/frontend.spec.ts | 88 ---------------------------- 2 files changed, 98 deletions(-) diff --git a/src/layer/segmentation/index.spec.ts b/src/layer/segmentation/index.spec.ts index 9ade3ae1e..95bc1b937 100644 --- a/src/layer/segmentation/index.spec.ts +++ b/src/layer/segmentation/index.spec.ts @@ -130,16 +130,6 @@ describe("layer/segmentation spatial skeleton chunk stats", () => { spatialSkeletonVisibleChunksNeeded: new WatchableValue(0), spatialSkeletonVisibleChunksAvailable: new WatchableValue(0), spatialSkeletonVisibleChunksLoaded: new WatchableValue(false), - displayState: { - spatialSkeletonGridChunkStats2d: new WatchableValue({ - presentCount: 0, - totalCount: 0, - }), - spatialSkeletonGridChunkStats3d: new WatchableValue({ - presentCount: 0, - totalCount: 0, - }), - }, updateSpatialSkeletonSourceState: vi.fn(), }, ); diff --git a/src/skeleton/frontend.spec.ts b/src/skeleton/frontend.spec.ts index 0f5e38770..599ec7c9d 100644 --- a/src/skeleton/frontend.spec.ts +++ b/src/skeleton/frontend.spec.ts @@ -2,10 +2,8 @@ import { describe, expect, it, vi } from "vitest"; import { resolveSpatiallyIndexedSkeletonSegmentPick } from "#src/skeleton/picking.js"; import { spatiallyIndexedSkeletonTextureAttributeSpecs } from "#src/skeleton/spatial_attribute_layout.js"; -import { WatchableValue } from "#src/trackable_value.js"; import { Uint64Set } from "#src/uint64_set.js"; import { DataType } from "#src/util/data_type.js"; -import { getObjectId } from "#src/util/object_id.js"; if (!("WebGL2RenderingContext" in globalThis)) { Object.defineProperty(globalThis, "WebGL2RenderingContext", { @@ -128,89 +126,3 @@ describe("SpatiallyIndexedSkeletonLayer browse exclusions", () => { }); }); -describe("SpatiallyIndexedSkeletonLayer chunk stats", () => { - it("dedupes 2d chunk stats across rendered views for the selected grid source", () => { - const coarseSource = { parameters: { gridIndex: 0 } }; - const fineSource = { parameters: { gridIndex: 1 } }; - const coarseSourceId = getObjectId(coarseSource); - const fineSourceId = getObjectId(fineSource); - const layer = Object.assign( - Object.create(SpatiallyIndexedSkeletonLayer.prototype), - { - displayState: { - spatialSkeletonGridLevel2d: { value: 1 }, - spatialSkeletonGridLevel3d: { value: 0 }, - spatialSkeletonGridChunkStats2d: new WatchableValue({ - presentCount: 0, - totalCount: 0, - }), - spatialSkeletonGridChunkStats3d: new WatchableValue({ - presentCount: 0, - totalCount: 0, - }), - }, - sources: [], - sources2d: [ - { - chunkSource: coarseSource, - chunkToMultiscaleTransform: {}, - }, - { - chunkSource: fineSource, - chunkToMultiscaleTransform: {}, - }, - ], - visibleChunkKeysByRenderedView: new Map(), - }, - ); - - (layer as any).setVisibleChunkKeysForRenderedView( - "2d", - 11, - new Map([ - [ - coarseSourceId, - { - presentChunkKeys: new Set(["ignored"]), - totalChunkKeys: new Set(["ignored"]), - }, - ], - [ - fineSourceId, - { - presentChunkKeys: new Set(["a"]), - totalChunkKeys: new Set(["a", "b"]), - }, - ], - ]), - ); - expect(layer.displayState.spatialSkeletonGridChunkStats2d.value).toEqual({ - presentCount: 1, - totalCount: 2, - }); - - (layer as any).setVisibleChunkKeysForRenderedView( - "2d", - 22, - new Map([ - [ - fineSourceId, - { - presentChunkKeys: new Set(["c"]), - totalChunkKeys: new Set(["b", "c"]), - }, - ], - ]), - ); - expect(layer.displayState.spatialSkeletonGridChunkStats2d.value).toEqual({ - presentCount: 2, - totalCount: 3, - }); - - (layer as any).clearVisibleChunkKeysForRenderedView("2d", 11); - expect(layer.displayState.spatialSkeletonGridChunkStats2d.value).toEqual({ - presentCount: 1, - totalCount: 2, - }); - }); -}); From a0fe01a5ec7c6f43970036dac8526b9998017873 Mon Sep 17 00:00:00 2001 From: Sean Martin Date: Mon, 4 May 2026 23:41:17 +0200 Subject: [PATCH 11/13] fix: only show current / current as lod in render scale widget --- src/widget/render_scale_widget.ts | 41 +++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/src/widget/render_scale_widget.ts b/src/widget/render_scale_widget.ts index 38a770ee4..a98d15f41 100644 --- a/src/widget/render_scale_widget.ts +++ b/src/widget/render_scale_widget.ts @@ -418,6 +418,47 @@ export class SpatialSkeletonGridRenderScaleWidget extends RenderScaleWidget { this.logScaleBinSize = this.histogram.logScaleBinSize; super.updateView(); } + + protected getLegendChunkCounts( + totalPresent: number, + totalNotPresent: number, + ) { + // When hovering, the base class already filtered to the hovered row. + if (this.hoverTarget.value !== undefined) { + return { + presentCount: totalPresent, + totalCount: totalPresent + totalNotPresent, + }; + } + // When not hovering, show counts only for the row closest to the target spacing. + const { histogram, target } = this; + const { value: histogramData, spatialScales } = histogram; + let closestScale: number | undefined; + let closestLogDist = Infinity; + const logTarget = Math.log2(target.value); + for (const scale of spatialScales.keys()) { + const dist = Math.abs(Math.log2(scale) - logTarget); + if (dist < closestLogDist) { + closestLogDist = dist; + closestScale = scale; + } + } + if (closestScale === undefined) { + return { + presentCount: totalPresent, + totalCount: totalPresent + totalNotPresent, + }; + } + const row = spatialScales.get(closestScale)!; + const base = 2 * row * numRenderScaleHistogramBins; + let present = 0; + let notPresent = 0; + for (let bin = 0; bin < numRenderScaleHistogramBins; ++bin) { + present += histogramData[base + bin]; + notPresent += histogramData[base + bin + numRenderScaleHistogramBins]; + } + return { presentCount: present, totalCount: present + notPresent }; + } } const TOOL_INPUT_EVENT_MAP = EventActionMap.fromObject({ From 3aa6395d4f7b84cfae883752ae90b9c1ea208474 Mon Sep 17 00:00:00 2001 From: Sean Martin Date: Mon, 4 May 2026 23:41:29 +0200 Subject: [PATCH 12/13] fix: don't triple count histogram for 3 panels --- src/skeleton/frontend.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/skeleton/frontend.ts b/src/skeleton/frontend.ts index f801139ee..590377f34 100644 --- a/src/skeleton/frontend.ts +++ b/src/skeleton/frontend.ts @@ -1809,6 +1809,14 @@ function getSpatialSkeletonGridSpacing( return Math.max(Math.min(chunkSize[0], chunkSize[1], chunkSize[2]), 1e-6); } +// Tracks chunk keys already counted for a given histogram within a single frame, +// preventing the same chunk from being counted multiple times when it falls within +// the visible frustum of more than one slice panel in the same frame. +const seenChunkKeysPerFrame = new WeakMap< + RenderScaleHistogram, + { frameNumber: number; keys: Set } +>(); + function updateSpatialSkeletonGridRenderScaleHistogram( histogram: RenderScaleHistogram, frameNumber: number, @@ -1829,6 +1837,12 @@ function updateSpatialSkeletonGridRenderScaleHistogram( if (scales.length === 0) { return; } + let seen = seenChunkKeysPerFrame.get(histogram); + if (seen === undefined || seen.frameNumber !== frameNumber) { + seen = { frameNumber, keys: new Set() }; + seenChunkKeysPerFrame.set(histogram, seen); + } + const seenKeys = seen.keys; for (const tsource of scales) { const gridIndex = (tsource.source as any).parameters?.gridIndex as | number @@ -1847,6 +1861,8 @@ function updateSpatialSkeletonGridRenderScaleHistogram( tsource, (positionInChunks) => { const key = `${positionInChunks.join()}${lodSuffix}`; + if (seenKeys.has(key)) return; + seenKeys.add(key); const chunk = source.chunks.get(key) as | SpatiallyIndexedSkeletonChunk | undefined; From 9dad6540a69fedbbd17b72a870247bde02f8bd1a Mon Sep 17 00:00:00 2001 From: Sean Martin Date: Tue, 5 May 2026 00:03:21 +0200 Subject: [PATCH 13/13] fix: remove some counting issues on panels --- src/skeleton/frontend.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/skeleton/frontend.ts b/src/skeleton/frontend.ts index 590377f34..a5d072365 100644 --- a/src/skeleton/frontend.ts +++ b/src/skeleton/frontend.ts @@ -1860,10 +1860,11 @@ function updateSpatialSkeletonGridRenderScaleHistogram( localPosition, tsource, (positionInChunks) => { - const key = `${positionInChunks.join()}${lodSuffix}`; - if (seenKeys.has(key)) return; - seenKeys.add(key); - const chunk = source.chunks.get(key) as + const chunkKey = `${positionInChunks.join()}${lodSuffix}`; + const seenKey = `${gridIndex}:${chunkKey}`; + if (seenKeys.has(seenKey)) return; + seenKeys.add(seenKey); + const chunk = source.chunks.get(chunkKey) as | SpatiallyIndexedSkeletonChunk | undefined; if (chunk?.state === ChunkState.GPU_MEMORY) { @@ -1877,8 +1878,9 @@ function updateSpatialSkeletonGridRenderScaleHistogram( const total = presentCount + missingCount; if (total > 0) { histogram.add(spacing, spacing, presentCount, missingCount); - } else { - // Keep all grid rows visible in the histogram even when currently empty. + } else if (!histogram.spatialScales.has(spacing)) { + // Keep the row visible in the histogram when no chunks are in view, + // but only if no earlier panel already populated it this frame. histogram.add(spacing, spacing, 0, 1, true); } }