From 317fbe4d99c293fec16b477bd406ce01284c9d32 Mon Sep 17 00:00:00 2001 From: airslice Date: Wed, 13 Sep 2023 11:10:47 +0800 Subject: [PATCH 01/12] wip: timeline manager --- web/src/beta/lib/core/Map/index.tsx | 2 + web/src/beta/lib/core/Map/types/index.ts | 4 +- web/src/beta/lib/core/Visualizer/hooks.ts | 31 ++- web/src/beta/lib/core/Visualizer/index.tsx | 3 +- .../lib/core/Visualizer/useTimelineManager.ts | 200 ++++++++++++++++++ .../lib/core/engines/Cesium/core/Clock.tsx | 47 ++-- web/src/beta/lib/core/engines/Cesium/hooks.ts | 8 - .../beta/lib/core/engines/Cesium/index.tsx | 5 +- 8 files changed, 263 insertions(+), 37 deletions(-) create mode 100644 web/src/beta/lib/core/Visualizer/useTimelineManager.ts diff --git a/web/src/beta/lib/core/Map/index.tsx b/web/src/beta/lib/core/Map/index.tsx index 7f17d6b0ea..4860663b48 100644 --- a/web/src/beta/lib/core/Map/index.tsx +++ b/web/src/beta/lib/core/Map/index.tsx @@ -42,6 +42,7 @@ function Map( overrides, selectedLayerId, layerSelectionReason, + timelineManager, onLayerSelect, ...props }: Props, @@ -77,6 +78,7 @@ function Map( onLayerSelect={handleEngineLayerSelect} layersRef={layersRef} requestingRenderMode={requestingRenderMode} + timelineManager={timelineManager} {...props}> ; removeTickEventListener: TickEvent; + timelineManager?: TimelineManager; }; export type EngineProps = { @@ -104,7 +106,6 @@ export type EngineProps = { isEditable?: boolean; isBuilt?: boolean; property?: SceneProperty; - overriddenClock?: Clock; camera?: Camera; small?: boolean; children?: ReactNode; @@ -121,6 +122,7 @@ export type EngineProps = { meta?: Record; layersRef?: RefObject; requestingRenderMode?: MutableRefObject; + timelineManager?: TimelineManager; onLayerSelect?: ( layerId: string | undefined, featureId?: string, diff --git a/web/src/beta/lib/core/Visualizer/hooks.ts b/web/src/beta/lib/core/Visualizer/hooks.ts index 21473795e3..d4c167a9c0 100644 --- a/web/src/beta/lib/core/Visualizer/hooks.ts +++ b/web/src/beta/lib/core/Visualizer/hooks.ts @@ -20,6 +20,7 @@ import type { } from "../Map"; import { useOverriddenProperty } from "../Map"; +import useTimelineManager from "./useTimelineManager"; import useViewport from "./useViewport"; const viewportMobileMaxWidth = 768; @@ -157,8 +158,35 @@ export default function useHooks( } }, [infobox]); + // timeline manager + const timelineManager = useTimelineManager({ + init: sceneProperty?.timeline, + engineClock: mapRef.current?.engine?.getClock(), + }); + // scene - const [overriddenSceneProperty, overrideSceneProperty] = useOverriddenProperty(sceneProperty); + const [overriddenSceneProperty, originalOverrideSceneProperty] = + useOverriddenProperty(sceneProperty); + + const overrideSceneProperty = useCallback( + (pluginId: string, property: SceneProperty) => { + // Timeline related override should be handled by TimelineManager + if (property.timeline) { + timelineManager.commit({ + cmd: "UPDATE", + payload: property.timeline, + commiter: { + source: "overrideSceneProperty", + id: pluginId, + }, + }); + } + // We can keep the logic the same as before. + // Just remember we will NOT use the timeline from overridden scene property directly. + originalOverrideSceneProperty(pluginId, property); + }, + [timelineManager, originalOverrideSceneProperty], + ); // clock const overriddenClock = useMemo(() => { @@ -283,6 +311,7 @@ export default function useHooks( infobox, isLayerDragging, shouldRender, + timelineManager, handleLayerSelect, handleBlockSelect: selectBlock, handleCameraChange: changeCamera, diff --git a/web/src/beta/lib/core/Visualizer/index.tsx b/web/src/beta/lib/core/Visualizer/index.tsx index 76b7e79ad2..94002f25e3 100644 --- a/web/src/beta/lib/core/Visualizer/index.tsx +++ b/web/src/beta/lib/core/Visualizer/index.tsx @@ -191,6 +191,7 @@ const Visualizer: FC> = memo( isLayerDragging, infobox, shouldRender, + timelineManager, handleLayerSelect, handleBlockSelect, handleCameraChange, @@ -285,7 +286,6 @@ const Visualizer: FC> = memo( layers={layers} engines={engines} camera={camera} - overriddenClock={overriddenClock} clusters={clusters} hiddenLayers={hiddenLayers} isLayerDragging={isLayerDragging} @@ -300,6 +300,7 @@ const Visualizer: FC> = memo( layerSelectionReason={layerSelectionReason} small={small} ready={ready} + timelineManager={timelineManager} onCameraChange={handleCameraChange} onLayerDrag={handleLayerDrag} onLayerDrop={handleLayerDrop} diff --git a/web/src/beta/lib/core/Visualizer/useTimelineManager.ts b/web/src/beta/lib/core/Visualizer/useTimelineManager.ts new file mode 100644 index 0000000000..3e63dbc147 --- /dev/null +++ b/web/src/beta/lib/core/Visualizer/useTimelineManager.ts @@ -0,0 +1,200 @@ +import { useCallback, useRef, useState, useMemo } from "react"; + +import { convertTime, truncMinutes } from "@reearth/beta/utils/time"; + +import { Clock } from "../Map"; + +export type TimelineManager = { + readonly timeline: Timeline; + readonly overriddenTimeline: Timeline; + commit: (props: TimelineCommit) => void; + on: (type: TimelineEventTypes, cb: () => void, commiter: TimelineCommiter) => void; + off: (type: TimelineEventTypes, cb: () => void, commiter: TimelineCommiter) => void; + // for connect engine onTick + onTick: (d: Date, clock: { start: Date; stop: Date }) => void; +}; + +export type Timeline = TimeDate & TimelineOptions; +type TimeDate = { + current?: Date; + start?: Date; + stop?: Date; +}; + +type TimelineData = TimeString & TimelineOptions; +type TimeString = { + current?: string; + start?: string; + stop?: string; +}; + +type TimelineOptions = { + animation: boolean; + step: number; + stepType: "rate" | "fixed"; + multiplier: number; + rangeType?: "unbounded" | "clamped" | "bounced"; +}; + +type TimelineCommand = "PLAY" | "PAUSE" | "UPDATE"; + +type TimelineCommit = { + cmd: TimelineCommand; + payload: Partial | undefined; + commiter: TimelineCommiter; +}; + +type TimelineCommiter = { + source: "overrideSceneProperty"; + id?: string; +}; + +type TimelineEventTypes = "commiterchange" | "tick"; + +const DEFAULT_RANGE = 86400000; // a day + +type Props = { + init?: Partial; + engineClock?: Clock; +}; + +export default ({ init, engineClock }: Props) => { + const [time, setTime] = useState({ + start: init?.start, + stop: init?.stop, + current: init?.current, + }); + + const [options, setOptions] = useState({ + animation: init?.animation ?? false, + step: init?.step ?? 1, + stepType: init?.stepType ?? "rate", + multiplier: init?.multiplier ?? 1, + rangeType: init?.rangeType ?? "unbounded", + }); + + const timeDates = useMemo(() => { + const { start, stop, current } = time; + const startTime = convertTime(start)?.getTime(); + const stopTime = convertTime(stop)?.getTime(); + const currentTime = convertTime(current)?.getTime(); + + // TODO: validate time + const now = Date.now(); + + const convertedStartTime = startTime + ? Math.min(now, startTime) + : stopTime + ? Math.min(now, stopTime - DEFAULT_RANGE) + : now - DEFAULT_RANGE; + + const convertedStopTime = stopTime + ? Math.max(stopTime, now) + : startTime + ? Math.max(now, startTime + DEFAULT_RANGE) + : now; + + return { + start: truncMinutes(new Date(convertedStartTime)), + stop: new Date(convertedStopTime), + current: new Date( + Math.max( + Math.min(currentTime || convertedStartTime, convertedStopTime), + convertedStartTime, + ), + ), + }; + }, [time]); + + // Commiter Record + const lastCommiter = useRef(); + + const commit = useCallback(({ cmd, payload, commiter }: TimelineCommit) => { + if (!cmd) return; + + if (cmd === "UPDATE") { + setTime({ start: payload?.start, stop: payload?.stop, current: payload?.current }); + } else if (cmd === "PLAY") { + setOptions(o => ({ ...o, animation: true })); + } else if (cmd === "PAUSE") { + setOptions(o => ({ ...o, animation: false })); + } + + // Last commiter + if ( + lastCommiter.current && + (commiter.source !== lastCommiter.current.source || commiter.id !== lastCommiter.current.id) + ) { + eventCallbacks.current.commiterchange.forEach(c => c.cb()); + } + lastCommiter.current = commiter; + }, []); + + const eventCallbacks = useRef<{ + [key in TimelineEventTypes]: { commiter: TimelineCommiter; cb: () => void }[]; + }>({ tick: [], commiterchange: [] }); + + const on = useCallback((type: TimelineEventTypes, cb: () => void, commiter: TimelineCommiter) => { + if (type !== "tick" && type !== "commiterchange") return; + eventCallbacks.current[type].push({ commiter, cb }); + }, []); + + const off = useCallback( + (type: TimelineEventTypes, cb: () => void, commiter: TimelineCommiter) => { + if (type !== "tick" && type !== "commiterchange") return; + eventCallbacks.current[type] = eventCallbacks.current[type].filter( + c => + !(c.commiter.source === commiter.source && c.commiter.id === commiter.id && c.cb === cb), + ); + }, + [], + ); + + const onTick = useCallback(() => { + eventCallbacks.current.tick.forEach(c => c.cb()); + }, []); + + const timelineManager = useMemo( + () => ({ + get timeline() { + return { + get start() { + return engineClock?.start; + }, + get stop() { + return engineClock?.stop; + }, + get current() { + return engineClock?.current; + }, + get animation() { + return options.animation; + }, + get step() { + return options.step; + }, + get stepType() { + return options.stepType; + }, + get multiplier() { + return options.multiplier; + }, + get rangeType() { + return options.rangeType; + }, + }; + }, + overriddenTimeline: { + ...timeDates, + ...options, + }, + commit, + on, + off, + onTick, + }), + [engineClock, options, timeDates, commit, off, on, onTick], + ); + + return timelineManager; +}; diff --git a/web/src/beta/lib/core/engines/Cesium/core/Clock.tsx b/web/src/beta/lib/core/engines/Cesium/core/Clock.tsx index ef7799eccd..e256e297a6 100644 --- a/web/src/beta/lib/core/engines/Cesium/core/Clock.tsx +++ b/web/src/beta/lib/core/engines/Cesium/core/Clock.tsx @@ -2,29 +2,30 @@ import { Clock as CesiumClock, ClockRange, ClockStep, JulianDate } from "cesium" import { useCallback, useEffect, useMemo } from "react"; import { Clock, useCesium } from "resium"; -import { truncMinutes } from "@reearth/beta/utils/time"; +// import { truncMinutes } from "@reearth/beta/utils/time"; -import type { Clock as ClockType, SceneProperty } from "../.."; +import type { SceneProperty } from "../.."; +import { type TimelineManager } from "../../../Visualizer/useTimelineManager"; export type Props = { property?: SceneProperty; - clock?: ClockType; - onTick?: (d: Date, clock: { start: Date; stop: Date }) => void; + // clock?: ClockType; + timelineManager?: TimelineManager; }; -export default function ReearthClock({ property, clock, onTick }: Props): JSX.Element | null { - const { animation, visible, stepType, rangeType, multiplier, step } = property?.timeline ?? {}; - const startTime = useMemo( - () => (clock?.start ? JulianDate.fromDate(clock.start) : undefined), - [clock?.start], - ); - const stopTime = useMemo( - () => (clock?.stop ? JulianDate.fromDate(clock?.stop) : undefined), - [clock?.stop], - ); +export default function ReearthClock({ + property, + // clock, + timelineManager, +}: Props): JSX.Element | null { + const { visible } = property?.timeline ?? {}; + const { start, stop, current, animation, stepType, rangeType, multiplier, step } = + timelineManager?.overriddenTimeline ?? {}; + const startTime = useMemo(() => (start ? JulianDate.fromDate(start) : undefined), [start]); + const stopTime = useMemo(() => (stop ? JulianDate.fromDate(stop) : undefined), [stop]); const currentTime = useMemo( - () => (clock?.current ? JulianDate.fromDate(clock?.current) : undefined), - [clock], + () => (current ? JulianDate.fromDate(current) : undefined), + [current], ); const clockStep = stepType === "fixed" ? ClockStep.TICK_DEPENDENT : ClockStep.SYSTEM_CLOCK_MULTIPLIER; @@ -37,19 +38,19 @@ export default function ReearthClock({ property, clock, onTick }: Props): JSX.El const start = JulianDate.toDate(clock.startTime); const stop = JulianDate.toDate(clock.stopTime); - // Truncate minutes for displaying correctly time on timeline widget - const truncatedStart = truncMinutes(new Date(start)); - if (viewer && start.toISOString() !== truncatedStart.toISOString()) { - viewer.clock.startTime = JulianDate.fromDate(truncatedStart); - } + // // Truncate minutes for displaying correctly time on timeline widget + // const truncatedStart = truncMinutes(new Date(start)); + // if (viewer && start.toISOString() !== truncatedStart.toISOString()) { + // viewer.clock.startTime = JulianDate.fromDate(truncatedStart); + // } // NOTE: Must not update state. This event will be called every frame. - onTick?.(JulianDate.toDate(clock.currentTime), { + timelineManager?.onTick?.(JulianDate.toDate(clock.currentTime), { start, stop, }); }, - [onTick, viewer], + [timelineManager], ); useEffect(() => { diff --git a/web/src/beta/lib/core/engines/Cesium/hooks.ts b/web/src/beta/lib/core/engines/Cesium/hooks.ts index e876ca50bf..0d287b1693 100644 --- a/web/src/beta/lib/core/engines/Cesium/hooks.ts +++ b/web/src/beta/lib/core/engines/Cesium/hooks.ts @@ -730,13 +730,6 @@ export default ({ [selectionReason, engineAPI, onLayerEdit], ); - const handleTick = useCallback( - (d: Date, clock: { start: Date; stop: Date }) => { - engineAPI.tickEventCallback?.current?.forEach(e => e(d, clock)); - }, - [engineAPI], - ); - useEffect(() => { if (!cesium.current?.cesiumElement) return; const allowCameraMove = !!(featureFlags & FEATURE_FLAGS.CAMERA_MOVE); @@ -825,7 +818,6 @@ export default ({ handleClick, handleCameraChange, handleCameraMoveEnd, - handleTick, }; }; diff --git a/web/src/beta/lib/core/engines/Cesium/index.tsx b/web/src/beta/lib/core/engines/Cesium/index.tsx index 879bcff2c2..d8610da2c2 100644 --- a/web/src/beta/lib/core/engines/Cesium/index.tsx +++ b/web/src/beta/lib/core/engines/Cesium/index.tsx @@ -34,7 +34,6 @@ const Cesium: React.ForwardRefRenderFunction = ( className, style, property, - overriddenClock, camera, small, ready, @@ -48,6 +47,7 @@ const Cesium: React.ForwardRefRenderFunction = ( layersRef, featureFlags, requestingRenderMode, + timelineManager, onLayerSelect, onCameraChange, onLayerDrag, @@ -73,7 +73,6 @@ const Cesium: React.ForwardRefRenderFunction = ( handleClick, handleCameraChange, handleCameraMoveEnd, - handleTick, } = useHooks({ ref, property, @@ -138,7 +137,7 @@ const Cesium: React.ForwardRefRenderFunction = ( onMouseLeave={mouseEventHandles.mouseleave} onWheel={mouseEventHandles.wheel}> - + From 4fb104b70fd2b971d089d21367911d1d89acecb4 Mon Sep 17 00:00:00 2001 From: airslice Date: Fri, 29 Sep 2023 13:58:41 +0800 Subject: [PATCH 02/12] wip --- web/src/beta/lib/core/Crust/Plugins/hooks.ts | 26 +-- web/src/beta/lib/core/Crust/Plugins/types.ts | 2 + .../Widgets/Widget/builtin/Timeline/hooks.ts | 72 ++++----- .../Widgets/Widget/builtin/Timeline/index.tsx | 6 +- .../lib/core/Crust/Widgets/Widget/index.tsx | 6 +- web/src/beta/lib/core/Crust/context.ts | 51 ++++-- web/src/beta/lib/core/Crust/index.tsx | 8 +- web/src/beta/lib/core/Visualizer/hooks.ts | 23 +-- web/src/beta/lib/core/Visualizer/index.tsx | 1 + .../lib/core/Visualizer/useTimelineManager.ts | 149 +++++++++++------- .../lib/core/engines/Cesium/core/Clock.tsx | 21 +-- web/src/beta/utils/time.ts | 4 +- 12 files changed, 217 insertions(+), 152 deletions(-) diff --git a/web/src/beta/lib/core/Crust/Plugins/hooks.ts b/web/src/beta/lib/core/Crust/Plugins/hooks.ts index 15148c78e9..069346009d 100644 --- a/web/src/beta/lib/core/Crust/Plugins/hooks.ts +++ b/web/src/beta/lib/core/Crust/Plugins/hooks.ts @@ -38,6 +38,7 @@ export default function ({ floatingWidgets, camera, interactionMode, + timelineManager, overrideInteractionMode, useExperimentalSandbox, overrideSceneProperty, @@ -62,19 +63,22 @@ export default function ({ const getTags = useGet(tags ?? []); const getCamera = useGet(camera); const getClock = useCallback(() => { - const clock = engineRef?.getClock(); return { - startTime: clock?.start, - stopTime: clock?.stop, - currentTime: clock?.current, - playing: clock?.playing, - paused: !clock?.playing, - speed: clock?.speed, - play: engineRef?.play, - pause: engineRef?.pause, - tick: engineRef?.tick, + startTime: timelineManager?.timeline?.start, + stopTime: timelineManager?.timeline?.stop, + currentTime: timelineManager?.timeline?.current, + playing: !!timelineManager?.timeline?.animation, + paused: !timelineManager?.timeline?.animation, + speed: timelineManager?.timeline?.multiplier, + play: () => { + timelineManager?.commit({ cmd: "PLAY", commiter: { source: "pluginAPI" } }); + }, + pause: () => { + timelineManager?.commit({ cmd: "PAUSE", commiter: { source: "pluginAPI" } }); + }, + tick: timelineManager?.tick, }; - }, [engineRef]); + }, [timelineManager]); const getInteractionMode = useGet( useMemo( () => ({ mode: interactionMode, override: overrideInteractionMode }), diff --git a/web/src/beta/lib/core/Crust/Plugins/types.ts b/web/src/beta/lib/core/Crust/Plugins/types.ts index 3a5de13b02..18024514a4 100644 --- a/web/src/beta/lib/core/Crust/Plugins/types.ts +++ b/web/src/beta/lib/core/Crust/Plugins/types.ts @@ -13,6 +13,7 @@ import type { } from "@reearth/beta/lib/core/Map"; import type { Viewport } from "@reearth/beta/lib/core/Visualizer"; +import { TimelineManager } from "../../Visualizer/useTimelineManager"; import type { MapRef, InteractionModeType } from "../types"; import type { InternalWidget, WidgetAlignSystem } from "../Widgets"; @@ -35,6 +36,7 @@ export type Props = PropsWithChildren<{ alignSystem?: WidgetAlignSystem; floatingWidgets?: InternalWidget[]; useExperimentalSandbox?: boolean; + timelineManager?: TimelineManager; overrideSceneProperty: (id: string, property: any) => void; camera?: Camera; interactionMode: InteractionModeType; diff --git a/web/src/beta/lib/core/Crust/Widgets/Widget/builtin/Timeline/hooks.ts b/web/src/beta/lib/core/Crust/Widgets/Widget/builtin/Timeline/hooks.ts index 69a2c24a5a..200f38e2ef 100644 --- a/web/src/beta/lib/core/Crust/Widgets/Widget/builtin/Timeline/hooks.ts +++ b/web/src/beta/lib/core/Crust/Widgets/Widget/builtin/Timeline/hooks.ts @@ -2,8 +2,9 @@ import { useState, useCallback, useEffect, useRef } from "react"; import type { TimeEventHandler } from "@reearth/beta/lib/core/Crust/Widgets/Widget/builtin/Timeline/UI"; import { TickEvent, TickEventCallback } from "@reearth/beta/lib/core/Map"; +import { TimelineManager } from "@reearth/beta/lib/core/Visualizer/useTimelineManager"; -import type { Clock, Widget } from "../../types"; +import type { Widget } from "../../types"; import { useVisible } from "../../useVisible"; const MAX_RANGE = 86400000; // a day @@ -21,8 +22,7 @@ const DEFAULT_SPEED = 1; export const useTimeline = ({ widget, - clock, - overriddenClock, + timelineManager, isMobile, onPlay, onPause, @@ -34,8 +34,7 @@ export const useTimeline = ({ onVisibilityChange, }: { widget: Widget; - clock?: Clock; - overriddenClock?: Partial; + timelineManager?: TimelineManager; isMobile?: boolean; onPlay?: () => void; onPause?: () => void; @@ -54,14 +53,19 @@ export const useTimeline = ({ }); const widgetId = widget.id; const [range, setRange] = useState(() => - makeRange(clock?.start?.getTime(), clock?.stop?.getTime()), + makeRange( + timelineManager?.timeline?.start?.getTime(), + timelineManager?.timeline?.stop?.getTime(), + ), ); const [isOpened, setIsOpened] = useState(true); - const [currentTime, setCurrentTime] = useState(() => getOrNewDate(clock?.current).getTime()); + const [currentTime, setCurrentTime] = useState(() => + getOrNewDate(timelineManager?.timeline?.current).getTime(), + ); const isClockInitialized = useRef(false); - const clockStartTime = clock?.start?.getTime(); - const clockStopTime = clock?.stop?.getTime(); - const clockSpeed = clock?.speed || DEFAULT_SPEED; + const clockStartTime = timelineManager?.timeline?.start?.getTime(); + const clockStopTime = timelineManager?.timeline?.stop?.getTime(); + const clockSpeed = timelineManager?.timeline?.multiplier || DEFAULT_SPEED; const [speed, setSpeed] = useState(clockSpeed); @@ -75,6 +79,7 @@ export const useTimeline = ({ setIsOpened(false); }, [widgetId, onExtend]); + const lastTime = useRef(); const switchCurrentTimeToStart = useCallback( (t: number, isRangeChanged: boolean) => { const cur = isRangeChanged @@ -84,7 +89,11 @@ export const useTimeline = ({ : t < range.start ? range.end : t; - onTimeChange?.(new Date(cur)); + + if (lastTime.current !== cur) { + lastTime.current = cur; + onTimeChange?.(new Date(cur)); + } return cur; }, [range, onTimeChange], @@ -92,22 +101,15 @@ export const useTimeline = ({ const handleTimeEvent: TimeEventHandler = useCallback( currentTime => { - if (!clock) { - return; - } const t = new Date(currentTime); onTimeChange?.(t); setCurrentTime(currentTime); }, - [clock, onTimeChange], + [onTimeChange], ); const handleOnPlay = useCallback( (playing: boolean) => { - if (!clock) { - return; - } - // Stop cesium animation if (playing) { onPlay?.(); @@ -116,15 +118,11 @@ export const useTimeline = ({ } onSpeedChange?.(Math.abs(speed)); }, - [clock, onPause, onPlay, onSpeedChange, speed], + [onPause, onPlay, onSpeedChange, speed], ); const handleOnPlayReversed = useCallback( (playing: boolean) => { - if (!clock) { - return; - } - // Stop cesium animation if (playing) { onPlay?.(); @@ -133,30 +131,29 @@ export const useTimeline = ({ } onSpeedChange?.(Math.abs(speed) * -1); }, - [clock, onPause, onPlay, onSpeedChange, speed], + [onPause, onPlay, onSpeedChange, speed], ); const handleOnSpeedChange = useCallback( (speed: number) => { setSpeed(speed); - if (clock) { - const absSpeed = Math.abs(speed); - // Maybe we need to throttle changing speed. - onSpeedChange?.((clock.speed ?? 1) > 0 ? absSpeed : absSpeed * -1); - } + + const absSpeed = Math.abs(speed); + // Maybe we need to throttle changing speed. + onSpeedChange?.((timelineManager?.timeline?.multiplier ?? 1) > 0 ? absSpeed : absSpeed * -1); }, - [clock, onSpeedChange], + [onSpeedChange, timelineManager], ); // Initialize clock value useEffect(() => { - if (clock && !isClockInitialized.current) { + if (!isClockInitialized.current) { isClockInitialized.current = true; queueMicrotask(() => { onSpeedChange?.(1); }); } - }, [clock, onSpeedChange, onTick]); + }, [onSpeedChange, onTick]); const handleRange = useCallback((start: number | undefined, stop: number | undefined) => { setRange(prev => { @@ -168,8 +165,8 @@ export const useTimeline = ({ }); }, []); - const overriddenStart = overriddenClock?.start?.getTime(); - const overriddenStop = overriddenClock?.stop?.getTime(); + const overriddenStart = timelineManager?.overriddenTimeline?.start?.getTime(); + const overriddenStop = timelineManager?.overriddenTimeline?.stop?.getTime(); // Sync cesium clock. useEffect(() => { @@ -191,7 +188,6 @@ export const useTimeline = ({ }; }, [ onTick, - clock?.playing, removeTickEventListener, switchCurrentTimeToStart, handleRange, @@ -205,12 +201,12 @@ export const useTimeline = ({ onTimeChangeRef.current = onTimeChange; }, [onTimeChange]); - const overriddenCurrentTime = overriddenClock?.current?.getTime(); + const overriddenCurrentTime = timelineManager?.overriddenTimeline?.current?.getTime(); useEffect(() => { if (overriddenCurrentTime) { const t = Math.max(Math.min(range.end, overriddenCurrentTime), range.start); setCurrentTime(t); - onTimeChangeRef.current?.(new Date(t)); + // onTimeChangeRef.current?.(new Date(t)); } }, [overriddenCurrentTime, range]); diff --git a/web/src/beta/lib/core/Crust/Widgets/Widget/builtin/Timeline/index.tsx b/web/src/beta/lib/core/Crust/Widgets/Widget/builtin/Timeline/index.tsx index 9d416cd5b9..a3ebe6c1d0 100644 --- a/web/src/beta/lib/core/Crust/Widgets/Widget/builtin/Timeline/index.tsx +++ b/web/src/beta/lib/core/Crust/Widgets/Widget/builtin/Timeline/index.tsx @@ -21,8 +21,7 @@ const Timeline = ({ onExtend, onVisibilityChange, context: { - clock, - overriddenClock, + timelineManager, onPlay, onPause, onSpeedChange, @@ -33,8 +32,7 @@ const Timeline = ({ }: Props): JSX.Element | null => { const { isOpened, currentTime, range, speed, events, visible } = useTimeline({ widget, - clock, - overriddenClock, + timelineManager, isMobile, onPlay, onPause, diff --git a/web/src/beta/lib/core/Crust/Widgets/Widget/index.tsx b/web/src/beta/lib/core/Crust/Widgets/Widget/index.tsx index 7a767724b3..133e5cdd00 100644 --- a/web/src/beta/lib/core/Crust/Widgets/Widget/index.tsx +++ b/web/src/beta/lib/core/Crust/Widgets/Widget/index.tsx @@ -2,6 +2,8 @@ import { ComponentType, ReactNode, useMemo } from "react"; import type { TickEvent } from "@reearth/beta/lib/core/Map"; +import { TimelineCommiter, TimelineManager } from "../../../Visualizer/useTimelineManager"; + import builtin, { isBuiltinWidget } from "./builtin"; import type { Theme, @@ -36,7 +38,7 @@ export type Props = { export type Context = { clock?: Clock; - overriddenClock?: Partial; + timelineManager?: TimelineManager; updateClockOnLoad?: boolean; camera?: Camera; initialCamera?: Camera; @@ -52,7 +54,7 @@ export type Context = { featureId: string | undefined, options?: { reason?: string }, ) => void; - onPlay?: () => void; + onPlay?: (commiter?: TimelineCommiter) => void; onPause?: () => void; onSpeedChange?: (speed: number) => void; onTimeChange?: (time: Date) => void; diff --git a/web/src/beta/lib/core/Crust/context.ts b/web/src/beta/lib/core/Crust/context.ts index c04274bdc0..450d5da74c 100644 --- a/web/src/beta/lib/core/Crust/context.ts +++ b/web/src/beta/lib/core/Crust/context.ts @@ -1,6 +1,8 @@ import { RefObject, useMemo } from "react"; -import { Camera, Clock, MapRef, SceneProperty } from "./types"; +import { TimelineCommiter, TimelineManager } from "../Visualizer/useTimelineManager"; + +import { Camera, MapRef, SceneProperty } from "./types"; import { Context as WidgetContext } from "./Widgets"; export const useWidgetContext = ({ @@ -8,7 +10,7 @@ export const useWidgetContext = ({ camera, selectedLayerId, sceneProperty, - overriddenClock, + timelineManager, }: Parameters[0]) => useMemo( () => @@ -17,9 +19,9 @@ export const useWidgetContext = ({ camera, selectedLayerId, sceneProperty, - overriddenClock, + timelineManager, }), - [camera, mapRef, sceneProperty, selectedLayerId, overriddenClock], + [camera, mapRef, sceneProperty, selectedLayerId, timelineManager], ); export function widgetContextFromMapRef({ @@ -27,7 +29,7 @@ export function widgetContextFromMapRef({ camera, selectedLayerId, sceneProperty, - overriddenClock, + timelineManager, }: { mapRef?: RefObject; camera?: Camera; @@ -36,7 +38,7 @@ export function widgetContextFromMapRef({ featureId?: string; }; sceneProperty?: SceneProperty; - overriddenClock: Clock; + timelineManager?: TimelineManager; }): WidgetContext { const engine = () => mapRef?.current?.engine; const layers = () => mapRef?.current?.layers; @@ -46,7 +48,7 @@ export function widgetContextFromMapRef({ get clock() { return engine()?.getClock(); }, - overriddenClock, + timelineManager, initialCamera: sceneProperty?.default?.camera, is2d: sceneProperty?.default?.sceneMode === "2d", selectedLayerId, @@ -69,12 +71,35 @@ export function widgetContextFromMapRef({ onFlyTo: (...args) => engine()?.flyTo(...args), onLookAt: (...args) => engine()?.lookAt(...args), onLayerSelect: (...args) => layers()?.select(...args), - onPause: (...args) => engine()?.pause(...args), - onPlay: (...args) => engine()?.play(...args), - onSpeedChange: (...args) => engine()?.changeSpeed(...args), - onTick: (...args) => engine()?.onTick(...args), - removeTickEventListener: (...args) => engine()?.removeTickEventListener(...args), - onTimeChange: (...args) => engine()?.changeTime(...args), + onPause: (commiter?: TimelineCommiter) => + timelineManager?.commit({ + cmd: "PAUSE", + commiter: { source: commiter?.source ?? "widgetContext", id: commiter?.id }, + }), + onPlay: (commiter?: TimelineCommiter) => + timelineManager?.commit({ + cmd: "PLAY", + commiter: { source: commiter?.source ?? "widgetContext", id: commiter?.id }, + }), + onSpeedChange: (speed, commiter?: TimelineCommiter) => + timelineManager?.commit({ + cmd: "UPDATE", + payload: { + multiplier: speed, + stepType: "rate", + }, + commiter: { source: commiter?.source ?? "widgetContext", id: commiter?.id }, + }), + onTick: cb => timelineManager?.onTick(cb), + removeTickEventListener: cb => timelineManager?.offTick(cb), + onTimeChange: (time, commiter?: TimelineCommiter) => + timelineManager?.commit({ + cmd: "UPDATE", + payload: { + current: time, + }, + commiter: { source: commiter?.source ?? "widgetContext", id: commiter?.id }, + }), onZoomIn: (...args) => engine()?.zoomIn(...args), onZoomOut: (...args) => engine()?.zoomOut(...args), }; diff --git a/web/src/beta/lib/core/Crust/index.tsx b/web/src/beta/lib/core/Crust/index.tsx index dd925eee88..743ce8e0f7 100644 --- a/web/src/beta/lib/core/Crust/index.tsx +++ b/web/src/beta/lib/core/Crust/index.tsx @@ -5,6 +5,7 @@ import type { SelectedFeatureInfo, Tag } from "@reearth/beta/lib/core/mantle"; import type { ComputedFeature, ComputedLayer, Feature } from "../mantle"; import type { Clock, LayerEditEvent, LayerSelectionReason } from "../Map"; import type { Viewport } from "../Visualizer"; +import type { TimelineManager } from "../Visualizer/useTimelineManager"; import { useWidgetContext } from "./context"; import useHooks from "./hooks"; @@ -90,6 +91,8 @@ export type Props = { // plugin externalPlugin: ExternalPluginProps; useExperimentalSandbox?: boolean; + // timeline manager + timelineManager?: TimelineManager; // widget events onWidgetLayoutUpdate?: ( id: string, @@ -128,7 +131,6 @@ export default function Crust({ isMobile, mapRef, sceneProperty, - overriddenClock, viewport, camera, interactionMode, @@ -151,6 +153,7 @@ export default function Crust({ selectedWidgetArea, externalPlugin, useExperimentalSandbox, + timelineManager, onWidgetLayoutUpdate, onWidgetAlignmentUpdate, onWidgetAreaSelect, @@ -180,7 +183,7 @@ export default function Crust({ camera, sceneProperty, selectedLayerId, - overriddenClock, + timelineManager, }); return ( @@ -203,6 +206,7 @@ export default function Crust({ overrideInteractionMode={overrideInteractionMode} useExperimentalSandbox={useExperimentalSandbox} overrideSceneProperty={overrideSceneProperty} + timelineManager={timelineManager} onLayerEdit={onLayerEdit}> { // Timeline related override should be handled by TimelineManager if (property.timeline) { - timelineManager.commit({ - cmd: "UPDATE", - payload: property.timeline, - commiter: { - source: "overrideSceneProperty", - id: pluginId, - }, - }); + const filteredTimeline = clone(property.timeline); + delete filteredTimeline.visible; + if (Object.keys(filteredTimeline).length > 0) { + timelineManager?.commit({ + cmd: "UPDATE", + payload: filteredTimeline, + commiter: { + source: "overrideSceneProperty", + id: pluginId, + }, + }); + } } // We can keep the logic the same as before. // Just remember we will NOT use the timeline from overridden scene property directly. diff --git a/web/src/beta/lib/core/Visualizer/index.tsx b/web/src/beta/lib/core/Visualizer/index.tsx index ea181efbae..18771dedb7 100644 --- a/web/src/beta/lib/core/Visualizer/index.tsx +++ b/web/src/beta/lib/core/Visualizer/index.tsx @@ -265,6 +265,7 @@ const Visualizer = memo( mapRef={mapRef} externalPlugin={{ pluginBaseUrl, pluginProperty }} useExperimentalSandbox={useExperimentalSandbox} + timelineManager={timelineManager} onWidgetLayoutUpdate={onWidgetLayoutUpdate} onWidgetAlignmentUpdate={onWidgetAlignmentUpdate} onWidgetAreaSelect={onWidgetAreaSelect} diff --git a/web/src/beta/lib/core/Visualizer/useTimelineManager.ts b/web/src/beta/lib/core/Visualizer/useTimelineManager.ts index 3e63dbc147..aa7f403af2 100644 --- a/web/src/beta/lib/core/Visualizer/useTimelineManager.ts +++ b/web/src/beta/lib/core/Visualizer/useTimelineManager.ts @@ -2,32 +2,29 @@ import { useCallback, useRef, useState, useMemo } from "react"; import { convertTime, truncMinutes } from "@reearth/beta/utils/time"; -import { Clock } from "../Map"; +import { EngineRef, WrappedRef } from "../Map"; export type TimelineManager = { readonly timeline: Timeline; readonly overriddenTimeline: Timeline; commit: (props: TimelineCommit) => void; - on: (type: TimelineEventTypes, cb: () => void, commiter: TimelineCommiter) => void; - off: (type: TimelineEventTypes, cb: () => void, commiter: TimelineCommiter) => void; + onTick: TickEvent; + offTick: TickEvent; + onCommiterChange: (cb: () => void) => void; + offCommiterChange: (cb: () => void) => void; // for connect engine onTick - onTick: (d: Date, clock: { start: Date; stop: Date }) => void; + handleTick: (d: Date, clock: { start: Date; stop: Date }) => void; + tick: (() => Date | void | undefined) | undefined; }; export type Timeline = TimeDate & TimelineOptions; + type TimeDate = { current?: Date; start?: Date; stop?: Date; }; -type TimelineData = TimeString & TimelineOptions; -type TimeString = { - current?: string; - start?: string; - stop?: string; -}; - type TimelineOptions = { animation: boolean; step: number; @@ -40,29 +37,40 @@ type TimelineCommand = "PLAY" | "PAUSE" | "UPDATE"; type TimelineCommit = { cmd: TimelineCommand; - payload: Partial | undefined; + payload?: + | ({ + current?: Date | string | undefined; + start?: Date | string | undefined; + stop?: Date | string | undefined; + } & Partial) + | undefined; commiter: TimelineCommiter; }; -type TimelineCommiter = { - source: "overrideSceneProperty"; +export type TimelineCommiter = { + source: "overrideSceneProperty" | "widgetContext" | "pluginAPI"; id?: string; }; -type TimelineEventTypes = "commiterchange" | "tick"; +export type TickEvent = (cb: TickEventCallback) => void; +export type TickEventCallback = (current: Date, clock: { start: Date; stop: Date }) => void; const DEFAULT_RANGE = 86400000; // a day type Props = { - init?: Partial; - engineClock?: Clock; + init?: { + current?: string; + start?: string; + stop?: string; + } & Partial; + engineRef?: WrappedRef; }; -export default ({ init, engineClock }: Props) => { - const [time, setTime] = useState({ - start: init?.start, - stop: init?.stop, - current: init?.current, +export default ({ init, engineRef }: Props) => { + const [time, setTime] = useState({ + start: convertTime(init?.start), + stop: convertTime(init?.stop), + current: convertTime(init?.current), }); const [options, setOptions] = useState({ @@ -73,11 +81,11 @@ export default ({ init, engineClock }: Props) => { rangeType: init?.rangeType ?? "unbounded", }); - const timeDates = useMemo(() => { + const validTimes = useMemo(() => { const { start, stop, current } = time; - const startTime = convertTime(start)?.getTime(); - const stopTime = convertTime(stop)?.getTime(); - const currentTime = convertTime(current)?.getTime(); + const startTime = start?.getTime(); + const stopTime = stop?.getTime(); + const currentTime = current?.getTime(); // TODO: validate time const now = Date.now(); @@ -110,10 +118,22 @@ export default ({ init, engineClock }: Props) => { const lastCommiter = useRef(); const commit = useCallback(({ cmd, payload, commiter }: TimelineCommit) => { + console.log("commit", cmd, payload, commiter); if (!cmd) return; if (cmd === "UPDATE") { - setTime({ start: payload?.start, stop: payload?.stop, current: payload?.current }); + setTime(t => ({ + start: payload?.start === undefined ? t.start : convertTime(payload?.start), + stop: payload?.stop === undefined ? t.stop : convertTime(payload?.stop), + current: payload?.current === undefined ? t.current : convertTime(payload?.current), + })); + setOptions(o => ({ + animation: payload?.animation === undefined ? o.animation : payload.animation, + step: payload?.step === undefined ? o.step : payload.step, + stepType: payload?.stepType === undefined ? o.stepType : payload.stepType, + multiplier: payload?.multiplier === undefined ? o.multiplier : payload.multiplier, + rangeType: payload?.rangeType === undefined ? o.rangeType : payload.rangeType, + })); } else if (cmd === "PLAY") { setOptions(o => ({ ...o, animation: true })); } else if (cmd === "PAUSE") { @@ -125,37 +145,42 @@ export default ({ init, engineClock }: Props) => { lastCommiter.current && (commiter.source !== lastCommiter.current.source || commiter.id !== lastCommiter.current.id) ) { - eventCallbacks.current.commiterchange.forEach(c => c.cb()); + commiterChangeEventCallbacks.current.forEach(cb => cb()); } lastCommiter.current = commiter; }, []); - const eventCallbacks = useRef<{ - [key in TimelineEventTypes]: { commiter: TimelineCommiter; cb: () => void }[]; - }>({ tick: [], commiterchange: [] }); - - const on = useCallback((type: TimelineEventTypes, cb: () => void, commiter: TimelineCommiter) => { - if (type !== "tick" && type !== "commiterchange") return; - eventCallbacks.current[type].push({ commiter, cb }); + const tickEventCallbacks = useRef([]); + const onTick = useCallback((cb: TickEventCallback) => { + tickEventCallbacks.current.push(cb); + }, []); + const offTick = useCallback((cb: TickEventCallback) => { + tickEventCallbacks.current = tickEventCallbacks.current.filter(c => c !== cb); }, []); - const off = useCallback( - (type: TimelineEventTypes, cb: () => void, commiter: TimelineCommiter) => { - if (type !== "tick" && type !== "commiterchange") return; - eventCallbacks.current[type] = eventCallbacks.current[type].filter( - c => - !(c.commiter.source === commiter.source && c.commiter.id === commiter.id && c.cb === cb), - ); - }, - [], - ); - - const onTick = useCallback(() => { - eventCallbacks.current.tick.forEach(c => c.cb()); + const commiterChangeEventCallbacks = useRef<(() => void)[]>([]); + const onCommiterChange = useCallback((cb: () => void) => { + commiterChangeEventCallbacks.current.push(cb); + }, []); + const offCommiterChange = useCallback((cb: () => void) => { + commiterChangeEventCallbacks.current = commiterChangeEventCallbacks.current.filter( + c => c !== cb, + ); }, []); - const timelineManager = useMemo( - () => ({ + const handleTick = useCallback(() => { + console.log("handleTick"); + tickEventCallbacks.current.forEach(cb => { + const engineClock = engineRef?.getClock(); + if (engineClock?.current && engineClock?.start && engineClock?.stop) { + cb(engineClock.current, { start: engineClock?.start, stop: engineClock?.stop }); + } + }); + }, [engineRef]); + + const timelineManager = useMemo(() => { + const engineClock = engineRef?.getClock(); + return { get timeline() { return { get start() { @@ -185,16 +210,28 @@ export default ({ init, engineClock }: Props) => { }; }, overriddenTimeline: { - ...timeDates, + ...validTimes, ...options, }, commit, - on, - off, onTick, - }), - [engineClock, options, timeDates, commit, off, on, onTick], - ); + offTick, + onCommiterChange, + offCommiterChange, + handleTick, + tick: engineRef?.tick, + }; + }, [ + options, + validTimes, + engineRef, + commit, + onTick, + offTick, + onCommiterChange, + offCommiterChange, + handleTick, + ]); return timelineManager; }; diff --git a/web/src/beta/lib/core/engines/Cesium/core/Clock.tsx b/web/src/beta/lib/core/engines/Cesium/core/Clock.tsx index e256e297a6..7d5a32dc3d 100644 --- a/web/src/beta/lib/core/engines/Cesium/core/Clock.tsx +++ b/web/src/beta/lib/core/engines/Cesium/core/Clock.tsx @@ -2,25 +2,19 @@ import { Clock as CesiumClock, ClockRange, ClockStep, JulianDate } from "cesium" import { useCallback, useEffect, useMemo } from "react"; import { Clock, useCesium } from "resium"; -// import { truncMinutes } from "@reearth/beta/utils/time"; - import type { SceneProperty } from "../.."; import { type TimelineManager } from "../../../Visualizer/useTimelineManager"; export type Props = { property?: SceneProperty; - // clock?: ClockType; timelineManager?: TimelineManager; }; -export default function ReearthClock({ - property, - // clock, - timelineManager, -}: Props): JSX.Element | null { +export default function ReearthClock({ property, timelineManager }: Props): JSX.Element | null { const { visible } = property?.timeline ?? {}; const { start, stop, current, animation, stepType, rangeType, multiplier, step } = timelineManager?.overriddenTimeline ?? {}; + console.log("aaa", animation); const startTime = useMemo(() => (start ? JulianDate.fromDate(start) : undefined), [start]); const stopTime = useMemo(() => (stop ? JulianDate.fromDate(stop) : undefined), [stop]); const currentTime = useMemo( @@ -38,14 +32,8 @@ export default function ReearthClock({ const start = JulianDate.toDate(clock.startTime); const stop = JulianDate.toDate(clock.stopTime); - // // Truncate minutes for displaying correctly time on timeline widget - // const truncatedStart = truncMinutes(new Date(start)); - // if (viewer && start.toISOString() !== truncatedStart.toISOString()) { - // viewer.clock.startTime = JulianDate.fromDate(truncatedStart); - // } - // NOTE: Must not update state. This event will be called every frame. - timelineManager?.onTick?.(JulianDate.toDate(clock.currentTime), { + timelineManager?.handleTick?.(JulianDate.toDate(clock.currentTime), { start, stop, }); @@ -68,9 +56,12 @@ export default function ReearthClock({ viewer.forceResize(); }, [viewer, visible]); + console.log("animation", animation); + return ( { +export const convertTime = (time: string | Date | undefined): Date | undefined => { if (!time) return; - + if (time instanceof Date) return time; try { return new Date(time); } catch { From e5617e7bd91fc999c809f13e84785930261b4835 Mon Sep 17 00:00:00 2001 From: airslice Date: Tue, 3 Oct 2023 18:18:51 +0800 Subject: [PATCH 03/12] feat: wip --- .../lib/core/Crust/Plugins/Plugin/hooks.ts | 3 + web/src/beta/lib/core/Crust/Plugins/api.ts | 104 ++++++++ web/src/beta/lib/core/Crust/Plugins/hooks.ts | 89 +++++-- .../lib/core/Crust/Plugins/plugin_types.ts | 10 + web/src/beta/lib/core/Crust/Plugins/types.ts | 5 +- .../Widgets/Widget/builtin/Timeline/hooks.ts | 30 +-- .../Widgets/Widget/builtin/Timeline/index.tsx | 4 +- .../lib/core/Crust/Widgets/Widget/index.tsx | 6 +- web/src/beta/lib/core/Crust/context.ts | 46 ++-- web/src/beta/lib/core/Crust/index.tsx | 10 +- web/src/beta/lib/core/Map/hooks.ts | 13 + web/src/beta/lib/core/Map/index.tsx | 7 +- web/src/beta/lib/core/Map/types/index.ts | 3 +- .../beta/lib/core/Map/useTimelineManager.ts | 189 ++++++++++++++ web/src/beta/lib/core/Visualizer/hooks.ts | 39 ++- web/src/beta/lib/core/Visualizer/index.tsx | 6 +- .../lib/core/Visualizer/useTimelineManager.ts | 237 ------------------ .../engines/Cesium/Feature/Resource/index.tsx | 43 +++- .../core/engines/Cesium/Feature/context.ts | 2 + .../lib/core/engines/Cesium/core/Clock.tsx | 38 +-- .../core/engines/Cesium/core/Indicator.tsx | 26 +- web/src/beta/lib/core/engines/Cesium/hooks.ts | 23 +- .../beta/lib/core/engines/Cesium/index.tsx | 5 +- web/src/services/api/widgetsApi/utils.ts | 2 +- 24 files changed, 567 insertions(+), 373 deletions(-) create mode 100644 web/src/beta/lib/core/Map/useTimelineManager.ts delete mode 100644 web/src/beta/lib/core/Visualizer/useTimelineManager.ts diff --git a/web/src/beta/lib/core/Crust/Plugins/Plugin/hooks.ts b/web/src/beta/lib/core/Crust/Plugins/Plugin/hooks.ts index cfc0566a2b..f30d222f9a 100644 --- a/web/src/beta/lib/core/Crust/Plugins/Plugin/hooks.ts +++ b/web/src/beta/lib/core/Crust/Plugins/Plugin/hooks.ts @@ -273,6 +273,7 @@ export function useAPI({ "tick", "resize", "layeredit", + "timelinecommit", ]); } @@ -424,12 +425,14 @@ export function useAPI({ moveWidget: onWidgetMove, pluginPostMessage: ctx.pluginInstances.postMessage, clientStorage: ctx.clientStorage, + timelineAPI: ctx.timelineAPI, }); }; }, [ ctx?.reearth, ctx?.pluginInstances, ctx?.clientStorage, + ctx?.timelineAPI, ctx?.overrideSceneProperty, extensionId, extensionType, diff --git a/web/src/beta/lib/core/Crust/Plugins/api.ts b/web/src/beta/lib/core/Crust/Plugins/api.ts index 373dedb35a..22bec2a3c6 100644 --- a/web/src/beta/lib/core/Crust/Plugins/api.ts +++ b/web/src/beta/lib/core/Crust/Plugins/api.ts @@ -9,6 +9,7 @@ import { } from "@reearth/beta/lib/core/Map"; import { merge } from "@reearth/beta/utils/object"; +import { TimelineAPI } from "../../Map/useTimelineManager"; import type { Block } from "../Infobox"; import type { MapRef } from "../types"; import type { Widget, WidgetLocationOptions } from "../Widgets"; @@ -54,6 +55,7 @@ export function exposed({ moveWidget, pluginPostMessage, clientStorage, + timelineAPI, }: { render: ( html: string, @@ -93,6 +95,7 @@ export function exposed({ moveWidget?: (widgetId: string, options: WidgetLocationOptions) => void; pluginPostMessage: (extentionId: string, msg: any, sender: string) => void; clientStorage: ClientStorage; + timelineAPI?: TimelineAPI; }): GlobalThis { return merge({ console: { @@ -174,6 +177,107 @@ export function exposed({ }; }, }), + clock: merge(commonReearth.clock, { + get play() { + return () => { + timelineAPI?.current?.commit({ + cmd: "PLAY", + committer: { + source: "pluginAPI", + id: + (plugin?.extensionType === "widget" + ? widget?.()?.id + : plugin?.extensionType === "block" + ? block?.()?.id + : "") ?? "", + }, + }); + }; + }, + get pause() { + return () => + timelineAPI?.current?.commit({ + cmd: "PAUSE", + committer: { + source: "pluginAPI", + id: + (plugin?.extensionType === "widget" + ? widget?.()?.id + : plugin?.extensionType === "block" + ? block?.()?.id + : "") ?? "", + }, + }); + }, + get setTime() { + return (time: { + start?: Date | string; + stop?: Date | string; + current?: Date | string; + }) => + timelineAPI?.current?.commit({ + cmd: "SET_TIME", + payload: { ...time }, + committer: { + source: "pluginAPI", + id: + (plugin?.extensionType === "widget" + ? widget?.()?.id + : plugin?.extensionType === "block" + ? block?.()?.id + : "") ?? "", + }, + }); + }, + get setSpeed() { + return (speed: number) => + timelineAPI?.current?.commit({ + cmd: "SET_OPTIONS", + payload: { multiplier: speed }, + committer: { + source: "pluginAPI", + id: + (plugin?.extensionType === "widget" + ? widget?.()?.id + : plugin?.extensionType === "block" + ? block?.()?.id + : "") ?? "", + }, + }); + }, + get setStepType() { + return (stepType: "fixed" | "rate") => + timelineAPI?.current?.commit({ + cmd: "SET_OPTIONS", + payload: { stepType }, + committer: { + source: "pluginAPI", + id: + (plugin?.extensionType === "widget" + ? widget?.()?.id + : plugin?.extensionType === "block" + ? block?.()?.id + : "") ?? "", + }, + }); + }, + get setRangeType() { + return (rangeType: "unbounded" | "clamped" | "bounced") => + timelineAPI?.current?.commit({ + cmd: "SET_OPTIONS", + payload: { rangeType }, + committer: { + source: "pluginAPI", + id: + (plugin?.extensionType === "widget" + ? widget?.()?.id + : plugin?.extensionType === "block" + ? block?.()?.id + : "") ?? "", + }, + }); + }, + }), plugin: { get id() { return plugin?.id; diff --git a/web/src/beta/lib/core/Crust/Plugins/hooks.ts b/web/src/beta/lib/core/Crust/Plugins/hooks.ts index 069346009d..ddb5476e49 100644 --- a/web/src/beta/lib/core/Crust/Plugins/hooks.ts +++ b/web/src/beta/lib/core/Crust/Plugins/hooks.ts @@ -10,6 +10,7 @@ import { type TickEventCallback, } from "@reearth/beta/lib/core/Map"; +import { TimelineCommitter } from "../../Map/useTimelineManager"; import { CameraOptions, FlyTo, FlyToDestination, LookAtDestination } from "../../types"; import { commonReearth } from "./api"; @@ -20,7 +21,7 @@ import usePluginInstances from "./usePluginInstances"; export type SelectedReearthEventType = Pick< ReearthEventType, - "cameramove" | "select" | "tick" | "resize" | keyof MouseEvents | "layeredit" + "cameramove" | "select" | "tick" | "timelinecommit" | "resize" | keyof MouseEvents | "layeredit" >; export default function ({ @@ -38,7 +39,7 @@ export default function ({ floatingWidgets, camera, interactionMode, - timelineManager, + timelineAPI, overrideInteractionMode, useExperimentalSandbox, overrideSceneProperty, @@ -64,21 +65,67 @@ export default function ({ const getCamera = useGet(camera); const getClock = useCallback(() => { return { - startTime: timelineManager?.timeline?.start, - stopTime: timelineManager?.timeline?.stop, - currentTime: timelineManager?.timeline?.current, - playing: !!timelineManager?.timeline?.animation, - paused: !timelineManager?.timeline?.animation, - speed: timelineManager?.timeline?.multiplier, + get startTime() { + return timelineAPI?.current?.timeline?.start; + }, + get stopTime() { + return timelineAPI?.current?.timeline?.stop; + }, + get currentTime() { + return timelineAPI?.current?.timeline?.current; + }, + get playing() { + return !!timelineAPI?.current?.options?.animation; + }, + get paused() { + return !timelineAPI?.current?.options?.animation; + }, + get speed() { + return timelineAPI?.current?.options?.multiplier; + }, play: () => { - timelineManager?.commit({ cmd: "PLAY", commiter: { source: "pluginAPI" } }); + timelineAPI?.current?.commit({ + cmd: "PLAY", + committer: { source: "pluginAPI", id: "window" }, + }); }, pause: () => { - timelineManager?.commit({ cmd: "PAUSE", commiter: { source: "pluginAPI" } }); + timelineAPI?.current?.commit({ + cmd: "PAUSE", + committer: { source: "pluginAPI", id: "window" }, + }); + }, + setTime: (time: { start?: Date | string; stop?: Date | string; current?: Date | string }) => { + timelineAPI?.current?.commit({ + cmd: "SET_TIME", + payload: { ...time }, + committer: { source: "pluginAPI", id: "window" }, + }); + }, + setSpeed: (speed: number) => { + timelineAPI?.current?.commit({ + cmd: "SET_OPTIONS", + payload: { multiplier: speed }, + committer: { source: "pluginAPI", id: "window" }, + }); }, - tick: timelineManager?.tick, + setStepType: (stepType: "rate" | "fixed") => { + timelineAPI?.current?.commit({ + cmd: "SET_OPTIONS", + payload: { stepType }, + committer: { source: "pluginAPI", id: "window" }, + }); + }, + setRangeType: (rangeType: "unbounded" | "clamped" | "bounced") => { + timelineAPI?.current?.commit({ + cmd: "SET_OPTIONS", + payload: { rangeType }, + committer: { source: "pluginAPI", id: "window" }, + }); + }, + tick: timelineAPI?.current?.tick, }; - }, [timelineManager]); + }, [timelineAPI]); const getInteractionMode = useGet( useMemo( () => ({ mode: interactionMode, override: overrideInteractionMode }), @@ -356,6 +403,7 @@ export default function ({ overrideSceneProperty, pluginInstances, clientStorage, + timelineAPI, useExperimentalSandbox, }), [ @@ -406,6 +454,7 @@ export default function ({ overrideSceneProperty, pluginInstances, clientStorage, + timelineAPI, useExperimentalSandbox, findFeatureById, findFeaturesByIds, @@ -445,9 +494,16 @@ export default function ({ const onTickEvent = useCallback( (fn: TickEventCallback) => { - mapRef?.current?.engine.onTick?.(fn); + timelineAPI?.current?.onTick(fn); }, - [mapRef], + [timelineAPI], + ); + + const onTimelineCommitEvent = useCallback( + (fn: (committer: TimelineCommitter) => void) => { + timelineAPI?.current?.onCommit(fn); + }, + [timelineAPI], ); useEffect(() => { @@ -480,7 +536,10 @@ export default function ({ onTickEvent(e => { emit("tick", e); }); - }, [emit, onMouseEvent, onLayerEdit, onTickEvent]); + onTimelineCommitEvent(e => { + emit("timelinecommit", e); + }); + }, [emit, onMouseEvent, onLayerEdit, onTickEvent, onTimelineCommitEvent]); // expose plugin API for developers useEffect(() => { diff --git a/web/src/beta/lib/core/Crust/Plugins/plugin_types.ts b/web/src/beta/lib/core/Crust/Plugins/plugin_types.ts index 68ef60f83a..3c47014760 100644 --- a/web/src/beta/lib/core/Crust/Plugins/plugin_types.ts +++ b/web/src/beta/lib/core/Crust/Plugins/plugin_types.ts @@ -20,6 +20,7 @@ import type { Feature, } from "@reearth/beta/lib/core/Map"; +import { TimelineCommitter } from "../../Map/useTimelineManager"; import { CameraOptions, FlyToDestination, LookAtDestination } from "../../types"; import { Block } from "../Infobox"; import { InteractionModeType } from "../types"; @@ -176,6 +177,14 @@ export type Clock = { readonly tick?: () => Date | void; readonly play?: () => void; readonly pause?: () => void; + readonly setTime?: (time: { + start?: Date | string; + stop?: Date | string; + current?: Date | string; + }) => void; + readonly setSpeed?: (speed: number) => void; + readonly setRangeType?: (rangeType: "unbounded" | "clamped" | "bounced") => void; + readonly setStepType?: (stepType: "rate" | "fixed") => void; }; export type InteractionMode = { @@ -210,6 +219,7 @@ export type ReearthEventType = { mouseleave: [props: MouseEvent]; wheel: [props: MouseEvent]; tick: [props: Date]; + timelinecommit: [props: TimelineCommitter]; resize: [props: ViewportSize]; modalclose: []; popupclose: []; diff --git a/web/src/beta/lib/core/Crust/Plugins/types.ts b/web/src/beta/lib/core/Crust/Plugins/types.ts index 18024514a4..c1c0663086 100644 --- a/web/src/beta/lib/core/Crust/Plugins/types.ts +++ b/web/src/beta/lib/core/Crust/Plugins/types.ts @@ -13,7 +13,7 @@ import type { } from "@reearth/beta/lib/core/Map"; import type { Viewport } from "@reearth/beta/lib/core/Visualizer"; -import { TimelineManager } from "../../Visualizer/useTimelineManager"; +import { TimelineAPI } from "../../Map/useTimelineManager"; import type { MapRef, InteractionModeType } from "../types"; import type { InternalWidget, WidgetAlignSystem } from "../Widgets"; @@ -36,7 +36,7 @@ export type Props = PropsWithChildren<{ alignSystem?: WidgetAlignSystem; floatingWidgets?: InternalWidget[]; useExperimentalSandbox?: boolean; - timelineManager?: TimelineManager; + timelineAPI?: TimelineAPI; overrideSceneProperty: (id: string, property: any) => void; camera?: Camera; interactionMode: InteractionModeType; @@ -48,6 +48,7 @@ export type Context = { reearth: CommonReearth; pluginInstances: PluginInstances; clientStorage: ClientStorage; + timelineAPI?: TimelineAPI; useExperimentalSandbox?: boolean; overrideSceneProperty: (id: string, property: any) => void; }; diff --git a/web/src/beta/lib/core/Crust/Widgets/Widget/builtin/Timeline/hooks.ts b/web/src/beta/lib/core/Crust/Widgets/Widget/builtin/Timeline/hooks.ts index 200f38e2ef..7f38c0fb3b 100644 --- a/web/src/beta/lib/core/Crust/Widgets/Widget/builtin/Timeline/hooks.ts +++ b/web/src/beta/lib/core/Crust/Widgets/Widget/builtin/Timeline/hooks.ts @@ -2,7 +2,7 @@ import { useState, useCallback, useEffect, useRef } from "react"; import type { TimeEventHandler } from "@reearth/beta/lib/core/Crust/Widgets/Widget/builtin/Timeline/UI"; import { TickEvent, TickEventCallback } from "@reearth/beta/lib/core/Map"; -import { TimelineManager } from "@reearth/beta/lib/core/Visualizer/useTimelineManager"; +import { TimelineAPI } from "@reearth/beta/lib/core/Map/useTimelineManager"; import type { Widget } from "../../types"; import { useVisible } from "../../useVisible"; @@ -22,7 +22,7 @@ const DEFAULT_SPEED = 1; export const useTimeline = ({ widget, - timelineManager, + timelineAPI, isMobile, onPlay, onPause, @@ -34,7 +34,7 @@ export const useTimeline = ({ onVisibilityChange, }: { widget: Widget; - timelineManager?: TimelineManager; + timelineAPI?: TimelineAPI; isMobile?: boolean; onPlay?: () => void; onPause?: () => void; @@ -54,18 +54,18 @@ export const useTimeline = ({ const widgetId = widget.id; const [range, setRange] = useState(() => makeRange( - timelineManager?.timeline?.start?.getTime(), - timelineManager?.timeline?.stop?.getTime(), + timelineAPI?.current?.timeline?.start?.getTime(), + timelineAPI?.current?.timeline?.stop?.getTime(), ), ); const [isOpened, setIsOpened] = useState(true); const [currentTime, setCurrentTime] = useState(() => - getOrNewDate(timelineManager?.timeline?.current).getTime(), + getOrNewDate(timelineAPI?.current?.timeline?.current).getTime(), ); const isClockInitialized = useRef(false); - const clockStartTime = timelineManager?.timeline?.start?.getTime(); - const clockStopTime = timelineManager?.timeline?.stop?.getTime(); - const clockSpeed = timelineManager?.timeline?.multiplier || DEFAULT_SPEED; + const clockStartTime = timelineAPI?.current?.timeline?.start?.getTime(); + const clockStopTime = timelineAPI?.current?.timeline?.stop?.getTime(); + const clockSpeed = timelineAPI?.current?.options?.multiplier || DEFAULT_SPEED; const [speed, setSpeed] = useState(clockSpeed); @@ -140,9 +140,11 @@ export const useTimeline = ({ const absSpeed = Math.abs(speed); // Maybe we need to throttle changing speed. - onSpeedChange?.((timelineManager?.timeline?.multiplier ?? 1) > 0 ? absSpeed : absSpeed * -1); + onSpeedChange?.( + (timelineAPI?.current?.options?.multiplier ?? 1) > 0 ? absSpeed : absSpeed * -1, + ); }, - [onSpeedChange, timelineManager], + [onSpeedChange, timelineAPI], ); // Initialize clock value @@ -165,8 +167,8 @@ export const useTimeline = ({ }); }, []); - const overriddenStart = timelineManager?.overriddenTimeline?.start?.getTime(); - const overriddenStop = timelineManager?.overriddenTimeline?.stop?.getTime(); + const overriddenStart = timelineAPI?.current?.computedTimeline?.start?.getTime(); + const overriddenStop = timelineAPI?.current?.computedTimeline?.stop?.getTime(); // Sync cesium clock. useEffect(() => { @@ -201,7 +203,7 @@ export const useTimeline = ({ onTimeChangeRef.current = onTimeChange; }, [onTimeChange]); - const overriddenCurrentTime = timelineManager?.overriddenTimeline?.current?.getTime(); + const overriddenCurrentTime = timelineAPI?.current?.computedTimeline?.current?.getTime(); useEffect(() => { if (overriddenCurrentTime) { const t = Math.max(Math.min(range.end, overriddenCurrentTime), range.start); diff --git a/web/src/beta/lib/core/Crust/Widgets/Widget/builtin/Timeline/index.tsx b/web/src/beta/lib/core/Crust/Widgets/Widget/builtin/Timeline/index.tsx index a3ebe6c1d0..ef475a045b 100644 --- a/web/src/beta/lib/core/Crust/Widgets/Widget/builtin/Timeline/index.tsx +++ b/web/src/beta/lib/core/Crust/Widgets/Widget/builtin/Timeline/index.tsx @@ -21,7 +21,7 @@ const Timeline = ({ onExtend, onVisibilityChange, context: { - timelineManager, + timelineAPI, onPlay, onPause, onSpeedChange, @@ -32,7 +32,7 @@ const Timeline = ({ }: Props): JSX.Element | null => { const { isOpened, currentTime, range, speed, events, visible } = useTimeline({ widget, - timelineManager, + timelineAPI, isMobile, onPlay, onPause, diff --git a/web/src/beta/lib/core/Crust/Widgets/Widget/index.tsx b/web/src/beta/lib/core/Crust/Widgets/Widget/index.tsx index 133e5cdd00..a1666e0432 100644 --- a/web/src/beta/lib/core/Crust/Widgets/Widget/index.tsx +++ b/web/src/beta/lib/core/Crust/Widgets/Widget/index.tsx @@ -2,7 +2,7 @@ import { ComponentType, ReactNode, useMemo } from "react"; import type { TickEvent } from "@reearth/beta/lib/core/Map"; -import { TimelineCommiter, TimelineManager } from "../../../Visualizer/useTimelineManager"; +import { TimelineAPI, TimelineCommitter } from "../../../Map/useTimelineManager"; import builtin, { isBuiltinWidget } from "./builtin"; import type { @@ -38,7 +38,7 @@ export type Props = { export type Context = { clock?: Clock; - timelineManager?: TimelineManager; + timelineAPI?: TimelineAPI; updateClockOnLoad?: boolean; camera?: Camera; initialCamera?: Camera; @@ -54,7 +54,7 @@ export type Context = { featureId: string | undefined, options?: { reason?: string }, ) => void; - onPlay?: (commiter?: TimelineCommiter) => void; + onPlay?: (committer?: TimelineCommitter) => void; onPause?: () => void; onSpeedChange?: (speed: number) => void; onTimeChange?: (time: Date) => void; diff --git a/web/src/beta/lib/core/Crust/context.ts b/web/src/beta/lib/core/Crust/context.ts index 450d5da74c..0f61dfcadf 100644 --- a/web/src/beta/lib/core/Crust/context.ts +++ b/web/src/beta/lib/core/Crust/context.ts @@ -1,6 +1,6 @@ import { RefObject, useMemo } from "react"; -import { TimelineCommiter, TimelineManager } from "../Visualizer/useTimelineManager"; +import { TimelineAPI, TimelineCommitter } from "../Map/useTimelineManager"; import { Camera, MapRef, SceneProperty } from "./types"; import { Context as WidgetContext } from "./Widgets"; @@ -10,7 +10,7 @@ export const useWidgetContext = ({ camera, selectedLayerId, sceneProperty, - timelineManager, + timelineAPI, }: Parameters[0]) => useMemo( () => @@ -19,9 +19,9 @@ export const useWidgetContext = ({ camera, selectedLayerId, sceneProperty, - timelineManager, + timelineAPI, }), - [camera, mapRef, sceneProperty, selectedLayerId, timelineManager], + [camera, mapRef, sceneProperty, selectedLayerId, timelineAPI], ); export function widgetContextFromMapRef({ @@ -29,7 +29,7 @@ export function widgetContextFromMapRef({ camera, selectedLayerId, sceneProperty, - timelineManager, + timelineAPI, }: { mapRef?: RefObject; camera?: Camera; @@ -38,7 +38,7 @@ export function widgetContextFromMapRef({ featureId?: string; }; sceneProperty?: SceneProperty; - timelineManager?: TimelineManager; + timelineAPI?: TimelineAPI; }): WidgetContext { const engine = () => mapRef?.current?.engine; const layers = () => mapRef?.current?.layers; @@ -48,7 +48,7 @@ export function widgetContextFromMapRef({ get clock() { return engine()?.getClock(); }, - timelineManager, + timelineAPI, initialCamera: sceneProperty?.default?.camera, is2d: sceneProperty?.default?.sceneMode === "2d", selectedLayerId, @@ -71,34 +71,34 @@ export function widgetContextFromMapRef({ onFlyTo: (...args) => engine()?.flyTo(...args), onLookAt: (...args) => engine()?.lookAt(...args), onLayerSelect: (...args) => layers()?.select(...args), - onPause: (commiter?: TimelineCommiter) => - timelineManager?.commit({ + onPause: (committer?: TimelineCommitter) => + timelineAPI?.current?.commit({ cmd: "PAUSE", - commiter: { source: commiter?.source ?? "widgetContext", id: commiter?.id }, + committer: { source: committer?.source ?? "widgetContext", id: committer?.id }, }), - onPlay: (commiter?: TimelineCommiter) => - timelineManager?.commit({ + onPlay: (committer?: TimelineCommitter) => + timelineAPI?.current?.commit({ cmd: "PLAY", - commiter: { source: commiter?.source ?? "widgetContext", id: commiter?.id }, + committer: { source: committer?.source ?? "widgetContext", id: committer?.id }, }), - onSpeedChange: (speed, commiter?: TimelineCommiter) => - timelineManager?.commit({ - cmd: "UPDATE", + onSpeedChange: (speed, committer?: TimelineCommitter) => + timelineAPI?.current?.commit({ + cmd: "SET_OPTIONS", payload: { multiplier: speed, stepType: "rate", }, - commiter: { source: commiter?.source ?? "widgetContext", id: commiter?.id }, + committer: { source: committer?.source ?? "widgetContext", id: committer?.id }, }), - onTick: cb => timelineManager?.onTick(cb), - removeTickEventListener: cb => timelineManager?.offTick(cb), - onTimeChange: (time, commiter?: TimelineCommiter) => - timelineManager?.commit({ - cmd: "UPDATE", + onTick: cb => timelineAPI?.current?.onTick(cb), + removeTickEventListener: cb => timelineAPI?.current?.offTick(cb), + onTimeChange: (time, committer?: TimelineCommitter) => + timelineAPI?.current?.commit({ + cmd: "SET_TIME", payload: { current: time, }, - commiter: { source: commiter?.source ?? "widgetContext", id: commiter?.id }, + committer: { source: committer?.source ?? "widgetContext", id: committer?.id }, }), onZoomIn: (...args) => engine()?.zoomIn(...args), onZoomOut: (...args) => engine()?.zoomOut(...args), diff --git a/web/src/beta/lib/core/Crust/index.tsx b/web/src/beta/lib/core/Crust/index.tsx index 743ce8e0f7..b787a0e3c3 100644 --- a/web/src/beta/lib/core/Crust/index.tsx +++ b/web/src/beta/lib/core/Crust/index.tsx @@ -4,8 +4,8 @@ import type { SelectedFeatureInfo, Tag } from "@reearth/beta/lib/core/mantle"; import type { ComputedFeature, ComputedLayer, Feature } from "../mantle"; import type { Clock, LayerEditEvent, LayerSelectionReason } from "../Map"; +import type { TimelineAPI } from "../Map/useTimelineManager"; import type { Viewport } from "../Visualizer"; -import type { TimelineManager } from "../Visualizer/useTimelineManager"; import { useWidgetContext } from "./context"; import useHooks from "./hooks"; @@ -92,7 +92,7 @@ export type Props = { externalPlugin: ExternalPluginProps; useExperimentalSandbox?: boolean; // timeline manager - timelineManager?: TimelineManager; + timelineAPI?: TimelineAPI; // widget events onWidgetLayoutUpdate?: ( id: string, @@ -153,7 +153,7 @@ export default function Crust({ selectedWidgetArea, externalPlugin, useExperimentalSandbox, - timelineManager, + timelineAPI, onWidgetLayoutUpdate, onWidgetAlignmentUpdate, onWidgetAreaSelect, @@ -183,7 +183,7 @@ export default function Crust({ camera, sceneProperty, selectedLayerId, - timelineManager, + timelineAPI, }); return ( @@ -206,7 +206,7 @@ export default function Crust({ overrideInteractionMode={overrideInteractionMode} useExperimentalSandbox={useExperimentalSandbox} overrideSceneProperty={overrideSceneProperty} - timelineManager={timelineManager} + timelineAPI={timelineAPI} onLayerEdit={onLayerEdit}> ; @@ -26,6 +30,8 @@ export default function ({ layerId?: string; featureId?: string; }; + sceneProperty?: SceneProperty; + timelineAPI?: TimelineAPI; onLayerSelect?: ( layerId: string | undefined, featureId: string | undefined, @@ -100,11 +106,18 @@ export default function ({ ); }, [onLayerSelect, selectedLayer]); + const timelineManager = useTimelineManager({ + init: sceneProperty?.timeline, + engineRef, + timelineAPI, + }); + return { engineRef, layersRef, selectedLayer, requestingRenderMode, + timelineManager, handleLayerSelect, handleEngineLayerSelect, }; diff --git a/web/src/beta/lib/core/Map/index.tsx b/web/src/beta/lib/core/Map/index.tsx index 4860663b48..2b156efcb5 100644 --- a/web/src/beta/lib/core/Map/index.tsx +++ b/web/src/beta/lib/core/Map/index.tsx @@ -42,7 +42,8 @@ function Map( overrides, selectedLayerId, layerSelectionReason, - timelineManager, + timelineAPI, + sceneProperty, onLayerSelect, ...props }: Props, @@ -55,11 +56,14 @@ function Map( layersRef, selectedLayer, requestingRenderMode, + timelineManager, handleLayerSelect, handleEngineLayerSelect, } = useHooks({ ref, selectedLayerId, + sceneProperty, + timelineAPI, onLayerSelect, }); @@ -78,6 +82,7 @@ function Map( onLayerSelect={handleEngineLayerSelect} layersRef={layersRef} requestingRenderMode={requestingRenderMode} + timelineAPI={timelineAPI} timelineManager={timelineManager} {...props}> ; layersRef?: RefObject; requestingRenderMode?: MutableRefObject; + timelineAPI?: TimelineAPI; timelineManager?: TimelineManager; onLayerSelect?: ( layerId: string | undefined, diff --git a/web/src/beta/lib/core/Map/useTimelineManager.ts b/web/src/beta/lib/core/Map/useTimelineManager.ts new file mode 100644 index 0000000000..78490b15d0 --- /dev/null +++ b/web/src/beta/lib/core/Map/useTimelineManager.ts @@ -0,0 +1,189 @@ +import { useCallback, useRef, useState, useMemo, MutableRefObject, RefObject } from "react"; + +import { convertTime } from "@reearth/beta/utils/time"; + +import { EngineRef } from "."; + +export type TimelineManager = { + readonly timeline: Timeline; + readonly options: TimelineOptions; + readonly computedTimeline: Timeline; + commit: (props: TimelineCommit) => void; + onTick: TickEvent; + offTick: TickEvent; + onCommit: (cb: (committer: TimelineCommitter) => void) => void; + offCommit: (cb: (committer: TimelineCommitter) => void) => void; + // for connect engine onTick + handleTick: (d: Date, clock: { start: Date; stop: Date }) => void; + tick: (() => Date | void | undefined) | undefined; +}; + +export type TimelineAPI = MutableRefObject; + +export type Timeline = { + current?: Date; + start?: Date; + stop?: Date; +}; + +type TimelineOptions = { + animation: boolean; + stepType: "rate" | "fixed"; + multiplier: number; + rangeType?: "unbounded" | "clamped" | "bounced"; +}; + +type TimelineCommand = "PLAY" | "PAUSE" | "SET_TIME" | "SET_OPTIONS"; + +type TimelineCommit = { + cmd: TimelineCommand; + payload?: + | ({ + current?: Date | string | undefined; + start?: Date | string | undefined; + stop?: Date | string | undefined; + } & Partial) + | undefined; + committer: TimelineCommitter; +}; + +export type TimelineCommitter = { + source: "overrideSceneProperty" | "widgetContext" | "pluginAPI" | "featureResource"; + id?: string; +}; + +export type TickEvent = (cb: TickEventCallback) => void; +export type TickEventCallback = (current: Date, clock: { start: Date; stop: Date }) => void; + +const DEFAULT_RANGE = 86400000; // a day + +type Props = { + init?: { + current?: string; + start?: string; + stop?: string; + animation?: boolean; + step?: number; + stepType?: "rate" | "fixed"; + multiplier?: number; + rangeType?: "unbounded" | "clamped" | "bounced"; + }; + engineRef?: RefObject; + timelineAPI?: TimelineAPI; +}; + +export default ({ init, engineRef, timelineAPI }: Props) => { + const [time, setTime] = useState({ + start: convertTime(init?.start), + stop: convertTime(init?.stop), + current: convertTime(init?.current), + }); + + const [options, setOptions] = useState({ + animation: init?.animation ?? false, + stepType: init?.stepType ?? "rate", + multiplier: init?.stepType === "fixed" ? init?.step ?? 1 : init?.multiplier ?? 1, + rangeType: init?.rangeType ?? "unbounded", + }); + + const validTimes = useMemo(() => { + const { start, stop, current } = time; + const startTime = start?.getTime() ?? new Date().getTime(); + const stopTime = stop?.getTime() ?? new Date().getTime(); + const currentTime = current?.getTime() ?? new Date().getTime(); + + const convertedStartTime = startTime > currentTime ? currentTime : startTime; + const convertedStopTime = stopTime <= currentTime ? currentTime + DEFAULT_RANGE : stopTime; + + return { + start: new Date(convertedStartTime), + stop: new Date(convertedStopTime), + current: new Date(currentTime), + }; + }, [time]); + + const commit = useCallback(({ cmd, payload, committer }: TimelineCommit) => { + if (!cmd) return; + + if (cmd === "PLAY") { + setOptions(o => ({ ...o, animation: true })); + } else if (cmd === "PAUSE") { + setOptions(o => ({ ...o, animation: false })); + } else if (cmd === "SET_TIME") { + setTime(t => ({ + start: payload?.start === undefined ? t.start : convertTime(payload?.start), + stop: payload?.stop === undefined ? t.stop : convertTime(payload?.stop), + current: payload?.current === undefined ? t.current : convertTime(payload?.current), + })); + } else if (cmd === "SET_OPTIONS") { + setOptions(o => ({ + ...o, + stepType: payload?.stepType === undefined ? o.stepType : payload.stepType, + multiplier: payload?.multiplier === undefined ? o.multiplier : payload.multiplier, + rangeType: payload?.rangeType === undefined ? o.rangeType : payload.rangeType, + })); + } + + commitEventCallbacks.current.forEach(cb => cb(committer)); + }, []); + + const tickEventCallbacks = useRef([]); + const onTick = useCallback((cb: TickEventCallback) => { + tickEventCallbacks.current.push(cb); + }, []); + const offTick = useCallback((cb: TickEventCallback) => { + tickEventCallbacks.current = tickEventCallbacks.current.filter(c => c !== cb); + }, []); + + const commitEventCallbacks = useRef<((committer: TimelineCommitter) => void)[]>([]); + const onCommit = useCallback((cb: (committer: TimelineCommitter) => void) => { + commitEventCallbacks.current.push(cb); + }, []); + const offCommit = useCallback((cb: (committer: TimelineCommitter) => void) => { + commitEventCallbacks.current = commitEventCallbacks.current.filter(c => c !== cb); + }, []); + + const handleTick = useCallback(() => { + tickEventCallbacks.current.forEach(cb => { + const engineClock = engineRef?.current?.getClock(); + if (engineClock?.current && engineClock?.start && engineClock?.stop) { + cb(engineClock.current, { start: engineClock?.start, stop: engineClock?.stop }); + } + }); + }, [engineRef]); + + const timelineManager = useMemo(() => { + return { + get timeline() { + return { + get start() { + return engineRef?.current?.getClock()?.start; + }, + get stop() { + return engineRef?.current?.getClock()?.stop; + }, + get current() { + return engineRef?.current?.getClock()?.current; + }, + }; + }, + computedTimeline: { + ...validTimes, + }, + options, + commit, + onTick, + offTick, + onCommit, + offCommit, + handleTick, + tick: engineRef?.current?.tick, + }; + }, [options, validTimes, engineRef, commit, onTick, offTick, onCommit, offCommit, handleTick]); + + if (timelineAPI) { + timelineAPI.current = timelineManager; + } + + return timelineManager; +}; diff --git a/web/src/beta/lib/core/Visualizer/hooks.ts b/web/src/beta/lib/core/Visualizer/hooks.ts index 2d3547f5ea..276d823499 100644 --- a/web/src/beta/lib/core/Visualizer/hooks.ts +++ b/web/src/beta/lib/core/Visualizer/hooks.ts @@ -20,8 +20,8 @@ import type { DefaultInfobox, } from "../Map"; import { useOverriddenProperty } from "../Map"; +import { TimelineAPI } from "../Map/useTimelineManager"; -import useTimelineManager from "./useTimelineManager"; import useViewport from "./useViewport"; const viewportMobileMaxWidth = 768; @@ -159,10 +159,11 @@ export default function useHooks( }, [infobox]); // timeline manager - const timelineManager = useTimelineManager({ - init: sceneProperty?.timeline, - engineRef: mapRef.current?.engine, - }); + // const { timelineManager, timelineAPI } = useTimelineManager({ + // init: sceneProperty?.timeline, + // engineRef: mapRef.current?.engine, + // }); + const timelineAPI: TimelineAPI = useRef(); // scene const [overriddenSceneProperty, originalOverrideSceneProperty] = @@ -175,10 +176,26 @@ export default function useHooks( const filteredTimeline = clone(property.timeline); delete filteredTimeline.visible; if (Object.keys(filteredTimeline).length > 0) { - timelineManager?.commit({ - cmd: "UPDATE", - payload: filteredTimeline, - commiter: { + timelineAPI?.current?.commit({ + cmd: "SET_TIME", + payload: { + start: filteredTimeline.start, + stop: filteredTimeline.stop, + current: filteredTimeline.current, + }, + committer: { + source: "overrideSceneProperty", + id: pluginId, + }, + }); + timelineAPI?.current?.commit({ + cmd: "SET_OPTIONS", + payload: { + stepType: filteredTimeline.stepType, + multiplier: filteredTimeline.multiplier, + rangeType: filteredTimeline.rangeType, + }, + committer: { source: "overrideSceneProperty", id: pluginId, }, @@ -189,7 +206,7 @@ export default function useHooks( // Just remember we will NOT use the timeline from overridden scene property directly. originalOverrideSceneProperty(pluginId, property); }, - [timelineManager, originalOverrideSceneProperty], + [timelineAPI, originalOverrideSceneProperty], ); // clock @@ -315,7 +332,7 @@ export default function useHooks( infobox, isLayerDragging, shouldRender, - timelineManager, + timelineAPI, handleLayerSelect, handleBlockSelect: selectBlock, handleCameraChange: changeCamera, diff --git a/web/src/beta/lib/core/Visualizer/index.tsx b/web/src/beta/lib/core/Visualizer/index.tsx index 18771dedb7..00da480030 100644 --- a/web/src/beta/lib/core/Visualizer/index.tsx +++ b/web/src/beta/lib/core/Visualizer/index.tsx @@ -191,7 +191,7 @@ const Visualizer = memo( isLayerDragging, infobox, shouldRender, - timelineManager, + timelineAPI, handleLayerSelect, handleBlockSelect, handleCameraChange, @@ -265,7 +265,7 @@ const Visualizer = memo( mapRef={mapRef} externalPlugin={{ pluginBaseUrl, pluginProperty }} useExperimentalSandbox={useExperimentalSandbox} - timelineManager={timelineManager} + timelineAPI={timelineAPI} onWidgetLayoutUpdate={onWidgetLayoutUpdate} onWidgetAlignmentUpdate={onWidgetAlignmentUpdate} onWidgetAreaSelect={onWidgetAreaSelect} @@ -301,7 +301,7 @@ const Visualizer = memo( layerSelectionReason={layerSelectionReason} small={small} ready={ready} - timelineManager={timelineManager} + timelineAPI={timelineAPI} onCameraChange={handleCameraChange} onLayerDrag={handleLayerDrag} onLayerDrop={handleLayerDrop} diff --git a/web/src/beta/lib/core/Visualizer/useTimelineManager.ts b/web/src/beta/lib/core/Visualizer/useTimelineManager.ts deleted file mode 100644 index aa7f403af2..0000000000 --- a/web/src/beta/lib/core/Visualizer/useTimelineManager.ts +++ /dev/null @@ -1,237 +0,0 @@ -import { useCallback, useRef, useState, useMemo } from "react"; - -import { convertTime, truncMinutes } from "@reearth/beta/utils/time"; - -import { EngineRef, WrappedRef } from "../Map"; - -export type TimelineManager = { - readonly timeline: Timeline; - readonly overriddenTimeline: Timeline; - commit: (props: TimelineCommit) => void; - onTick: TickEvent; - offTick: TickEvent; - onCommiterChange: (cb: () => void) => void; - offCommiterChange: (cb: () => void) => void; - // for connect engine onTick - handleTick: (d: Date, clock: { start: Date; stop: Date }) => void; - tick: (() => Date | void | undefined) | undefined; -}; - -export type Timeline = TimeDate & TimelineOptions; - -type TimeDate = { - current?: Date; - start?: Date; - stop?: Date; -}; - -type TimelineOptions = { - animation: boolean; - step: number; - stepType: "rate" | "fixed"; - multiplier: number; - rangeType?: "unbounded" | "clamped" | "bounced"; -}; - -type TimelineCommand = "PLAY" | "PAUSE" | "UPDATE"; - -type TimelineCommit = { - cmd: TimelineCommand; - payload?: - | ({ - current?: Date | string | undefined; - start?: Date | string | undefined; - stop?: Date | string | undefined; - } & Partial) - | undefined; - commiter: TimelineCommiter; -}; - -export type TimelineCommiter = { - source: "overrideSceneProperty" | "widgetContext" | "pluginAPI"; - id?: string; -}; - -export type TickEvent = (cb: TickEventCallback) => void; -export type TickEventCallback = (current: Date, clock: { start: Date; stop: Date }) => void; - -const DEFAULT_RANGE = 86400000; // a day - -type Props = { - init?: { - current?: string; - start?: string; - stop?: string; - } & Partial; - engineRef?: WrappedRef; -}; - -export default ({ init, engineRef }: Props) => { - const [time, setTime] = useState({ - start: convertTime(init?.start), - stop: convertTime(init?.stop), - current: convertTime(init?.current), - }); - - const [options, setOptions] = useState({ - animation: init?.animation ?? false, - step: init?.step ?? 1, - stepType: init?.stepType ?? "rate", - multiplier: init?.multiplier ?? 1, - rangeType: init?.rangeType ?? "unbounded", - }); - - const validTimes = useMemo(() => { - const { start, stop, current } = time; - const startTime = start?.getTime(); - const stopTime = stop?.getTime(); - const currentTime = current?.getTime(); - - // TODO: validate time - const now = Date.now(); - - const convertedStartTime = startTime - ? Math.min(now, startTime) - : stopTime - ? Math.min(now, stopTime - DEFAULT_RANGE) - : now - DEFAULT_RANGE; - - const convertedStopTime = stopTime - ? Math.max(stopTime, now) - : startTime - ? Math.max(now, startTime + DEFAULT_RANGE) - : now; - - return { - start: truncMinutes(new Date(convertedStartTime)), - stop: new Date(convertedStopTime), - current: new Date( - Math.max( - Math.min(currentTime || convertedStartTime, convertedStopTime), - convertedStartTime, - ), - ), - }; - }, [time]); - - // Commiter Record - const lastCommiter = useRef(); - - const commit = useCallback(({ cmd, payload, commiter }: TimelineCommit) => { - console.log("commit", cmd, payload, commiter); - if (!cmd) return; - - if (cmd === "UPDATE") { - setTime(t => ({ - start: payload?.start === undefined ? t.start : convertTime(payload?.start), - stop: payload?.stop === undefined ? t.stop : convertTime(payload?.stop), - current: payload?.current === undefined ? t.current : convertTime(payload?.current), - })); - setOptions(o => ({ - animation: payload?.animation === undefined ? o.animation : payload.animation, - step: payload?.step === undefined ? o.step : payload.step, - stepType: payload?.stepType === undefined ? o.stepType : payload.stepType, - multiplier: payload?.multiplier === undefined ? o.multiplier : payload.multiplier, - rangeType: payload?.rangeType === undefined ? o.rangeType : payload.rangeType, - })); - } else if (cmd === "PLAY") { - setOptions(o => ({ ...o, animation: true })); - } else if (cmd === "PAUSE") { - setOptions(o => ({ ...o, animation: false })); - } - - // Last commiter - if ( - lastCommiter.current && - (commiter.source !== lastCommiter.current.source || commiter.id !== lastCommiter.current.id) - ) { - commiterChangeEventCallbacks.current.forEach(cb => cb()); - } - lastCommiter.current = commiter; - }, []); - - const tickEventCallbacks = useRef([]); - const onTick = useCallback((cb: TickEventCallback) => { - tickEventCallbacks.current.push(cb); - }, []); - const offTick = useCallback((cb: TickEventCallback) => { - tickEventCallbacks.current = tickEventCallbacks.current.filter(c => c !== cb); - }, []); - - const commiterChangeEventCallbacks = useRef<(() => void)[]>([]); - const onCommiterChange = useCallback((cb: () => void) => { - commiterChangeEventCallbacks.current.push(cb); - }, []); - const offCommiterChange = useCallback((cb: () => void) => { - commiterChangeEventCallbacks.current = commiterChangeEventCallbacks.current.filter( - c => c !== cb, - ); - }, []); - - const handleTick = useCallback(() => { - console.log("handleTick"); - tickEventCallbacks.current.forEach(cb => { - const engineClock = engineRef?.getClock(); - if (engineClock?.current && engineClock?.start && engineClock?.stop) { - cb(engineClock.current, { start: engineClock?.start, stop: engineClock?.stop }); - } - }); - }, [engineRef]); - - const timelineManager = useMemo(() => { - const engineClock = engineRef?.getClock(); - return { - get timeline() { - return { - get start() { - return engineClock?.start; - }, - get stop() { - return engineClock?.stop; - }, - get current() { - return engineClock?.current; - }, - get animation() { - return options.animation; - }, - get step() { - return options.step; - }, - get stepType() { - return options.stepType; - }, - get multiplier() { - return options.multiplier; - }, - get rangeType() { - return options.rangeType; - }, - }; - }, - overriddenTimeline: { - ...validTimes, - ...options, - }, - commit, - onTick, - offTick, - onCommiterChange, - offCommiterChange, - handleTick, - tick: engineRef?.tick, - }; - }, [ - options, - validTimes, - engineRef, - commit, - onTick, - offTick, - onCommiterChange, - offCommiterChange, - handleTick, - ]); - - return timelineManager; -}; diff --git a/web/src/beta/lib/core/engines/Cesium/Feature/Resource/index.tsx b/web/src/beta/lib/core/engines/Cesium/Feature/Resource/index.tsx index d6ba226eb2..25113b8d73 100644 --- a/web/src/beta/lib/core/engines/Cesium/Feature/Resource/index.tsx +++ b/web/src/beta/lib/core/engines/Cesium/Feature/Resource/index.tsx @@ -1,4 +1,4 @@ -import { Entity, type DataSource, Color } from "cesium"; +import { Entity, type DataSource, Color, JulianDate } from "cesium"; import { useCallback, useEffect, useMemo, useRef } from "react"; import { KmlDataSource, CzmlDataSource, GeoJsonDataSource, useCesium } from "resium"; @@ -73,7 +73,7 @@ export default function Resource({ const actualType = ext ? types[ext] : type !== "auto" ? type : undefined; const Component = actualType ? comps[actualType] : undefined; - const { requestRender } = useContext(); + const { requestRender, timelineManager } = useContext(); const handleChange = useCallback( (e: DataSource) => { @@ -107,33 +107,50 @@ export default function Resource({ ); const initialClock = useRef({ - start: viewer?.clock.startTime, - stop: viewer?.clock.stopTime, - current: viewer?.clock.currentTime, + start: timelineManager?.timeline?.start, + stop: timelineManager?.timeline?.stop, + current: timelineManager?.timeline?.current, }); const handleLoad = useCallback( (ds: DataSource) => { - if (!viewer?.clock) return; if (!updateClock) { if ( initialClock.current.current && initialClock.current.start && initialClock.current.stop ) { - viewer.clock.currentTime = initialClock.current.current; - viewer.clock.startTime = initialClock.current.start; - viewer.clock.stopTime = initialClock.current.stop; + timelineManager?.commit({ + cmd: "SET_TIME", + payload: { + start: initialClock.current.start, + stop: initialClock.current.stop, + current: initialClock.current.current, + }, + committer: { + source: "featureResource", + id: layer?.id, + }, + }); } return; } if (ds.clock) { - viewer.clock.currentTime = ds.clock.currentTime; - viewer.clock.startTime = ds.clock.startTime; - viewer.clock.stopTime = ds.clock.stopTime; + timelineManager?.commit({ + cmd: "SET_TIME", + payload: { + start: JulianDate.toDate(ds.clock.currentTime), + stop: JulianDate.toDate(ds.clock.startTime), + current: JulianDate.toDate(ds.clock.stopTime), + }, + committer: { + source: "featureResource", + id: layer?.id, + }, + }); } requestRender?.(); }, - [updateClock, viewer?.clock, requestRender], + [updateClock, timelineManager, layer?.id, requestRender], ); // convert hexCodeColorString to ColorValue?s diff --git a/web/src/beta/lib/core/engines/Cesium/Feature/context.ts b/web/src/beta/lib/core/engines/Cesium/Feature/context.ts index 2e2c11f41b..2a33a72e8d 100644 --- a/web/src/beta/lib/core/engines/Cesium/Feature/context.ts +++ b/web/src/beta/lib/core/engines/Cesium/Feature/context.ts @@ -3,10 +3,12 @@ import { createContext, useContext as useReactContext } from "react"; import { LayerEditEvent } from "@reearth/beta/lib/core/Map"; import type { Camera, LayerSelectionReason } from "../.."; +import { TimelineManager } from "../../../Map/useTimelineManager"; import type { FlyTo } from "../../../types"; export type Context = { selectionReason?: LayerSelectionReason; + timelineManager?: TimelineManager; getCamera?: () => Camera | undefined; flyTo?: FlyTo; onLayerEdit?: (e: LayerEditEvent) => void; diff --git a/web/src/beta/lib/core/engines/Cesium/core/Clock.tsx b/web/src/beta/lib/core/engines/Cesium/core/Clock.tsx index 7d5a32dc3d..c244213816 100644 --- a/web/src/beta/lib/core/engines/Cesium/core/Clock.tsx +++ b/web/src/beta/lib/core/engines/Cesium/core/Clock.tsx @@ -1,20 +1,17 @@ import { Clock as CesiumClock, ClockRange, ClockStep, JulianDate } from "cesium"; -import { useCallback, useEffect, useMemo } from "react"; -import { Clock, useCesium } from "resium"; +import { useCallback, useMemo } from "react"; +import { Clock } from "resium"; -import type { SceneProperty } from "../.."; -import { type TimelineManager } from "../../../Visualizer/useTimelineManager"; +import { type TimelineManager } from "../../../Map/useTimelineManager"; export type Props = { - property?: SceneProperty; timelineManager?: TimelineManager; }; -export default function ReearthClock({ property, timelineManager }: Props): JSX.Element | null { - const { visible } = property?.timeline ?? {}; - const { start, stop, current, animation, stepType, rangeType, multiplier, step } = - timelineManager?.overriddenTimeline ?? {}; - console.log("aaa", animation); +export default function ReearthClock({ timelineManager }: Props): JSX.Element | null { + const { start, stop, current } = timelineManager?.computedTimeline ?? {}; + const { animation, stepType, rangeType, multiplier } = timelineManager?.options ?? {}; + const startTime = useMemo(() => (start ? JulianDate.fromDate(start) : undefined), [start]); const stopTime = useMemo(() => (stop ? JulianDate.fromDate(stop) : undefined), [stop]); const currentTime = useMemo( @@ -23,9 +20,7 @@ export default function ReearthClock({ property, timelineManager }: Props): JSX. ); const clockStep = stepType === "fixed" ? ClockStep.TICK_DEPENDENT : ClockStep.SYSTEM_CLOCK_MULTIPLIER; - const clockMultiplier = stepType === "fixed" ? step ?? 1 : multiplier ?? 1; - - const { viewer } = useCesium(); + const clockMultiplier = multiplier ?? 1; const handleTick = useCallback( (clock: CesiumClock) => { @@ -41,23 +36,6 @@ export default function ReearthClock({ property, timelineManager }: Props): JSX. [timelineManager], ); - useEffect(() => { - if (!viewer) return; - if (viewer.animation?.container) { - (viewer.animation.container as HTMLDivElement).style.visibility = visible - ? "visible" - : "hidden"; - } - if (viewer.timeline?.container) { - (viewer.timeline.container as HTMLDivElement).style.visibility = visible - ? "visible" - : "hidden"; - } - viewer.forceResize(); - }, [viewer, visible]); - - console.log("animation", animation); - return ( (); @@ -40,9 +46,12 @@ export default function Indicator({ className, property }: Props): JSX.Element | const handleTick = () => { if (viewer.isDestroyed()) return; const selected = viewer.selectedEntity; + const currentTime = timelineManager?.timeline?.current + ? JulianDate.fromDate(timelineManager.timeline.current) + : undefined; if ( !selected?.isShowing || - !selected.isAvailable(viewer.clock.currentTime) || + (currentTime && !selected.isAvailable(currentTime)) || !selected.position ) { setIsVisible(false); @@ -60,8 +69,8 @@ export default function Indicator({ className, property }: Props): JSX.Element | // https://github.com/CesiumGS/cesium/blob/main/Source/DataSources/BoundingSphereState.js#L24 if (state !== 2 /* BoundingSphereState.FAILED */) { position = boundingSphere.center; - } else if (selected.position) { - position = selected.position.getValue(viewer.clock.currentTime, position); + } else if (selected.position && currentTime) { + position = selected.position.getValue(currentTime, position); } if (position) { @@ -73,12 +82,11 @@ export default function Indicator({ className, property }: Props): JSX.Element | } }; - viewer.clock.onTick.addEventListener(handleTick); + timelineManager?.onTick(handleTick); return () => { - if (viewer.isDestroyed()) return; - viewer.clock.onTick.removeEventListener(handleTick); + timelineManager?.offTick(handleTick); }; - }, [viewer]); + }, [viewer, timelineManager]); return transiton !== "unmounted" && pos ? ( indicator_type === "crosshair" ? ( diff --git a/web/src/beta/lib/core/engines/Cesium/hooks.ts b/web/src/beta/lib/core/engines/Cesium/hooks.ts index 4c0c3b3896..32a980452a 100644 --- a/web/src/beta/lib/core/engines/Cesium/hooks.ts +++ b/web/src/beta/lib/core/engines/Cesium/hooks.ts @@ -34,6 +34,7 @@ import type { LayerEditEvent, } from ".."; import { FORCE_REQUEST_RENDER, NO_REQUEST_RENDER, REQUEST_RENDER_ONCE } from "../../Map/hooks"; +import { TimelineManager } from "../../Map/useTimelineManager"; import { useCameraLimiter } from "./cameraLimiter"; import { getCamera, isDraggable, isSelectable, getLocationFromScreen } from "./common"; @@ -57,6 +58,7 @@ export default ({ featureFlags, requestingRenderMode, shouldRender, + timelineManager, onLayerSelect, onCameraChange, onLayerDrag, @@ -79,6 +81,7 @@ export default ({ featureFlags: number; requestingRenderMode?: React.MutableRefObject; shouldRender?: boolean; + timelineManager?: TimelineManager; onLayerSelect?: ( layerId?: string, featureId?: string, @@ -722,12 +725,13 @@ export default ({ const context = useMemo( () => ({ selectionReason, + timelineManager, flyTo: engineAPI.flyTo, getCamera: engineAPI.getCamera, onLayerEdit, requestRender: engineAPI.requestRender, }), - [selectionReason, engineAPI, onLayerEdit], + [selectionReason, engineAPI, onLayerEdit, timelineManager], ); useEffect(() => { @@ -803,6 +807,23 @@ export default ({ } }, [isLayerDragging, shouldRender, requestingRenderMode]); + // cesium timeline & animation widget + useEffect(() => { + const viewer = cesium.current?.cesiumElement; + if (!viewer) return; + if (viewer.animation?.container) { + (viewer.animation.container as HTMLDivElement).style.visibility = property?.timeline?.visible + ? "visible" + : "hidden"; + } + if (viewer.timeline?.container) { + (viewer.timeline.container as HTMLDivElement).style.visibility = property?.timeline?.visible + ? "visible" + : "hidden"; + } + viewer.forceResize(); + }, [property]); + return { backgroundColor, cesium, diff --git a/web/src/beta/lib/core/engines/Cesium/index.tsx b/web/src/beta/lib/core/engines/Cesium/index.tsx index d8610da2c2..68ee612905 100644 --- a/web/src/beta/lib/core/engines/Cesium/index.tsx +++ b/web/src/beta/lib/core/engines/Cesium/index.tsx @@ -86,6 +86,7 @@ const Cesium: React.ForwardRefRenderFunction = ( featureFlags, requestingRenderMode, shouldRender, + timelineManager, onLayerSelect, onCameraChange, onLayerDrag, @@ -137,9 +138,9 @@ const Cesium: React.ForwardRefRenderFunction = ( onMouseLeave={mouseEventHandles.mouseleave} onWheel={mouseEventHandles.wheel}> - + - + {/* remove default click event */} diff --git a/web/src/services/api/widgetsApi/utils.ts b/web/src/services/api/widgetsApi/utils.ts index 7d6e93c3f4..8653b1453f 100644 --- a/web/src/services/api/widgetsApi/utils.ts +++ b/web/src/services/api/widgetsApi/utils.ts @@ -41,7 +41,7 @@ export const getInstallableWidgets = (rawScene?: GetSceneQuery) => { .filter( e => e.type === PluginExtensionType.Widget && - AVAILABLE_WIDGET_IDS.includes(`reearth/${e.extensionId}`), + (AVAILABLE_WIDGET_IDS.includes(`reearth/${e.extensionId}`) || plugin.id !== "reearth"), ) .map((e): InstallableWidget => { return { From c725945d515e50431e58c31a82f9fdcce9fb7fe9 Mon Sep 17 00:00:00 2001 From: airslice Date: Wed, 4 Oct 2023 09:34:40 +0800 Subject: [PATCH 04/12] refactor: rename & clean up --- .../lib/core/Crust/Plugins/Plugin/hooks.ts | 4 +- web/src/beta/lib/core/Crust/Plugins/api.ts | 16 +++---- web/src/beta/lib/core/Crust/Plugins/hooks.ts | 42 +++++++++---------- web/src/beta/lib/core/Crust/Plugins/types.ts | 4 +- .../Widgets/Widget/builtin/Timeline/hooks.ts | 26 ++++++------ .../Widgets/Widget/builtin/Timeline/index.tsx | 4 +- .../lib/core/Crust/Widgets/Widget/index.tsx | 2 +- web/src/beta/lib/core/Crust/context.ts | 24 +++++------ web/src/beta/lib/core/Crust/index.tsx | 8 ++-- web/src/beta/lib/core/Map/hooks.ts | 6 +-- web/src/beta/lib/core/Map/index.tsx | 6 +-- web/src/beta/lib/core/Map/types/index.ts | 2 +- .../beta/lib/core/Map/useTimelineManager.ts | 9 ++-- web/src/beta/lib/core/Visualizer/hooks.ts | 18 +++----- web/src/beta/lib/core/Visualizer/index.tsx | 6 +-- 15 files changed, 84 insertions(+), 93 deletions(-) diff --git a/web/src/beta/lib/core/Crust/Plugins/Plugin/hooks.ts b/web/src/beta/lib/core/Crust/Plugins/Plugin/hooks.ts index f30d222f9a..991b04c962 100644 --- a/web/src/beta/lib/core/Crust/Plugins/Plugin/hooks.ts +++ b/web/src/beta/lib/core/Crust/Plugins/Plugin/hooks.ts @@ -425,14 +425,14 @@ export function useAPI({ moveWidget: onWidgetMove, pluginPostMessage: ctx.pluginInstances.postMessage, clientStorage: ctx.clientStorage, - timelineAPI: ctx.timelineAPI, + timelineRef: ctx.timelineRef, }); }; }, [ ctx?.reearth, ctx?.pluginInstances, ctx?.clientStorage, - ctx?.timelineAPI, + ctx?.timelineRef, ctx?.overrideSceneProperty, extensionId, extensionType, diff --git a/web/src/beta/lib/core/Crust/Plugins/api.ts b/web/src/beta/lib/core/Crust/Plugins/api.ts index 22bec2a3c6..898d5241d3 100644 --- a/web/src/beta/lib/core/Crust/Plugins/api.ts +++ b/web/src/beta/lib/core/Crust/Plugins/api.ts @@ -55,7 +55,7 @@ export function exposed({ moveWidget, pluginPostMessage, clientStorage, - timelineAPI, + timelineRef, }: { render: ( html: string, @@ -95,7 +95,7 @@ export function exposed({ moveWidget?: (widgetId: string, options: WidgetLocationOptions) => void; pluginPostMessage: (extentionId: string, msg: any, sender: string) => void; clientStorage: ClientStorage; - timelineAPI?: TimelineAPI; + timelineRef?: TimelineAPI; }): GlobalThis { return merge({ console: { @@ -180,7 +180,7 @@ export function exposed({ clock: merge(commonReearth.clock, { get play() { return () => { - timelineAPI?.current?.commit({ + timelineRef?.current?.commit({ cmd: "PLAY", committer: { source: "pluginAPI", @@ -196,7 +196,7 @@ export function exposed({ }, get pause() { return () => - timelineAPI?.current?.commit({ + timelineRef?.current?.commit({ cmd: "PAUSE", committer: { source: "pluginAPI", @@ -215,7 +215,7 @@ export function exposed({ stop?: Date | string; current?: Date | string; }) => - timelineAPI?.current?.commit({ + timelineRef?.current?.commit({ cmd: "SET_TIME", payload: { ...time }, committer: { @@ -231,7 +231,7 @@ export function exposed({ }, get setSpeed() { return (speed: number) => - timelineAPI?.current?.commit({ + timelineRef?.current?.commit({ cmd: "SET_OPTIONS", payload: { multiplier: speed }, committer: { @@ -247,7 +247,7 @@ export function exposed({ }, get setStepType() { return (stepType: "fixed" | "rate") => - timelineAPI?.current?.commit({ + timelineRef?.current?.commit({ cmd: "SET_OPTIONS", payload: { stepType }, committer: { @@ -263,7 +263,7 @@ export function exposed({ }, get setRangeType() { return (rangeType: "unbounded" | "clamped" | "bounced") => - timelineAPI?.current?.commit({ + timelineRef?.current?.commit({ cmd: "SET_OPTIONS", payload: { rangeType }, committer: { diff --git a/web/src/beta/lib/core/Crust/Plugins/hooks.ts b/web/src/beta/lib/core/Crust/Plugins/hooks.ts index ddb5476e49..7ea464d0cf 100644 --- a/web/src/beta/lib/core/Crust/Plugins/hooks.ts +++ b/web/src/beta/lib/core/Crust/Plugins/hooks.ts @@ -39,7 +39,7 @@ export default function ({ floatingWidgets, camera, interactionMode, - timelineAPI, + timelineRef, overrideInteractionMode, useExperimentalSandbox, overrideSceneProperty, @@ -66,66 +66,66 @@ export default function ({ const getClock = useCallback(() => { return { get startTime() { - return timelineAPI?.current?.timeline?.start; + return timelineRef?.current?.timeline?.start; }, get stopTime() { - return timelineAPI?.current?.timeline?.stop; + return timelineRef?.current?.timeline?.stop; }, get currentTime() { - return timelineAPI?.current?.timeline?.current; + return timelineRef?.current?.timeline?.current; }, get playing() { - return !!timelineAPI?.current?.options?.animation; + return !!timelineRef?.current?.options?.animation; }, get paused() { - return !timelineAPI?.current?.options?.animation; + return !timelineRef?.current?.options?.animation; }, get speed() { - return timelineAPI?.current?.options?.multiplier; + return timelineRef?.current?.options?.multiplier; }, play: () => { - timelineAPI?.current?.commit({ + timelineRef?.current?.commit({ cmd: "PLAY", committer: { source: "pluginAPI", id: "window" }, }); }, pause: () => { - timelineAPI?.current?.commit({ + timelineRef?.current?.commit({ cmd: "PAUSE", committer: { source: "pluginAPI", id: "window" }, }); }, setTime: (time: { start?: Date | string; stop?: Date | string; current?: Date | string }) => { - timelineAPI?.current?.commit({ + timelineRef?.current?.commit({ cmd: "SET_TIME", payload: { ...time }, committer: { source: "pluginAPI", id: "window" }, }); }, setSpeed: (speed: number) => { - timelineAPI?.current?.commit({ + timelineRef?.current?.commit({ cmd: "SET_OPTIONS", payload: { multiplier: speed }, committer: { source: "pluginAPI", id: "window" }, }); }, setStepType: (stepType: "rate" | "fixed") => { - timelineAPI?.current?.commit({ + timelineRef?.current?.commit({ cmd: "SET_OPTIONS", payload: { stepType }, committer: { source: "pluginAPI", id: "window" }, }); }, setRangeType: (rangeType: "unbounded" | "clamped" | "bounced") => { - timelineAPI?.current?.commit({ + timelineRef?.current?.commit({ cmd: "SET_OPTIONS", payload: { rangeType }, committer: { source: "pluginAPI", id: "window" }, }); }, - tick: timelineAPI?.current?.tick, + tick: timelineRef?.current?.tick, }; - }, [timelineAPI]); + }, [timelineRef]); const getInteractionMode = useGet( useMemo( () => ({ mode: interactionMode, override: overrideInteractionMode }), @@ -403,7 +403,7 @@ export default function ({ overrideSceneProperty, pluginInstances, clientStorage, - timelineAPI, + timelineRef, useExperimentalSandbox, }), [ @@ -454,7 +454,7 @@ export default function ({ overrideSceneProperty, pluginInstances, clientStorage, - timelineAPI, + timelineRef, useExperimentalSandbox, findFeatureById, findFeaturesByIds, @@ -494,16 +494,16 @@ export default function ({ const onTickEvent = useCallback( (fn: TickEventCallback) => { - timelineAPI?.current?.onTick(fn); + timelineRef?.current?.onTick(fn); }, - [timelineAPI], + [timelineRef], ); const onTimelineCommitEvent = useCallback( (fn: (committer: TimelineCommitter) => void) => { - timelineAPI?.current?.onCommit(fn); + timelineRef?.current?.onCommit(fn); }, - [timelineAPI], + [timelineRef], ); useEffect(() => { diff --git a/web/src/beta/lib/core/Crust/Plugins/types.ts b/web/src/beta/lib/core/Crust/Plugins/types.ts index c1c0663086..eec989be1b 100644 --- a/web/src/beta/lib/core/Crust/Plugins/types.ts +++ b/web/src/beta/lib/core/Crust/Plugins/types.ts @@ -36,7 +36,7 @@ export type Props = PropsWithChildren<{ alignSystem?: WidgetAlignSystem; floatingWidgets?: InternalWidget[]; useExperimentalSandbox?: boolean; - timelineAPI?: TimelineAPI; + timelineRef?: TimelineAPI; overrideSceneProperty: (id: string, property: any) => void; camera?: Camera; interactionMode: InteractionModeType; @@ -48,7 +48,7 @@ export type Context = { reearth: CommonReearth; pluginInstances: PluginInstances; clientStorage: ClientStorage; - timelineAPI?: TimelineAPI; + timelineRef?: TimelineAPI; useExperimentalSandbox?: boolean; overrideSceneProperty: (id: string, property: any) => void; }; diff --git a/web/src/beta/lib/core/Crust/Widgets/Widget/builtin/Timeline/hooks.ts b/web/src/beta/lib/core/Crust/Widgets/Widget/builtin/Timeline/hooks.ts index 7f38c0fb3b..bb64a0bbd7 100644 --- a/web/src/beta/lib/core/Crust/Widgets/Widget/builtin/Timeline/hooks.ts +++ b/web/src/beta/lib/core/Crust/Widgets/Widget/builtin/Timeline/hooks.ts @@ -22,7 +22,7 @@ const DEFAULT_SPEED = 1; export const useTimeline = ({ widget, - timelineAPI, + timelineRef, isMobile, onPlay, onPause, @@ -34,7 +34,7 @@ export const useTimeline = ({ onVisibilityChange, }: { widget: Widget; - timelineAPI?: TimelineAPI; + timelineRef?: TimelineAPI; isMobile?: boolean; onPlay?: () => void; onPause?: () => void; @@ -54,18 +54,18 @@ export const useTimeline = ({ const widgetId = widget.id; const [range, setRange] = useState(() => makeRange( - timelineAPI?.current?.timeline?.start?.getTime(), - timelineAPI?.current?.timeline?.stop?.getTime(), + timelineRef?.current?.timeline?.start?.getTime(), + timelineRef?.current?.timeline?.stop?.getTime(), ), ); const [isOpened, setIsOpened] = useState(true); const [currentTime, setCurrentTime] = useState(() => - getOrNewDate(timelineAPI?.current?.timeline?.current).getTime(), + getOrNewDate(timelineRef?.current?.timeline?.current).getTime(), ); const isClockInitialized = useRef(false); - const clockStartTime = timelineAPI?.current?.timeline?.start?.getTime(); - const clockStopTime = timelineAPI?.current?.timeline?.stop?.getTime(); - const clockSpeed = timelineAPI?.current?.options?.multiplier || DEFAULT_SPEED; + const clockStartTime = timelineRef?.current?.timeline?.start?.getTime(); + const clockStopTime = timelineRef?.current?.timeline?.stop?.getTime(); + const clockSpeed = timelineRef?.current?.options?.multiplier || DEFAULT_SPEED; const [speed, setSpeed] = useState(clockSpeed); @@ -141,10 +141,10 @@ export const useTimeline = ({ const absSpeed = Math.abs(speed); // Maybe we need to throttle changing speed. onSpeedChange?.( - (timelineAPI?.current?.options?.multiplier ?? 1) > 0 ? absSpeed : absSpeed * -1, + (timelineRef?.current?.options?.multiplier ?? 1) > 0 ? absSpeed : absSpeed * -1, ); }, - [onSpeedChange, timelineAPI], + [onSpeedChange, timelineRef], ); // Initialize clock value @@ -167,8 +167,8 @@ export const useTimeline = ({ }); }, []); - const overriddenStart = timelineAPI?.current?.computedTimeline?.start?.getTime(); - const overriddenStop = timelineAPI?.current?.computedTimeline?.stop?.getTime(); + const overriddenStart = timelineRef?.current?.computedTimeline?.start?.getTime(); + const overriddenStop = timelineRef?.current?.computedTimeline?.stop?.getTime(); // Sync cesium clock. useEffect(() => { @@ -203,7 +203,7 @@ export const useTimeline = ({ onTimeChangeRef.current = onTimeChange; }, [onTimeChange]); - const overriddenCurrentTime = timelineAPI?.current?.computedTimeline?.current?.getTime(); + const overriddenCurrentTime = timelineRef?.current?.computedTimeline?.current?.getTime(); useEffect(() => { if (overriddenCurrentTime) { const t = Math.max(Math.min(range.end, overriddenCurrentTime), range.start); diff --git a/web/src/beta/lib/core/Crust/Widgets/Widget/builtin/Timeline/index.tsx b/web/src/beta/lib/core/Crust/Widgets/Widget/builtin/Timeline/index.tsx index ef475a045b..c5d89ddfce 100644 --- a/web/src/beta/lib/core/Crust/Widgets/Widget/builtin/Timeline/index.tsx +++ b/web/src/beta/lib/core/Crust/Widgets/Widget/builtin/Timeline/index.tsx @@ -21,7 +21,7 @@ const Timeline = ({ onExtend, onVisibilityChange, context: { - timelineAPI, + timelineRef, onPlay, onPause, onSpeedChange, @@ -32,7 +32,7 @@ const Timeline = ({ }: Props): JSX.Element | null => { const { isOpened, currentTime, range, speed, events, visible } = useTimeline({ widget, - timelineAPI, + timelineRef, isMobile, onPlay, onPause, diff --git a/web/src/beta/lib/core/Crust/Widgets/Widget/index.tsx b/web/src/beta/lib/core/Crust/Widgets/Widget/index.tsx index a1666e0432..d5d6e59fe0 100644 --- a/web/src/beta/lib/core/Crust/Widgets/Widget/index.tsx +++ b/web/src/beta/lib/core/Crust/Widgets/Widget/index.tsx @@ -38,7 +38,7 @@ export type Props = { export type Context = { clock?: Clock; - timelineAPI?: TimelineAPI; + timelineRef?: TimelineAPI; updateClockOnLoad?: boolean; camera?: Camera; initialCamera?: Camera; diff --git a/web/src/beta/lib/core/Crust/context.ts b/web/src/beta/lib/core/Crust/context.ts index 0f61dfcadf..1c80940775 100644 --- a/web/src/beta/lib/core/Crust/context.ts +++ b/web/src/beta/lib/core/Crust/context.ts @@ -10,7 +10,7 @@ export const useWidgetContext = ({ camera, selectedLayerId, sceneProperty, - timelineAPI, + timelineRef, }: Parameters[0]) => useMemo( () => @@ -19,9 +19,9 @@ export const useWidgetContext = ({ camera, selectedLayerId, sceneProperty, - timelineAPI, + timelineRef, }), - [camera, mapRef, sceneProperty, selectedLayerId, timelineAPI], + [camera, mapRef, sceneProperty, selectedLayerId, timelineRef], ); export function widgetContextFromMapRef({ @@ -29,7 +29,7 @@ export function widgetContextFromMapRef({ camera, selectedLayerId, sceneProperty, - timelineAPI, + timelineRef, }: { mapRef?: RefObject; camera?: Camera; @@ -38,7 +38,7 @@ export function widgetContextFromMapRef({ featureId?: string; }; sceneProperty?: SceneProperty; - timelineAPI?: TimelineAPI; + timelineRef?: TimelineAPI; }): WidgetContext { const engine = () => mapRef?.current?.engine; const layers = () => mapRef?.current?.layers; @@ -48,7 +48,7 @@ export function widgetContextFromMapRef({ get clock() { return engine()?.getClock(); }, - timelineAPI, + timelineRef, initialCamera: sceneProperty?.default?.camera, is2d: sceneProperty?.default?.sceneMode === "2d", selectedLayerId, @@ -72,17 +72,17 @@ export function widgetContextFromMapRef({ onLookAt: (...args) => engine()?.lookAt(...args), onLayerSelect: (...args) => layers()?.select(...args), onPause: (committer?: TimelineCommitter) => - timelineAPI?.current?.commit({ + timelineRef?.current?.commit({ cmd: "PAUSE", committer: { source: committer?.source ?? "widgetContext", id: committer?.id }, }), onPlay: (committer?: TimelineCommitter) => - timelineAPI?.current?.commit({ + timelineRef?.current?.commit({ cmd: "PLAY", committer: { source: committer?.source ?? "widgetContext", id: committer?.id }, }), onSpeedChange: (speed, committer?: TimelineCommitter) => - timelineAPI?.current?.commit({ + timelineRef?.current?.commit({ cmd: "SET_OPTIONS", payload: { multiplier: speed, @@ -90,10 +90,10 @@ export function widgetContextFromMapRef({ }, committer: { source: committer?.source ?? "widgetContext", id: committer?.id }, }), - onTick: cb => timelineAPI?.current?.onTick(cb), - removeTickEventListener: cb => timelineAPI?.current?.offTick(cb), + onTick: cb => timelineRef?.current?.onTick(cb), + removeTickEventListener: cb => timelineRef?.current?.offTick(cb), onTimeChange: (time, committer?: TimelineCommitter) => - timelineAPI?.current?.commit({ + timelineRef?.current?.commit({ cmd: "SET_TIME", payload: { current: time, diff --git a/web/src/beta/lib/core/Crust/index.tsx b/web/src/beta/lib/core/Crust/index.tsx index b787a0e3c3..144758f11e 100644 --- a/web/src/beta/lib/core/Crust/index.tsx +++ b/web/src/beta/lib/core/Crust/index.tsx @@ -92,7 +92,7 @@ export type Props = { externalPlugin: ExternalPluginProps; useExperimentalSandbox?: boolean; // timeline manager - timelineAPI?: TimelineAPI; + timelineRef?: TimelineAPI; // widget events onWidgetLayoutUpdate?: ( id: string, @@ -153,7 +153,7 @@ export default function Crust({ selectedWidgetArea, externalPlugin, useExperimentalSandbox, - timelineAPI, + timelineRef, onWidgetLayoutUpdate, onWidgetAlignmentUpdate, onWidgetAreaSelect, @@ -183,7 +183,7 @@ export default function Crust({ camera, sceneProperty, selectedLayerId, - timelineAPI, + timelineRef, }); return ( @@ -206,7 +206,7 @@ export default function Crust({ overrideInteractionMode={overrideInteractionMode} useExperimentalSandbox={useExperimentalSandbox} overrideSceneProperty={overrideSceneProperty} - timelineAPI={timelineAPI} + timelineRef={timelineRef} onLayerEdit={onLayerEdit}> ; @@ -31,7 +31,7 @@ export default function ({ featureId?: string; }; sceneProperty?: SceneProperty; - timelineAPI?: TimelineAPI; + timelineRef?: TimelineAPI; onLayerSelect?: ( layerId: string | undefined, featureId: string | undefined, @@ -109,7 +109,7 @@ export default function ({ const timelineManager = useTimelineManager({ init: sceneProperty?.timeline, engineRef, - timelineAPI, + timelineRef, }); return { diff --git a/web/src/beta/lib/core/Map/index.tsx b/web/src/beta/lib/core/Map/index.tsx index 2b156efcb5..a641713ef5 100644 --- a/web/src/beta/lib/core/Map/index.tsx +++ b/web/src/beta/lib/core/Map/index.tsx @@ -42,7 +42,7 @@ function Map( overrides, selectedLayerId, layerSelectionReason, - timelineAPI, + timelineRef, sceneProperty, onLayerSelect, ...props @@ -63,7 +63,7 @@ function Map( ref, selectedLayerId, sceneProperty, - timelineAPI, + timelineRef, onLayerSelect, }); @@ -82,7 +82,7 @@ function Map( onLayerSelect={handleEngineLayerSelect} layersRef={layersRef} requestingRenderMode={requestingRenderMode} - timelineAPI={timelineAPI} + timelineRef={timelineRef} timelineManager={timelineManager} {...props}> ; layersRef?: RefObject; requestingRenderMode?: MutableRefObject; - timelineAPI?: TimelineAPI; + timelineRef?: TimelineAPI; timelineManager?: TimelineManager; onLayerSelect?: ( layerId: string | undefined, diff --git a/web/src/beta/lib/core/Map/useTimelineManager.ts b/web/src/beta/lib/core/Map/useTimelineManager.ts index 78490b15d0..a715ce68be 100644 --- a/web/src/beta/lib/core/Map/useTimelineManager.ts +++ b/web/src/beta/lib/core/Map/useTimelineManager.ts @@ -13,7 +13,6 @@ export type TimelineManager = { offTick: TickEvent; onCommit: (cb: (committer: TimelineCommitter) => void) => void; offCommit: (cb: (committer: TimelineCommitter) => void) => void; - // for connect engine onTick handleTick: (d: Date, clock: { start: Date; stop: Date }) => void; tick: (() => Date | void | undefined) | undefined; }; @@ -69,10 +68,10 @@ type Props = { rangeType?: "unbounded" | "clamped" | "bounced"; }; engineRef?: RefObject; - timelineAPI?: TimelineAPI; + timelineRef?: TimelineAPI; }; -export default ({ init, engineRef, timelineAPI }: Props) => { +export default ({ init, engineRef, timelineRef }: Props) => { const [time, setTime] = useState({ start: convertTime(init?.start), stop: convertTime(init?.stop), @@ -181,8 +180,8 @@ export default ({ init, engineRef, timelineAPI }: Props) => { }; }, [options, validTimes, engineRef, commit, onTick, offTick, onCommit, offCommit, handleTick]); - if (timelineAPI) { - timelineAPI.current = timelineManager; + if (timelineRef) { + timelineRef.current = timelineManager; } return timelineManager; diff --git a/web/src/beta/lib/core/Visualizer/hooks.ts b/web/src/beta/lib/core/Visualizer/hooks.ts index 276d823499..70ad109b45 100644 --- a/web/src/beta/lib/core/Visualizer/hooks.ts +++ b/web/src/beta/lib/core/Visualizer/hooks.ts @@ -158,12 +158,7 @@ export default function useHooks( } }, [infobox]); - // timeline manager - // const { timelineManager, timelineAPI } = useTimelineManager({ - // init: sceneProperty?.timeline, - // engineRef: mapRef.current?.engine, - // }); - const timelineAPI: TimelineAPI = useRef(); + const timelineRef: TimelineAPI = useRef(); // scene const [overriddenSceneProperty, originalOverrideSceneProperty] = @@ -171,12 +166,11 @@ export default function useHooks( const overrideSceneProperty = useCallback( (pluginId: string, property: SceneProperty) => { - // Timeline related override should be handled by TimelineManager if (property.timeline) { const filteredTimeline = clone(property.timeline); delete filteredTimeline.visible; if (Object.keys(filteredTimeline).length > 0) { - timelineAPI?.current?.commit({ + timelineRef?.current?.commit({ cmd: "SET_TIME", payload: { start: filteredTimeline.start, @@ -188,7 +182,7 @@ export default function useHooks( id: pluginId, }, }); - timelineAPI?.current?.commit({ + timelineRef?.current?.commit({ cmd: "SET_OPTIONS", payload: { stepType: filteredTimeline.stepType, @@ -202,11 +196,9 @@ export default function useHooks( }); } } - // We can keep the logic the same as before. - // Just remember we will NOT use the timeline from overridden scene property directly. originalOverrideSceneProperty(pluginId, property); }, - [timelineAPI, originalOverrideSceneProperty], + [timelineRef, originalOverrideSceneProperty], ); // clock @@ -332,7 +324,7 @@ export default function useHooks( infobox, isLayerDragging, shouldRender, - timelineAPI, + timelineRef, handleLayerSelect, handleBlockSelect: selectBlock, handleCameraChange: changeCamera, diff --git a/web/src/beta/lib/core/Visualizer/index.tsx b/web/src/beta/lib/core/Visualizer/index.tsx index 00da480030..bfa6e5898e 100644 --- a/web/src/beta/lib/core/Visualizer/index.tsx +++ b/web/src/beta/lib/core/Visualizer/index.tsx @@ -191,7 +191,7 @@ const Visualizer = memo( isLayerDragging, infobox, shouldRender, - timelineAPI, + timelineRef, handleLayerSelect, handleBlockSelect, handleCameraChange, @@ -265,7 +265,7 @@ const Visualizer = memo( mapRef={mapRef} externalPlugin={{ pluginBaseUrl, pluginProperty }} useExperimentalSandbox={useExperimentalSandbox} - timelineAPI={timelineAPI} + timelineRef={timelineRef} onWidgetLayoutUpdate={onWidgetLayoutUpdate} onWidgetAlignmentUpdate={onWidgetAlignmentUpdate} onWidgetAreaSelect={onWidgetAreaSelect} @@ -301,7 +301,7 @@ const Visualizer = memo( layerSelectionReason={layerSelectionReason} small={small} ready={ready} - timelineAPI={timelineAPI} + timelineRef={timelineRef} onCameraChange={handleCameraChange} onLayerDrag={handleLayerDrag} onLayerDrop={handleLayerDrop} From 3c0ffc3998a3e029593712616ff836b83f39aed8 Mon Sep 17 00:00:00 2001 From: airslice Date: Wed, 4 Oct 2023 09:38:29 +0800 Subject: [PATCH 05/12] refactor: rename & clean up --- .../lib/core/Crust/Plugins/Plugin/hooks.ts | 4 +- web/src/beta/lib/core/Crust/Plugins/api.ts | 18 +++---- web/src/beta/lib/core/Crust/Plugins/hooks.ts | 42 +++++++-------- web/src/beta/lib/core/Crust/Plugins/types.ts | 6 +-- .../Widgets/Widget/builtin/Timeline/hooks.ts | 28 +++++----- .../Widgets/Widget/builtin/Timeline/index.tsx | 4 +- .../lib/core/Crust/Widgets/Widget/index.tsx | 4 +- web/src/beta/lib/core/Crust/context.ts | 26 ++++----- web/src/beta/lib/core/Crust/index.tsx | 13 +++-- web/src/beta/lib/core/Map/hooks.ts | 8 +-- web/src/beta/lib/core/Map/index.tsx | 6 +-- web/src/beta/lib/core/Map/types/index.ts | 4 +- .../beta/lib/core/Map/useTimelineManager.ts | 10 ++-- web/src/beta/lib/core/Visualizer/hooks.ts | 53 +++---------------- web/src/beta/lib/core/Visualizer/index.tsx | 8 ++- 15 files changed, 95 insertions(+), 139 deletions(-) diff --git a/web/src/beta/lib/core/Crust/Plugins/Plugin/hooks.ts b/web/src/beta/lib/core/Crust/Plugins/Plugin/hooks.ts index 991b04c962..4038466341 100644 --- a/web/src/beta/lib/core/Crust/Plugins/Plugin/hooks.ts +++ b/web/src/beta/lib/core/Crust/Plugins/Plugin/hooks.ts @@ -425,14 +425,14 @@ export function useAPI({ moveWidget: onWidgetMove, pluginPostMessage: ctx.pluginInstances.postMessage, clientStorage: ctx.clientStorage, - timelineRef: ctx.timelineRef, + timelineManagerRef: ctx.timelineManagerRef, }); }; }, [ ctx?.reearth, ctx?.pluginInstances, ctx?.clientStorage, - ctx?.timelineRef, + ctx?.timelineManagerRef, ctx?.overrideSceneProperty, extensionId, extensionType, diff --git a/web/src/beta/lib/core/Crust/Plugins/api.ts b/web/src/beta/lib/core/Crust/Plugins/api.ts index 898d5241d3..4c51d9f566 100644 --- a/web/src/beta/lib/core/Crust/Plugins/api.ts +++ b/web/src/beta/lib/core/Crust/Plugins/api.ts @@ -9,7 +9,7 @@ import { } from "@reearth/beta/lib/core/Map"; import { merge } from "@reearth/beta/utils/object"; -import { TimelineAPI } from "../../Map/useTimelineManager"; +import { TimelineManagerRef } from "../../Map/useTimelineManager"; import type { Block } from "../Infobox"; import type { MapRef } from "../types"; import type { Widget, WidgetLocationOptions } from "../Widgets"; @@ -55,7 +55,7 @@ export function exposed({ moveWidget, pluginPostMessage, clientStorage, - timelineRef, + timelineManagerRef, }: { render: ( html: string, @@ -95,7 +95,7 @@ export function exposed({ moveWidget?: (widgetId: string, options: WidgetLocationOptions) => void; pluginPostMessage: (extentionId: string, msg: any, sender: string) => void; clientStorage: ClientStorage; - timelineRef?: TimelineAPI; + timelineManagerRef?: TimelineManagerRef; }): GlobalThis { return merge({ console: { @@ -180,7 +180,7 @@ export function exposed({ clock: merge(commonReearth.clock, { get play() { return () => { - timelineRef?.current?.commit({ + timelineManagerRef?.current?.commit({ cmd: "PLAY", committer: { source: "pluginAPI", @@ -196,7 +196,7 @@ export function exposed({ }, get pause() { return () => - timelineRef?.current?.commit({ + timelineManagerRef?.current?.commit({ cmd: "PAUSE", committer: { source: "pluginAPI", @@ -215,7 +215,7 @@ export function exposed({ stop?: Date | string; current?: Date | string; }) => - timelineRef?.current?.commit({ + timelineManagerRef?.current?.commit({ cmd: "SET_TIME", payload: { ...time }, committer: { @@ -231,7 +231,7 @@ export function exposed({ }, get setSpeed() { return (speed: number) => - timelineRef?.current?.commit({ + timelineManagerRef?.current?.commit({ cmd: "SET_OPTIONS", payload: { multiplier: speed }, committer: { @@ -247,7 +247,7 @@ export function exposed({ }, get setStepType() { return (stepType: "fixed" | "rate") => - timelineRef?.current?.commit({ + timelineManagerRef?.current?.commit({ cmd: "SET_OPTIONS", payload: { stepType }, committer: { @@ -263,7 +263,7 @@ export function exposed({ }, get setRangeType() { return (rangeType: "unbounded" | "clamped" | "bounced") => - timelineRef?.current?.commit({ + timelineManagerRef?.current?.commit({ cmd: "SET_OPTIONS", payload: { rangeType }, committer: { diff --git a/web/src/beta/lib/core/Crust/Plugins/hooks.ts b/web/src/beta/lib/core/Crust/Plugins/hooks.ts index 7ea464d0cf..86786e3e54 100644 --- a/web/src/beta/lib/core/Crust/Plugins/hooks.ts +++ b/web/src/beta/lib/core/Crust/Plugins/hooks.ts @@ -39,7 +39,7 @@ export default function ({ floatingWidgets, camera, interactionMode, - timelineRef, + timelineManagerRef, overrideInteractionMode, useExperimentalSandbox, overrideSceneProperty, @@ -66,66 +66,66 @@ export default function ({ const getClock = useCallback(() => { return { get startTime() { - return timelineRef?.current?.timeline?.start; + return timelineManagerRef?.current?.timeline?.start; }, get stopTime() { - return timelineRef?.current?.timeline?.stop; + return timelineManagerRef?.current?.timeline?.stop; }, get currentTime() { - return timelineRef?.current?.timeline?.current; + return timelineManagerRef?.current?.timeline?.current; }, get playing() { - return !!timelineRef?.current?.options?.animation; + return !!timelineManagerRef?.current?.options?.animation; }, get paused() { - return !timelineRef?.current?.options?.animation; + return !timelineManagerRef?.current?.options?.animation; }, get speed() { - return timelineRef?.current?.options?.multiplier; + return timelineManagerRef?.current?.options?.multiplier; }, play: () => { - timelineRef?.current?.commit({ + timelineManagerRef?.current?.commit({ cmd: "PLAY", committer: { source: "pluginAPI", id: "window" }, }); }, pause: () => { - timelineRef?.current?.commit({ + timelineManagerRef?.current?.commit({ cmd: "PAUSE", committer: { source: "pluginAPI", id: "window" }, }); }, setTime: (time: { start?: Date | string; stop?: Date | string; current?: Date | string }) => { - timelineRef?.current?.commit({ + timelineManagerRef?.current?.commit({ cmd: "SET_TIME", payload: { ...time }, committer: { source: "pluginAPI", id: "window" }, }); }, setSpeed: (speed: number) => { - timelineRef?.current?.commit({ + timelineManagerRef?.current?.commit({ cmd: "SET_OPTIONS", payload: { multiplier: speed }, committer: { source: "pluginAPI", id: "window" }, }); }, setStepType: (stepType: "rate" | "fixed") => { - timelineRef?.current?.commit({ + timelineManagerRef?.current?.commit({ cmd: "SET_OPTIONS", payload: { stepType }, committer: { source: "pluginAPI", id: "window" }, }); }, setRangeType: (rangeType: "unbounded" | "clamped" | "bounced") => { - timelineRef?.current?.commit({ + timelineManagerRef?.current?.commit({ cmd: "SET_OPTIONS", payload: { rangeType }, committer: { source: "pluginAPI", id: "window" }, }); }, - tick: timelineRef?.current?.tick, + tick: timelineManagerRef?.current?.tick, }; - }, [timelineRef]); + }, [timelineManagerRef]); const getInteractionMode = useGet( useMemo( () => ({ mode: interactionMode, override: overrideInteractionMode }), @@ -403,7 +403,7 @@ export default function ({ overrideSceneProperty, pluginInstances, clientStorage, - timelineRef, + timelineManagerRef, useExperimentalSandbox, }), [ @@ -454,7 +454,7 @@ export default function ({ overrideSceneProperty, pluginInstances, clientStorage, - timelineRef, + timelineManagerRef, useExperimentalSandbox, findFeatureById, findFeaturesByIds, @@ -494,16 +494,16 @@ export default function ({ const onTickEvent = useCallback( (fn: TickEventCallback) => { - timelineRef?.current?.onTick(fn); + timelineManagerRef?.current?.onTick(fn); }, - [timelineRef], + [timelineManagerRef], ); const onTimelineCommitEvent = useCallback( (fn: (committer: TimelineCommitter) => void) => { - timelineRef?.current?.onCommit(fn); + timelineManagerRef?.current?.onCommit(fn); }, - [timelineRef], + [timelineManagerRef], ); useEffect(() => { diff --git a/web/src/beta/lib/core/Crust/Plugins/types.ts b/web/src/beta/lib/core/Crust/Plugins/types.ts index eec989be1b..68d95dd23b 100644 --- a/web/src/beta/lib/core/Crust/Plugins/types.ts +++ b/web/src/beta/lib/core/Crust/Plugins/types.ts @@ -13,7 +13,7 @@ import type { } from "@reearth/beta/lib/core/Map"; import type { Viewport } from "@reearth/beta/lib/core/Visualizer"; -import { TimelineAPI } from "../../Map/useTimelineManager"; +import { TimelineManagerRef } from "../../Map/useTimelineManager"; import type { MapRef, InteractionModeType } from "../types"; import type { InternalWidget, WidgetAlignSystem } from "../Widgets"; @@ -36,7 +36,7 @@ export type Props = PropsWithChildren<{ alignSystem?: WidgetAlignSystem; floatingWidgets?: InternalWidget[]; useExperimentalSandbox?: boolean; - timelineRef?: TimelineAPI; + timelineManagerRef?: TimelineManagerRef; overrideSceneProperty: (id: string, property: any) => void; camera?: Camera; interactionMode: InteractionModeType; @@ -48,7 +48,7 @@ export type Context = { reearth: CommonReearth; pluginInstances: PluginInstances; clientStorage: ClientStorage; - timelineRef?: TimelineAPI; + timelineManagerRef?: TimelineManagerRef; useExperimentalSandbox?: boolean; overrideSceneProperty: (id: string, property: any) => void; }; diff --git a/web/src/beta/lib/core/Crust/Widgets/Widget/builtin/Timeline/hooks.ts b/web/src/beta/lib/core/Crust/Widgets/Widget/builtin/Timeline/hooks.ts index bb64a0bbd7..35e109d484 100644 --- a/web/src/beta/lib/core/Crust/Widgets/Widget/builtin/Timeline/hooks.ts +++ b/web/src/beta/lib/core/Crust/Widgets/Widget/builtin/Timeline/hooks.ts @@ -2,7 +2,7 @@ import { useState, useCallback, useEffect, useRef } from "react"; import type { TimeEventHandler } from "@reearth/beta/lib/core/Crust/Widgets/Widget/builtin/Timeline/UI"; import { TickEvent, TickEventCallback } from "@reearth/beta/lib/core/Map"; -import { TimelineAPI } from "@reearth/beta/lib/core/Map/useTimelineManager"; +import { TimelineManagerRef } from "@reearth/beta/lib/core/Map/useTimelineManager"; import type { Widget } from "../../types"; import { useVisible } from "../../useVisible"; @@ -22,7 +22,7 @@ const DEFAULT_SPEED = 1; export const useTimeline = ({ widget, - timelineRef, + timelineManagerRef, isMobile, onPlay, onPause, @@ -34,7 +34,7 @@ export const useTimeline = ({ onVisibilityChange, }: { widget: Widget; - timelineRef?: TimelineAPI; + timelineManagerRef?: TimelineManagerRef; isMobile?: boolean; onPlay?: () => void; onPause?: () => void; @@ -54,18 +54,18 @@ export const useTimeline = ({ const widgetId = widget.id; const [range, setRange] = useState(() => makeRange( - timelineRef?.current?.timeline?.start?.getTime(), - timelineRef?.current?.timeline?.stop?.getTime(), + timelineManagerRef?.current?.timeline?.start?.getTime(), + timelineManagerRef?.current?.timeline?.stop?.getTime(), ), ); const [isOpened, setIsOpened] = useState(true); const [currentTime, setCurrentTime] = useState(() => - getOrNewDate(timelineRef?.current?.timeline?.current).getTime(), + getOrNewDate(timelineManagerRef?.current?.timeline?.current).getTime(), ); const isClockInitialized = useRef(false); - const clockStartTime = timelineRef?.current?.timeline?.start?.getTime(); - const clockStopTime = timelineRef?.current?.timeline?.stop?.getTime(); - const clockSpeed = timelineRef?.current?.options?.multiplier || DEFAULT_SPEED; + const clockStartTime = timelineManagerRef?.current?.timeline?.start?.getTime(); + const clockStopTime = timelineManagerRef?.current?.timeline?.stop?.getTime(); + const clockSpeed = timelineManagerRef?.current?.options?.multiplier || DEFAULT_SPEED; const [speed, setSpeed] = useState(clockSpeed); @@ -141,10 +141,10 @@ export const useTimeline = ({ const absSpeed = Math.abs(speed); // Maybe we need to throttle changing speed. onSpeedChange?.( - (timelineRef?.current?.options?.multiplier ?? 1) > 0 ? absSpeed : absSpeed * -1, + (timelineManagerRef?.current?.options?.multiplier ?? 1) > 0 ? absSpeed : absSpeed * -1, ); }, - [onSpeedChange, timelineRef], + [onSpeedChange, timelineManagerRef], ); // Initialize clock value @@ -167,8 +167,8 @@ export const useTimeline = ({ }); }, []); - const overriddenStart = timelineRef?.current?.computedTimeline?.start?.getTime(); - const overriddenStop = timelineRef?.current?.computedTimeline?.stop?.getTime(); + const overriddenStart = timelineManagerRef?.current?.computedTimeline?.start?.getTime(); + const overriddenStop = timelineManagerRef?.current?.computedTimeline?.stop?.getTime(); // Sync cesium clock. useEffect(() => { @@ -203,7 +203,7 @@ export const useTimeline = ({ onTimeChangeRef.current = onTimeChange; }, [onTimeChange]); - const overriddenCurrentTime = timelineRef?.current?.computedTimeline?.current?.getTime(); + const overriddenCurrentTime = timelineManagerRef?.current?.computedTimeline?.current?.getTime(); useEffect(() => { if (overriddenCurrentTime) { const t = Math.max(Math.min(range.end, overriddenCurrentTime), range.start); diff --git a/web/src/beta/lib/core/Crust/Widgets/Widget/builtin/Timeline/index.tsx b/web/src/beta/lib/core/Crust/Widgets/Widget/builtin/Timeline/index.tsx index c5d89ddfce..c6c058f1af 100644 --- a/web/src/beta/lib/core/Crust/Widgets/Widget/builtin/Timeline/index.tsx +++ b/web/src/beta/lib/core/Crust/Widgets/Widget/builtin/Timeline/index.tsx @@ -21,7 +21,7 @@ const Timeline = ({ onExtend, onVisibilityChange, context: { - timelineRef, + timelineManagerRef, onPlay, onPause, onSpeedChange, @@ -32,7 +32,7 @@ const Timeline = ({ }: Props): JSX.Element | null => { const { isOpened, currentTime, range, speed, events, visible } = useTimeline({ widget, - timelineRef, + timelineManagerRef, isMobile, onPlay, onPause, diff --git a/web/src/beta/lib/core/Crust/Widgets/Widget/index.tsx b/web/src/beta/lib/core/Crust/Widgets/Widget/index.tsx index d5d6e59fe0..b35ba1960e 100644 --- a/web/src/beta/lib/core/Crust/Widgets/Widget/index.tsx +++ b/web/src/beta/lib/core/Crust/Widgets/Widget/index.tsx @@ -2,7 +2,7 @@ import { ComponentType, ReactNode, useMemo } from "react"; import type { TickEvent } from "@reearth/beta/lib/core/Map"; -import { TimelineAPI, TimelineCommitter } from "../../../Map/useTimelineManager"; +import { TimelineManagerRef, TimelineCommitter } from "../../../Map/useTimelineManager"; import builtin, { isBuiltinWidget } from "./builtin"; import type { @@ -38,7 +38,7 @@ export type Props = { export type Context = { clock?: Clock; - timelineRef?: TimelineAPI; + timelineManagerRef?: TimelineManagerRef; updateClockOnLoad?: boolean; camera?: Camera; initialCamera?: Camera; diff --git a/web/src/beta/lib/core/Crust/context.ts b/web/src/beta/lib/core/Crust/context.ts index 1c80940775..77b4f9647d 100644 --- a/web/src/beta/lib/core/Crust/context.ts +++ b/web/src/beta/lib/core/Crust/context.ts @@ -1,6 +1,6 @@ import { RefObject, useMemo } from "react"; -import { TimelineAPI, TimelineCommitter } from "../Map/useTimelineManager"; +import { TimelineManagerRef, TimelineCommitter } from "../Map/useTimelineManager"; import { Camera, MapRef, SceneProperty } from "./types"; import { Context as WidgetContext } from "./Widgets"; @@ -10,7 +10,7 @@ export const useWidgetContext = ({ camera, selectedLayerId, sceneProperty, - timelineRef, + timelineManagerRef, }: Parameters[0]) => useMemo( () => @@ -19,9 +19,9 @@ export const useWidgetContext = ({ camera, selectedLayerId, sceneProperty, - timelineRef, + timelineManagerRef, }), - [camera, mapRef, sceneProperty, selectedLayerId, timelineRef], + [camera, mapRef, sceneProperty, selectedLayerId, timelineManagerRef], ); export function widgetContextFromMapRef({ @@ -29,7 +29,7 @@ export function widgetContextFromMapRef({ camera, selectedLayerId, sceneProperty, - timelineRef, + timelineManagerRef, }: { mapRef?: RefObject; camera?: Camera; @@ -38,7 +38,7 @@ export function widgetContextFromMapRef({ featureId?: string; }; sceneProperty?: SceneProperty; - timelineRef?: TimelineAPI; + timelineManagerRef?: TimelineManagerRef; }): WidgetContext { const engine = () => mapRef?.current?.engine; const layers = () => mapRef?.current?.layers; @@ -48,7 +48,7 @@ export function widgetContextFromMapRef({ get clock() { return engine()?.getClock(); }, - timelineRef, + timelineManagerRef, initialCamera: sceneProperty?.default?.camera, is2d: sceneProperty?.default?.sceneMode === "2d", selectedLayerId, @@ -72,17 +72,17 @@ export function widgetContextFromMapRef({ onLookAt: (...args) => engine()?.lookAt(...args), onLayerSelect: (...args) => layers()?.select(...args), onPause: (committer?: TimelineCommitter) => - timelineRef?.current?.commit({ + timelineManagerRef?.current?.commit({ cmd: "PAUSE", committer: { source: committer?.source ?? "widgetContext", id: committer?.id }, }), onPlay: (committer?: TimelineCommitter) => - timelineRef?.current?.commit({ + timelineManagerRef?.current?.commit({ cmd: "PLAY", committer: { source: committer?.source ?? "widgetContext", id: committer?.id }, }), onSpeedChange: (speed, committer?: TimelineCommitter) => - timelineRef?.current?.commit({ + timelineManagerRef?.current?.commit({ cmd: "SET_OPTIONS", payload: { multiplier: speed, @@ -90,10 +90,10 @@ export function widgetContextFromMapRef({ }, committer: { source: committer?.source ?? "widgetContext", id: committer?.id }, }), - onTick: cb => timelineRef?.current?.onTick(cb), - removeTickEventListener: cb => timelineRef?.current?.offTick(cb), + onTick: cb => timelineManagerRef?.current?.onTick(cb), + removeTickEventListener: cb => timelineManagerRef?.current?.offTick(cb), onTimeChange: (time, committer?: TimelineCommitter) => - timelineRef?.current?.commit({ + timelineManagerRef?.current?.commit({ cmd: "SET_TIME", payload: { current: time, diff --git a/web/src/beta/lib/core/Crust/index.tsx b/web/src/beta/lib/core/Crust/index.tsx index 144758f11e..12feed7b84 100644 --- a/web/src/beta/lib/core/Crust/index.tsx +++ b/web/src/beta/lib/core/Crust/index.tsx @@ -3,8 +3,8 @@ import type { ReactNode, RefObject } from "react"; import type { SelectedFeatureInfo, Tag } from "@reearth/beta/lib/core/mantle"; import type { ComputedFeature, ComputedLayer, Feature } from "../mantle"; -import type { Clock, LayerEditEvent, LayerSelectionReason } from "../Map"; -import type { TimelineAPI } from "../Map/useTimelineManager"; +import type { LayerEditEvent, LayerSelectionReason } from "../Map"; +import type { TimelineManagerRef } from "../Map/useTimelineManager"; import type { Viewport } from "../Visualizer"; import { useWidgetContext } from "./context"; @@ -60,7 +60,6 @@ export type Props = { isMobile?: boolean; mapRef?: RefObject; sceneProperty?: SceneProperty; - overriddenClock: Clock; viewport?: Viewport; camera?: Camera; interactionMode: InteractionModeType; @@ -92,7 +91,7 @@ export type Props = { externalPlugin: ExternalPluginProps; useExperimentalSandbox?: boolean; // timeline manager - timelineRef?: TimelineAPI; + timelineManagerRef?: TimelineManagerRef; // widget events onWidgetLayoutUpdate?: ( id: string, @@ -153,7 +152,7 @@ export default function Crust({ selectedWidgetArea, externalPlugin, useExperimentalSandbox, - timelineRef, + timelineManagerRef, onWidgetLayoutUpdate, onWidgetAlignmentUpdate, onWidgetAreaSelect, @@ -183,7 +182,7 @@ export default function Crust({ camera, sceneProperty, selectedLayerId, - timelineRef, + timelineManagerRef, }); return ( @@ -206,7 +205,7 @@ export default function Crust({ overrideInteractionMode={overrideInteractionMode} useExperimentalSandbox={useExperimentalSandbox} overrideSceneProperty={overrideSceneProperty} - timelineRef={timelineRef} + timelineManagerRef={timelineManagerRef} onLayerEdit={onLayerEdit}> ; @@ -31,7 +31,7 @@ export default function ({ featureId?: string; }; sceneProperty?: SceneProperty; - timelineRef?: TimelineAPI; + timelineManagerRef?: TimelineManagerRef; onLayerSelect?: ( layerId: string | undefined, featureId: string | undefined, @@ -109,7 +109,7 @@ export default function ({ const timelineManager = useTimelineManager({ init: sceneProperty?.timeline, engineRef, - timelineRef, + timelineManagerRef, }); return { diff --git a/web/src/beta/lib/core/Map/index.tsx b/web/src/beta/lib/core/Map/index.tsx index a641713ef5..6fbb8606ec 100644 --- a/web/src/beta/lib/core/Map/index.tsx +++ b/web/src/beta/lib/core/Map/index.tsx @@ -42,7 +42,7 @@ function Map( overrides, selectedLayerId, layerSelectionReason, - timelineRef, + timelineManagerRef, sceneProperty, onLayerSelect, ...props @@ -63,7 +63,7 @@ function Map( ref, selectedLayerId, sceneProperty, - timelineRef, + timelineManagerRef, onLayerSelect, }); @@ -82,7 +82,7 @@ function Map( onLayerSelect={handleEngineLayerSelect} layersRef={layersRef} requestingRenderMode={requestingRenderMode} - timelineRef={timelineRef} + timelineManagerRef={timelineManagerRef} timelineManager={timelineManager} {...props}> ; layersRef?: RefObject; requestingRenderMode?: MutableRefObject; - timelineRef?: TimelineAPI; + timelineManagerRef?: TimelineManagerRef; timelineManager?: TimelineManager; onLayerSelect?: ( layerId: string | undefined, diff --git a/web/src/beta/lib/core/Map/useTimelineManager.ts b/web/src/beta/lib/core/Map/useTimelineManager.ts index a715ce68be..62e77a7a55 100644 --- a/web/src/beta/lib/core/Map/useTimelineManager.ts +++ b/web/src/beta/lib/core/Map/useTimelineManager.ts @@ -17,7 +17,7 @@ export type TimelineManager = { tick: (() => Date | void | undefined) | undefined; }; -export type TimelineAPI = MutableRefObject; +export type TimelineManagerRef = MutableRefObject; export type Timeline = { current?: Date; @@ -68,10 +68,10 @@ type Props = { rangeType?: "unbounded" | "clamped" | "bounced"; }; engineRef?: RefObject; - timelineRef?: TimelineAPI; + timelineManagerRef?: TimelineManagerRef; }; -export default ({ init, engineRef, timelineRef }: Props) => { +export default ({ init, engineRef, timelineManagerRef }: Props) => { const [time, setTime] = useState({ start: convertTime(init?.start), stop: convertTime(init?.stop), @@ -180,8 +180,8 @@ export default ({ init, engineRef, timelineRef }: Props) => { }; }, [options, validTimes, engineRef, commit, onTick, offTick, onCommit, offCommit, handleTick]); - if (timelineRef) { - timelineRef.current = timelineManager; + if (timelineManagerRef) { + timelineManagerRef.current = timelineManager; } return timelineManager; diff --git a/web/src/beta/lib/core/Visualizer/hooks.ts b/web/src/beta/lib/core/Visualizer/hooks.ts index 70ad109b45..01fef5b23c 100644 --- a/web/src/beta/lib/core/Visualizer/hooks.ts +++ b/web/src/beta/lib/core/Visualizer/hooks.ts @@ -3,7 +3,6 @@ import { Ref, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useS import { useWindowSize } from "react-use"; // TODO: Move these utils -import { convertTime, truncMinutes } from "@reearth/beta/utils/time"; import { type DropOptions, useDrop } from "@reearth/beta/utils/use-dnd"; import type { Block, BuiltinWidgets, InteractionModeType } from "../Crust"; @@ -20,7 +19,7 @@ import type { DefaultInfobox, } from "../Map"; import { useOverriddenProperty } from "../Map"; -import { TimelineAPI } from "../Map/useTimelineManager"; +import { TimelineManagerRef } from "../Map/useTimelineManager"; import useViewport from "./useViewport"; @@ -158,7 +157,7 @@ export default function useHooks( } }, [infobox]); - const timelineRef: TimelineAPI = useRef(); + const timelineManagerRef: TimelineManagerRef = useRef(); // scene const [overriddenSceneProperty, originalOverrideSceneProperty] = @@ -170,7 +169,7 @@ export default function useHooks( const filteredTimeline = clone(property.timeline); delete filteredTimeline.visible; if (Object.keys(filteredTimeline).length > 0) { - timelineRef?.current?.commit({ + timelineManagerRef?.current?.commit({ cmd: "SET_TIME", payload: { start: filteredTimeline.start, @@ -182,7 +181,7 @@ export default function useHooks( id: pluginId, }, }); - timelineRef?.current?.commit({ + timelineManagerRef?.current?.commit({ cmd: "SET_OPTIONS", payload: { stepType: filteredTimeline.stepType, @@ -198,48 +197,9 @@ export default function useHooks( } originalOverrideSceneProperty(pluginId, property); }, - [timelineRef, originalOverrideSceneProperty], + [timelineManagerRef, originalOverrideSceneProperty], ); - // clock - const overriddenClock = useMemo(() => { - const { start, stop, current } = overriddenSceneProperty.timeline || {}; - const startTime = convertTime(start)?.getTime(); - const stopTime = convertTime(stop)?.getTime(); - const currentTime = convertTime(current)?.getTime(); - - const DEFAULT_NEXT_RANGE = 86400000; // a day - - // To avoid out of range error in Cesium, we need to turn back a hour. - const now = Date.now() - 3600000; - - const convertedStartTime = startTime - ? Math.min(now, startTime) - : stopTime - ? Math.min(now, stopTime - DEFAULT_NEXT_RANGE) - : now - DEFAULT_NEXT_RANGE; - - const convertedStopTime = stopTime - ? Math.min(stopTime, now) - : startTime - ? Math.min(now, startTime + DEFAULT_NEXT_RANGE) - : now; - - return { - start: start || stop ? truncMinutes(new Date(convertedStartTime)) : undefined, - stop: start || stop ? new Date(convertedStopTime) : undefined, - current: - start || stop || current - ? new Date( - Math.max( - Math.min(convertedStopTime, currentTime || convertedStartTime), - convertedStartTime, - ), - ) - : undefined, - }; - }, [overriddenSceneProperty]); - // block const [selectedBlock, selectBlock] = useValue(initialSelectedBlockId, onBlockSelect); @@ -319,12 +279,11 @@ export default function useHooks( featureFlags, isMobile, overriddenSceneProperty, - overriddenClock, isDroppable, infobox, isLayerDragging, shouldRender, - timelineRef, + timelineManagerRef, handleLayerSelect, handleBlockSelect: selectBlock, handleCameraChange: changeCamera, diff --git a/web/src/beta/lib/core/Visualizer/index.tsx b/web/src/beta/lib/core/Visualizer/index.tsx index bfa6e5898e..7f2528949b 100644 --- a/web/src/beta/lib/core/Visualizer/index.tsx +++ b/web/src/beta/lib/core/Visualizer/index.tsx @@ -186,12 +186,11 @@ const Visualizer = memo( featureFlags, isMobile, overriddenSceneProperty, - overriddenClock, isDroppable, isLayerDragging, infobox, shouldRender, - timelineRef, + timelineManagerRef, handleLayerSelect, handleBlockSelect, handleCameraChange, @@ -242,7 +241,6 @@ const Visualizer = memo( inEditor={inEditor} sceneProperty={overriddenSceneProperty} overrideSceneProperty={overrideSceneProperty} - overriddenClock={overriddenClock} blocks={infobox?.blocks} camera={camera} interactionMode={interactionMode} @@ -265,7 +263,7 @@ const Visualizer = memo( mapRef={mapRef} externalPlugin={{ pluginBaseUrl, pluginProperty }} useExperimentalSandbox={useExperimentalSandbox} - timelineRef={timelineRef} + timelineManagerRef={timelineManagerRef} onWidgetLayoutUpdate={onWidgetLayoutUpdate} onWidgetAlignmentUpdate={onWidgetAlignmentUpdate} onWidgetAreaSelect={onWidgetAreaSelect} @@ -301,7 +299,7 @@ const Visualizer = memo( layerSelectionReason={layerSelectionReason} small={small} ready={ready} - timelineRef={timelineRef} + timelineManagerRef={timelineManagerRef} onCameraChange={handleCameraChange} onLayerDrag={handleLayerDrag} onLayerDrop={handleLayerDrop} From cf40c00c7ac27f388a5f6845bada8ce766658361 Mon Sep 17 00:00:00 2001 From: airslice Date: Wed, 4 Oct 2023 10:01:10 +0800 Subject: [PATCH 06/12] refactor: remove unnecessary get --- .../beta/lib/core/Map/useTimelineManager.ts | 40 +++++++++++-------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/web/src/beta/lib/core/Map/useTimelineManager.ts b/web/src/beta/lib/core/Map/useTimelineManager.ts index 62e77a7a55..0356dfc89d 100644 --- a/web/src/beta/lib/core/Map/useTimelineManager.ts +++ b/web/src/beta/lib/core/Map/useTimelineManager.ts @@ -85,7 +85,7 @@ export default ({ init, engineRef, timelineManagerRef }: Props) => { rangeType: init?.rangeType ?? "unbounded", }); - const validTimes = useMemo(() => { + const computedTimeline = useMemo(() => { const { start, stop, current } = time; const startTime = start?.getTime() ?? new Date().getTime(); const stopTime = stop?.getTime() ?? new Date().getTime(); @@ -153,23 +153,19 @@ export default ({ init, engineRef, timelineManagerRef }: Props) => { const timelineManager = useMemo(() => { return { - get timeline() { - return { - get start() { - return engineRef?.current?.getClock()?.start; - }, - get stop() { - return engineRef?.current?.getClock()?.stop; - }, - get current() { - return engineRef?.current?.getClock()?.current; - }, - }; - }, - computedTimeline: { - ...validTimes, + timeline: { + get start() { + return engineRef?.current?.getClock()?.start; + }, + get stop() { + return engineRef?.current?.getClock()?.stop; + }, + get current() { + return engineRef?.current?.getClock()?.current; + }, }, options, + computedTimeline, commit, onTick, offTick, @@ -178,7 +174,17 @@ export default ({ init, engineRef, timelineManagerRef }: Props) => { handleTick, tick: engineRef?.current?.tick, }; - }, [options, validTimes, engineRef, commit, onTick, offTick, onCommit, offCommit, handleTick]); + }, [ + options, + computedTimeline, + engineRef, + commit, + onTick, + offTick, + onCommit, + offCommit, + handleTick, + ]); if (timelineManagerRef) { timelineManagerRef.current = timelineManager; From 2d8165fb93792705cfc7226589000a6c0cbf3cec Mon Sep 17 00:00:00 2001 From: airslice Date: Wed, 4 Oct 2023 10:43:54 +0800 Subject: [PATCH 07/12] refactor: add stepType and rangeType to API --- web/src/beta/lib/core/Crust/Plugins/hooks.ts | 6 ++++++ web/src/beta/lib/core/Crust/Plugins/plugin_types.ts | 2 ++ 2 files changed, 8 insertions(+) diff --git a/web/src/beta/lib/core/Crust/Plugins/hooks.ts b/web/src/beta/lib/core/Crust/Plugins/hooks.ts index 86786e3e54..b3165f463d 100644 --- a/web/src/beta/lib/core/Crust/Plugins/hooks.ts +++ b/web/src/beta/lib/core/Crust/Plugins/hooks.ts @@ -83,6 +83,12 @@ export default function ({ get speed() { return timelineManagerRef?.current?.options?.multiplier; }, + get stepType() { + return timelineManagerRef?.current?.options?.stepType; + }, + get rangeType() { + return timelineManagerRef?.current?.options?.rangeType; + }, play: () => { timelineManagerRef?.current?.commit({ cmd: "PLAY", diff --git a/web/src/beta/lib/core/Crust/Plugins/plugin_types.ts b/web/src/beta/lib/core/Crust/Plugins/plugin_types.ts index 3c47014760..af27d2676d 100644 --- a/web/src/beta/lib/core/Crust/Plugins/plugin_types.ts +++ b/web/src/beta/lib/core/Crust/Plugins/plugin_types.ts @@ -174,6 +174,8 @@ export type Clock = { paused?: boolean; /** Speed of time. Specifies a multiplier for the speed of time in reality. Default is 1. */ speed?: number; + stepType?: "rate" | "fixed"; + rangeType?: "unbounded" | "clamped" | "bounced"; readonly tick?: () => Date | void; readonly play?: () => void; readonly pause?: () => void; From ff3decc0a432fede81eea0e3118283cee87cda43 Mon Sep 17 00:00:00 2001 From: airslice Date: Wed, 4 Oct 2023 14:10:31 +0800 Subject: [PATCH 08/12] refactor: scene property override & plugin API --- .../lib/core/Crust/Plugins/plugin_types.ts | 6 +- web/src/beta/lib/core/Visualizer/hooks.ts | 69 ++++++++++++------- 2 files changed, 48 insertions(+), 27 deletions(-) diff --git a/web/src/beta/lib/core/Crust/Plugins/plugin_types.ts b/web/src/beta/lib/core/Crust/Plugins/plugin_types.ts index af27d2676d..7fd71d9b21 100644 --- a/web/src/beta/lib/core/Crust/Plugins/plugin_types.ts +++ b/web/src/beta/lib/core/Crust/Plugins/plugin_types.ts @@ -180,9 +180,9 @@ export type Clock = { readonly play?: () => void; readonly pause?: () => void; readonly setTime?: (time: { - start?: Date | string; - stop?: Date | string; - current?: Date | string; + start: Date | string; + stop: Date | string; + current: Date | string; }) => void; readonly setSpeed?: (speed: number) => void; readonly setRangeType?: (rangeType: "unbounded" | "clamped" | "bounced") => void; diff --git a/web/src/beta/lib/core/Visualizer/hooks.ts b/web/src/beta/lib/core/Visualizer/hooks.ts index 01fef5b23c..7f35c58082 100644 --- a/web/src/beta/lib/core/Visualizer/hooks.ts +++ b/web/src/beta/lib/core/Visualizer/hooks.ts @@ -169,30 +169,51 @@ export default function useHooks( const filteredTimeline = clone(property.timeline); delete filteredTimeline.visible; if (Object.keys(filteredTimeline).length > 0) { - timelineManagerRef?.current?.commit({ - cmd: "SET_TIME", - payload: { - start: filteredTimeline.start, - stop: filteredTimeline.stop, - current: filteredTimeline.current, - }, - committer: { - source: "overrideSceneProperty", - id: pluginId, - }, - }); - timelineManagerRef?.current?.commit({ - cmd: "SET_OPTIONS", - payload: { - stepType: filteredTimeline.stepType, - multiplier: filteredTimeline.multiplier, - rangeType: filteredTimeline.rangeType, - }, - committer: { - source: "overrideSceneProperty", - id: pluginId, - }, - }); + if ( + filteredTimeline.current !== undefined || + filteredTimeline.start !== undefined || + filteredTimeline.stop !== undefined + ) { + timelineManagerRef?.current?.commit({ + cmd: "SET_TIME", + payload: { + start: filteredTimeline.start, + stop: filteredTimeline.stop, + current: filteredTimeline.current, + }, + committer: { + source: "overrideSceneProperty", + id: pluginId, + }, + }); + } + if ( + filteredTimeline.multiplier !== undefined || + filteredTimeline.stepType !== undefined || + filteredTimeline.rangeType !== undefined + ) { + timelineManagerRef?.current?.commit({ + cmd: "SET_OPTIONS", + payload: { + stepType: filteredTimeline.stepType, + multiplier: filteredTimeline.multiplier, + rangeType: filteredTimeline.rangeType, + }, + committer: { + source: "overrideSceneProperty", + id: pluginId, + }, + }); + } + if (filteredTimeline.animation !== undefined) { + timelineManagerRef?.current?.commit({ + cmd: filteredTimeline.animation ? "PLAY" : "PAUSE", + committer: { + source: "overrideSceneProperty", + id: pluginId, + }, + }); + } } } originalOverrideSceneProperty(pluginId, property); From 585ff512a86951f7cda33536fa06ebf01ebc6b21 Mon Sep 17 00:00:00 2001 From: airslice Date: Wed, 4 Oct 2023 16:14:28 +0800 Subject: [PATCH 09/12] refactor: add timelineManagerRef to MapRef --- web/src/beta/lib/core/Map/hooks.ts | 3 ++- web/src/beta/lib/core/Map/ref.ts | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/web/src/beta/lib/core/Map/hooks.ts b/web/src/beta/lib/core/Map/hooks.ts index 66bc0acc6a..6f1c9c020a 100644 --- a/web/src/beta/lib/core/Map/hooks.ts +++ b/web/src/beta/lib/core/Map/hooks.ts @@ -50,8 +50,9 @@ export default function ({ mapRef({ engineRef, layersRef, + timelineManagerRef, }), - [], + [timelineManagerRef], ); // Order in which selectedLayerId prop propagates from the outside: Map -> Layers -> Engine diff --git a/web/src/beta/lib/core/Map/ref.ts b/web/src/beta/lib/core/Map/ref.ts index d5851b575f..5194ccc0d1 100644 --- a/web/src/beta/lib/core/Map/ref.ts +++ b/web/src/beta/lib/core/Map/ref.ts @@ -1,11 +1,13 @@ import type { RefObject } from "react"; import type { EngineRef, LayersRef } from "./types"; +import { TimelineManagerRef } from "./useTimelineManager"; import { FunctionKeys, WrappedRef, wrapRef } from "./utils"; export type MapRef = { engine: WrappedRef; layers: WrappedRef; + timeline?: TimelineManagerRef; }; const engineRefKeys: FunctionKeys = { @@ -89,12 +91,15 @@ const layersRefKeys: FunctionKeys = { export function mapRef({ engineRef, layersRef, + timelineManagerRef, }: { engineRef: RefObject; layersRef: RefObject; + timelineManagerRef?: TimelineManagerRef; }): MapRef { return { engine: wrapRef(engineRef, engineRefKeys), layers: wrapRef(layersRef, layersRefKeys), + timeline: timelineManagerRef, }; } From afce98409ac642509041a4f182f7138c63c0cba9 Mon Sep 17 00:00:00 2001 From: airslice Date: Thu, 5 Oct 2023 11:04:59 +0800 Subject: [PATCH 10/12] refactor: use timelineManagerRef --- web/src/beta/lib/core/Map/hooks.ts | 3 +-- web/src/beta/lib/core/Map/index.tsx | 2 -- web/src/beta/lib/core/Map/types/index.ts | 4 +--- web/src/beta/lib/core/Map/useTimelineManager.ts | 4 ++-- .../core/engines/Cesium/Feature/Resource/index.tsx | 14 +++++++------- .../lib/core/engines/Cesium/Feature/context.ts | 4 ++-- .../beta/lib/core/engines/Cesium/core/Clock.tsx | 14 +++++++------- .../lib/core/engines/Cesium/core/Indicator.tsx | 13 +++++++------ web/src/beta/lib/core/engines/Cesium/hooks.ts | 10 +++++----- web/src/beta/lib/core/engines/Cesium/index.tsx | 8 ++++---- 10 files changed, 36 insertions(+), 40 deletions(-) diff --git a/web/src/beta/lib/core/Map/hooks.ts b/web/src/beta/lib/core/Map/hooks.ts index 6f1c9c020a..17ac5199fb 100644 --- a/web/src/beta/lib/core/Map/hooks.ts +++ b/web/src/beta/lib/core/Map/hooks.ts @@ -107,7 +107,7 @@ export default function ({ ); }, [onLayerSelect, selectedLayer]); - const timelineManager = useTimelineManager({ + useTimelineManager({ init: sceneProperty?.timeline, engineRef, timelineManagerRef, @@ -118,7 +118,6 @@ export default function ({ layersRef, selectedLayer, requestingRenderMode, - timelineManager, handleLayerSelect, handleEngineLayerSelect, }; diff --git a/web/src/beta/lib/core/Map/index.tsx b/web/src/beta/lib/core/Map/index.tsx index 6fbb8606ec..2771cc9920 100644 --- a/web/src/beta/lib/core/Map/index.tsx +++ b/web/src/beta/lib/core/Map/index.tsx @@ -56,7 +56,6 @@ function Map( layersRef, selectedLayer, requestingRenderMode, - timelineManager, handleLayerSelect, handleEngineLayerSelect, } = useHooks({ @@ -83,7 +82,6 @@ function Map( layersRef={layersRef} requestingRenderMode={requestingRenderMode} timelineManagerRef={timelineManagerRef} - timelineManager={timelineManager} {...props}> ; removeTickEventListener: TickEvent; - timelineManager?: TimelineManager; findFeatureById: (layerId: string, featureId: string) => Feature | undefined; findFeaturesByIds: (layerId: string, featureId: string[]) => Feature[] | undefined; }; @@ -132,7 +131,6 @@ export type EngineProps = { layersRef?: RefObject; requestingRenderMode?: MutableRefObject; timelineManagerRef?: TimelineManagerRef; - timelineManager?: TimelineManager; onLayerSelect?: ( layerId: string | undefined, featureId?: string, diff --git a/web/src/beta/lib/core/Map/useTimelineManager.ts b/web/src/beta/lib/core/Map/useTimelineManager.ts index 0356dfc89d..61ff35b6d4 100644 --- a/web/src/beta/lib/core/Map/useTimelineManager.ts +++ b/web/src/beta/lib/core/Map/useTimelineManager.ts @@ -4,7 +4,7 @@ import { convertTime } from "@reearth/beta/utils/time"; import { EngineRef } from "."; -export type TimelineManager = { +type TimelineManager = { readonly timeline: Timeline; readonly options: TimelineOptions; readonly computedTimeline: Timeline; @@ -190,5 +190,5 @@ export default ({ init, engineRef, timelineManagerRef }: Props) => { timelineManagerRef.current = timelineManager; } - return timelineManager; + return null; }; diff --git a/web/src/beta/lib/core/engines/Cesium/Feature/Resource/index.tsx b/web/src/beta/lib/core/engines/Cesium/Feature/Resource/index.tsx index 25113b8d73..667b19804f 100644 --- a/web/src/beta/lib/core/engines/Cesium/Feature/Resource/index.tsx +++ b/web/src/beta/lib/core/engines/Cesium/Feature/Resource/index.tsx @@ -73,7 +73,7 @@ export default function Resource({ const actualType = ext ? types[ext] : type !== "auto" ? type : undefined; const Component = actualType ? comps[actualType] : undefined; - const { requestRender, timelineManager } = useContext(); + const { requestRender, timelineManagerRef } = useContext(); const handleChange = useCallback( (e: DataSource) => { @@ -107,9 +107,9 @@ export default function Resource({ ); const initialClock = useRef({ - start: timelineManager?.timeline?.start, - stop: timelineManager?.timeline?.stop, - current: timelineManager?.timeline?.current, + start: timelineManagerRef?.current?.timeline?.start, + stop: timelineManagerRef?.current?.timeline?.stop, + current: timelineManagerRef?.current?.timeline?.current, }); const handleLoad = useCallback( (ds: DataSource) => { @@ -119,7 +119,7 @@ export default function Resource({ initialClock.current.start && initialClock.current.stop ) { - timelineManager?.commit({ + timelineManagerRef?.current?.commit({ cmd: "SET_TIME", payload: { start: initialClock.current.start, @@ -135,7 +135,7 @@ export default function Resource({ return; } if (ds.clock) { - timelineManager?.commit({ + timelineManagerRef?.current?.commit({ cmd: "SET_TIME", payload: { start: JulianDate.toDate(ds.clock.currentTime), @@ -150,7 +150,7 @@ export default function Resource({ } requestRender?.(); }, - [updateClock, timelineManager, layer?.id, requestRender], + [updateClock, timelineManagerRef, layer?.id, requestRender], ); // convert hexCodeColorString to ColorValue?s diff --git a/web/src/beta/lib/core/engines/Cesium/Feature/context.ts b/web/src/beta/lib/core/engines/Cesium/Feature/context.ts index 2a33a72e8d..6ee58becd6 100644 --- a/web/src/beta/lib/core/engines/Cesium/Feature/context.ts +++ b/web/src/beta/lib/core/engines/Cesium/Feature/context.ts @@ -3,12 +3,12 @@ import { createContext, useContext as useReactContext } from "react"; import { LayerEditEvent } from "@reearth/beta/lib/core/Map"; import type { Camera, LayerSelectionReason } from "../.."; -import { TimelineManager } from "../../../Map/useTimelineManager"; +import { TimelineManagerRef } from "../../../Map/useTimelineManager"; import type { FlyTo } from "../../../types"; export type Context = { selectionReason?: LayerSelectionReason; - timelineManager?: TimelineManager; + timelineManagerRef?: TimelineManagerRef; getCamera?: () => Camera | undefined; flyTo?: FlyTo; onLayerEdit?: (e: LayerEditEvent) => void; diff --git a/web/src/beta/lib/core/engines/Cesium/core/Clock.tsx b/web/src/beta/lib/core/engines/Cesium/core/Clock.tsx index c244213816..bf058334be 100644 --- a/web/src/beta/lib/core/engines/Cesium/core/Clock.tsx +++ b/web/src/beta/lib/core/engines/Cesium/core/Clock.tsx @@ -2,15 +2,15 @@ import { Clock as CesiumClock, ClockRange, ClockStep, JulianDate } from "cesium" import { useCallback, useMemo } from "react"; import { Clock } from "resium"; -import { type TimelineManager } from "../../../Map/useTimelineManager"; +import { TimelineManagerRef } from "../../../Map/useTimelineManager"; export type Props = { - timelineManager?: TimelineManager; + timelineManagerRef?: TimelineManagerRef; }; -export default function ReearthClock({ timelineManager }: Props): JSX.Element | null { - const { start, stop, current } = timelineManager?.computedTimeline ?? {}; - const { animation, stepType, rangeType, multiplier } = timelineManager?.options ?? {}; +export default function ReearthClock({ timelineManagerRef }: Props): JSX.Element | null { + const { start, stop, current } = timelineManagerRef?.current?.computedTimeline ?? {}; + const { animation, stepType, rangeType, multiplier } = timelineManagerRef?.current?.options ?? {}; const startTime = useMemo(() => (start ? JulianDate.fromDate(start) : undefined), [start]); const stopTime = useMemo(() => (stop ? JulianDate.fromDate(stop) : undefined), [stop]); @@ -28,12 +28,12 @@ export default function ReearthClock({ timelineManager }: Props): JSX.Element | const stop = JulianDate.toDate(clock.stopTime); // NOTE: Must not update state. This event will be called every frame. - timelineManager?.handleTick?.(JulianDate.toDate(clock.currentTime), { + timelineManagerRef?.current?.handleTick?.(JulianDate.toDate(clock.currentTime), { start, stop, }); }, - [timelineManager], + [timelineManagerRef], ); return ( diff --git a/web/src/beta/lib/core/engines/Cesium/core/Indicator.tsx b/web/src/beta/lib/core/engines/Cesium/core/Indicator.tsx index 68a7e6ac27..ccbfeefe8d 100644 --- a/web/src/beta/lib/core/engines/Cesium/core/Indicator.tsx +++ b/web/src/beta/lib/core/engines/Cesium/core/Indicator.tsx @@ -7,19 +7,19 @@ import Icon from "@reearth/beta/components/Icon"; import { styled } from "@reearth/services/theme"; import type { SceneProperty } from "../.."; -import { TimelineManager } from "../../../Map/useTimelineManager"; +import { TimelineManagerRef } from "../../../Map/useTimelineManager"; import { useIcon } from "../common"; export type Props = { className?: string; property?: SceneProperty; - timelineManager?: TimelineManager; + timelineManagerRef?: TimelineManagerRef; }; export default function Indicator({ className, property, - timelineManager, + timelineManagerRef, }: Props): JSX.Element | null { const { viewer } = useCesium(); const [isVisible, setIsVisible] = useState(true); @@ -46,8 +46,8 @@ export default function Indicator({ const handleTick = () => { if (viewer.isDestroyed()) return; const selected = viewer.selectedEntity; - const currentTime = timelineManager?.timeline?.current - ? JulianDate.fromDate(timelineManager.timeline.current) + const currentTime = timelineManagerRef?.current?.timeline?.current + ? JulianDate.fromDate(timelineManagerRef?.current?.timeline.current) : undefined; if ( !selected?.isShowing || @@ -82,11 +82,12 @@ export default function Indicator({ } }; + const timelineManager = timelineManagerRef?.current; timelineManager?.onTick(handleTick); return () => { timelineManager?.offTick(handleTick); }; - }, [viewer, timelineManager]); + }, [viewer, timelineManagerRef]); return transiton !== "unmounted" && pos ? ( indicator_type === "crosshair" ? ( diff --git a/web/src/beta/lib/core/engines/Cesium/hooks.ts b/web/src/beta/lib/core/engines/Cesium/hooks.ts index 32a980452a..6bea89ec10 100644 --- a/web/src/beta/lib/core/engines/Cesium/hooks.ts +++ b/web/src/beta/lib/core/engines/Cesium/hooks.ts @@ -34,7 +34,7 @@ import type { LayerEditEvent, } from ".."; import { FORCE_REQUEST_RENDER, NO_REQUEST_RENDER, REQUEST_RENDER_ONCE } from "../../Map/hooks"; -import { TimelineManager } from "../../Map/useTimelineManager"; +import { TimelineManagerRef } from "../../Map/useTimelineManager"; import { useCameraLimiter } from "./cameraLimiter"; import { getCamera, isDraggable, isSelectable, getLocationFromScreen } from "./common"; @@ -58,7 +58,7 @@ export default ({ featureFlags, requestingRenderMode, shouldRender, - timelineManager, + timelineManagerRef, onLayerSelect, onCameraChange, onLayerDrag, @@ -81,7 +81,7 @@ export default ({ featureFlags: number; requestingRenderMode?: React.MutableRefObject; shouldRender?: boolean; - timelineManager?: TimelineManager; + timelineManagerRef?: TimelineManagerRef; onLayerSelect?: ( layerId?: string, featureId?: string, @@ -725,13 +725,13 @@ export default ({ const context = useMemo( () => ({ selectionReason, - timelineManager, + timelineManagerRef, flyTo: engineAPI.flyTo, getCamera: engineAPI.getCamera, onLayerEdit, requestRender: engineAPI.requestRender, }), - [selectionReason, engineAPI, onLayerEdit, timelineManager], + [selectionReason, engineAPI, onLayerEdit, timelineManagerRef], ); useEffect(() => { diff --git a/web/src/beta/lib/core/engines/Cesium/index.tsx b/web/src/beta/lib/core/engines/Cesium/index.tsx index 68ee612905..f213bd1856 100644 --- a/web/src/beta/lib/core/engines/Cesium/index.tsx +++ b/web/src/beta/lib/core/engines/Cesium/index.tsx @@ -47,7 +47,7 @@ const Cesium: React.ForwardRefRenderFunction = ( layersRef, featureFlags, requestingRenderMode, - timelineManager, + timelineManagerRef, onLayerSelect, onCameraChange, onLayerDrag, @@ -86,7 +86,7 @@ const Cesium: React.ForwardRefRenderFunction = ( featureFlags, requestingRenderMode, shouldRender, - timelineManager, + timelineManagerRef, onLayerSelect, onCameraChange, onLayerDrag, @@ -138,9 +138,9 @@ const Cesium: React.ForwardRefRenderFunction = ( onMouseLeave={mouseEventHandles.mouseleave} onWheel={mouseEventHandles.wheel}> - + - + {/* remove default click event */} From 90ec5fefb3a55df573268f33d5cf8bcf2e82cad6 Mon Sep 17 00:00:00 2001 From: airslice Date: Thu, 5 Oct 2023 11:24:22 +0800 Subject: [PATCH 11/12] refactor: add source type --- web/src/beta/lib/core/Map/useTimelineManager.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/web/src/beta/lib/core/Map/useTimelineManager.ts b/web/src/beta/lib/core/Map/useTimelineManager.ts index 61ff35b6d4..cad0590f05 100644 --- a/web/src/beta/lib/core/Map/useTimelineManager.ts +++ b/web/src/beta/lib/core/Map/useTimelineManager.ts @@ -47,7 +47,12 @@ type TimelineCommit = { }; export type TimelineCommitter = { - source: "overrideSceneProperty" | "widgetContext" | "pluginAPI" | "featureResource"; + source: + | "overrideSceneProperty" + | "widgetContext" + | "pluginAPI" + | "featureResource" + | "storyTimelineBlock"; id?: string; }; From 8ee6db2d6551142cadfdb3d823619642e86d3dbe Mon Sep 17 00:00:00 2001 From: airslice Date: Thu, 5 Oct 2023 17:00:56 +0800 Subject: [PATCH 12/12] refactor: type --- web/src/beta/lib/core/Crust/Plugins/api.ts | 6 +- web/src/beta/lib/core/Crust/Plugins/hooks.ts | 2 +- web/src/beta/lib/core/Crust/context.ts | 2 + .../beta/lib/core/Map/useTimelineManager.ts | 85 +++++++++++-------- web/src/beta/lib/core/Visualizer/hooks.ts | 8 +- 5 files changed, 59 insertions(+), 44 deletions(-) diff --git a/web/src/beta/lib/core/Crust/Plugins/api.ts b/web/src/beta/lib/core/Crust/Plugins/api.ts index 4c51d9f566..a15aaccca1 100644 --- a/web/src/beta/lib/core/Crust/Plugins/api.ts +++ b/web/src/beta/lib/core/Crust/Plugins/api.ts @@ -210,11 +210,7 @@ export function exposed({ }); }, get setTime() { - return (time: { - start?: Date | string; - stop?: Date | string; - current?: Date | string; - }) => + return (time: { start: Date | string; stop: Date | string; current: Date | string }) => timelineManagerRef?.current?.commit({ cmd: "SET_TIME", payload: { ...time }, diff --git a/web/src/beta/lib/core/Crust/Plugins/hooks.ts b/web/src/beta/lib/core/Crust/Plugins/hooks.ts index b3165f463d..6d659821e7 100644 --- a/web/src/beta/lib/core/Crust/Plugins/hooks.ts +++ b/web/src/beta/lib/core/Crust/Plugins/hooks.ts @@ -101,7 +101,7 @@ export default function ({ committer: { source: "pluginAPI", id: "window" }, }); }, - setTime: (time: { start?: Date | string; stop?: Date | string; current?: Date | string }) => { + setTime: (time: { start: Date | string; stop: Date | string; current: Date | string }) => { timelineManagerRef?.current?.commit({ cmd: "SET_TIME", payload: { ...time }, diff --git a/web/src/beta/lib/core/Crust/context.ts b/web/src/beta/lib/core/Crust/context.ts index 77b4f9647d..0e2d537493 100644 --- a/web/src/beta/lib/core/Crust/context.ts +++ b/web/src/beta/lib/core/Crust/context.ts @@ -96,7 +96,9 @@ export function widgetContextFromMapRef({ timelineManagerRef?.current?.commit({ cmd: "SET_TIME", payload: { + start: timelineManagerRef.current?.computedTimeline.start, current: time, + stop: timelineManagerRef.current?.computedTimeline.stop, }, committer: { source: committer?.source ?? "widgetContext", id: committer?.id }, }), diff --git a/web/src/beta/lib/core/Map/useTimelineManager.ts b/web/src/beta/lib/core/Map/useTimelineManager.ts index cad0590f05..e84ac4ecef 100644 --- a/web/src/beta/lib/core/Map/useTimelineManager.ts +++ b/web/src/beta/lib/core/Map/useTimelineManager.ts @@ -5,10 +5,10 @@ import { convertTime } from "@reearth/beta/utils/time"; import { EngineRef } from "."; type TimelineManager = { - readonly timeline: Timeline; + readonly timeline: EngineClock; readonly options: TimelineOptions; readonly computedTimeline: Timeline; - commit: (props: TimelineCommit) => void; + commit: (commit: TimelineCommit) => void; onTick: TickEvent; offTick: TickEvent; onCommit: (cb: (committer: TimelineCommitter) => void) => void; @@ -20,9 +20,15 @@ type TimelineManager = { export type TimelineManagerRef = MutableRefObject; export type Timeline = { - current?: Date; - start?: Date; - stop?: Date; + current: Date; + start: Date; + stop: Date; +}; + +type EngineClock = { + current: Date | undefined; + start: Date | undefined; + stop: Date | undefined; }; type TimelineOptions = { @@ -32,17 +38,25 @@ type TimelineOptions = { rangeType?: "unbounded" | "clamped" | "bounced"; }; -type TimelineCommand = "PLAY" | "PAUSE" | "SET_TIME" | "SET_OPTIONS"; - -type TimelineCommit = { - cmd: TimelineCommand; - payload?: - | ({ - current?: Date | string | undefined; - start?: Date | string | undefined; - stop?: Date | string | undefined; - } & Partial) - | undefined; +type PlayCommand = { + cmd: "PLAY"; +}; +type PauseCommand = { + cmd: "PAUSE"; +}; +type SetTimeCommand = { + cmd: "SET_TIME"; + payload: { + current: Date | string; + start: Date | string; + stop: Date | string; + }; +}; +type SetOptionsCommand = { + cmd: "SET_OPTIONS"; + payload: Partial>; +}; +type TimelineCommit = (PlayCommand | PauseCommand | SetTimeCommand | SetOptionsCommand) & { committer: TimelineCommitter; }; @@ -78,9 +92,9 @@ type Props = { export default ({ init, engineRef, timelineManagerRef }: Props) => { const [time, setTime] = useState({ - start: convertTime(init?.start), - stop: convertTime(init?.stop), - current: convertTime(init?.current), + start: convertTime(init?.start) ?? new Date(), + stop: convertTime(init?.stop) ?? new Date(), + current: convertTime(init?.current) ?? new Date(), }); const [options, setOptions] = useState({ @@ -92,9 +106,9 @@ export default ({ init, engineRef, timelineManagerRef }: Props) => { const computedTimeline = useMemo(() => { const { start, stop, current } = time; - const startTime = start?.getTime() ?? new Date().getTime(); - const stopTime = stop?.getTime() ?? new Date().getTime(); - const currentTime = current?.getTime() ?? new Date().getTime(); + const startTime = start.getTime(); + const stopTime = stop.getTime(); + const currentTime = current.getTime(); const convertedStartTime = startTime > currentTime ? currentTime : startTime; const convertedStopTime = stopTime <= currentTime ? currentTime + DEFAULT_RANGE : stopTime; @@ -106,29 +120,30 @@ export default ({ init, engineRef, timelineManagerRef }: Props) => { }; }, [time]); - const commit = useCallback(({ cmd, payload, committer }: TimelineCommit) => { - if (!cmd) return; + const commit = useCallback((commit: TimelineCommit) => { + if (!commit.cmd) return; - if (cmd === "PLAY") { + if (commit.cmd === "PLAY") { setOptions(o => ({ ...o, animation: true })); - } else if (cmd === "PAUSE") { + } else if (commit.cmd === "PAUSE") { setOptions(o => ({ ...o, animation: false })); - } else if (cmd === "SET_TIME") { + } else if (commit.cmd === "SET_TIME") { setTime(t => ({ - start: payload?.start === undefined ? t.start : convertTime(payload?.start), - stop: payload?.stop === undefined ? t.stop : convertTime(payload?.stop), - current: payload?.current === undefined ? t.current : convertTime(payload?.current), + start: convertTime(commit.payload.start) ?? t.start, + stop: convertTime(commit.payload.stop) ?? t.stop, + current: convertTime(commit.payload.current) ?? t.current, })); - } else if (cmd === "SET_OPTIONS") { + } else if (commit.cmd === "SET_OPTIONS") { setOptions(o => ({ ...o, - stepType: payload?.stepType === undefined ? o.stepType : payload.stepType, - multiplier: payload?.multiplier === undefined ? o.multiplier : payload.multiplier, - rangeType: payload?.rangeType === undefined ? o.rangeType : payload.rangeType, + stepType: commit.payload?.stepType === undefined ? o.stepType : commit.payload.stepType, + multiplier: + commit.payload?.multiplier === undefined ? o.multiplier : commit.payload.multiplier, + rangeType: commit.payload?.rangeType === undefined ? o.rangeType : commit.payload.rangeType, })); } - commitEventCallbacks.current.forEach(cb => cb(committer)); + commitEventCallbacks.current.forEach(cb => cb(commit.committer)); }, []); const tickEventCallbacks = useRef([]); diff --git a/web/src/beta/lib/core/Visualizer/hooks.ts b/web/src/beta/lib/core/Visualizer/hooks.ts index 7f35c58082..7db007ccaf 100644 --- a/web/src/beta/lib/core/Visualizer/hooks.ts +++ b/web/src/beta/lib/core/Visualizer/hooks.ts @@ -177,9 +177,11 @@ export default function useHooks( timelineManagerRef?.current?.commit({ cmd: "SET_TIME", payload: { - start: filteredTimeline.start, - stop: filteredTimeline.stop, - current: filteredTimeline.current, + start: + filteredTimeline.start ?? timelineManagerRef?.current?.computedTimeline.start, + stop: filteredTimeline.stop ?? timelineManagerRef?.current?.computedTimeline.stop, + current: + filteredTimeline.current ?? timelineManagerRef?.current?.computedTimeline.current, }, committer: { source: "overrideSceneProperty",