From 61c4936c29893ccd705b39ca64b01fbeb8a6e901 Mon Sep 17 00:00:00 2001 From: Tanish Manku Date: Wed, 29 Apr 2026 09:48:59 -0700 Subject: [PATCH] PageScene: useEditorCanvas hook + tabIndex on viewport canvas The Phase 6 hooks (useFreeCam, useViewportPointer, useViewportKeyboard) need a React-shaped accessor for the canvas DOM node so their useEffects re-run when the canvas mounts/unmounts. Promote the existing editorCanvasSignal / camViewCanvasSignal into useEditorCanvas / useCamViewCanvas hooks that bridge signal -> React state via a subscribe. Add tabIndex={0} and outline:none to the editor canvas so it can receive keyboard focus without showing a focus ring. --- .../pages/PageScene/comps/EngineCanvases.tsx | 2 ++ .../pages/PageScene/hooks/useEditorCanvas.ts | 26 +++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 frontend/apps/artcraft/app/src/pages/PageScene/hooks/useEditorCanvas.ts diff --git a/frontend/apps/artcraft/app/src/pages/PageScene/comps/EngineCanvases.tsx b/frontend/apps/artcraft/app/src/pages/PageScene/comps/EngineCanvases.tsx index 6de4ee4a42..64ec4755ca 100644 --- a/frontend/apps/artcraft/app/src/pages/PageScene/comps/EngineCanvases.tsx +++ b/frontend/apps/artcraft/app/src/pages/PageScene/comps/EngineCanvases.tsx @@ -17,6 +17,8 @@ export const EditorCanvas = () => { id="video-scene" width="1280px" height="720px" + tabIndex={0} + style={{ outline: "none" }} /> ); }; diff --git a/frontend/apps/artcraft/app/src/pages/PageScene/hooks/useEditorCanvas.ts b/frontend/apps/artcraft/app/src/pages/PageScene/hooks/useEditorCanvas.ts new file mode 100644 index 0000000000..4ff6c796ad --- /dev/null +++ b/frontend/apps/artcraft/app/src/pages/PageScene/hooks/useEditorCanvas.ts @@ -0,0 +1,26 @@ +import { useEffect, useState } from "react"; +import { + camViewCanvasSignal, + editorCanvasSignal, +} from "~/pages/PageScene/contexts/EngineContext"; + +// React-shaped accessors for the canvas DOM nodes that the engine +// renders into. The underlying storage is a signal (set by +// EngineCanvases on mount); these hooks convert that to React state so +// dependent useEffects re-run when the canvas mounts/unmounts. + +export function useEditorCanvas(): HTMLCanvasElement | null { + const [canvas, setCanvas] = useState( + editorCanvasSignal.value, + ); + useEffect(() => editorCanvasSignal.subscribe(setCanvas), []); + return canvas; +} + +export function useCamViewCanvas(): HTMLCanvasElement | null { + const [canvas, setCanvas] = useState( + camViewCanvasSignal.value, + ); + useEffect(() => camViewCanvasSignal.subscribe(setCanvas), []); + return canvas; +}