Skip to content

Commit

Permalink
fix: deselect on click on PIXI canvas (#1599)
Browse files Browse the repository at this point in the history
  • Loading branch information
kswenson authored Nov 6, 2024
1 parent 9ffa190 commit 3fde474
Show file tree
Hide file tree
Showing 4 changed files with 64 additions and 26 deletions.
5 changes: 4 additions & 1 deletion v3/src/components/data-display/data-display-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
pointRadiusSelectionAddend, Rect, rTreeRect
} from "./data-display-types"
import {IDataConfigurationModel } from "./models/data-configuration-model"
import {IPixiPointStyle, PixiPoints} from "./pixi/pixi-points"
import {getPixiPointsDispatcher, IPixiPointStyle, PixiPoints} from "./pixi/pixi-points"
import {CaseDataWithSubPlot} from "./d3-types"

export const maxWidthOfStringsD3 = (strings: Iterable<string>) => {
Expand Down Expand Up @@ -51,6 +51,9 @@ export const computePointRadius = (numPoints: number, pointSizeMultiplier: numbe
}

export function handleClickOnCase(event: PointerEvent, caseID: string, dataset?: IDataSet) {
// click occurred on a point, so don't deselect
getPixiPointsDispatcher(event)?.cancelAnimationFrame("deselectAll")

const extendSelection = event.shiftKey,
caseIsSelected = dataset?.isCaseSelected(caseID)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import { PixiPoints } from "../pixi/pixi-points"
import { usePixiPointerDown } from "./use-pixi-pointer-down"

export function usePixiPointerDownDeselect(pixiPointsArray: PixiPoints[], model?: IDataDisplayContentModel) {
usePixiPointerDown(pixiPointsArray, event => {
usePixiPointerDown(pixiPointsArray, (event, pixiPoints: PixiPoints) => {
if (!event.shiftKey && !event.metaKey && !event.ctrlKey) {
const datasetsArray = model?.datasetsArray ?? []
datasetsArray.forEach(data => selectAllCases(data, false))
pixiPoints.requestAnimationFrame("deselectAll", () => {
const datasetsArray = model?.datasetsArray ?? []
datasetsArray.forEach(data => selectAllCases(data, false))
})
}
})
}
11 changes: 6 additions & 5 deletions v3/src/components/data-display/hooks/use-pixi-pointer-down.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,19 @@ import { useEffect } from "react"
import { useTileModelContext } from "../../../hooks/use-tile-model-context"
import { PixiPoints } from "../pixi/pixi-points"

export function usePixiPointerDown(pixiPointsArray: PixiPoints[], onPointerDown: (event: PointerEvent) => void) {
type OnPointerDownCallback = (event: PointerEvent, pixiPoints: PixiPoints) => void

export function usePixiPointerDown(pixiPointsArray: PixiPoints[], onPointerDown: OnPointerDownCallback) {
const { isTileSelected } = useTileModelContext()

useEffect(() => {
const handlePointerDownCapture = (event: PointerEvent) => {
// Browser events are dispatched directly to the PIXI canvas.
// Re-dispatched events are dispatched to elements behind the PIXI canvas.
const canvases: Array<EventTarget | null> = pixiPointsArray.map(pixiPoints => pixiPoints.canvas)
const isBrowserEventOnPixiCanvas = canvases.includes(event.target)
const pixiPointsIndex = pixiPointsArray.findIndex(pixiPoints => event.target === pixiPoints.canvas)
// first click selects tile; deselection only occurs once the tile is already selected
if (isBrowserEventOnPixiCanvas && isTileSelected()) {
onPointerDown(event)
if (pixiPointsIndex >= 0 && isTileSelected()) {
onPointerDown(event, pixiPointsArray[pixiPointsIndex])
}
}
window.addEventListener("pointerdown", handlePointerDownCapture, { capture: true })
Expand Down
66 changes: 49 additions & 17 deletions v3/src/components/data-display/pixi/pixi-points.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,16 @@ export interface IPixiPointStyle {
height?: number
}

export const PixiPointsAnimationFrameRequestIds = ["deselectAll"] as const
export type PixiPointsAnimationFrameRequestId = typeof PixiPointsAnimationFrameRequestIds[number]

// map from dispatched event to the PixiPoints instance that dispatched it
const pixiDispatchedEventsMap = new WeakMap<Event | PIXI.FederatedEvent, PixiPoints>()

export function getPixiPointsDispatcher(event: Event) {
return pixiDispatchedEventsMap.get(event)
}

// PixiPoints layer can be setup to distribute events from background to elements laying underneath.
export interface IBackgroundEventDistributionOptions {
elementToHide: HTMLElement | SVGElement // element which should be hidden to obtain element laying underneath
Expand Down Expand Up @@ -105,6 +115,9 @@ export class PixiPoints {
onPointDrag?: PixiPointEventHandler
onPointDragEnd?: PixiPointEventHandler

// map from id string to requestAnimationFrame id number
animationFrames = new Map<PixiPointsAnimationFrameRequestId, number>()

async init(options?: IPixiPointsOptions) {
// Automatically determines the most appropriate renderer for the current environment.
// The function will prioritize the WebGL renderer as it is the most tested safe API to use. In the near future as
Expand Down Expand Up @@ -542,6 +555,15 @@ export class PixiPoints {
}
}

dispatchEvent(targetElement: Element | null, event: Event, pixiEvent: PIXI.FederatedEvent) {
if (targetElement) {
// associate this PixiPoints instance with dispatched events
pixiDispatchedEventsMap.set(event, this)
pixiDispatchedEventsMap.set(pixiEvent, this)
targetElement.dispatchEvent(event)
}
}

setupBackgroundEventDistribution(options: IBackgroundEventDistributionOptions) {
const { elementToHide } = options

Expand All @@ -558,23 +580,17 @@ export class PixiPoints {
this.background.eventMode = "static"
// Click event redistribution.
this.background.on("click", (event: PIXI.FederatedPointerEvent) => {
const elementUnderneath = getElementUnderCanvas(event)
// Dispatch the same event to the element under the cursor.
if (elementUnderneath) {
elementUnderneath.dispatchEvent(new MouseEvent("click", event))
}
this.dispatchEvent(getElementUnderCanvas(event), new MouseEvent("click", event), event)
})

this.background.on("pointerdown", (event: PIXI.FederatedPointerEvent) => {
const elementUnderneath = getElementUnderCanvas(event)
// Dispatch the same event to the element under the cursor.
if (elementUnderneath) {
elementUnderneath.dispatchEvent(new PointerEvent("pointerdown", event))
}
this.dispatchEvent(getElementUnderCanvas(event), new PointerEvent("pointerdown", event), event)
})

// Handle mousemove events by dispatching mouseover/mouseout events to the elements beneath the cursor.
let mouseoverElement: Element | undefined
let mouseoverElement: Element | null = null
this.background.on("mousemove", (event: PIXI.FederatedPointerEvent) => {
const elementUnderneath = getElementUnderCanvas(event)
if (elementUnderneath && elementUnderneath === mouseoverElement) {
Expand All @@ -583,23 +599,39 @@ export class PixiPoints {
}
if (elementUnderneath) {
if (mouseoverElement && mouseoverElement !== elementUnderneath) {
mouseoverElement.dispatchEvent(new MouseEvent("mouseout", event))
this.dispatchEvent(mouseoverElement, new MouseEvent("mouseout", event), event)
}
elementUnderneath.dispatchEvent(new MouseEvent("mouseover", event))
this.dispatchEvent(elementUnderneath, new MouseEvent("mouseover", event), event)
mouseoverElement = elementUnderneath
} else if (mouseoverElement) {
mouseoverElement.dispatchEvent(new MouseEvent("mouseout", event))
mouseoverElement = undefined
this.dispatchEvent(mouseoverElement, new MouseEvent("mouseout", event), event)
mouseoverElement = null
}
})
this.background.on("mouseout", (event: PIXI.FederatedPointerEvent) => {
if (mouseoverElement) {
mouseoverElement.dispatchEvent(new MouseEvent("mouseout", event))
mouseoverElement = undefined
}
this.dispatchEvent(mouseoverElement, new MouseEvent("mouseout", event), event)
mouseoverElement = null
})
}

requestAnimationFrame(requestId: PixiPointsAnimationFrameRequestId, callback: () => void) {
// can only have one pending request of a given type
if (!this.animationFrames.get(requestId)) {
this.animationFrames.set(requestId, requestAnimationFrame(() => {
callback()
this.animationFrames.delete(requestId)
}))
}
}

cancelAnimationFrame(requestId: PixiPointsAnimationFrameRequestId) {
const frameToCancel = this.animationFrames.get(requestId)
if (frameToCancel != null) {
cancelAnimationFrame(frameToCancel)
this.animationFrames.delete(requestId)
}
}

setupSpriteInteractivity(sprite: PIXI.Sprite) {
sprite.eventMode = "static"
sprite.cursor = "pointer"
Expand Down

0 comments on commit 3fde474

Please sign in to comment.