diff --git a/ui-charts/src/spaceTimeChart/components/ConflictLayer.tsx b/ui-charts/src/spaceTimeChart/components/ConflictLayer.tsx index 27070fc7..78a3aaaa 100644 --- a/ui-charts/src/spaceTimeChart/components/ConflictLayer.tsx +++ b/ui-charts/src/spaceTimeChart/components/ConflictLayer.tsx @@ -1,7 +1,7 @@ import { useCallback } from 'react'; import { useDraw, usePicking } from '../hooks/useCanvas'; -import { type DrawingFunction, type PickingDrawingFunction } from '../lib/types'; +import type { DrawingFunction, PickingDrawingFunction, PickingElement } from '../lib/types'; import { drawAliasedRect } from '../utils/canvas'; import { indexToColor, hexToRgb } from '../utils/colors'; @@ -12,6 +12,17 @@ export type Conflict = { spaceEnd: number; }; +export type ConflictPickingElement = PickingElement & { + type: 'conflict'; + conflictIndex: number; +}; + +export function isConflictPickingElement( + element: PickingElement +): element is ConflictPickingElement { + return element.type === 'conflict'; +} + export type ConflictLayerProps = { conflicts: Conflict[]; }; @@ -58,7 +69,8 @@ export const ConflictLayer = ({ conflicts }: ConflictLayerProps) => { const height = getSpacePixel(conflict.spaceEnd) - y; const border = BORDERS[0].size; - const index = registerPickingElement({ type: 'conflict', conflictIndex }); + const pickingElement: ConflictPickingElement = { type: 'conflict', conflictIndex }; + const index = registerPickingElement(pickingElement); const color = hexToRgb(indexToColor(index)); drawAliasedRect( diff --git a/ui-charts/src/spaceTimeChart/components/PathLayer.tsx b/ui-charts/src/spaceTimeChart/components/PathLayer.tsx index f562d0af..7786c0e1 100644 --- a/ui-charts/src/spaceTimeChart/components/PathLayer.tsx +++ b/ui-charts/src/spaceTimeChart/components/PathLayer.tsx @@ -11,6 +11,7 @@ import { type OperationalPoint, type PathData, type PickingDrawingFunction, + type PickingElement, type Point, type SpaceTimeChartContextType, } from '../lib/types'; @@ -28,6 +29,23 @@ const DEFAULT_PICKING_TOLERANCE = 5; const PAUSE_THICKNESS = 7; const PAUSE_OPACITY = 0.2; +export type PointPickingElement = PickingElement & { type: 'point'; pathId: string; point: Point }; + +export type SegmentPickingElement = PickingElement & { + type: 'segment'; + pathId: string; + from: Point; + to: Point; +}; + +export function isPointPickingElement(element: PickingElement): element is PointPickingElement { + return element.type === 'point'; +} + +export function isSegmentPickingElement(element: PickingElement): element is SegmentPickingElement { + return element.type === 'segment'; +} + type PathStyle = { width: number; endWidth: number; @@ -396,12 +414,13 @@ export const PathLayer = ({ getPathSegments(stcContext).forEach((point, i, a) => { if (i) { const previousPoint = a[i - 1]; - const index = registerPickingElement({ + const pickingElement: SegmentPickingElement = { type: 'segment', pathId: path.id, from: previousPoint, to: point, - }); + }; + const index = registerPickingElement(pickingElement); const lineColor = hexToRgb(indexToColor(index)); drawAliasedLine( imageData, @@ -416,11 +435,12 @@ export const PathLayer = ({ // Draw snap points: getSnapPoints(stcContext).forEach((point) => { - const index = registerPickingElement({ + const pickingElement: PointPickingElement = { type: 'point', pathId: path.id, point, - }); + }; + const index = registerPickingElement(pickingElement); const lineColor = hexToRgb(indexToColor(index)); drawAliasedDisc( imageData, diff --git a/ui-charts/src/spaceTimeChart/lib/types.ts b/ui-charts/src/spaceTimeChart/lib/types.ts index 483a7016..35706ddf 100644 --- a/ui-charts/src/spaceTimeChart/lib/types.ts +++ b/ui-charts/src/spaceTimeChart/lib/types.ts @@ -101,10 +101,7 @@ export const LAYERS = ['background', 'graduations', 'paths', 'overlay', 'caption export type LayerType = (typeof LAYERS)[number]; // PICKING SPECIFIC TYPES: -export type PickingElement = - | { type: 'point'; pathId: string; point: Point } - | { type: 'segment'; pathId: string; from: Point; to: Point } - | { type: 'conflict'; conflictIndex: number }; +export type PickingElement = { type: string }; export type HoveredItem = { layer: PickingLayerType; element: PickingElement }; export type DrawingFunction = ( diff --git a/ui-charts/src/spaceTimeChart/stories/layers.stories.tsx b/ui-charts/src/spaceTimeChart/stories/layers.stories.tsx index 52af9261..8c8e302c 100644 --- a/ui-charts/src/spaceTimeChart/stories/layers.stories.tsx +++ b/ui-charts/src/spaceTimeChart/stories/layers.stories.tsx @@ -9,6 +9,7 @@ import { OccupancyBlockLayer, SpaceTimeChart, PathLayer, + isConflictPickingElement, } from '..'; import { OPERATIONAL_POINTS, PATHS, START_DATE } from './lib/paths'; import { X_ZOOM_LEVEL, Y_ZOOM_LEVEL } from './lib/utils'; @@ -123,7 +124,7 @@ const Wrapper = () => { yOffset={0} onHoveredChildUpdate={({ item }) => { let conflict = null; - if (item?.element?.type === 'conflict') { + if (item && isConflictPickingElement(item.element)) { conflict = CONFLICTS[item.element.conflictIndex]; } setHoveredConflict(conflict); diff --git a/ui-charts/src/spaceTimeChart/stories/paths-interactions.stories.tsx b/ui-charts/src/spaceTimeChart/stories/paths-interactions.stories.tsx index e96e8589..316928c2 100644 --- a/ui-charts/src/spaceTimeChart/stories/paths-interactions.stories.tsx +++ b/ui-charts/src/spaceTimeChart/stories/paths-interactions.stories.tsx @@ -4,7 +4,7 @@ import type { Meta } from '@storybook/react'; import cx from 'classnames'; import { keyBy } from 'lodash'; -import { SpaceTimeChart, PathLayer } from '..'; +import { SpaceTimeChart, PathLayer, isPointPickingElement, isSegmentPickingElement } from '..'; import { OPERATIONAL_POINTS, PATHS } from './lib/paths'; import { X_ZOOM_LEVEL, Y_ZOOM_LEVEL, zoom } from './lib/utils'; import { type PathData, type Point } from '../lib/types'; @@ -115,7 +115,10 @@ const Wrapper = ({ } }} onHoveredChildUpdate={({ item }) => { - const hoveredPathId = item && 'pathId' in item.element ? item.element.pathId : null; + const hoveredPathId = + item && (isPointPickingElement(item.element) || isSegmentPickingElement(item.element)) + ? item.element.pathId + : null; setState((prev) => ({ ...prev, hoveredPathId })); }} onPan={({ initialPosition, position, initialData, data, isPanning }) => { diff --git a/ui-charts/src/spaceTimeChart/stories/scroll-navigation.stories.tsx b/ui-charts/src/spaceTimeChart/stories/scroll-navigation.stories.tsx index 748f5bd0..39f95aa0 100644 --- a/ui-charts/src/spaceTimeChart/stories/scroll-navigation.stories.tsx +++ b/ui-charts/src/spaceTimeChart/stories/scroll-navigation.stories.tsx @@ -4,7 +4,7 @@ import type { Meta } from '@storybook/react'; import cx from 'classnames'; import { keyBy } from 'lodash'; -import { SpaceTimeChart, PathLayer } from '..'; +import { SpaceTimeChart, PathLayer, isPointPickingElement, isSegmentPickingElement } from '..'; import { OPERATIONAL_POINTS, PATHS } from './lib/paths'; import { X_ZOOM_LEVEL, Y_ZOOM_LEVEL, zoom } from './lib/utils'; import { type Point } from '../lib/types'; @@ -79,7 +79,10 @@ const Wrapper = ({ spaceScaleType }: WrapperProps) => { })); }} onHoveredChildUpdate={({ item }) => { - const hoveredPathId = item && 'pathId' in item.element ? item.element.pathId : null; + const hoveredPathId = + item && (isPointPickingElement(item.element) || isSegmentPickingElement(item.element)) + ? item.element.pathId + : null; setState((s) => ({ ...s, hoveredPathId })); }} onPan={({ initialPosition, position, isPanning }) => { diff --git a/ui-charts/src/spaceTimeChart/utils/snapping.ts b/ui-charts/src/spaceTimeChart/utils/snapping.ts index 1a02fc2e..0057cbed 100644 --- a/ui-charts/src/spaceTimeChart/utils/snapping.ts +++ b/ui-charts/src/spaceTimeChart/utils/snapping.ts @@ -1,3 +1,4 @@ +import { isPointPickingElement, isSegmentPickingElement } from '../components/PathLayer'; import { type HoveredItem, type Point } from '../lib/types'; /** @@ -48,16 +49,15 @@ export function getClosestPointOnSegment( export function snapPosition(mousePosition: Point, hoveredItem: HoveredItem | null) { if (!mousePosition || !hoveredItem) return mousePosition; - switch (hoveredItem.element.type) { - case 'point': - return hoveredItem.element.point; - case 'segment': - return getClosestPointOnSegment( - mousePosition, - hoveredItem.element.from, - hoveredItem.element.to - ); - default: - return mousePosition; + if (isPointPickingElement(hoveredItem.element)) { + return hoveredItem.element.point; + } else if (isSegmentPickingElement(hoveredItem.element)) { + return getClosestPointOnSegment( + mousePosition, + hoveredItem.element.from, + hoveredItem.element.to + ); + } else { + return mousePosition; } }