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/layer/segmentation/index.ts b/src/layer/segmentation/index.ts index c7980e21a..9311d453e 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, @@ -597,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, @@ -815,81 +798,23 @@ class SegmentationUserLayerDisplayState implements SegmentationDisplayState { ); this.spatialSkeletonGridResolutionTarget2d.changed.add(() => { - if (this.suppressSpatialSkeletonGridResolutionTarget2d) return; - this.spatialSkeletonGridResolutionTarget2dExplicit = true; - this.applySpatialSkeletonGridResolutionTarget("2d"); - }); - this.spatialSkeletonGridResolutionTarget3d.changed.add(() => { - if (this.suppressSpatialSkeletonGridResolutionTarget3d) return; - 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 levels = this.spatialSkeletonGridLevels.value; + if (levels.length > 0) { + this.setSpatialSkeletonGridLevel( + "2d", + findClosestSpatialSkeletonGridLevelBySpacing(levels, this.spatialSkeletonGridResolutionTarget2d.value), ); - 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, + this.spatialSkeletonGridResolutionTarget3d.changed.add(() => { + const levels = this.spatialSkeletonGridLevels.value; + if (levels.length > 0) { + this.setSpatialSkeletonGridLevel( + "3d", + findClosestSpatialSkeletonGridLevelBySpacing(levels, this.spatialSkeletonGridResolutionTarget3d.value), ); - 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(); @@ -928,18 +853,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, - }); - spatialSkeletonGridChunkStats3d = new WatchableValue({ - presentCount: 0, - totalCount: 0, - }); spatialSkeletonGridRenderScaleHistogram2d = new RenderScaleHistogram(); spatialSkeletonGridRenderScaleHistogram3d = new RenderScaleHistogram(); spatialSkeletonLod2d = new WatchableValue(0); @@ -948,16 +861,6 @@ class SegmentationUserLayerDisplayState implements SegmentationDisplayState { SpatialSkeletonNodeFilterType, SpatialSkeletonNodeFilterType.NONE, ); - 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(); @@ -1009,208 +912,31 @@ class SegmentationUserLayerDisplayState implements SegmentationDisplayState { } this.spatialSkeletonGridLevels.value = levels; if (levels.length === 0) return; - const target3dIndex = this.spatialSkeletonGridResolutionTarget3dExplicit - ? findClosestSpatialSkeletonGridLevelBySpacing( - levels, - this.getSpatialSkeletonGridTargetSpacing("3d"), - ) - : this.spatialSkeletonGridLevel3dExplicit - ? this.spatialSkeletonGridLevel3d.value - : findClosestSpatialSkeletonGridLevel(levels, this.skeletonLod.value); - const resolved3dIndex = this.setSpatialSkeletonGridLevel( - "3d", - target3dIndex, - this.spatialSkeletonGridResolutionTarget3dExplicit || - this.spatialSkeletonGridLevel3dExplicit, - ); - const target2dIndex = this.spatialSkeletonGridResolutionTarget2dExplicit - ? findClosestSpatialSkeletonGridLevelBySpacing( - levels, - this.getSpatialSkeletonGridTargetSpacing("2d"), - ) - : this.spatialSkeletonGridLevel2dExplicit - ? this.spatialSkeletonGridLevel2d.value - : resolved3dIndex; - this.setSpatialSkeletonGridLevel( - "2d", - target2dIndex, - this.spatialSkeletonGridResolutionTarget2dExplicit || - this.spatialSkeletonGridLevel2dExplicit, - ); - 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.suppressSpatialSkeletonGridResolutionTarget3d = false; - } - if (!this.spatialSkeletonGridResolutionTarget2dExplicit) { - const resolved2dIndex = Math.min( - 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; - this.suppressSpatialSkeletonGridResolutionTarget2d = true; - this.spatialSkeletonGridResolutionTarget2d.value = targetValue; - 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; - 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 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( + const target3dIndex = findClosestSpatialSkeletonGridLevelBySpacing( + levels, + this.spatialSkeletonGridResolutionTarget3d.value, + ); + this.setSpatialSkeletonGridLevel("3d", target3dIndex); + const target2dIndex = findClosestSpatialSkeletonGridLevelBySpacing( levels, - targetSpacing, + this.spatialSkeletonGridResolutionTarget2d.value, ); - const markExplicit = - kind === "2d" - ? this.spatialSkeletonGridResolutionTarget2dExplicit - : this.spatialSkeletonGridResolutionTarget3dExplicit; - this.setSpatialSkeletonGridLevel(kind, index, markExplicit); + this.setSpatialSkeletonGridLevel("2d", target2dIndex); } - 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; @@ -1616,9 +1342,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, ); @@ -1631,18 +1354,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, ); @@ -2360,9 +2071,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], ); @@ -2372,32 +2080,8 @@ 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], - ); - this.displayState.applySpatialSkeletonGridLevel3dFromSpec( - specification[json_keys.SPATIAL_SKELETON_GRID_LEVEL_3D_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], ); @@ -2465,26 +2149,12 @@ 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_CROSS_SECTION_RENDER_SCALE_JSON_KEY] = + this.displayState.spatialSkeletonGridResolutionTarget2d.toJSON(); + x[json_keys.SKELETON_PERSPECTIVE_RENDER_SCALE_JSON_KEY] = + this.displayState.spatialSkeletonGridResolutionTarget3d.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..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, @@ -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) => @@ -85,19 +85,14 @@ 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, - relative: layer.displayState.spatialSkeletonGridResolutionRelative2d, - pixelSize: layer.displayState.spatialSkeletonGridPixelSize2d, - chunkStats: layer.displayState.spatialSkeletonGridChunkStats2d, - relativeTooltip: - "Interpret the 2D skeleton grid resolution target as relative to zoom", - })), + }), SpatialSkeletonGridRenderScaleWidget), }, { 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) => @@ -109,15 +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, - relative: layer.displayState.spatialSkeletonGridResolutionRelative3d, - pixelSize: layer.displayState.spatialSkeletonGridPixelSize3d, - chunkStats: layer.displayState.spatialSkeletonGridChunkStats3d, - relativeTooltip: - "Interpret the 3D skeleton grid resolution target as relative to zoom", - })), + }), SpatialSkeletonGridRenderScaleWidget), }, { label: "Opacity (3d)", 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, - ); -} 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, - }); - }); -}); diff --git a/src/skeleton/frontend.ts b/src/skeleton/frontend.ts index b265eb49b..a5d072365 100644 --- a/src/skeleton/frontend.ts +++ b/src/skeleton/frontend.ts @@ -93,8 +93,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, @@ -1696,32 +1694,26 @@ 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, 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), ); @@ -1729,7 +1721,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, @@ -1767,74 +1759,7 @@ export class MultiscaleSliceViewSpatiallyIndexedSkeletonLayer extends SliceViewR ); } - private updateChunkStatsWatchable() { - const { presentCount, totalCount } = mergeVisibleSpatialChunkKeyCounts( - this.visibleChunkKeysBySliceView.values(), - ); - updateSpatialSkeletonGridChunkStatsWatchable( - (this.displayState as any).spatialSkeletonGridChunkStats2d as - | WatchableValue - | undefined, - 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) { - const displayState = this.displayState as any; - const lodValue = displayState.spatialSkeletonLod2d?.value as number; - const sliceView = renderContext.sliceView; - this.registerChunkStatsSliceView( - sliceView as RefCounted & { rpcId: number }, - ); - if ( - displayState.objectAlpha?.value <= 0.0 && - displayState.hiddenObjectAlpha?.value <= 0.0 - ) { - 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, - ), - ); - } + draw(_renderContext: SliceViewRenderContext) {} } type SpatiallyIndexedSkeletonSourceEntry = @@ -1884,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, @@ -1894,8 +1827,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) { @@ -1906,7 +1837,12 @@ function updateSpatialSkeletonGridRenderScaleHistogram( if (scales.length === 0) { return; } - const safePixelSize = Math.max(pixelSize, 1e-6); + 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 @@ -1924,8 +1860,11 @@ function updateSpatialSkeletonGridRenderScaleHistogram( localPosition, tsource, (positionInChunks) => { - const key = `${positionInChunks.join()}${lodSuffix}`; - 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) { @@ -1936,13 +1875,13 @@ 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); - } else { - // Keep all grid rows visible in the histogram even when currently empty. - histogram.add(spacing, renderScale, 0, 1, true); + histogram.add(spacing, spacing, presentCount, missingCount); + } 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); } } } @@ -1952,125 +1891,16 @@ 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; + spatialSkeletonLod2d?: WatchableValueInterface; + spatialSkeletonGridLevels?: WatchableValueInterface< + Array<{ size: { x: number; y: number; z: number }; lod: number }> + >; + spatialSkeletonGridRenderScaleHistogram2d?: RenderScaleHistogram; + spatialSkeletonGridRenderScaleHistogram3d?: RenderScaleHistogram; } export class SpatiallyIndexedSkeletonLayer @@ -2108,10 +1938,6 @@ export class SpatiallyIndexedSkeletonLayer SpatiallyIndexedSkeletonView, VisibleSpatialChunksBySource >(); - private visibleChunkKeysByRenderedView = new Map< - SpatiallyIndexedSkeletonView, - Map - >(); gridLevel: WatchableValueInterface; lod: WatchableValueInterface; private selectedNodeId: @@ -2506,12 +2332,14 @@ 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; @@ -2566,7 +2394,9 @@ export class SpatiallyIndexedSkeletonLayer const inspectionState = this.inspectionState; if (inspectionState !== undefined) { this.registerDisposer( - inspectionState.nodeDataVersion.changed.add(requestRedraw), + inspectionState.nodeDataVersion.changed.add(() => { + this.redrawNeeded.dispatch(); + }), ); } // TODO (SKM): there should be maybe be a redraw on lod changing @@ -2655,83 +2485,6 @@ export class SpatiallyIndexedSkeletonLayer return this.visibleChunksByView.get(view); } - private getGridLevelForView(view: SpatiallyIndexedSkeletonView) { - const displayState = this.displayState as any; - return ( - view === "2d" - ? displayState.spatialSkeletonGridLevel2d?.value - : displayState.spatialSkeletonGridLevel3d?.value - ) as number | undefined; - } - - private getGridChunkStatsWatchable(view: SpatiallyIndexedSkeletonView) { - return ( - view === "2d" - ? (this.displayState as any).spatialSkeletonGridChunkStats2d - : (this.displayState as any).spatialSkeletonGridChunkStats3d - ) as WatchableValue | undefined; - } - - 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, @@ -2856,18 +2609,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) { @@ -2877,10 +2625,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(); @@ -2896,7 +2640,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 @@ -2905,20 +2648,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( @@ -3388,12 +3123,12 @@ 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)); } } @@ -3404,12 +3139,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; @@ -3587,55 +3316,23 @@ 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, renderContext.projectionParameters, 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, @@ -3644,8 +3341,6 @@ export class PerspectiveViewSpatiallyIndexedSkeletonLayer extends PerspectiveVie this.base.localPosition.value, lodValue, levels, - relative, - pixelSize, ); } this.base.draw( @@ -3657,9 +3352,7 @@ export class PerspectiveViewSpatiallyIndexedSkeletonLayer extends PerspectiveVie attachment, { view: "3d", - gridLevel: displayState.spatialSkeletonGridLevel3d?.value as - | number - | undefined, + gridLevel: displayState.spatialSkeletonGridLevel3d?.value, lod: lodValue, }, ); @@ -3672,8 +3365,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, @@ -3684,7 +3378,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, @@ -3729,41 +3422,7 @@ export class SliceViewSpatiallyIndexedSkeletonLayer extends SliceViewRenderLayer return undefined; } - draw(renderContext: SliceViewRenderContext) { - const displayState = this.base.displayState as any; - const lodValue = displayState.spatialSkeletonLod2d?.value as - | number - | undefined; - 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.objectAlpha?.value <= 0.0 && - displayState.hiddenObjectAlpha?.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, - ), - ); - } + draw(_renderContext: SliceViewRenderContext) {} } export class SliceViewPanelSpatiallyIndexedSkeletonLayer extends SliceViewPanelRenderLayer { @@ -3787,22 +3446,23 @@ 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)); } @@ -3959,42 +3619,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, @@ -4003,8 +3641,6 @@ export class SliceViewPanelSpatiallyIndexedSkeletonLayer extends SliceViewPanelR this.base.localPosition.value, lodValue, levels, - relative, - pixelSize, ); } this.base.draw( @@ -4016,9 +3652,7 @@ export class SliceViewPanelSpatiallyIndexedSkeletonLayer extends SliceViewPanelR attachment, { view: "2d", - gridLevel: displayState.spatialSkeletonGridLevel2d?.value as - | number - | undefined, + gridLevel: displayState.spatialSkeletonGridLevel2d?.value, lod: lodValue, }, ); @@ -4031,10 +3665,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.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 1efa63ecb..a98d15f41 100644 --- a/src/widget/render_scale_widget.ts +++ b/src/widget/render_scale_widget.ts @@ -26,11 +26,7 @@ import { renderScaleHistogramBinSize, renderScaleHistogramOrigin, } from "#src/render_scale_statistics.js"; -import { TrackableBooleanCheckbox } from "#src/trackable_boolean.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"; @@ -69,19 +65,6 @@ export interface RenderScaleWidgetOptions { target: TrackableValueInterface; } -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 { label = document.createElement("div"); element = document.createElement("div"); @@ -429,98 +412,52 @@ export class VolumeRenderingRenderScaleWidget extends RenderScaleWidget { export class SpatialSkeletonGridRenderScaleWidget extends RenderScaleWidget { protected unitOfTarget = "nm"; - private relative?: WatchableValueInterface; - 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: { - 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"; - } - - adjustViaWheel(event: WheelEvent) { - this.syncScaleConfig(); - super.adjustViaWheel(event); + super.updateView(); } protected getLegendChunkCounts( totalPresent: number, totalNotPresent: number, ) { - const chunkStats = this.chunkStats?.value; - if (chunkStats !== undefined) { - return chunkStats; + // When hovering, the base class already filtered to the hovered row. + if (this.hoverTarget.value !== undefined) { + return { + presentCount: totalPresent, + totalCount: totalPresent + totalNotPresent, + }; } - return super.getLegendChunkCounts(totalPresent, totalNotPresent); - } - - updateView() { - this.syncScaleConfig(); - this.unitOfTarget = this.relative?.value === true ? "px" : "nm"; - super.updateView(); + // 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 }; } } @@ -568,49 +505,3 @@ export function renderScaleLayerControl< }, }; } - -export function spatialSkeletonGridRenderScaleLayerControl< - LayerType extends UserLayer, ->( - getter: (layer: LayerType) => SpatialSkeletonGridRenderScaleWidgetOptions, -): LayerControlFactory { - return { - makeControl: (layer, context) => { - const { - histogram, - target, - relative, - pixelSize, - chunkStats, - relativeLabel, - relativeTooltip, - } = getter(layer); - const control = context.registerDisposer( - new SpatialSkeletonGridRenderScaleWidget(histogram, target, { - relative, - pixelSize, - chunkStats, - relativeLabel, - relativeTooltip, - }), - ); - 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(); - }); - }, - }; -}