From b20fa35e9f1380684338cd50a1469d8ea7c52d1b Mon Sep 17 00:00:00 2001 From: Valentin Chanas Date: Mon, 24 Feb 2025 23:56:54 +0100 Subject: [PATCH] save --- .../src/consts.ts | 4 + .../src/helpers.ts | 9 ++ .../hooks/useManchetteWithSpaceTimeChart.ts | 153 +++++++++++++++--- .../src/stories/rectangle-zoom.stories.tsx | 86 ++++++++++ .../src/styles/stories/rectangle-zoom.css | 26 +++ .../src/components/RectangleZoom.tsx | 29 ++++ ui-spacetimechart/src/index.ts | 1 + 7 files changed, 283 insertions(+), 25 deletions(-) create mode 100644 ui-manchette-with-spacetimechart/src/stories/rectangle-zoom.stories.tsx create mode 100644 ui-manchette-with-spacetimechart/src/styles/stories/rectangle-zoom.css create mode 100644 ui-spacetimechart/src/components/RectangleZoom.tsx diff --git a/ui-manchette-with-spacetimechart/src/consts.ts b/ui-manchette-with-spacetimechart/src/consts.ts index c85b1bcb1..3052e14e6 100644 --- a/ui-manchette-with-spacetimechart/src/consts.ts +++ b/ui-manchette-with-spacetimechart/src/consts.ts @@ -8,6 +8,10 @@ export const DEFAULT_ZOOM_MS_PER_PX = 7500; export const MIN_ZOOM_X = 0; export const MAX_ZOOM_X = 100; +export const MIN_ZOOM_METRE_PER_PX = 10000; +export const MAX_ZOOM_METRE_PER_PX = 10; +export const DEFAULT_ZOOM_METRE_PER_PX = 300; + export const MIN_ZOOM_Y = 1; export const MAX_ZOOM_Y = 10.5; export const ZOOM_Y_DELTA = 0.5; diff --git a/ui-manchette-with-spacetimechart/src/helpers.ts b/ui-manchette-with-spacetimechart/src/helpers.ts index 1536bde12..c817f278c 100644 --- a/ui-manchette-with-spacetimechart/src/helpers.ts +++ b/ui-manchette-with-spacetimechart/src/helpers.ts @@ -3,8 +3,10 @@ import { clamp } from 'lodash'; import { BASE_WAYPOINT_HEIGHT, + MAX_ZOOM_METRE_PER_PX, MAX_ZOOM_MS_PER_PX, MAX_ZOOM_X, + MIN_ZOOM_METRE_PER_PX, MIN_ZOOM_MS_PER_PX, MIN_ZOOM_X, } from './consts'; @@ -132,6 +134,13 @@ export const timeScaleToZoomValue = (timeScale: number) => (100 * Math.log(timeScale / MIN_ZOOM_MS_PER_PX)) / Math.log(MAX_ZOOM_MS_PER_PX / MIN_ZOOM_MS_PER_PX); +export const zoomValueToSpaceScale = (slider: number) => + MIN_ZOOM_METRE_PER_PX * Math.pow(MAX_ZOOM_METRE_PER_PX / MIN_ZOOM_METRE_PER_PX, slider / 100); + +export const spaceScaleToZoomValue = (spaceScale: number) => + (100 * Math.log(spaceScale / MIN_ZOOM_METRE_PER_PX)) / + Math.log(MAX_ZOOM_METRE_PER_PX / MIN_ZOOM_METRE_PER_PX); + /** Zoom on X axis and center on the mouse position */ export const zoomX = ( currentZoom: number, diff --git a/ui-manchette-with-spacetimechart/src/hooks/useManchetteWithSpaceTimeChart.ts b/ui-manchette-with-spacetimechart/src/hooks/useManchetteWithSpaceTimeChart.ts index c5f0d52a0..7e1025755 100644 --- a/ui-manchette-with-spacetimechart/src/hooks/useManchetteWithSpaceTimeChart.ts +++ b/ui-manchette-with-spacetimechart/src/hooks/useManchetteWithSpaceTimeChart.ts @@ -7,25 +7,45 @@ import type { } from '@osrd-project/ui-spacetimechart/dist/lib/types'; import usePaths from './usePaths'; -import { MAX_ZOOM_Y, MIN_ZOOM_Y, ZOOM_Y_DELTA, DEFAULT_ZOOM_MS_PER_PX } from '../consts'; +import { + MAX_ZOOM_Y, + MIN_ZOOM_Y, + ZOOM_Y_DELTA, + DEFAULT_ZOOM_MS_PER_PX, + MAX_ZOOM_MS_PER_PX, + MIN_ZOOM_MS_PER_PX, + MAX_ZOOM_METRE_PER_PX, + MIN_ZOOM_METRE_PER_PX, +} from '../consts'; import { computeWaypointsToDisplay, getScales, zoomX, zoomValueToTimeScale, timeScaleToZoomValue, + spaceScaleToZoomValue, } from '../helpers'; import { getDiff } from '../utils/point'; +import { clamp } from 'lodash'; type State = { xZoom: number; yZoom: number; + timeOrigin: number; + spaceOrigin: number; xOffset: number; /** the current y-scroll of the view. always updates */ yOffset: number; /** only update after a zoom. used to update back the view scroll value */ scrollTo: number | null; panning: { initialOffset: { x: number; y: number } } | null; + zoomMode: boolean; + rect: { + timeStart: Date; + timeEnd: Date; + spaceStart: number; // mm + spaceEnd: number; // mm + } | null; isProportional: boolean; waypointsChart: Waypoint[]; scales: SpaceScale[]; @@ -43,17 +63,34 @@ const useManchettesWithSpaceTimeChart = ( const [state, setState] = useState({ xZoom: timeScaleToZoomValue(DEFAULT_ZOOM_MS_PER_PX), yZoom: 1, + timeOrigin: Math.min(...projectPathTrainResult.map((p) => +p.departureTime)), + spaceOrigin: 0, xOffset: 0, yOffset: 0, scrollTo: null, panning: null, + zoomMode: false, + rect: null, isProportional: true, waypointsChart: [], scales: [], }); - const { xZoom, yZoom, xOffset, yOffset, scrollTo, panning, isProportional } = state; + const { + xZoom, + yZoom, + timeOrigin, + spaceOrigin, + xOffset, + yOffset, + scrollTo, + panning, + zoomMode, + rect, + isProportional, + } = state; + console.log(yZoom); const paths = usePaths(projectPathTrainResult, selectedTrain); const waypointsToDisplay = useMemo( @@ -72,6 +109,42 @@ const useManchettesWithSpaceTimeChart = ( [waypointsToDisplay] ); + const handleRectangleZoom = useCallback( + ({ + scales: { chosenTimeScale, chosenSpaceScale }, + overrideState, + }: { + scales: { chosenTimeScale: number; chosenSpaceScale: number }; + overrideState?: Partial; + }) => { + setState((prev) => { + if (prev.zoomMode || !prev.rect) { + return prev; + } + + const newTimeScale = clamp(chosenTimeScale, MAX_ZOOM_MS_PER_PX, MIN_ZOOM_MS_PER_PX); + const newSpaceScale = clamp(chosenSpaceScale, MAX_ZOOM_METRE_PER_PX, MIN_ZOOM_METRE_PER_PX); + const timeZoomValue = timeScaleToZoomValue(newTimeScale); + const spaceZoomValue = spaceScaleToZoomValue(newSpaceScale); + + const leftRectSide = Math.min(Number(prev.rect.timeStart), Number(prev.rect.timeEnd)); + const topRectSide = Math.min(prev.rect.spaceStart, prev.rect.spaceEnd); + const newXOffset = (timeOrigin - leftRectSide) / newTimeScale; + const newYOffset = (spaceOrigin - topRectSide) / newSpaceScale; + + return { + ...prev, + timeZoomValue: timeZoomValue, + spaceZoomValue: spaceZoomValue, + xOffset: newXOffset, + yOffset: newYOffset, + ...overrideState, + }; + }); + }, + [timeOrigin, spaceOrigin] + ); + const zoomYIn = useCallback(() => { if (yZoom < MAX_ZOOM_Y) { const newYZoom = yZoom + ZOOM_Y_DELTA; @@ -99,6 +172,16 @@ const useManchettesWithSpaceTimeChart = ( } }, [yZoom, yOffset]); + const handleXZoom = useCallback( + (newXZoom: number, xPosition = (spaceTimeChartRef?.current?.offsetWidth || 0) / 2) => { + setState((prev) => ({ + ...prev, + ...zoomX(prev.xZoom, prev.xOffset, newXZoom, xPosition), + })); + }, + [spaceTimeChartRef] + ); + useEffect(() => { if (scrollTo !== null && manchetteWithSpaceTimeChartContainer.current) { manchetteWithSpaceTimeChartContainer.current.scrollTo({ @@ -152,6 +235,11 @@ const useManchettesWithSpaceTimeChart = ( [simplifiedWaypoints, height, isProportional, yZoom] ); + const toggleZoomMode = useCallback(() => { + setState((prev) => ({ ...prev, zoomMode: !prev.zoomMode })); + }, []); + // console.log('computedScales', computedScales); + const manchetteProps = useMemo( () => ({ waypoints: waypointsToDisplay, @@ -166,16 +254,6 @@ const useManchettesWithSpaceTimeChart = ( [waypointsToDisplay, zoomYIn, zoomYOut, resetZoom, toggleMode, yZoom, isProportional, yOffset] ); - const handleXZoom = useCallback( - (newXZoom: number, xPosition = (spaceTimeChartRef?.current?.offsetWidth || 0) / 2) => { - setState((prev) => ({ - ...prev, - ...zoomX(prev.xZoom, prev.xOffset, newXZoom, xPosition), - })); - }, - [spaceTimeChartRef] - ); - const spaceTimeChartProps = useMemo( () => ({ operationalPoints: simplifiedWaypoints, @@ -184,24 +262,47 @@ const useManchettesWithSpaceTimeChart = ( paths, xOffset, yOffset: -yOffset + 14, + timeOrigin, + spaceOrigin, + rect, onZoom: ({ delta, position }: Parameters>[0]) => { if (isShiftPressed) { handleXZoom(xZoom + delta, position.x); } }, - onPan: (payload: { - initialPosition: { x: number; y: number }; - position: { x: number; y: number }; - isPanning: boolean; - }) => { + onPan: (payload: Parameters>[0]) => { const diff = getDiff(payload.initialPosition, payload.position); - const newState = { ...state }; + setState((prev) => { + if (!payload.isPanning) { + return { + ...prev, + panning: null, + zoomMode: false, + }; + } - if (!payload.isPanning) { - newState.panning = null; - } else if (!panning) { - newState.panning = { initialOffset: { x: xOffset, y: yOffset } }; - } else { + if (state.zoomMode) { + const newRect = { + timeStart: new Date(payload.initialData.time), + timeEnd: new Date(payload.data.time), + spaceStart: payload.initialData.position, + spaceEnd: payload.data.position, + }; + + return { + ...prev, + rect: newRect, + }; + } + + if (!panning) { + return { + ...prev, + panning: { initialOffset: { x: xOffset, y: yOffset } }, + }; + } + + const newState = { ...prev }; const { initialOffset } = panning; newState.xOffset = initialOffset.x + diff.x; @@ -215,8 +316,8 @@ const useManchettesWithSpaceTimeChart = ( newState.yOffset = newYPos; manchetteWithSpaceTimeChartContainer.current.scrollTop = newYPos; } - } - setState(newState); + return newState; + }); }, }), [ @@ -241,6 +342,8 @@ const useManchettesWithSpaceTimeChart = ( handleScroll, handleXZoom, xZoom, + toggleZoomMode, + zoomMode, }), [manchetteProps, spaceTimeChartProps, handleScroll, handleXZoom, xZoom] ); diff --git a/ui-manchette-with-spacetimechart/src/stories/rectangle-zoom.stories.tsx b/ui-manchette-with-spacetimechart/src/stories/rectangle-zoom.stories.tsx new file mode 100644 index 000000000..a432eb97d --- /dev/null +++ b/ui-manchette-with-spacetimechart/src/stories/rectangle-zoom.stories.tsx @@ -0,0 +1,86 @@ +import React, { useRef } from 'react'; + +import Manchette, { type ProjectPathTrainResult, type Waypoint } from '@osrd-project/ui-manchette'; +import { PathLayer, SpaceTimeChart, RectangleZoom } from '@osrd-project/ui-spacetimechart'; +import type { Meta } from '@storybook/react'; + +import '@osrd-project/ui-core/dist/theme.css'; +import '@osrd-project/ui-manchette/dist/theme.css'; +import '@osrd-project/ui-manchette-with-spacetimechart/dist/theme.css'; +import '../styles/stories/rectangle-zoom.css'; + +import { SAMPLE_WAYPOINTS, SAMPLE_PATHS_DATA } from '../assets/sampleData'; +import useManchettesWithSpaceTimeChart from '../hooks/useManchetteWithSpaceTimeChart'; +import { ZoomIn } from '@osrd-project/ui-icons'; + +type ManchetteWithSpaceTimeWrapperProps = { + waypoints: Waypoint[]; + projectPathTrainResult: ProjectPathTrainResult[]; + selectedTrain: number; +}; + +const DEFAULT_HEIGHT = 561; + +const ManchetteWithSpaceTimeWrapper = ({ + waypoints, + projectPathTrainResult, + selectedTrain, +}: ManchetteWithSpaceTimeWrapperProps) => { + const manchetteWithSpaceTimeChartRef = useRef(null); + + const { manchetteProps, spaceTimeChartProps, handleScroll, toggleZoomMode, zoomMode } = + useManchettesWithSpaceTimeChart( + waypoints, + projectPathTrainResult, + manchetteWithSpaceTimeChartRef, + selectedTrain + ); + + return ( +
+
+
+ +
+
+ +
+ + {spaceTimeChartProps.paths.map((path) => ( + + ))} + {spaceTimeChartProps.rect && } + +
+
+
+ ); +}; + +const meta: Meta = { + title: 'Manchette with SpaceTimeChart/Zoom rectangle', + component: ManchetteWithSpaceTimeWrapper, +}; + +export default meta; + +export const Default = { + args: { + waypoints: SAMPLE_WAYPOINTS, + projectPathTrainResult: SAMPLE_PATHS_DATA, + selectedTrain: 1, + }, +}; diff --git a/ui-manchette-with-spacetimechart/src/styles/stories/rectangle-zoom.css b/ui-manchette-with-spacetimechart/src/styles/stories/rectangle-zoom.css new file mode 100644 index 000000000..f01931233 --- /dev/null +++ b/ui-manchette-with-spacetimechart/src/styles/stories/rectangle-zoom.css @@ -0,0 +1,26 @@ +.space-time-chart-container { + .toolbar { + position: absolute; + right: 7px; + top: 7px; + z-index: 10; + border: 1px solid theme('colors.black.10'); + border-radius: 5px; + + button { + outline: none; + background: theme('colors.white.100'); + color: theme('colors.black.100'); + padding: 5px 7px 0; + } + + button:not(:last-child) { + border-radius: 5px 0 0 5px; + border-right: 1px solid theme('colors.black.10'); + } + + button:last-child { + border-radius: 5px; + } + } +} diff --git a/ui-spacetimechart/src/components/RectangleZoom.tsx b/ui-spacetimechart/src/components/RectangleZoom.tsx new file mode 100644 index 000000000..7fbe06d51 --- /dev/null +++ b/ui-spacetimechart/src/components/RectangleZoom.tsx @@ -0,0 +1,29 @@ +import React from 'react'; + +import { DottedBorderRect } from './DottedBorderRect'; + +export type CanvasRectProps = { + rect: { + timeStart: Date; + timeEnd: Date; + spaceStart: number; // mm + spaceEnd: number; // mm + }; +}; + +export const RectangleZoom = ({ rect }: CanvasRectProps) => { + if (!rect) { + return null; + } + return ( + + ); +}; diff --git a/ui-spacetimechart/src/index.ts b/ui-spacetimechart/src/index.ts index ab79245f3..90784d123 100644 --- a/ui-spacetimechart/src/index.ts +++ b/ui-spacetimechart/src/index.ts @@ -9,3 +9,4 @@ export * from './components/ConflictTooltip'; export * from './components/OccupancyBlockLayer'; export * from './components/WorkScheduleLayer'; export * from './components/PatternRect'; +export * from './components/RectangleZoom';