diff --git a/src/skeleton/frontend.spec.ts b/src/skeleton/frontend.spec.ts index 6fc32c77c..d6670a78d 100644 --- a/src/skeleton/frontend.spec.ts +++ b/src/skeleton/frontend.spec.ts @@ -129,13 +129,8 @@ describe("SpatiallyIndexedSkeletonLayer selected node outline color", () => { }, saturation: { value: 0 }, hoverHighlight: { value: true }, - segmentSelectionState: { isSelected }, + segmentSelectionState: { isSelected, baseValue: 101n }, }; - const getCachedNodeSnapshot = vi.fn(() => ({ - nodeId: 101, - segmentId: 11, - position: new Float32Array([1, 2, 3]), - })); const layer = Object.assign( Object.create(SpatiallyIndexedSkeletonLayer.prototype), { @@ -144,15 +139,12 @@ describe("SpatiallyIndexedSkeletonLayer selected node outline color", () => { selectedNodeOutlineColorGeneration: 0, cachedSelectedNodeOutlineColorGeneration: -1, displayState, - getCachedNodeSnapshot, }, ); const outlineColor = (layer as any).getSelectedNodeOutlineColor(); const cachedOutlineColor = (layer as any).getSelectedNodeOutlineColor(); - expect(getCachedNodeSnapshot).toHaveBeenCalledWith(101); - expect(getCachedNodeSnapshot).toHaveBeenCalledTimes(1); expect(isSelected).not.toHaveBeenCalled(); expect(cachedOutlineColor).toBe(outlineColor); expect(outlineColor[0]).toBeCloseTo(1); @@ -181,13 +173,11 @@ describe("SpatiallyIndexedSkeletonLayer selected node outline color", () => { }, saturation: { value: 1 }, hoverHighlight: { value: false }, - segmentSelectionState: { isSelected: vi.fn(() => false) }, + segmentSelectionState: { + isSelected: vi.fn(() => false), + baseValue: 101n, + }, }; - const getCachedNodeSnapshot = vi.fn((nodeId: number) => ({ - nodeId, - segmentId: 11, - position: new Float32Array([1, 2, 3]), - })); const layer = Object.assign( Object.create(SpatiallyIndexedSkeletonLayer.prototype), { @@ -196,7 +186,6 @@ describe("SpatiallyIndexedSkeletonLayer selected node outline color", () => { selectedNodeOutlineColorGeneration: 0, cachedSelectedNodeOutlineColorGeneration: -1, displayState, - getCachedNodeSnapshot, }, ); @@ -205,7 +194,6 @@ describe("SpatiallyIndexedSkeletonLayer selected node outline color", () => { ++(layer as any).selectedNodeOutlineColorGeneration; (layer as any).getSelectedNodeOutlineColor(); - expect(getCachedNodeSnapshot).toHaveBeenCalledTimes(2); expect(computeSegmentColor).toHaveBeenCalledTimes(2); }); @@ -226,13 +214,11 @@ describe("SpatiallyIndexedSkeletonLayer selected node outline color", () => { }, saturation: { value: 1 }, hoverHighlight: { value: false }, - segmentSelectionState: { isSelected: vi.fn(() => false) }, + segmentSelectionState: { + isSelected: vi.fn(() => false), + baseValue: 101n, + }, }; - const getCachedNodeSnapshot = vi.fn(() => ({ - nodeId: 101, - segmentId: 11, - position: new Float32Array([1, 2, 3]), - })); const layer = Object.assign( Object.create(SpatiallyIndexedSkeletonLayer.prototype), { @@ -241,7 +227,6 @@ describe("SpatiallyIndexedSkeletonLayer selected node outline color", () => { selectedNodeOutlineColorGeneration: 0, cachedSelectedNodeOutlineColorGeneration: -1, displayState, - getCachedNodeSnapshot, }, ); @@ -249,54 +234,8 @@ describe("SpatiallyIndexedSkeletonLayer selected node outline color", () => { ++(layer as any).selectedNodeOutlineColorGeneration; (layer as any).getSelectedNodeOutlineColor(); - expect(getCachedNodeSnapshot).toHaveBeenCalledTimes(2); expect(computeSegmentColor).toHaveBeenCalledTimes(2); }); - - it("returns the fallback outline color for an invalid selected segment", () => { - const computeSegmentColor = vi.fn(); - const displayState = { - segmentationColorGroupState: { - value: { - segmentStatedColors: new Map(), - segmentDefaultColor: { value: undefined }, - segmentColorHash: { compute: computeSegmentColor }, - }, - }, - saturation: { value: 1 }, - hoverHighlight: { value: false }, - segmentSelectionState: { isSelected: vi.fn(() => false) }, - }; - const getCachedNodeSnapshot = vi.fn(() => ({ - nodeId: 101, - segmentId: 0, - position: new Float32Array([1, 2, 3]), - })); - const layer = Object.assign( - Object.create(SpatiallyIndexedSkeletonLayer.prototype), - { - selectedNodeId: { value: 101 }, - selectedNodeOutlineColor: vec3.create(), - selectedNodeOutlineColorGeneration: 0, - cachedSelectedNodeOutlineColorGeneration: -1, - displayState, - getCachedNodeSnapshot, - }, - ); - - const outlineColor = (layer as any).getSelectedNodeOutlineColor(); - const cachedOutlineColor = (layer as any).getSelectedNodeOutlineColor(); - - expect(getCachedNodeSnapshot).toHaveBeenCalledWith(101); - expect(getCachedNodeSnapshot).toHaveBeenCalledTimes(1); - expect(computeSegmentColor).not.toHaveBeenCalled(); - expect(outlineColor[0]).toBeCloseTo(1); - expect(outlineColor[1]).toBeCloseTo(0.95); - expect(outlineColor[2]).toBeCloseTo(0.35); - expect(cachedOutlineColor[0]).toBeCloseTo(1); - expect(cachedOutlineColor[1]).toBeCloseTo(0.95); - expect(cachedOutlineColor[2]).toBeCloseTo(0.35); - }); }); describe("SpatiallyIndexedSkeletonLayer targeted source invalidation", () => { diff --git a/src/skeleton/frontend.ts b/src/skeleton/frontend.ts index c97e30a0b..24c772ac8 100644 --- a/src/skeleton/frontend.ts +++ b/src/skeleton/frontend.ts @@ -107,6 +107,7 @@ import type { SliceViewPanelReadyRenderContext, } from "#src/sliceview/renderlayer.js"; import { SliceViewPanelRenderLayer } from "#src/sliceview/renderlayer.js"; +import { TrackableBoolean } from "#src/trackable_boolean.js"; import type { WatchableValueInterface } from "#src/trackable_value.js"; import { makeCachedLazyDerivedWatchableValue, @@ -118,7 +119,6 @@ import { Uint64Set } from "#src/uint64_set.js"; import { gatherUpdate } from "#src/util/array.js"; import { computeHighVisibilityContrastColor } from "#src/util/color.js"; import { hsvToRgb } from "#src/util/colorspace.js"; -import { TrackableBoolean } from "#src/trackable_boolean.js"; import { DataType } from "#src/util/data_type.js"; import { RefCounted } from "#src/util/disposable.js"; import type { ValueOrError } from "#src/util/error.js"; @@ -178,7 +178,6 @@ import { OneDimensionalTextureAccessHelper, setOneDimensionalTextureData, TextureFormat, - updateOneDimensionalTextureElement, } from "#src/webgl/texture_access.js"; import { defineVertexId, VertexIdHelper } from "#src/webgl/vertex_id.js"; import type { RPC } from "#src/worker_rpc.js"; @@ -190,8 +189,6 @@ const DEBUG_SPATIAL_SKELETON_CHUNKS = false; const tempChunkKeyToColorMap = new Map(); const tempMat4 = mat4.create(); -const OVERLAY_SELECTED_FLOAT_ZERO = new Float32Array([0]); -const OVERLAY_SELECTED_FLOAT_ONE = new Float32Array([1]); const DEFAULT_FRAGMENT_MAIN = `void main() { emitDefault(); } @@ -286,7 +283,7 @@ class RenderHelper extends RefCounted { private vertexIdHelper; private segmentAttributeIndex: number | undefined; private segmentColorAttributeIndex: number | undefined; - private selectedNodeAttributeIndex: number | undefined; + private nodeIdAttributeIndex: number | undefined; private visibleSegmentsShaderManager = new HashSetShaderManager( "visibleSegments", ); @@ -369,10 +366,7 @@ void spatialChunkCull() { ); } for (let i = 1; i < numAttributes; ++i) { - if ( - i === this.segmentAttributeIndex || - i === this.selectedNodeAttributeIndex - ) { + if (i === this.segmentAttributeIndex || i === this.nodeIdAttributeIndex) { continue; } const info = vertexAttributes[i]; @@ -461,11 +455,9 @@ void spatialChunkCull() { const hoverAdjustFragment = params.hoverHighlight ? ` - if (segmentId.value.x == uHoveredSegmentId.x && - segmentId.value.y == uHoveredSegmentId.y) { - if (saturation > 0.5) { saturation -= 0.5; } - else { saturation += 0.5; } - }` + float isHovered = float(segmentId.value.x == uHoveredSegmentId.x && + segmentId.value.y == uHoveredSegmentId.y); + saturation += isHovered * (0.5 - step(0.5, saturation));` : ""; builder.addFragmentCode(` @@ -600,11 +592,11 @@ vec4 getSegmentAppearance(highp uint segmentValue) { this.segmentAttributeIndex = segmentAttrIndex >= 0 ? segmentAttrIndex : undefined; this.segmentColorAttributeIndex = base.segmentColorAttributeIndex; - const selectedNodeAttrIndex = this.vertexAttributes.findIndex( - (x) => x.name === selectedNodeAttribute.name, + const nodeIdAttrIndex = this.vertexAttributes.findIndex( + (x) => x.name === nodeIdAttribute.name, ); - this.selectedNodeAttributeIndex = - selectedNodeAttrIndex >= 0 ? selectedNodeAttrIndex : undefined; + this.nodeIdAttributeIndex = + nodeIdAttrIndex >= 0 ? nodeIdAttrIndex : undefined; const segmentationGroupState = base.displayState.segmentationGroupState.value; @@ -766,8 +758,9 @@ void emitDefault() { ); builder.addUniform("highp float", "uNodeDiameter"); let selectedOutlineWidthExpression = "0.0"; - if (this.selectedNodeAttributeIndex !== undefined) { + if (this.nodeIdAttributeIndex !== undefined) { builder.addUniform("highp vec3", "uSelectedNodeOutlineColor"); + builder.addUniform("highp int", "uSelectedNodeId"); builder.addVarying("highp float", "vSelectedNode", "flat"); const selectedOutlineMinWidth = this.targetIsSliceView ? SELECTED_NODE_OUTLINE_MIN_WIDTH_2D @@ -775,7 +768,7 @@ void emitDefault() { const selectedOutlineMaxWidth = this.targetIsSliceView ? SELECTED_NODE_OUTLINE_MAX_WIDTH_2D : SELECTED_NODE_OUTLINE_MAX_WIDTH_3D; - selectedOutlineWidthExpression = `((vSelectedNode > 0.5) ? clamp(0.25 * uNodeDiameter, ${selectedOutlineMinWidth}, ${selectedOutlineMaxWidth}) : 0.0)`; + selectedOutlineWidthExpression = `(vSelectedNode * clamp(0.25 * uNodeDiameter, ${selectedOutlineMinWidth}, ${selectedOutlineMaxWidth}))`; } let vertexMain = ` highp uint vertexIndex = uint(gl_InstanceID); @@ -786,8 +779,8 @@ highp vec3 vertexPosition = readAttribute0(vertexIndex); if (skeletonParams.spatialChunkCulling) { vertexMain += `vCullPos = vertexPosition;\n`; } - if (this.selectedNodeAttributeIndex !== undefined) { - vertexMain += `vSelectedNode = readAttribute${this.selectedNodeAttributeIndex}(vertexIndex);\n`; + if (this.nodeIdAttributeIndex !== undefined) { + vertexMain += `vSelectedNode = float(readAttribute${this.nodeIdAttributeIndex}(vertexIndex).value == uSelectedNodeId);\n`; } if ( skeletonParams.dynamicSegmentAppearance && @@ -811,14 +804,10 @@ emitCircle( // saturation and hover highlight all resolved in the shader via // getSegmentAppearance(). uColor is unused in this path. const segmentExpression = `vSegmentValue`; - const selectedNodeExpression = - this.selectedNodeAttributeIndex === undefined - ? undefined - : "vSelectedNode"; - const borderColorExpression = - selectedNodeExpression === undefined - ? "renderColor" - : `((${selectedNodeExpression} > 0.5) ? vec4(uSelectedNodeOutlineColor, renderColor.a) : renderColor)`; + const hasNodeIdSelection = this.nodeIdAttributeIndex !== undefined; + const borderColorExpression = hasNodeIdSelection + ? `mix(renderColor, vec4(uSelectedNodeOutlineColor, renderColor.a), vSelectedNode)` + : "renderColor"; builder.addFragmentCode(` vec4 segmentColor() { return getSegmentAppearance(${segmentExpression}); @@ -861,14 +850,10 @@ void emitDefault() { } else { // Per-vertex color attribute path: color comes from a per-vertex // attribute; alpha is taken from the attribute's alpha component. - const selectedNodeExpression = - this.selectedNodeAttributeIndex === undefined - ? undefined - : "vSelectedNode"; - const borderColorExpression = - selectedNodeExpression === undefined - ? "renderColor" - : `((${selectedNodeExpression} > 0.5) ? vec4(uSelectedNodeOutlineColor, renderColor.a) : renderColor)`; + const hasNodeIdSelection = this.nodeIdAttributeIndex !== undefined; + const borderColorExpression = hasNodeIdSelection + ? `mix(renderColor, vec4(uSelectedNodeOutlineColor, renderColor.a), vSelectedNode)` + : "renderColor"; builder.addFragmentCode(` vec4 segmentColor() { return ${segmentColorExpression}; @@ -1564,12 +1549,12 @@ const segmentAttribute: VertexAttributeRenderInfo = { glslDataType: getShaderType(DataType.UINT32, 1), }; -const selectedNodeAttribute: VertexAttributeRenderInfo = { - dataType: DataType.FLOAT32, +const nodeIdAttribute: VertexAttributeRenderInfo = { + dataType: DataType.INT32, numComponents: 1, - name: "selectedNodeAttr", - webglDataType: WebGL2RenderingContext.FLOAT, - glslDataType: "float", + name: "nodeId", + webglDataType: WebGL2RenderingContext.INT, + glslDataType: getShaderType(DataType.INT32, 1), }; interface SkeletonChunkBase extends SkeletonGPUGeometry { @@ -1687,6 +1672,31 @@ export class SpatiallyIndexedSkeletonChunk copyToGPU(gl: GL) { super.copyToGPU(gl); uploadSkeletonChunkToGPU(gl, this); + // Upload nodeIds as the 3rd vertex attribute texture (index 2). + // vertexAttributeOffsets only covers position (0) and segment (1), so we + // handle nodeId separately here since it is stored outside the packed buffer. + const nodeIdFormat = this.source.attributeTextureFormats[2]; + if ( + nodeIdFormat !== undefined && + this.nodeIds.length === this.numVertices && + this.numVertices > 0 + ) { + const texture = gl.createTexture(); + gl.bindTexture(WebGL2RenderingContext.TEXTURE_2D, texture); + setOneDimensionalTextureData( + gl, + nodeIdFormat, + new Uint8Array( + this.nodeIds.buffer, + this.nodeIds.byteOffset, + this.nodeIds.byteLength, + ), + ); + gl.bindTexture(WebGL2RenderingContext.TEXTURE_2D, null); + this.vertexAttributeTextures[2] = texture; + } else { + this.vertexAttributeTextures[2] = null; + } } freeGPUMemory(gl: GL) { @@ -1703,6 +1713,7 @@ type SpatiallyIndexedSkeletonChunkListener = ( const spatiallyIndexedSkeletonTextureAttributeSpecs = Object.freeze([ { name: "position", dataType: DataType.FLOAT32, numComponents: 3 }, { name: "segment", dataType: DataType.UINT32, numComponents: 1 }, + { name: "nodeId", dataType: DataType.INT32, numComponents: 1 }, ]); export class SpatiallyIndexedSkeletonSource extends SliceViewChunkSource< @@ -1715,7 +1726,11 @@ export class SpatiallyIndexedSkeletonSource extends SliceViewChunkSource< constructor(chunkManager: ChunkManager, options: any) { super(chunkManager, options); - this.vertexAttributes = [vertexPositionAttribute, segmentAttribute]; + this.vertexAttributes = [ + vertexPositionAttribute, + segmentAttribute, + nodeIdAttribute, + ]; } get attributeTextureFormats() { @@ -1834,8 +1849,6 @@ class SkeletonOverlayChunk implements SkeletonGPUGeometry { readonly pickNodePositions: Float32Array; readonly pickSegmentIds: Uint32Array; readonly pickEdgeSegmentIds: Uint32Array; - private readonly nodeIdToVertexIndex: Map; - private readonly selectedFormat: TextureFormat; constructor( gl: GL, @@ -1854,9 +1867,9 @@ class SkeletonOverlayChunk implements SkeletonGPUGeometry { geometry.segmentIds.byteLength, ), new Uint8Array( - geometry.selected.buffer, - geometry.selected.byteOffset, - geometry.selected.byteLength, + geometry.nodeIds.buffer, + geometry.nodeIds.byteOffset, + geometry.nodeIds.byteLength, ), ]; const overlayTextures: (WebGLTexture | null)[] = @@ -1881,52 +1894,6 @@ class SkeletonOverlayChunk implements SkeletonGPUGeometry { this.pickNodePositions = geometry.positions; this.pickSegmentIds = geometry.pickSegmentIds; this.pickEdgeSegmentIds = geometry.pickEdgeSegmentIds; - const nodeIdToVertexIndex = new Map(); - const { nodeIds } = geometry; - for (let i = 0; i < nodeIds.length; i++) { - const nodeId = nodeIds[i]; - if (nodeId > 0) nodeIdToVertexIndex.set(nodeId, i); - } - this.nodeIdToVertexIndex = nodeIdToVertexIndex; - this.selectedFormat = formats[2]; - } - - // Updates the selected-node highlight in-place without a full GPU rebuild. - // Clears oldNodeId's texel and sets newNodeId's texel. - updateSelectedNode( - gl: GL, - oldNodeId: number | undefined, - newNodeId: number | undefined, - ) { - if (oldNodeId === newNodeId) return; - const texture = this.vertexAttributeTextures[2]; - if (texture === null) return; - if (oldNodeId !== undefined) { - const idx = this.nodeIdToVertexIndex.get(oldNodeId); - if (idx !== undefined) { - updateOneDimensionalTextureElement( - gl, - texture, - this.selectedFormat, - this.numVertices, - idx, - OVERLAY_SELECTED_FLOAT_ZERO, - ); - } - } - if (newNodeId !== undefined) { - const idx = this.nodeIdToVertexIndex.get(newNodeId); - if (idx !== undefined) { - updateOneDimensionalTextureElement( - gl, - texture, - this.selectedFormat, - this.numVertices, - idx, - OVERLAY_SELECTED_FLOAT_ONE, - ); - } - } } dispose(gl: GL) { @@ -2058,7 +2025,7 @@ export class SpatiallyIndexedSkeletonLayer redrawNeeded = new NullarySignal(); vertexAttributes: VertexAttributeRenderInfo[]; segmentColorAttributeIndex: number | undefined; - selectedNodeAttributeIndex: number | undefined; + nodeIdAttributeIndex: number | undefined; readonly browsePassLayerView: SkeletonShaderContext; readonly skeletonShaderParameters: WatchableValue; readonly browsePassSkeletonShaderParameters: WatchableValueInterface; @@ -2093,9 +2060,7 @@ export class SpatiallyIndexedSkeletonLayer | undefined; private inspectionState: SpatiallyIndexedSkeletonInspectionState | undefined; private overlayChunk: SkeletonOverlayChunk | undefined; - private overlayChunkKey: string | undefined; private overlayGeometryKey: string | undefined; - private cachedSelectedNodeId: number | undefined; private overlayRebuildFrame = -1; private pendingOverlaySegmentLoads = new Set(); private browseExcludedSegments = new Uint64Set(); @@ -2113,9 +2078,7 @@ export class SpatiallyIndexedSkeletonLayer private disposeOverlayChunk() { this.overlayChunk?.dispose(this.gl); this.overlayChunk = undefined; - this.overlayChunkKey = undefined; this.overlayGeometryKey = undefined; - this.cachedSelectedNodeId = undefined; } private requestOverlaySegmentLoad(segmentId: number) { @@ -2174,20 +2137,15 @@ export class SpatiallyIndexedSkeletonLayer if (isCacheValid) { return this.selectedNodeOutlineColor; } - const segmentId = this.getCachedNodeSnapshot(selectedNodeId)?.segmentId; - const normalizedSegmentId = Math.round(Number(segmentId)); - if ( - !Number.isSafeInteger(normalizedSegmentId) || - normalizedSegmentId <= 0 - ) { - this.cachedSelectedNodeOutlineColorGeneration = currentGeneration; - this.selectedNodeOutlineColor.set(SELECTED_NODE_OUTLINE_FALLBACK_COLOR); + const segmentId = this.displayState.segmentSelectionState.baseValue; + if (segmentId === undefined) { return SELECTED_NODE_OUTLINE_FALLBACK_COLOR; } + this.cachedSelectedNodeOutlineColorGeneration = currentGeneration; return computeHighVisibilityContrastColor( this.selectedNodeOutlineColor, - getBaseObjectColor(this.displayState, BigInt(normalizedSegmentId)), + getBaseObjectColor(this.displayState, segmentId), ); } @@ -2335,27 +2293,16 @@ export class SpatiallyIndexedSkeletonLayer } const overlayGeometryKey = this.getOverlayGeometryKey(loadedSegmentIds); - const selectedNodeId = this.selectedNodeId?.value; - const overlayChunkKey = `${overlayGeometryKey}|selected:${selectedNodeId ?? ""}`; if (this.overlayChunk !== undefined) { if (this.overlayGeometryKey === overlayGeometryKey) { - // Geometry unchanged — update only the selected-node highlight in-place - // rather than reallocating all GPU textures. - if (this.overlayChunkKey !== overlayChunkKey) { - this.overlayChunk.updateSelectedNode( - this.gl, - this.cachedSelectedNodeId, - selectedNodeId, - ); - this.overlayChunkKey = overlayChunkKey; - this.cachedSelectedNodeId = selectedNodeId; - } + // Geometry unchanged — selection is driven by uSelectedNodeId uniform + // at draw time, so no GPU rebuild is needed when selection changes. return this.overlayChunk; } } - // Pass 2: geometry cache miss — collect node sets and rebuild. + // Geometry cache miss — collect node sets and rebuild. const segmentNodeSets: (readonly SpatiallyIndexedSkeletonNode[])[] = []; for (const segmentId of loadedSegmentIds) { const segmentNodes = @@ -2367,19 +2314,14 @@ export class SpatiallyIndexedSkeletonLayer this.disposeOverlayChunk(); const geometry = buildSpatiallyIndexedSkeletonOverlayGeometry( segmentNodeSets, - { - selectedNodeId, - getPendingNodePosition: this.getPendingNodePositionOverride, - }, + { getPendingNodePosition: this.getPendingNodePositionOverride }, ); this.overlayChunk = new SkeletonOverlayChunk( this.gl, geometry, this.overlayAttributeTextureFormats, ); - this.overlayChunkKey = overlayChunkKey; this.overlayGeometryKey = overlayGeometryKey; - this.cachedSelectedNodeId = selectedNodeId; return this.overlayChunk; } @@ -2462,10 +2404,7 @@ export class SpatiallyIndexedSkeletonLayer }), ); - this.vertexAttributes = [ - ...this.source.vertexAttributes, - selectedNodeAttribute, - ]; + this.vertexAttributes = [...this.source.vertexAttributes]; this.skeletonShaderParameters = new WatchableValue({ dynamicSegmentAppearance: true, @@ -2543,11 +2482,10 @@ export class SpatiallyIndexedSkeletonLayer displayState: this.displayState, skeletonShaderParameters: this.browsePassSkeletonShaderParameters, }; - const selectedNodeIndex = this.vertexAttributes.findIndex( - (x) => x.name === selectedNodeAttribute.name, + const nodeIdIndex = this.vertexAttributes.findIndex( + (x) => x.name === nodeIdAttribute.name, ); - this.selectedNodeAttributeIndex = - selectedNodeIndex >= 0 ? selectedNodeIndex : undefined; + this.nodeIdAttributeIndex = nodeIdIndex >= 0 ? nodeIdIndex : undefined; const requestRedraw = () => this.redrawNeeded.dispatch(); const selectedNodeWatchable = this.selectedNodeId; if (selectedNodeWatchable?.changed) { @@ -2983,6 +2921,16 @@ export class SpatiallyIndexedSkeletonLayer if (passState === undefined) return; const { gl, edgeShader, nodeShader, skeletonParams } = passState; + nodeShader.bind(); + gl.uniform3fv( + nodeShader.uniform("uSelectedNodeOutlineColor"), + this.getSelectedNodeOutlineColor(), + ); + gl.uniform1i( + nodeShader.uniform("uSelectedNodeId"), + this.selectedNodeId?.value ?? -1, + ); + const chunkOrigin = vec3.create(); const chunkBound = vec3.create(); for (const { chunk, chunkLayout } of visibleChunks) { @@ -3101,6 +3049,10 @@ export class SpatiallyIndexedSkeletonLayer nodeShader.uniform("uSelectedNodeOutlineColor"), this.getSelectedNodeOutlineColor(), ); + gl.uniform1i( + nodeShader.uniform("uSelectedNodeId"), + this.selectedNodeId?.value ?? -1, + ); if (renderContext.emitPickID) { const edgePickId = diff --git a/src/skeleton/segment_overlay.spec.ts b/src/skeleton/segment_overlay.spec.ts index 63ad56df8..c49532db5 100644 --- a/src/skeleton/segment_overlay.spec.ts +++ b/src/skeleton/segment_overlay.spec.ts @@ -55,7 +55,6 @@ describe("buildSpatiallyIndexedSkeletonOverlayGeometry", () => { ], ], { - selectedNodeId: 2, getPendingNodePosition: (nodeId) => nodeId === 3 ? new Float32Array([70, 80, 90]) : undefined, }, @@ -64,7 +63,6 @@ describe("buildSpatiallyIndexedSkeletonOverlayGeometry", () => { expect(geometry.numVertices).toBe(3); expect([...geometry.nodeIds]).toEqual([1, 2, 3]); expect([...geometry.segmentIds]).toEqual([11, 11, 13]); - expect([...geometry.selected]).toEqual([0, 1, 0]); expect([...geometry.positions]).toEqual([1, 2, 3, 4, 5, 6, 70, 80, 90]); expect([...geometry.indices]).toEqual([1, 0]); expect([...geometry.pickEdgeSegmentIds]).toEqual([11]); diff --git a/src/skeleton/segment_overlay.ts b/src/skeleton/segment_overlay.ts index 91cb56c6c..822875f23 100644 --- a/src/skeleton/segment_overlay.ts +++ b/src/skeleton/segment_overlay.ts @@ -27,21 +27,19 @@ let gpuScratchCapacity = 0; // in vertices // Layout per capacity-slot (cap = gpuScratchCapacity): // [0, cap*4) — segmentIds (Uint32, 4 B/vertex) -// [cap*4, cap*8) — selected (Float32, 4 B/vertex) -// [cap*8, cap*16) — edgeIndices (Uint32 pairs, 8 B/vertex max) -// [cap*16, cap*20) — edgeSegIds (Uint32, 4 B/vertex max) +// [cap*4, cap*12) — edgeIndices (Uint32 pairs, 8 B/vertex max) +// [cap*12, cap*16) — edgeSegIds (Uint32, 4 B/vertex max) function ensureGpuScratch(numVertices: number) { if (numVertices > gpuScratchCapacity) { const cap = Math.max(numVertices, gpuScratchCapacity * 2, 64); - gpuScratchBuffer = new ArrayBuffer(cap * 20); + gpuScratchBuffer = new ArrayBuffer(cap * 16); gpuScratchCapacity = cap; } const cap = gpuScratchCapacity; return { segmentIds: new Uint32Array(gpuScratchBuffer, 0, numVertices), - selected: new Float32Array(gpuScratchBuffer, cap * 4, numVertices), - edgeIndices: new Uint32Array(gpuScratchBuffer, cap * 8, numVertices * 2), - edgeSegIds: new Uint32Array(gpuScratchBuffer, cap * 16, numVertices), + edgeIndices: new Uint32Array(gpuScratchBuffer, cap * 4, numVertices * 2), + edgeSegIds: new Uint32Array(gpuScratchBuffer, cap * 12, numVertices), }; } @@ -55,7 +53,6 @@ export interface SpatiallyIndexedSkeletonOverlayNodeLike { export interface SpatiallyIndexedSkeletonOverlayGeometry { positions: Float32Array; segmentIds: Uint32Array; - selected: Float32Array; nodeIds: Int32Array; pickSegmentIds: Uint32Array; pickEdgeSegmentIds: Uint32Array; @@ -66,11 +63,10 @@ export interface SpatiallyIndexedSkeletonOverlayGeometry { export function buildSpatiallyIndexedSkeletonOverlayGeometry( segmentNodeSets: readonly (readonly SpatiallyIndexedSkeletonOverlayNodeLike[])[], options: { - selectedNodeId?: number; getPendingNodePosition?: (nodeId: number) => ArrayLike | undefined; } = {}, ): SpatiallyIndexedSkeletonOverlayGeometry { - const { selectedNodeId, getPendingNodePosition } = options; + const { getPendingNodePosition } = options; const nodeIndex = new Map(); const orderedNodes: SpatiallyIndexedSkeletonOverlayNodeLike[] = []; @@ -94,7 +90,7 @@ export function buildSpatiallyIndexedSkeletonOverlayGeometry( // valid until SkeletonOverlayChunk uploads them to the GPU (synchronous), after // which this buffer is safe to reuse on the next build. const scratch = ensureGpuScratch(numVertices); - const { segmentIds, selected, edgeIndices, edgeSegIds } = scratch; + const { segmentIds, edgeIndices, edgeSegIds } = scratch; orderedNodes.forEach((node, index) => { const position = getPendingNodePosition?.(node.nodeId) ?? node.position; @@ -105,8 +101,6 @@ export function buildSpatiallyIndexedSkeletonOverlayGeometry( segmentIds[index] = Math.max(0, Math.round(Number(node.segmentId))); pickSegmentIds[index] = segmentIds[index]; nodeIds[index] = Math.round(Number(node.nodeId)); - selected[index] = - selectedNodeId !== undefined && node.nodeId === selectedNodeId ? 1 : 0; }); let edgeCount = 0; @@ -133,7 +127,6 @@ export function buildSpatiallyIndexedSkeletonOverlayGeometry( positions, // Subarray views into the scratch: consumed immediately by GPU upload. segmentIds: segmentIds.subarray(0, numVertices), - selected: selected.subarray(0, numVertices), nodeIds, pickSegmentIds, // Compact copy: CPU-retained by SkeletonOverlayChunk for edge picking. diff --git a/src/ui/command_palette.spec.ts b/src/ui/command_palette.spec.ts index af532113f..a067a03ec 100644 --- a/src/ui/command_palette.spec.ts +++ b/src/ui/command_palette.spec.ts @@ -38,7 +38,9 @@ describe("collectActionBindings", () => { const map = new EventActionMap(); map.set("keya", "some-action"); const bindings = collectActionBindings(makeViewer(map)); - expect(bindings.map((binding) => binding.actionId)).toContain("some-action"); + expect(bindings.map((binding) => binding.actionId)).toContain( + "some-action", + ); }); it("excludes mouse and wheel events", () => { diff --git a/src/ui/skeleton_tab.ts b/src/ui/skeleton_tab.ts index 781cd7c0d..be24369f3 100644 --- a/src/ui/skeleton_tab.ts +++ b/src/ui/skeleton_tab.ts @@ -683,7 +683,7 @@ export class SpatialSkeletonEditTab extends Tab { try { getSegmentNavigationGraph(segmentId); return true; - } catch (error) { + } catch { StatusMessage.showTemporaryMessage(NAVIGATE_FROM_SPATIAL_INDEX_MESSAGE); return false; } diff --git a/src/viewer.ts b/src/viewer.ts index e7839ffab..2c740bb3d 100644 --- a/src/viewer.ts +++ b/src/viewer.ts @@ -1172,14 +1172,13 @@ export class Viewer extends RefCounted implements ViewerState { openPalette = new CommandPalette(this, dispatchTarget); }; this.bindAction("open-command-palette", openCommandPalette); - // Document-level capture fires before bubble handlers, ensuring F1 works - // even when focus is inside a tool's input element outside viewer.element. + // Document-level capture to ensure that the command palette opens even when focus is inside a tool's input element outside viewer.element. this.registerDisposer( registerEventListener( document, "keydown", (event: KeyboardEvent) => { - if (event.code === "F1") { + if (event.code === "KeyP" && event.ctrlKey) { event.preventDefault(); openCommandPalette(); } diff --git a/src/webgl/texture_access.browser_test.ts b/src/webgl/texture_access.browser_test.ts index 2f3a4f616..0cb1030eb 100644 --- a/src/webgl/texture_access.browser_test.ts +++ b/src/webgl/texture_access.browser_test.ts @@ -22,7 +22,6 @@ import { OneDimensionalTextureAccessHelper, setOneDimensionalTextureData, TextureFormat, - updateOneDimensionalTextureElement, } from "#src/webgl/texture_access.js"; function testTextureAccess(dataLength: number) { @@ -100,157 +99,3 @@ describe("one_dimensional_texture_access", () => { test1dTextureAccess(128 * 128 * 128); }); }); - -describe("updateOneDimensionalTextureElement", () => { - test("updates a single element in a 1-D uint32 texture", () => { - const dataType = DataType.UINT32; - fragmentShaderTest( - { uOffset: "uint" }, - { outputValue: dataType }, - (tester) => { - const { gl, builder } = tester; - const numComponents = 1; - const format = new TextureFormat(); - computeTextureFormat(format, dataType, numComponents); - - const dataLength = 128; - const data = new Uint32Array(dataLength); - for (let i = 0; i < data.length; ++i) { - data[i] = i; - } - - const accessHelper = new OneDimensionalTextureAccessHelper( - "textureUpdate", - ); - const textureUnitSymbol = Symbol("textureUnit"); - accessHelper.defineShader(builder); - builder.addTextureSampler("usampler2D", "uSampler", textureUnitSymbol); - builder.addFragmentCode( - accessHelper.getAccessor( - "readValue", - "uSampler", - dataType, - numComponents, - ), - ); - builder.setFragmentMain(` -outputValue = readValue(uOffset); -`); - - tester.build(); - const { shader } = tester; - shader.bind(); - - const textureUnit = shader.textureUnit(textureUnitSymbol); - const texture = gl.createTexture()!; - tester.registerDisposer(() => { - gl.deleteTexture(texture); - }); - gl.bindTexture(gl.TEXTURE_2D, texture); - setOneDimensionalTextureData(gl, format, data); - gl.bindTexture(gl.TEXTURE_2D, null); - - const updatedIndex = 42; - const updatedValue = 99999; - const update = new Uint32Array([updatedValue]); - updateOneDimensionalTextureElement( - gl, - texture, - format, - dataLength, - updatedIndex, - update, - ); - - for (let i = 0; i < dataLength; ++i) { - gl.activeTexture(gl.TEXTURE0 + textureUnit); - gl.bindTexture(gl.TEXTURE_2D, texture); - tester.execute({ uOffset: i }); - gl.bindTexture(gl.TEXTURE_2D, null); - const expected = i === updatedIndex ? updatedValue : i; - expect(tester.values.outputValue, `offset=${i}`).toBe(expected); - } - }, - ); - }); - - test("out-of-bounds index is a no-op", () => { - const dataType = DataType.UINT32; - fragmentShaderTest( - { uOffset: "uint" }, - { outputValue: dataType }, - (tester) => { - const { gl, builder } = tester; - const numComponents = 1; - const format = new TextureFormat(); - computeTextureFormat(format, dataType, numComponents); - - const dataLength = 16; - const data = new Uint32Array(dataLength); - for (let i = 0; i < data.length; ++i) { - data[i] = i; - } - - const accessHelper = new OneDimensionalTextureAccessHelper( - "textureOob", - ); - const textureUnitSymbol = Symbol("textureUnit"); - accessHelper.defineShader(builder); - builder.addTextureSampler("usampler2D", "uSampler", textureUnitSymbol); - builder.addFragmentCode( - accessHelper.getAccessor( - "readValue", - "uSampler", - dataType, - numComponents, - ), - ); - builder.setFragmentMain(` -outputValue = readValue(uOffset); -`); - - tester.build(); - const { shader } = tester; - shader.bind(); - - const textureUnit = shader.textureUnit(textureUnitSymbol); - const texture = gl.createTexture()!; - tester.registerDisposer(() => { - gl.deleteTexture(texture); - }); - gl.bindTexture(gl.TEXTURE_2D, texture); - setOneDimensionalTextureData(gl, format, data); - gl.bindTexture(gl.TEXTURE_2D, null); - - const sentinel = new Uint32Array([99999]); - updateOneDimensionalTextureElement( - gl, - texture, - format, - dataLength, - -1, - sentinel, - ); - updateOneDimensionalTextureElement( - gl, - texture, - format, - dataLength, - dataLength, - sentinel, - ); - - for (let i = 0; i < dataLength; ++i) { - gl.activeTexture(gl.TEXTURE0 + textureUnit); - gl.bindTexture(gl.TEXTURE_2D, texture); - tester.execute({ uOffset: i }); - gl.bindTexture(gl.TEXTURE_2D, null); - expect( - tester.values.outputValue, - `element ${i} unchanged after oob update`, - ).toBe(i); - } - }, - ); - }); -}); diff --git a/src/webgl/texture_access.ts b/src/webgl/texture_access.ts index 471c41b1a..d3a58b5e5 100644 --- a/src/webgl/texture_access.ts +++ b/src/webgl/texture_access.ts @@ -347,50 +347,6 @@ export function setOneDimensionalTextureData( ); } -function getOneDimensionalTextureRowCapacity(gl: GL, numElements: number) { - const minX = Math.ceil(numElements / gl.maxTextureSize); - return 1 << Math.ceil(Math.log2(Math.max(minX, 1))); -} - -export function updateOneDimensionalTextureElement( - gl: GL, - texture: WebGLTexture, - format: TextureFormat, - numElements: number, - elementIndex: number, - data: TypedArray, -) { - if (elementIndex < 0 || elementIndex >= numElements) { - return; - } - const { arrayConstructor, texelsPerElement, textureFormat, texelType } = - format; - if (data.constructor !== arrayConstructor) { - data = new arrayConstructor( - data.buffer, - data.byteOffset, - data.byteLength / arrayConstructor.BYTES_PER_ELEMENT, - ); - } - const elementsPerRow = getOneDimensionalTextureRowCapacity(gl, numElements); - const x = (elementIndex % elementsPerRow) * texelsPerElement; - const y = Math.floor(elementIndex / elementsPerRow); - gl.bindTexture(WebGL2RenderingContext.TEXTURE_2D, texture); - gl.pixelStorei(WebGL2RenderingContext.UNPACK_ALIGNMENT, 1); - gl.texSubImage2D( - WebGL2RenderingContext.TEXTURE_2D, - 0, - x, - y, - texelsPerElement, - 1, - textureFormat, - texelType, - data, - ); - gl.bindTexture(WebGL2RenderingContext.TEXTURE_2D, null); -} - export function setTwoDimensionalTextureData( gl: GL, format: TextureFormat,