From 464b00345b2e2f611741aed61a4a61ceaa69cfaa Mon Sep 17 00:00:00 2001 From: Sean Martin Date: Mon, 4 May 2026 18:52:20 +0200 Subject: [PATCH 1/5] wip: experiment with layer consolidation --- src/layer/segmentation/index.spec.ts | 37 +--- src/layer/segmentation/index.ts | 45 ++--- src/skeleton/backend.ts | 113 +++-------- src/skeleton/base.ts | 2 - src/skeleton/frontend.ts | 272 +++++---------------------- src/ui/spatial_skeleton_edit_tool.ts | 4 - 6 files changed, 92 insertions(+), 381 deletions(-) diff --git a/src/layer/segmentation/index.spec.ts b/src/layer/segmentation/index.spec.ts index 9ade3ae1e..878ff7189 100644 --- a/src/layer/segmentation/index.spec.ts +++ b/src/layer/segmentation/index.spec.ts @@ -25,8 +25,6 @@ const { SegmentationUserLayer } = await import( const { PerspectiveViewSpatiallyIndexedSkeletonLayer, SliceViewPanelSpatiallyIndexedSkeletonLayer, - SliceViewSpatiallyIndexedSkeletonLayer, - MultiscaleSliceViewSpatiallyIndexedSkeletonLayer, } = await import("#src/skeleton/frontend.js"); const { SegmentSelectionState } = await import( @@ -81,30 +79,14 @@ function makeSpatialSkeletonLayerWithSource(source: unknown) { describe("layer/segmentation spatial skeleton chunk stats", () => { it("tracks combined chunk load state from the loading render layers only", () => { + // After the 2D/3D backend unification, only PerspectiveViewSpatiallyIndexedSkeletonLayer + // contributes to the chunk stats (it handles both 2D and 3D views via the shared backend). const perspectiveLayer = Object.assign( Object.create(PerspectiveViewSpatiallyIndexedSkeletonLayer.prototype), { layerChunkProgressInfo: { - numVisibleChunksNeeded: 5, - numVisibleChunksAvailable: 3, - }, - }, - ); - const sliceLayer = Object.assign( - Object.create(SliceViewSpatiallyIndexedSkeletonLayer.prototype), - { - layerChunkProgressInfo: { - numVisibleChunksNeeded: 4, - numVisibleChunksAvailable: 2, - }, - }, - ); - const multiscaleSliceLayer = Object.assign( - Object.create(MultiscaleSliceViewSpatiallyIndexedSkeletonLayer.prototype), - { - layerChunkProgressInfo: { - numVisibleChunksNeeded: 6, - numVisibleChunksAvailable: 5, + numVisibleChunksNeeded: 9, + numVisibleChunksAvailable: 7, }, }, ); @@ -121,12 +103,7 @@ describe("layer/segmentation spatial skeleton chunk stats", () => { const layer = Object.assign( Object.create(SegmentationUserLayer.prototype), { - renderLayers: [ - perspectiveLayer, - sliceLayer, - multiscaleSliceLayer, - slicePanelLayer, - ], + renderLayers: [perspectiveLayer, slicePanelLayer], spatialSkeletonVisibleChunksNeeded: new WatchableValue(0), spatialSkeletonVisibleChunksAvailable: new WatchableValue(0), spatialSkeletonVisibleChunksLoaded: new WatchableValue(false), @@ -146,8 +123,8 @@ describe("layer/segmentation spatial skeleton chunk stats", () => { layer.updateSpatialSkeletonChunkLoadState(); - expect(layer.spatialSkeletonVisibleChunksNeeded.value).toBe(15); - expect(layer.spatialSkeletonVisibleChunksAvailable.value).toBe(10); + expect(layer.spatialSkeletonVisibleChunksNeeded.value).toBe(9); + expect(layer.spatialSkeletonVisibleChunksAvailable.value).toBe(7); expect(layer.spatialSkeletonVisibleChunksLoaded.value).toBe(false); }); }); diff --git a/src/layer/segmentation/index.ts b/src/layer/segmentation/index.ts index c7980e21a..c5647a7f5 100644 --- a/src/layer/segmentation/index.ts +++ b/src/layer/segmentation/index.ts @@ -129,11 +129,9 @@ import { SliceViewPanelSkeletonLayer, PerspectiveViewSpatiallyIndexedSkeletonLayer, SliceViewPanelSpatiallyIndexedSkeletonLayer, - SliceViewSpatiallyIndexedSkeletonLayer, SpatiallyIndexedSkeletonLayer, SpatiallyIndexedSkeletonSource, MultiscaleSpatiallyIndexedSkeletonSource, - MultiscaleSliceViewSpatiallyIndexedSkeletonLayer, } from "#src/skeleton/frontend.js"; import { classifySpatialSkeletonDisplayNodeType as getSpatialSkeletonDisplayNodeType, @@ -1698,8 +1696,7 @@ export class SegmentationUserLayer extends Base { !layers.some( (layer) => (layer instanceof PerspectiveViewSpatiallyIndexedSkeletonLayer || - layer instanceof SliceViewPanelSpatiallyIndexedSkeletonLayer || - layer instanceof SliceViewSpatiallyIndexedSkeletonLayer) && + layer instanceof SliceViewPanelSpatiallyIndexedSkeletonLayer) && getSpatiallyIndexedSkeletonSource(layer.base) !== undefined, ), { changed: this.layersChanged, value: this.renderLayers }, @@ -1799,30 +1796,18 @@ export class SegmentationUserLayer extends Base { if (layer instanceof SliceViewPanelSpatiallyIndexedSkeletonLayer) { return layer.base; } - if (layer instanceof SliceViewSpatiallyIndexedSkeletonLayer) { - return layer.base; - } } return undefined; }; getSpatialSkeletonChunkStats(kind: "2d" | "3d") { + // 2D chunks are now handled by the same backend as 3D, so only report + // under "3d" to avoid double-counting in updateSpatialSkeletonChunkLoadState. + if (kind === "2d") return { presentCount: 0, totalCount: 0 }; let needed = 0; let available = 0; for (const layer of this.renderLayers) { - if ( - kind === "3d" && - layer instanceof PerspectiveViewSpatiallyIndexedSkeletonLayer - ) { - needed += layer.layerChunkProgressInfo.numVisibleChunksNeeded; - available += layer.layerChunkProgressInfo.numVisibleChunksAvailable; - continue; - } - if ( - kind === "2d" && - (layer instanceof SliceViewSpatiallyIndexedSkeletonLayer || - layer instanceof MultiscaleSliceViewSpatiallyIndexedSkeletonLayer) - ) { + if (layer instanceof PerspectiveViewSpatiallyIndexedSkeletonLayer) { needed += layer.layerChunkProgressInfo.numVisibleChunksNeeded; available += layer.layerChunkProgressInfo.numVisibleChunksAvailable; } @@ -1857,8 +1842,7 @@ export class SegmentationUserLayer extends Base { for (const layer of this.renderLayers) { if ( layer instanceof PerspectiveViewSpatiallyIndexedSkeletonLayer || - layer instanceof SliceViewPanelSpatiallyIndexedSkeletonLayer || - layer instanceof SliceViewSpatiallyIndexedSkeletonLayer + layer instanceof SliceViewPanelSpatiallyIndexedSkeletonLayer ) { hasSpatialSkeletonLayer = true; break; @@ -1876,8 +1860,7 @@ export class SegmentationUserLayer extends Base { for (const layer of this.renderLayers) { if ( layer instanceof PerspectiveViewSpatiallyIndexedSkeletonLayer || - layer instanceof SliceViewPanelSpatiallyIndexedSkeletonLayer || - layer instanceof SliceViewSpatiallyIndexedSkeletonLayer + layer instanceof SliceViewPanelSpatiallyIndexedSkeletonLayer ) { return layer.base.source; } @@ -2091,13 +2074,6 @@ export class SegmentationUserLayer extends Base { ), ); } else if (mesh instanceof MultiscaleSpatiallyIndexedSkeletonSource) { - const base = new MultiscaleSliceViewSpatiallyIndexedSkeletonLayer( - this.manager.chunkManager, - mesh, - displayState, - ); - loadedSubsource.addRenderLayer(base); - const perspectiveSources = mesh.getPerspectiveSources(); const slicePanelSources = mesh.getSliceViewPanelSources(); const sharedSpatialSkeletonSources = @@ -2114,6 +2090,8 @@ export class SegmentationUserLayer extends Base { { gridLevel: displayState.spatialSkeletonGridLevel3d, lod: displayState.skeletonLod, + gridLevel2d: displayState.spatialSkeletonGridLevel2d, + lod2d: displayState.spatialSkeletonLod2d, sources2d: slicePanelSources, selectedNodeId: this.selectedSpatialSkeletonNodeId, pendingNodePositionVersion: @@ -2150,6 +2128,8 @@ export class SegmentationUserLayer extends Base { { gridLevel: displayState.spatialSkeletonGridLevel3d, lod: displayState.skeletonLod, + gridLevel2d: displayState.spatialSkeletonGridLevel2d, + lod2d: displayState.spatialSkeletonLod2d, selectedNodeId: this.selectedSpatialSkeletonNodeId, pendingNodePositionVersion: this.spatialSkeletonState.pendingNodePositionVersion, @@ -2163,9 +2143,6 @@ export class SegmentationUserLayer extends Base { loadedSubsource.addRenderLayer( new PerspectiveViewSpatiallyIndexedSkeletonLayer(base.addRef()), ); - loadedSubsource.addRenderLayer( - new SliceViewSpatiallyIndexedSkeletonLayer(base.addRef()), - ); loadedSubsource.addRenderLayer( new SliceViewPanelSpatiallyIndexedSkeletonLayer( /* transfer ownership */ base, diff --git a/src/skeleton/backend.ts b/src/skeleton/backend.ts index a5deb22e6..e7d11540c 100644 --- a/src/skeleton/backend.ts +++ b/src/skeleton/backend.ts @@ -43,7 +43,6 @@ import { SKELETON_LAYER_RPC_ID, SPATIALLY_INDEXED_SKELETON_RENDER_LAYER_RPC_ID, SPATIALLY_INDEXED_SKELETON_RENDER_LAYER_UPDATE_SOURCES_RPC_ID, - SPATIALLY_INDEXED_SKELETON_SLICEVIEW_RENDER_LAYER_RPC_ID, } from "#src/skeleton/base.js"; import { freeSkeletonChunkSystemMemory, @@ -61,11 +60,9 @@ import { SCALE_PRIORITY_MULTIPLIER, SliceViewChunk, SliceViewChunkSourceBackend, - SliceViewRenderLayerBackend, } from "#src/sliceview/backend.js"; import { forEachVisibleVolumetricChunk, - type SliceViewBase, type SliceViewChunkSpecification, type SliceViewProjectionParameters, type TransformedSource, @@ -368,6 +365,8 @@ export class SpatiallyIndexedSkeletonRenderLayerBackend extends withChunkManager renderScaleTarget: SharedWatchableValue; skeletonLod: SharedWatchableValue; skeletonGridLevel: SharedWatchableValue; + skeletonLod2d: SharedWatchableValue; + skeletonGridLevel2d: SharedWatchableValue; private pendingLodCleanup = false; constructor(rpc: RPC, options: any) { @@ -376,6 +375,8 @@ export class SpatiallyIndexedSkeletonRenderLayerBackend extends withChunkManager this.localPosition = rpc.get(options.localPosition); this.skeletonLod = rpc.get(options.skeletonLod); this.skeletonGridLevel = rpc.get(options.skeletonGridLevel); + this.skeletonLod2d = rpc.get(options.skeletonLod2d); + this.skeletonGridLevel2d = rpc.get(options.skeletonGridLevel2d); const scheduleUpdateChunkPriorities = () => this.chunkManager.scheduleUpdateChunkPriorities(); this.registerDisposer( @@ -387,6 +388,9 @@ export class SpatiallyIndexedSkeletonRenderLayerBackend extends withChunkManager this.registerDisposer( this.skeletonGridLevel.changed.add(scheduleUpdateChunkPriorities), ); + this.registerDisposer( + this.skeletonGridLevel2d.changed.add(scheduleUpdateChunkPriorities), + ); // Debounce LOD changes to avoid making requests for every slider value const debouncedLodUpdate = debounce(() => { @@ -400,6 +404,18 @@ export class SpatiallyIndexedSkeletonRenderLayerBackend extends withChunkManager debouncedLodUpdate(); }), ); + + const debouncedLodUpdate2d = debounce(() => { + scheduleUpdateChunkPriorities(); + }, SPATIALLY_INDEXED_SKELETON_LOD_DEBOUNCE_MS); + this.registerDisposer(() => debouncedLodUpdate2d.cancel()); + + this.registerDisposer( + this.skeletonLod2d.changed.add(() => { + this.pendingLodCleanup = true; + debouncedLodUpdate2d(); + }), + ); this.registerDisposer( this.chunkManager.recomputeChunkPriorities.add(() => this.recomputeChunkPriorities(), @@ -514,7 +530,10 @@ export class SpatiallyIndexedSkeletonRenderLayerBackend extends withChunkManager } } const renderScaleTarget = this.renderScaleTarget.value; - const skeletonGridLevel = this.skeletonGridLevel.value; + const is2dView = pixelSize !== undefined; + const skeletonGridLevel = ( + is2dView ? this.skeletonGridLevel2d : this.skeletonGridLevel + ).value; const selectScales = ( scales: TransformedSource< @@ -606,7 +625,7 @@ export class SpatiallyIndexedSkeletonRenderLayerBackend extends withChunkManager return selected; }; - const lodValue = this.skeletonLod.value; + const lodValue = (is2dView ? this.skeletonLod2d : this.skeletonLod).value; for (const scales of transformedSources) { const selectedScales = selectScales(scales); for (const { tsource, scaleIndex } of selectedScales) { @@ -653,87 +672,3 @@ export class SpatiallyIndexedSkeletonRenderLayerBackend extends withChunkManager } } } - -@registerSharedObject(SPATIALLY_INDEXED_SKELETON_SLICEVIEW_RENDER_LAYER_RPC_ID) -export class SpatiallyIndexedSkeletonSliceViewRenderLayerBackend extends SliceViewRenderLayerBackend { - skeletonGridLevel: SharedWatchableValue; - skeletonLod: SharedWatchableValue; - private chunkManager_: ChunkManager; - private pendingLodCleanup = false; - private trackedSources = new Set(); - - constructor(rpc: RPC, options: any) { - super(rpc, options); - this.skeletonGridLevel = rpc.get(options.skeletonGridLevel); - this.skeletonLod = rpc.get(options.skeletonLod); - const chunkManager = rpc.get(options.chunkManager); - this.chunkManager_ = chunkManager; - const scheduleUpdateChunkPriorities = () => - chunkManager.scheduleUpdateChunkPriorities(); - this.registerDisposer( - this.skeletonGridLevel.changed.add(scheduleUpdateChunkPriorities), - ); - // Debounce LOD changes to avoid making requests for every slider value. - const debouncedLodUpdate = debounce(() => { - scheduleUpdateChunkPriorities(); - }, SPATIALLY_INDEXED_SKELETON_LOD_DEBOUNCE_MS); - this.registerDisposer(() => debouncedLodUpdate.cancel()); - - this.registerDisposer( - this.skeletonLod.changed.add(() => { - this.pendingLodCleanup = true; - debouncedLodUpdate(); - }), - ); - this.registerDisposer( - chunkManager.recomputeChunkPrioritiesLate.add(() => { - if (!this.pendingLodCleanup) return; - cancelStaleSpatiallyIndexedSkeletonDownloads( - chunkManager, - this.trackedSources, - chunkManager.recomputeChunkPriorities.count, - ); - this.pendingLodCleanup = false; - }), - ); - } - - prepareChunkSourceForRequest(source: SpatiallyIndexedSkeletonSourceBackend) { - this.trackedSources.add(source); - source.currentLod = this.skeletonLod.value; - source.currentRequestGeneration = - this.chunkManager_.recomputeChunkPriorities.count; - source.currentRequestOwner = - SpatiallyIndexedSkeletonChunkRequestOwner.VIEW_2D; - } - - filterVisibleSources( - sliceView: SliceViewBase, - sources: readonly TransformedSource[], - ): Iterable { - const lodValue = this.skeletonLod.value; - for (const tsource of sources) { - const source = tsource.source as SpatiallyIndexedSkeletonSourceBackend; - this.trackedSources.add(source); - source.currentLod = lodValue; - source.currentRequestGeneration = - this.chunkManager_.recomputeChunkPriorities.count; - source.currentRequestOwner = - SpatiallyIndexedSkeletonChunkRequestOwner.VIEW_2D; - } - - if ( - sources.length > 0 && - sources.every( - (source) => getSpatiallyIndexedSkeletonGridIndex(source) !== undefined, - ) - ) { - return selectSpatiallyIndexedSkeletonEntriesByGrid( - sources, - this.skeletonGridLevel.value, - getSpatiallyIndexedSkeletonGridIndex, - ); - } - return super.filterVisibleSources(sliceView, sources); - } -} diff --git a/src/skeleton/base.ts b/src/skeleton/base.ts index a848400a1..051e19f1a 100644 --- a/src/skeleton/base.ts +++ b/src/skeleton/base.ts @@ -22,8 +22,6 @@ export const SPATIALLY_INDEXED_SKELETON_RENDER_LAYER_RPC_ID = "skeleton/SpatiallyIndexedSkeletonRenderLayer"; export const SPATIALLY_INDEXED_SKELETON_RENDER_LAYER_UPDATE_SOURCES_RPC_ID = "skeleton/SpatiallyIndexedSkeletonRenderLayer.updateSources"; -export const SPATIALLY_INDEXED_SKELETON_SLICEVIEW_RENDER_LAYER_RPC_ID = - "skeleton/SpatiallyIndexedSkeletonSliceViewRenderLayer"; export interface VertexAttributeInfo { dataType: DataType; diff --git a/src/skeleton/frontend.ts b/src/skeleton/frontend.ts index 93f64dd69..e6badce16 100644 --- a/src/skeleton/frontend.ts +++ b/src/skeleton/frontend.ts @@ -72,7 +72,6 @@ import { SKELETON_LAYER_RPC_ID, SPATIALLY_INDEXED_SKELETON_RENDER_LAYER_RPC_ID, SPATIALLY_INDEXED_SKELETON_RENDER_LAYER_UPDATE_SOURCES_RPC_ID, - SPATIALLY_INDEXED_SKELETON_SLICEVIEW_RENDER_LAYER_RPC_ID, } from "#src/skeleton/base.js"; import { uploadVertexAttributesToGPU } from "#src/skeleton/gpu_upload_utils.js"; import { buildSpatiallyIndexedSkeletonOverlayGeometry } from "#src/skeleton/overlay_geometry.js"; @@ -1693,154 +1692,14 @@ 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; - super(chunkManager, multiscaleSource, { - transform: (displayState as any).transform, - localPosition: (displayState as any).localPosition, - renderScaleTarget, - visibleSourcesInvalidation: - gridLevel2d === undefined ? [] : [gridLevel2d], - }); - this.renderOptions = ( - displayState as any - ).skeletonRenderingOptions.params2d; - this.registerDisposer( - this.renderOptions.mode.changed.add(this.redrawNeeded.dispatch), - ); - this.registerDisposer( - this.renderOptions.lineWidth.changed.add(this.redrawNeeded.dispatch), - ); - const rpc = this.chunkManager.rpc!; - const lod2d = (displayState as any).spatialSkeletonLod2d; - if (gridLevel2d !== undefined && lod2d !== undefined) { - this.rpcTransfer = { - ...this.rpcTransfer, - chunkManager: this.chunkManager.rpcId, - skeletonGridLevel: this.registerDisposer( - SharedWatchableValue.makeFromExisting(rpc, gridLevel2d), - ).rpcId, - skeletonLod: this.registerDisposer( - SharedWatchableValue.makeFromExisting(rpc, lod2d), - ).rpcId, - }; - } - this.initializeCounterpart(); - } - - filterVisibleSources( - sliceView: SliceViewBase, - sources: readonly TransformedSource[], - ): Iterable { - const gridLevel = (this.displayState as any).spatialSkeletonGridLevel2d - ?.value as number | undefined; - if ( - gridLevel === undefined || - sources.length === 0 || - !sources.every( - (source) => getSpatiallyIndexedSkeletonGridIndex(source) !== undefined, - ) - ) { - return super.filterVisibleSources(sliceView, sources); - } - return selectSpatiallyIndexedSkeletonEntriesByGrid( - sources, - gridLevel, - getSpatiallyIndexedSkeletonGridIndex, - ); - } - - 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 - | undefined; - const sliceView = renderContext.sliceView; - this.registerChunkStatsSliceView( - sliceView as RefCounted & { rpcId: number }, - ); - if (displayState.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, - ), - ); - } -} - type SpatiallyIndexedSkeletonSourceEntry = SliceViewSingleResolutionSource; interface SpatiallyIndexedSkeletonLayerOptions { gridLevel?: WatchableValueInterface; lod?: WatchableValueInterface; + gridLevel2d?: WatchableValueInterface; + lod2d?: WatchableValueInterface; sources2d?: SpatiallyIndexedSkeletonSourceEntry[]; selectedNodeId?: WatchableValueInterface; pendingNodePositionVersion?: WatchableValueInterface; @@ -2112,6 +1971,8 @@ export class SpatiallyIndexedSkeletonLayer >(); gridLevel: WatchableValueInterface; lod: WatchableValueInterface; + gridLevel2d: WatchableValueInterface; + lod2d: WatchableValueInterface; private selectedNodeId: | WatchableValueInterface | undefined; @@ -2514,6 +2375,14 @@ export class SpatiallyIndexedSkeletonLayer new WatchableValue(0); this.lod = options.lod ?? (displayState as any).skeletonLod ?? new WatchableValue(0); + this.gridLevel2d = + options.gridLevel2d ?? + (displayState as any).spatialSkeletonGridLevel2d ?? + this.gridLevel; + this.lod2d = + options.lod2d ?? + (displayState as any).spatialSkeletonLod2d ?? + this.lod; this.selectedNodeId = options.selectedNodeId; this.pendingNodePositionVersion = options.pendingNodePositionVersion; this.getPendingNodePositionOverride = options.getPendingNodePosition; @@ -2661,6 +2530,14 @@ export class SpatiallyIndexedSkeletonLayer SharedWatchableValue.makeFromExisting(rpc, this.gridLevel), ); + const skeletonLod2dWatchable = this.registerDisposer( + SharedWatchableValue.makeFromExisting(rpc, this.lod2d), + ); + + const skeletonGridLevel2dWatchable = this.registerDisposer( + SharedWatchableValue.makeFromExisting(rpc, this.gridLevel2d), + ); + sharedObject.initializeCounterpart(rpc, { chunkManager: chunkManager.rpcId, localPosition: this.registerDisposer( @@ -2669,6 +2546,8 @@ export class SpatiallyIndexedSkeletonLayer renderScaleTarget: renderScaleTargetWatchable.rpcId, skeletonLod: skeletonLodWatchable.rpcId, skeletonGridLevel: skeletonGridLevelWatchable.rpcId, + skeletonLod2d: skeletonLod2dWatchable.rpcId, + skeletonGridLevel2d: skeletonGridLevel2dWatchable.rpcId, }); this.backend = sharedObject; } @@ -3735,93 +3614,16 @@ export class PerspectiveViewSpatiallyIndexedSkeletonLayer extends PerspectiveVie } } -export class SliceViewSpatiallyIndexedSkeletonLayer extends SliceViewRenderLayer { - private renderOptions: ViewSpecificSkeletonRenderingOptions; - private trackedChunkStatsSliceViews = new Set(); - constructor(public base: SpatiallyIndexedSkeletonLayer) { - super( - base.chunkManager, - { - getSources: () => { - return [ - [ - { - chunkSource: base.source, - chunkToMultiscaleTransform: mat4.create(), - }, - ], - ]; - }, - } as any, - { - transform: base.displayState.transform, - localPosition: (base.displayState as any).localPosition, - }, - ); - // @ts-expect-error RenderHelper requires panel-specific initialization here. - this.renderHelper = this.registerDisposer(new RenderHelper(base, true)); - this.renderOptions = base.displayState.skeletonRenderingOptions.params2d; - this.layerChunkProgressInfo = base.layerChunkProgressInfo; - this.registerDisposer(base); - const { renderOptions } = this; - this.registerDisposer( - renderOptions.mode.changed.add(this.redrawNeeded.dispatch), - ); - this.registerDisposer( - renderOptions.lineWidth.changed.add(this.redrawNeeded.dispatch), - ); - this.registerDisposer(base.redrawNeeded.add(this.redrawNeeded.dispatch)); - this.initializeCounterpart(); - } - get gl() { - return this.base.gl; - } - - getValueAt(position: Float32Array) { - position; - 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 || 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, - ), - ); - } -} export class SliceViewPanelSpatiallyIndexedSkeletonLayer extends SliceViewPanelRenderLayer { private renderHelper: RenderHelper; private browseRenderHelper: RenderHelper; private renderOptions: ViewSpecificSkeletonRenderingOptions; private transformedSources: TransformedSource[][] = []; + backend: ChunkRenderLayerFrontend; constructor(public base: SpatiallyIndexedSkeletonLayer) { super(); + this.backend = base.backend; this.renderHelper = this.registerDisposer(new RenderHelper(base, true)); this.browseRenderHelper = this.registerDisposer( new RenderHelper(base.chunkGeometryRenderLayerInterface, true), @@ -3968,6 +3770,21 @@ export class SliceViewPanelSpatiallyIndexedSkeletonLayer extends SliceViewPanelR >, ) { super.attach(attachment); + attachment.registerDisposer(() => + this.base.clearVisibleChunkKeysForRenderedView( + "2d", + attachment.view.rpcId, + ), + ); + + const backend = this.backend; + if (backend && backend.rpc) { + backend.rpc.invoke(RENDERED_VIEW_ADD_LAYER_RPC_ID, { + layer: backend.rpcId, + view: attachment.view.rpcId, + }); + } + const baseLayer = this.base; const redrawNeeded = this.redrawNeeded; attachment.registerDisposer( @@ -3991,7 +3808,17 @@ export class SliceViewPanelSpatiallyIndexedSkeletonLayer extends SliceViewPanelR context.registerDisposer(tsource.source); } } + attachment.view.flushBackendProjectionParameters(); this.transformedSources = transformedSources; + baseLayer.rpc!.invoke( + SPATIALLY_INDEXED_SKELETON_RENDER_LAYER_UPDATE_SOURCES_RPC_ID, + { + layer: baseLayer.backend.rpcId, + view: attachment.view.rpcId, + displayDimensionRenderInfo, + sources: serializeAllTransformedSources(transformedSources), + }, + ); redrawNeeded.dispatch(); return transformedSources; }, @@ -4031,6 +3858,7 @@ export class SliceViewPanelSpatiallyIndexedSkeletonLayer extends SliceViewPanelR this.transformedSources, renderContext.sliceView.projectionParameters.value, lodValue, + attachment.view.rpcId, ); const levels = displayState.spatialSkeletonGridLevels?.value as | Array<{ size: { x: number; y: number; z: number }; lod: number }> diff --git a/src/ui/spatial_skeleton_edit_tool.ts b/src/ui/spatial_skeleton_edit_tool.ts index 85717fd42..00443d811 100644 --- a/src/ui/spatial_skeleton_edit_tool.ts +++ b/src/ui/spatial_skeleton_edit_tool.ts @@ -41,7 +41,6 @@ import type { SpatiallyIndexedSkeletonLayer } from "#src/skeleton/frontend.js"; import { PerspectiveViewSpatiallyIndexedSkeletonLayer, SliceViewPanelSpatiallyIndexedSkeletonLayer, - SliceViewSpatiallyIndexedSkeletonLayer, } from "#src/skeleton/frontend.js"; import { StatusMessage } from "#src/status.js"; import type { SpatialSkeletonToolPointInfo } from "#src/ui/spatial_skeleton_tool_messages.js"; @@ -174,9 +173,6 @@ abstract class SpatialSkeletonToolBase extends LayerTool if (pickedLayer instanceof SliceViewPanelSpatiallyIndexedSkeletonLayer) { return pickedLayer.base; } - if (pickedLayer instanceof SliceViewSpatiallyIndexedSkeletonLayer) { - return pickedLayer.base; - } return this.layer.getSpatiallyIndexedSkeletonLayer(); } From 0d1273f10280db24f91f0c3433589bc08deeab39 Mon Sep 17 00:00:00 2001 From: Sean Martin Date: Tue, 5 May 2026 17:57:04 +0200 Subject: [PATCH 2/5] fix: correct access --- src/skeleton/frontend.ts | 26 ++++++++------------------ 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/src/skeleton/frontend.ts b/src/skeleton/frontend.ts index 83d47e4c1..bbfcc6b75 100644 --- a/src/skeleton/frontend.ts +++ b/src/skeleton/frontend.ts @@ -57,10 +57,7 @@ import { getVisibleSegments, getObjectKey, } from "#src/segmentation_display_state/base.js"; -import type { - SegmentationDisplayState3D, - SegmentationDisplayState, -} from "#src/segmentation_display_state/frontend.js"; +import type { SegmentationDisplayState3D } from "#src/segmentation_display_state/frontend.js"; import { forEachVisibleSegmentToDraw, registerRedrawWhenSegmentationDisplayState3DChanged, @@ -86,14 +83,12 @@ import { SkeletonRenderMode } from "#src/skeleton/render_mode.js"; import { getSpatiallyIndexedSkeletonGridIndex, getSpatiallyIndexedSkeletonSourceView, - selectSpatiallyIndexedSkeletonEntriesByGrid, selectSpatiallyIndexedSkeletonEntriesForView, type SpatiallyIndexedSkeletonView, } from "#src/skeleton/source_selection.js"; import { spatiallyIndexedSkeletonTextureAttributeSpecs } from "#src/skeleton/spatial_attribute_layout.js"; import { forEachVisibleVolumetricChunk, - type SliceViewBase, type SliceViewChunkSpecification, type TransformedSource, } from "#src/sliceview/base.js"; @@ -109,13 +104,9 @@ import { import type { SliceViewPanel } from "#src/sliceview/panel.js"; import type { SliceViewPanelRenderContext, - SliceViewRenderContext, SliceViewPanelReadyRenderContext, } from "#src/sliceview/renderlayer.js"; -import { - SliceViewPanelRenderLayer, - SliceViewRenderLayer, -} from "#src/sliceview/renderlayer.js"; +import { SliceViewPanelRenderLayer } from "#src/sliceview/renderlayer.js"; import type { WatchableValueInterface } from "#src/trackable_value.js"; import { makeCachedLazyDerivedWatchableValue, @@ -2273,6 +2264,12 @@ export class SpatiallyIndexedSkeletonLayer new WatchableValue(0); this.lod = options.lod ?? spatialDisplayState.skeletonLod ?? new WatchableValue(0); + this.gridLevel2d = + options.gridLevel2d ?? + spatialDisplayState.spatialSkeletonGridLevel2d ?? + this.gridLevel; + this.lod2d = + options.lod2d ?? spatialDisplayState.spatialSkeletonLod2d ?? this.lod; this.selectedNodeId = options.selectedNodeId; this.pendingNodePositionVersion = options.pendingNodePositionVersion; this.getPendingNodePositionOverride = options.getPendingNodePosition; @@ -3476,12 +3473,6 @@ export class SliceViewPanelSpatiallyIndexedSkeletonLayer extends SliceViewPanelR >, ) { super.attach(attachment); - attachment.registerDisposer(() => - this.base.clearVisibleChunkKeysForRenderedView( - "2d", - attachment.view.rpcId, - ), - ); const backend = this.backend; if (backend && backend.rpc) { @@ -3549,7 +3540,6 @@ export class SliceViewPanelSpatiallyIndexedSkeletonLayer extends SliceViewPanelR this.transformedSources, renderContext.sliceView.projectionParameters.value, lodValue, - attachment.view.rpcId, ); const levels = displayState.spatialSkeletonGridLevels?.value; const histogram = displayState.spatialSkeletonGridRenderScaleHistogram2d; From ceb62baf6d5e6669818b610377160137c344466d Mon Sep 17 00:00:00 2001 From: Sean Martin Date: Tue, 5 May 2026 19:09:55 +0200 Subject: [PATCH 3/5] refactor: combine debounces into one --- src/skeleton/backend.ts | 24 ++++++------------------ 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/src/skeleton/backend.ts b/src/skeleton/backend.ts index e7d11540c..2d392e669 100644 --- a/src/skeleton/backend.ts +++ b/src/skeleton/backend.ts @@ -398,24 +398,12 @@ export class SpatiallyIndexedSkeletonRenderLayerBackend extends withChunkManager }, SPATIALLY_INDEXED_SKELETON_LOD_DEBOUNCE_MS); this.registerDisposer(() => debouncedLodUpdate.cancel()); - this.registerDisposer( - this.skeletonLod.changed.add(() => { - this.pendingLodCleanup = true; - debouncedLodUpdate(); - }), - ); - - const debouncedLodUpdate2d = debounce(() => { - scheduleUpdateChunkPriorities(); - }, SPATIALLY_INDEXED_SKELETON_LOD_DEBOUNCE_MS); - this.registerDisposer(() => debouncedLodUpdate2d.cancel()); - - this.registerDisposer( - this.skeletonLod2d.changed.add(() => { - this.pendingLodCleanup = true; - debouncedLodUpdate2d(); - }), - ); + const onLodChanged = () => { + this.pendingLodCleanup = true; + debouncedLodUpdate(); + }; + this.registerDisposer(this.skeletonLod.changed.add(onLodChanged)); + this.registerDisposer(this.skeletonLod2d.changed.add(onLodChanged)); this.registerDisposer( this.chunkManager.recomputeChunkPriorities.add(() => this.recomputeChunkPriorities(), From 259a574508238aeb7694d05bdd6f09218302c198 Mon Sep 17 00:00:00 2001 From: Sean Martin Date: Tue, 5 May 2026 19:10:17 +0200 Subject: [PATCH 4/5] fix: set view owner --- src/skeleton/backend.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/skeleton/backend.ts b/src/skeleton/backend.ts index 2d392e669..def849509 100644 --- a/src/skeleton/backend.ts +++ b/src/skeleton/backend.ts @@ -631,8 +631,9 @@ export class SpatiallyIndexedSkeletonRenderLayerBackend extends withChunkManager basePriority + SCALE_PRIORITY_MULTIPLIER * scaleIndex; source.currentLod = lodValue; source.currentRequestGeneration = currentGeneration; - source.currentRequestOwner = - SpatiallyIndexedSkeletonChunkRequestOwner.VIEW_3D; + source.currentRequestOwner = is2dView + ? SpatiallyIndexedSkeletonChunkRequestOwner.VIEW_2D + : SpatiallyIndexedSkeletonChunkRequestOwner.VIEW_3D; forEachVisibleVolumetricChunk( projectionParameters, this.localPosition.value, From e006de254c930dada8f17376ff5e187cb544a905 Mon Sep 17 00:00:00 2001 From: Sean Martin Date: Tue, 5 May 2026 19:11:45 +0200 Subject: [PATCH 5/5] fix: remove double attachment --- src/skeleton/frontend.ts | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/src/skeleton/frontend.ts b/src/skeleton/frontend.ts index bbfcc6b75..e4bae5b71 100644 --- a/src/skeleton/frontend.ts +++ b/src/skeleton/frontend.ts @@ -41,7 +41,6 @@ import type { RenderLayerTransform, } from "#src/render_coordinate_transform.js"; import { getChunkTransformParameters } from "#src/render_coordinate_transform.js"; -import { RENDERED_VIEW_ADD_LAYER_RPC_ID } from "#src/render_layer_common.js"; import type { RenderScaleHistogram } from "#src/render_scale_statistics.js"; import type { RenderLayer, @@ -3080,16 +3079,6 @@ export class PerspectiveViewSpatiallyIndexedSkeletonLayer extends PerspectiveVie ) { super.attach(attachment); - // Manually add layer to backend - const backend = this.backend; - if (backend && backend.rpc) { - backend.rpc.invoke(RENDERED_VIEW_ADD_LAYER_RPC_ID, { - layer: backend.rpcId, - view: attachment.view.rpcId, - }); - } - - // Capture references to avoid losing 'this' context in callback const baseLayer = this.base; const redrawNeeded = this.redrawNeeded; @@ -3474,14 +3463,6 @@ export class SliceViewPanelSpatiallyIndexedSkeletonLayer extends SliceViewPanelR ) { super.attach(attachment); - const backend = this.backend; - if (backend && backend.rpc) { - backend.rpc.invoke(RENDERED_VIEW_ADD_LAYER_RPC_ID, { - layer: backend.rpcId, - view: attachment.view.rpcId, - }); - } - const baseLayer = this.base; const redrawNeeded = this.redrawNeeded; attachment.registerDisposer(