Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 0 additions & 56 deletions src/display_context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,6 @@
import { debounce } from "lodash-es";

import type { FrameNumberCounter } from "#src/chunk_manager/frontend.js";
import type {
PanelOverlaySource,
PanelOverlayTarget,
} from "#src/panel_overlay.js";
import { TrackableValue } from "#src/trackable_value.js";
import { animationFrameDebounce } from "#src/util/animation_frame_debounce.js";
import type { Borrowed } from "#src/util/disposable.js";
Expand Down Expand Up @@ -306,16 +302,6 @@ export abstract class RenderedPanel extends RefCounted {

abstract draw(): void;

// Repositions this panel's DOM overlays. Default no-op; overridden by panels
// that support overlays.
updateOverlays(): void {}

scheduleOverlayUpdate(): void {
if (this.visible) {
this.context.scheduleOverlayUpdate();
}
}

disposed() {
this.context.unmonitorPanel(this.element, this.monitorState);
this.context.removePanel(this);
Expand Down Expand Up @@ -654,45 +640,6 @@ export class DisplayContext extends RefCounted implements FrameNumberCounter {
animationFrameDebounce(() => this.draw()),
);

// Overlay sources shown on data panels, each with its optional panel-type
// target. Panels observe `panelOverlaysChanged` to add/remove their bindings.
readonly panelOverlays = new Map<PanelOverlaySource, PanelOverlayTarget>();
readonly panelOverlaysChanged = new NullarySignal();

/**
* Registers an overlay source shown on the data panels matching `target` (every
* data panel by default). Returns a disposer that removes it.
*/
registerPanelOverlay(
source: PanelOverlaySource,
target: PanelOverlayTarget = {},
): () => void {
this.panelOverlays.set(source, target);
this.panelOverlaysChanged.dispatch();
return () => {
if (this.panelOverlays.delete(source)) {
this.panelOverlaysChanged.dispatch();
}
};
}

// Repositions DOM overlays across all panels, coalesced per animation frame
// and independent of `scheduleRedraw`.
readonly scheduleOverlayUpdate = this.registerCancellable(
animationFrameDebounce(() => this.updateOverlays()),
);

private updateOverlays() {
this.ensureBoundsUpdated();
for (const panel of this.panels) {
if (!panel.shouldDraw) continue;
panel.ensureBoundsUpdated();
const { renderViewport } = panel;
if (renderViewport.width === 0 || renderViewport.height === 0) continue;
panel.updateOverlays();
}
}

ensureBoundsUpdated() {
const { resizeGeneration } = this;
if (this.boundsGeneration === resizeGeneration) return;
Expand Down Expand Up @@ -737,9 +684,6 @@ export class DisplayContext extends RefCounted implements FrameNumberCounter {
this.updateFinished.dispatch();
this.framerateMonitor.endLastTimeQuery(gl, ext);
this.framerateMonitor.grabAnyFinishedQueryResults(gl);
// Each panel's draw() already updated its overlays, so drop any pending
// overlay-only update.
this.scheduleOverlayUpdate.cancel();
}

getDepthArray(): Float32Array<ArrayBuffer> {
Expand Down
21 changes: 0 additions & 21 deletions src/layer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,6 @@ import {
PlaybackManager,
Position,
} from "#src/navigation_state.js";
import type { PanelOverlaySource } from "#src/panel_overlay.js";
import { isPanelOverlaySource } from "#src/panel_overlay.js";
import type { RenderLayerTransform } from "#src/render_coordinate_transform.js";
import {
RENDERED_VIEW_ADD_LAYER_RPC_ID,
Expand Down Expand Up @@ -1167,10 +1165,6 @@ export class MouseSelectionState implements PickState {
position: Float32Array = kEmptyFloat32Vec;
unsnappedPosition: Float32Array = kEmptyFloat32Vec;
active = false;
// When true, the global picking-indicator ring is hidden even though the mouse
// state is active. Set during a skeleton node move, where the on-screen node is
// driven by the drag preview rather than by picking.
pickingIndicatorSuppressed = false;
displayDimensions: DisplayDimensions | undefined = undefined;
pickedRenderLayer: RenderLayer | null = null;
pickedValue = 0n;
Expand Down Expand Up @@ -1695,21 +1689,6 @@ export function makeRenderedPanelVisibleLayerTracker<
info.registerDisposer(
layer.redrawNeeded.add(() => panel.scheduleRedraw()),
);
// Layers that contribute DOM panel overlays (e.g. skeleton
// selected/hovered node highlights) are bound to this panel; the binding
// (container + update wiring) is scoped to this per-(layer,panel) info.
const overlayPanel = panel as Partial<{
bindOverlaySource(
source: PanelOverlaySource,
owner: RefCounted,
): void;
}>;
if (
isPanelOverlaySource(layer) &&
typeof overlayPanel.bindOverlaySource === "function"
) {
overlayPanel.bindOverlaySource(layer, info);
}
const { backend } = layer;
if (backend) {
backend.rpc!.invoke(RENDERED_VIEW_ADD_LAYER_RPC_ID, {
Expand Down
10 changes: 0 additions & 10 deletions src/panel_overlay.css
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,10 @@
* limitations under the License.
*/

/* Per-panel overlay container: covers the panel, never intercepts pointer
events, and clips overlays to the panel bounds. */
.neuroglancer-panel-overlay-container {
position: absolute;
inset: 0;
pointer-events: none;
z-index: 10;
overflow: hidden;
}

/* Per-source sub-container within a panel; z-index is set from the source's
overlayPriority. */
.neuroglancer-panel-overlay-source {
position: absolute;
inset: 0;
pointer-events: none;
}
179 changes: 179 additions & 0 deletions src/panel_overlay.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
/**
* @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 {
applyRenderViewportToProjectionMatrix,
RenderViewport,
} from "#src/display_context.js";
import type { PanelOverlayHost } from "#src/panel_overlay.js";
import { PanelOverlayManager, projectToViewport } from "#src/panel_overlay.js";
import type { ProjectionParameters } from "#src/projection_parameters.js";
import { RefCounted } from "#src/util/disposable.js";
import { mat4 } from "#src/util/geom.js";

function makeParameters(options: {
projectionMat: mat4;
focalDistance: number;
logicalWidth: number;
logicalHeight: number;
visibleRegion?: {
leftFraction: number;
topFraction: number;
widthFraction: number;
heightFraction: number;
};
}): ProjectionParameters {
const {
focalDistance,
logicalWidth,
logicalHeight,
visibleRegion = {
leftFraction: 0,
topFraction: 0,
widthFraction: 1,
heightFraction: 1,
},
} = options;
const renderViewport = Object.assign(new RenderViewport(), {
logicalWidth,
logicalHeight,
visibleLeftFraction: visibleRegion.leftFraction,
visibleTopFraction: visibleRegion.topFraction,
visibleWidthFraction: visibleRegion.widthFraction,
visibleHeightFraction: visibleRegion.heightFraction,
});
const projectionMat = mat4.clone(options.projectionMat);
applyRenderViewportToProjectionMatrix(renderViewport, projectionMat);
const viewMatrix = mat4.fromTranslation(mat4.create(), [
0,
0,
-focalDistance,
]);
return {
...renderViewport,
projectionMat,
viewProjectionMat: mat4.multiply(mat4.create(), projectionMat, viewMatrix),
displayDimensionRenderInfo: {
displayDimensionIndices: Int32Array.of(0, 1, 2),
},
} as unknown as ProjectionParameters;
}

function makeDotOverlay(host: PanelOverlayHost) {
const dot = document.createElement("div");
host.container.appendChild(dot);
return Object.assign(new RefCounted(), {
update() {},
disposed() {
dot.remove();
},
});
}

describe("projectToViewport", () => {
it("puts the focal point at the panel center, on the focal plane", () => {
for (const projectionMat of [
mat4.perspective(mat4.create(), Math.PI / 2, 2, 0.5, 1.5),
mat4.ortho(mat4.create(), -2, 2, -1, 1, 0.5, 1.5),
]) {
const parameters = makeParameters({
projectionMat,
focalDistance: 1,
logicalWidth: 200,
logicalHeight: 100,
});
const point = projectToViewport(parameters, [0, 0, 0])!;
expect(point.viewportLeft).toBeCloseTo(100);
expect(point.viewportTop).toBeCloseTo(50);
expect(point.focalPlaneDepthFraction).toBeCloseTo(0);
}
});

it("keeps the focal point at the panel center when part of the panel is clipped", () => {
const parameters = makeParameters({
projectionMat: mat4.perspective(mat4.create(), Math.PI / 2, 2, 0.5, 1.5),
focalDistance: 1,
logicalWidth: 200,
logicalHeight: 100,
visibleRegion: {
leftFraction: 0.5,
topFraction: 0.25,
widthFraction: 0.5,
heightFraction: 0.75,
},
});
const point = projectToViewport(parameters, [0, 0, 0])!;
expect(point.viewportLeft).toBeCloseTo(100);
expect(point.viewportTop).toBeCloseTo(50);
});

it("gives no position for a point beyond the far plane", () => {
for (const projectionMat of [
mat4.perspective(mat4.create(), Math.PI / 2, 2, 0.5, 1.5),
mat4.ortho(mat4.create(), -2, 2, -1, 1, 0.5, 1.5),
]) {
const parameters = makeParameters({
projectionMat,
focalDistance: 1,
logicalWidth: 200,
logicalHeight: 100,
});
const twiceTheFocalDistance = [0, 0, -1];
expect(projectToViewport(parameters, twiceTheFocalDistance)).toBe(
undefined,
);
}
});

it("measures depth linearly from the focal plane under both projections", () => {
const perspective = makeParameters({
projectionMat: mat4.perspective(mat4.create(), Math.PI / 2, 2, 0.5, 1.5),
focalDistance: 1,
logicalWidth: 200,
logicalHeight: 100,
});
const orthographic = makeParameters({
projectionMat: mat4.ortho(mat4.create(), -2, 2, -1, 1, 0.5, 1.5),
focalDistance: 1,
logicalWidth: 200,
logicalHeight: 100,
});
const halfWayToFar = [0, 0, -0.25];
expect(
projectToViewport(perspective, halfWayToFar)!.focalPlaneDepthFraction,
).toBeCloseTo(0.5);
expect(
projectToViewport(orthographic, halfWayToFar)!.focalPlaneDepthFraction,
).toBeCloseTo(0.5);
});
});

describe("PanelOverlayManager", () => {
it("removes an overlay from the panel while other overlays stay", () => {
const panel = document.createElement("div");
const manager = new PanelOverlayManager(
panel,
() => undefined,
() => true,
);
const removeFirst = manager.add(makeDotOverlay);
manager.add(makeDotOverlay);
removeFirst();
manager.update();
expect(manager.container.children.length).toBe(1);
});
});
Loading
Loading