-
Notifications
You must be signed in to change notification settings - Fork 0
fix: Correct picking formulas #142
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,6 +23,10 @@ import type { DisplayContext } from "#src/display_context.js"; | |
| import { RenderedPanel } from "#src/display_context.js"; | ||
| import type { NavigationState } from "#src/navigation_state.js"; | ||
| import { PickIDManager } from "#src/object_picking.js"; | ||
| import { | ||
| clearOutOfBoundsPickData, | ||
| getPickDiameter, | ||
| } from "#src/rendered_data_panel_picking.js"; | ||
| import { | ||
| displayToLayerCoordinates, | ||
| layerToDisplayCoordinates, | ||
|
|
@@ -77,88 +81,14 @@ export class PickRequest { | |
|
|
||
| const pickRequestInterval = 30; | ||
|
|
||
| export function getPickDiameter(pickRadius: number): number { | ||
| return 1 + pickRadius * 2; | ||
| } | ||
|
|
||
| let _cachedPickRadius = -1; | ||
| let _cachedPickOffsetSequence: Uint32Array | undefined; | ||
|
|
||
| /** | ||
| * Sequence of offsets into C order (pickDiamater, pickDiamater) array in order of increasing | ||
| * distance from center. | ||
| */ | ||
| export function getPickOffsetSequence(pickRadius: number) { | ||
| if (pickRadius === _cachedPickRadius) { | ||
| return _cachedPickOffsetSequence!; | ||
| } | ||
| _cachedPickRadius = pickRadius; | ||
| const pickDiameter = 1 + pickRadius * 2; | ||
| const maxDist2 = pickRadius ** 2; | ||
| const getDist2 = (x: number, y: number) => | ||
| (x - pickRadius) ** 2 + (y - pickRadius) ** 2; | ||
|
|
||
| let offsets = new Uint32Array(pickDiameter * pickDiameter); | ||
| let count = 0; | ||
| for (let x = 0; x < pickDiameter; ++x) { | ||
| for (let y = 0; y < pickDiameter; ++y) { | ||
| if (getDist2(x, y) > maxDist2) continue; | ||
| offsets[count++] = y * pickDiameter + x; | ||
| } | ||
| } | ||
| offsets = offsets.subarray(0, count); | ||
| offsets.sort((a, b) => { | ||
| const x1 = a % pickDiameter; | ||
| const y1 = (a - x1) / pickDiameter; | ||
| const x2 = b % pickDiameter; | ||
| const y2 = (b - x2) / pickDiameter; | ||
| return getDist2(x1, y1) - getDist2(x2, y2); | ||
| }); | ||
| return (_cachedPickOffsetSequence = offsets); | ||
| } | ||
|
|
||
| /** | ||
| * Sets array elements to 0 that would be outside the viewport. | ||
| * | ||
| * @param buffer Array view, which contains a C order (pickDiameter, pickDiameter) array. | ||
| * @param baseOffset Offset into `buffer` corresponding to (0, 0). | ||
| * @param stride Stride between consecutive elements of the array. | ||
| * @param glWindowX Center x position, must be integer. | ||
| * @param glWindowY Center y position, must be integer. | ||
| * @param viewportWidth Width of viewport in pixels. | ||
| * @param viewportHeight Width of viewport in pixels. | ||
| */ | ||
| export function clearOutOfBoundsPickData( | ||
| buffer: Float32Array, | ||
| baseOffset: number, | ||
| stride: number, | ||
| glWindowX: number, | ||
| glWindowY: number, | ||
| viewportWidth: number, | ||
| viewportHeight: number, | ||
| pickRadius: number, | ||
| ) { | ||
| const pickDiameter = 1 + pickRadius * 2; | ||
| const startX = glWindowX - pickRadius; | ||
| const startY = glWindowY - pickRadius; | ||
| if ( | ||
| startX >= 0 && | ||
| startY >= 0 && | ||
| startX + pickDiameter <= viewportWidth && | ||
| startY + pickDiameter <= viewportHeight | ||
| ) { | ||
| return; | ||
| } | ||
| for (let relativeY = 0; relativeY < pickDiameter; ++relativeY) { | ||
| for (let relativeX = 0; relativeX < pickDiameter; ++relativeX) { | ||
| const x = startX + relativeX; | ||
| const y = startY + relativeY; | ||
| if (x < 0 || y < 0 || x >= viewportWidth || y >= viewportHeight) { | ||
| buffer[baseOffset + (y * pickDiameter + x) * stride] = 0; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| export { | ||
| clearOutOfBoundsPickData, | ||
| getCenteredPickWindowCoordinate, | ||
| getPickDiameter, | ||
| getPickOffsetSequence, | ||
| resolveNearestPanelPickSample, | ||
| type ResolvedPanelPickSample, | ||
| } from "#src/rendered_data_panel_picking.js"; | ||
|
|
||
| export abstract class RenderedDataPanel extends RenderedPanel { | ||
| /** | ||
|
|
@@ -234,13 +164,15 @@ export abstract class RenderedDataPanel extends RenderedPanel { | |
| gl.bindBuffer(WebGL2RenderingContext.PIXEL_PACK_BUFFER, buffer); | ||
| } | ||
| const { renderViewport } = this; | ||
| const glWindowX = | ||
| const glWindowX = Math.floor( | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note for when we make PR - Problem, without rounding to int (floor makes sense) it is possible we could end up with a floating point value here. This immediately goes into readPixels which is expecting an int, and how our float is handled is likely implementation dependent. It is also used in clearOutOfBoundsPickData, but with our other fix to use relative X and Y that could handle fractional data so it's less of a problem there. This change is related to the change to add 0.5 to the pick because now we're flooring and using the 0.5 to use the center. But I think we can remove the 0.5 add and land at the pixel edge instead of the pixel centre. In neuroglancer, the origin of a voxel is the lower corner anyway, not the centre. I've noted this bit elsewhere |
||
| this.mouseX - | ||
| renderViewport.visibleLeftFraction * renderViewport.logicalWidth; | ||
| const glWindowY = | ||
| renderViewport.visibleLeftFraction * renderViewport.logicalWidth, | ||
| ); | ||
| const glWindowY = Math.floor( | ||
| renderViewport.height - | ||
| (this.mouseY - | ||
| renderViewport.visibleTopFraction * renderViewport.logicalHeight); | ||
| (this.mouseY - | ||
| renderViewport.visibleTopFraction * renderViewport.logicalHeight), | ||
| ); | ||
| this.issuePickRequest(glWindowX, glWindowY, pickRadius); | ||
| pickRequest.sync = gl.fenceSync( | ||
| WebGL2RenderingContext.SYNC_GPU_COMMANDS_COMPLETE, | ||
|
|
@@ -882,7 +814,7 @@ export abstract class RenderedDataPanel extends RenderedPanel { | |
| ((clientX - bounds.left) / bounds.width) * element.offsetWidth - | ||
| element.clientLeft; | ||
| const mouseY = | ||
| ((clientY - bounds.top) / bounds.height) * element.offsetHeight + | ||
| ((clientY - bounds.top) / bounds.height) * element.offsetHeight - | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note for PR - definite problem - the border around the panels is accidentally getting added to mouse position. Result would be a 4 pixel off selection in Y, and moreso if the border size is expanded. |
||
| element.clientTop; | ||
| const { mouseState } = this.viewer; | ||
| mouseState.pageX = clientX + window.scrollX; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| /** | ||
| * @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 { | ||
| clearOutOfBoundsPickData, | ||
| getPickDiameter, | ||
| getPickOffsetSequence, | ||
| resolveNearestPanelPickSample, | ||
| } from "#src/rendered_data_panel_picking.js"; | ||
|
|
||
| describe("resolveNearestPanelPickSample", () => { | ||
| it("dereferences slice pick data using the sampled offset", () => { | ||
| const pickRadius = 5; | ||
| const pickOffsetSequence = getPickOffsetSequence(pickRadius); | ||
| const targetOffset = pickOffsetSequence[1]; | ||
| const data = new Float32Array(4 * getPickDiameter(pickRadius) ** 2); | ||
| data[4 * targetOffset] = 17; | ||
|
|
||
| expect( | ||
| resolveNearestPanelPickSample(data, pickOffsetSequence, pickRadius), | ||
| ).toEqual({ | ||
| offset: targetOffset, | ||
| relativeX: targetOffset % getPickDiameter(pickRadius), | ||
| relativeY: | ||
| (targetOffset - (targetOffset % getPickDiameter(pickRadius))) / | ||
| getPickDiameter(pickRadius), | ||
| pickValue: 17, | ||
| depthValue: undefined, | ||
| }); | ||
| }); | ||
|
|
||
| it("returns depth and pick payload from the same sampled pixel", () => { | ||
| const pickRadius = 2; | ||
| const pickDiameter = getPickDiameter(pickRadius); | ||
| const pickOffsetSequence = getPickOffsetSequence(pickRadius); | ||
| const targetOffset = pickOffsetSequence[3]; | ||
| const data = new Float32Array(2 * 4 * pickDiameter * pickDiameter); | ||
| data[4 * targetOffset] = 0.25; | ||
| data[4 * pickDiameter * pickDiameter + 4 * targetOffset] = 23; | ||
|
|
||
| expect( | ||
| resolveNearestPanelPickSample(data, pickOffsetSequence, pickRadius, { | ||
| depthBaseOffset: 0, | ||
| pickBaseOffset: 4 * pickDiameter * pickDiameter, | ||
| }), | ||
| ).toEqual({ | ||
| offset: targetOffset, | ||
| relativeX: targetOffset % pickDiameter, | ||
| relativeY: (targetOffset - (targetOffset % pickDiameter)) / pickDiameter, | ||
| pickValue: 23, | ||
| depthValue: 0.25, | ||
| }); | ||
| }); | ||
| }); | ||
|
|
||
| describe("clearOutOfBoundsPickData", () => { | ||
| it("zeros the relative pick-window indices that fall outside the viewport", () => { | ||
| const pickRadius = 1; | ||
| const pickDiameter = getPickDiameter(pickRadius); | ||
| const buffer = new Float32Array(4 * pickDiameter * pickDiameter).fill(1); | ||
|
|
||
| clearOutOfBoundsPickData( | ||
| buffer, | ||
| 0, | ||
| 4, | ||
| 0, | ||
| 0, | ||
| 3, | ||
| 3, | ||
| pickRadius, | ||
| ); | ||
|
|
||
| expect(buffer[0]).toBe(0); | ||
| expect(buffer[4]).toBe(0); | ||
| expect(buffer[4 * pickDiameter]).toBe(0); | ||
| expect(buffer[4 * (pickDiameter + 1)]).toBe(1); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To change - We can avoid the re-export, neuroglancer doesn't guarantee a stable API