diff --git a/src/app/App.tsx b/src/app/App.tsx index ce5e61ae..79156d7b 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -884,6 +884,8 @@ export function App({ store, initialStatusMessage }: AppProps) { ); const [statusMessage, setStatusMessage] = useState(initialStatusMessage ?? "Slice 3.5 advanced rendering ready."); const [assetStatusMessage, setAssetStatusMessage] = useState(null); + const [hoveredAssetId, setHoveredAssetId] = useState(null); + const [addMenuPosition, setAddMenuPosition] = useState(null); const [preferredNavigationMode, setPreferredNavigationMode] = useState( primaryPlayerStart === null ? "orbitVisitor" : "firstPerson" ); @@ -913,6 +915,10 @@ export function App({ store, initialStatusMessage }: AppProps) { const loadedModelAssetsRef = useRef>({}); const loadedImageAssetsRef = useRef>({}); const loadedAudioAssetsRef = useRef>({}); + const lastPointerPositionRef = useRef({ + x: Math.round(window.innerWidth * 0.5), + y: Math.round(window.innerHeight * 0.5) + }); const documentValidation = validateSceneDocument(editorState.document); const runValidation = validateRuntimeSceneBuild(editorState.document, preferredNavigationMode); const diagnostics = [...documentValidation.errors, ...documentValidation.warnings, ...runValidation.errors, ...runValidation.warnings]; @@ -928,6 +934,8 @@ export function App({ store, initialStatusMessage }: AppProps) { ? "Ready for First Person" : "Ready for Orbit Visitor"; const advancedRendering = editorState.document.world.advancedRendering; + const hoveredAsset = hoveredAssetId === null ? null : editorState.document.assets[hoveredAssetId] ?? null; + const hoveredAssetStatusMessage = hoveredAsset === null ? null : formatAssetHoverStatus(hoveredAsset); useEffect(() => { setSceneNameDraft(editorState.document.name); @@ -1261,11 +1269,33 @@ export function App({ store, initialStatusMessage }: AppProps) { return; } + const handleWindowPointerMove = (event: globalThis.PointerEvent) => { + lastPointerPositionRef.current = { + x: event.clientX, + y: event.clientY + }; + }; + const handleWindowKeyDown = (event: globalThis.KeyboardEvent) => { if (isTextEntryTarget(event.target)) { return; } + if (event.key === "Escape" && addMenuPosition !== null) { + event.preventDefault(); + setAddMenuPosition(null); + return; + } + + if (event.shiftKey && event.code === "KeyA") { + event.preventDefault(); + setAddMenuPosition({ + x: lastPointerPositionRef.current.x, + y: lastPointerPositionRef.current.y + }); + return; + } + if ( event.code !== "NumpadComma" && !(event.key === "," && event.location === globalThis.KeyboardEvent.DOM_KEY_LOCATION_NUMPAD) @@ -1288,12 +1318,14 @@ export function App({ store, initialStatusMessage }: AppProps) { setStatusMessage(editorState.selection.kind === "none" ? "Framed the authored scene in the viewport." : "Framed the current selection."); }; + window.addEventListener("pointermove", handleWindowPointerMove); window.addEventListener("keydown", handleWindowKeyDown); return () => { + window.removeEventListener("pointermove", handleWindowPointerMove); window.removeEventListener("keydown", handleWindowKeyDown); }; - }, [activePanelId, editorState.selection, editorState.toolMode, brushList.length, entityList.length]); + }, [activePanelId, addMenuPosition, editorState.selection, editorState.toolMode, brushList.length, entityList.length]); const applySceneName = () => { const normalizedName = sceneNameDraft.trim() || "Untitled Scene"; @@ -1318,6 +1350,23 @@ export function App({ store, initialStatusMessage }: AppProps) { } }; + const openAddMenuAt = (position: HierarchicalMenuPosition) => { + setAddMenuPosition(position); + }; + + const closeAddMenu = () => { + setAddMenuPosition(null); + }; + + const handleOpenAddMenuFromButton = (event: ReactMouseEvent) => { + const rect = event.currentTarget.getBoundingClientRect(); + + openAddMenuAt({ + x: rect.left, + y: rect.bottom + 8 + }); + }; + const handleSetViewportLayoutMode = (nextLayoutMode: ViewportLayoutMode) => { if (editorState.viewportLayoutMode === nextLayoutMode) { return;