diff --git a/packages/dev/inspector-v2/src/components/gizmoToolbar.tsx b/packages/dev/inspector-v2/src/components/gizmoToolbar.tsx index e22434b9cdb..ab6eead09da 100644 --- a/packages/dev/inspector-v2/src/components/gizmoToolbar.tsx +++ b/packages/dev/inspector-v2/src/components/gizmoToolbar.tsx @@ -3,10 +3,11 @@ import type { FunctionComponent } from "react"; import type { IDisposable, Nullable, Scene, TransformNode } from "core/index"; import type { IGizmoService } from "../services/gizmoService"; +import type { GizmoMode, IGizmoToolbarService } from "../services/gizmoToolbarService"; import { makeStyles, Menu, MenuItemRadio, MenuList, MenuPopover, MenuTrigger, SplitButton, tokens, Tooltip } from "@fluentui/react-components"; import { ArrowExpandRegular, ArrowRotateClockwiseRegular, CubeRegular, GlobeRegular, SelectObjectRegular } from "@fluentui/react-icons"; -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect } from "react"; import { Bone } from "core/Bones/bone"; import { Camera } from "core/Cameras/camera"; @@ -18,11 +19,9 @@ import { Node } from "core/node"; import { TranslateIcon } from "shared-ui-components/fluent/icons"; import { Collapse } from "shared-ui-components/fluent/primitives/collapse"; import { ToggleButton } from "shared-ui-components/fluent/primitives/toggleButton"; -import { useProperty } from "../hooks/compoundPropertyHooks"; +import { useObservableState } from "../hooks/observableHooks"; import { useResource } from "../hooks/resourceHooks"; -type GizmoMode = "translate" | "rotate" | "scale" | "boundingBox"; - const useStyles = makeStyles({ coordinatesModeButton: { margin: `0 0 0 ${tokens.spacingHorizontalXS}`, @@ -32,8 +31,13 @@ const useStyles = makeStyles({ }, }); -export const GizmoToolbar: FunctionComponent<{ scene: Scene; entity: unknown; gizmoService: IGizmoService }> = (props) => { - const { scene, entity, gizmoService } = props; +export const GizmoToolbar: FunctionComponent<{ + scene: Scene; + entity: unknown; + gizmoService: IGizmoService; + gizmoToolbarService: IGizmoToolbarService; +}> = (props) => { + const { scene, entity, gizmoService, gizmoToolbarService } = props; const classes = useStyles(); @@ -55,9 +59,16 @@ export const GizmoToolbar: FunctionComponent<{ scene: Scene; entity: unknown; gi }, [scene]) ); - const coordinatesMode = useProperty(gizmoManager, "coordinatesMode"); + // Subscribe to gizmo mode from the service + const gizmoMode = useObservableState(() => gizmoToolbarService.gizmoMode, gizmoToolbarService.onGizmoModeChanged); + + // Subscribe to coordinates mode from the service + const coordinatesMode = useObservableState(() => gizmoToolbarService.coordinatesMode, gizmoToolbarService.onCoordinatesModeChanged); - const [gizmoMode, setGizmoMode] = useState(); + // Sync coordinates mode to gizmo manager + useEffect(() => { + gizmoManager.coordinatesMode = coordinatesMode === "local" ? GizmoCoordinatesMode.Local : GizmoCoordinatesMode.World; + }, [gizmoManager, coordinatesMode]); useEffect(() => { let visualizationGizmoRef: Nullable = null; @@ -78,23 +89,23 @@ export const GizmoToolbar: FunctionComponent<{ scene: Scene; entity: unknown; gi let resolvedGizmoMode = gizmoMode; if (!resolvedEntity) { - resolvedGizmoMode = undefined; + resolvedGizmoMode = null; } else { if (resolvedGizmoMode === "translate") { if (!(resolvedEntity as TransformNode).position) { - resolvedGizmoMode = undefined; + resolvedGizmoMode = null; } } else if (resolvedGizmoMode === "rotate") { if (!(resolvedEntity as TransformNode).rotation) { - resolvedGizmoMode = undefined; + resolvedGizmoMode = null; } } else if (resolvedGizmoMode === "scale") { if (!(resolvedEntity as TransformNode).scaling) { - resolvedGizmoMode = undefined; + resolvedGizmoMode = null; } } else { if (!(resolvedEntity instanceof AbstractMesh)) { - resolvedGizmoMode = undefined; + resolvedGizmoMode = null; } } } @@ -124,17 +135,26 @@ export const GizmoToolbar: FunctionComponent<{ scene: Scene; entity: unknown; gi }; }, [gizmoManager, gizmoMode, entity]); - const updateGizmoMode = useCallback((mode: GizmoMode) => { - setGizmoMode((currentMode) => (currentMode === mode ? undefined : mode)); - }, []); + const updateGizmoMode = useCallback( + (mode: GizmoMode) => { + gizmoToolbarService.gizmoMode = gizmoToolbarService.gizmoMode === mode ? null : mode; + }, + [gizmoToolbarService] + ); - const onCoordinatesModeChange = useCallback((e: MenuCheckedValueChangeEvent, data: MenuCheckedValueChangeData) => { - gizmoManager.coordinatesMode = Number(data.checkedItems[0]); - }, []); + const onCoordinatesModeChange = useCallback( + (e: MenuCheckedValueChangeEvent, data: MenuCheckedValueChangeData) => { + gizmoToolbarService.coordinatesMode = Number(data.checkedItems[0]) === GizmoCoordinatesMode.Local ? "local" : "world"; + }, + [gizmoToolbarService] + ); const toggleCoordinatesMode = useCallback(() => { - gizmoManager.coordinatesMode = coordinatesMode === GizmoCoordinatesMode.Local ? GizmoCoordinatesMode.World : GizmoCoordinatesMode.Local; - }, [gizmoManager, coordinatesMode]); + gizmoToolbarService.coordinatesMode = coordinatesMode === "local" ? "world" : "local"; + }, [gizmoToolbarService, coordinatesMode]); + + // Convert coordinatesMode string to GizmoCoordinatesMode number for the menu + const coordinatesModeValue = coordinatesMode === "local" ? GizmoCoordinatesMode.Local : GizmoCoordinatesMode.World; return ( <> @@ -144,7 +164,7 @@ export const GizmoToolbar: FunctionComponent<{ scene: Scene; entity: unknown; gi updateGizmoMode("boundingBox")} /> {/* TODO: gehalper factor this into a shared component */} - + {(triggerProps: MenuButtonProps) => ( @@ -157,7 +177,7 @@ export const GizmoToolbar: FunctionComponent<{ scene: Scene; entity: unknown; gi size="small" appearance="transparent" shape="rounded" - icon={coordinatesMode === GizmoCoordinatesMode.Local ? : } + icon={coordinatesMode === "local" ? : } > )} diff --git a/packages/dev/inspector-v2/src/index.ts b/packages/dev/inspector-v2/src/index.ts index 99a09fea711..b09f7620f85 100644 --- a/packages/dev/inspector-v2/src/index.ts +++ b/packages/dev/inspector-v2/src/index.ts @@ -40,6 +40,7 @@ export * from "./services/settingsContext"; export type { IShellService, ToolbarItemDefinition, SidePaneDefinition, CentralContentDefinition } from "./services/shellService"; export { ShellServiceIdentity } from "./services/shellService"; export * from "./inspector"; +export type { GizmoMode, CoordinatesMode } from "./services/gizmoToolbarService"; export { ConvertOptions, Inspector } from "./legacy/inspector"; export { AttachDebugLayer, DetachDebugLayer } from "./legacy/debugLayer"; diff --git a/packages/dev/inspector-v2/src/inspector.tsx b/packages/dev/inspector-v2/src/inspector.tsx index 08a25457eb2..d90dd0b6446 100644 --- a/packages/dev/inspector-v2/src/inspector.tsx +++ b/packages/dev/inspector-v2/src/inspector.tsx @@ -3,7 +3,9 @@ import type { Nullable } from "core/types"; import type { ServiceDefinition } from "./modularity/serviceDefinition"; import type { ModularToolOptions } from "./modularTool"; import type { ISceneContext } from "./services/sceneContext"; +import type { ISelectionService } from "./services/selectionService"; import type { IShellService } from "./services/shellService"; +import type { GizmoMode, CoordinatesMode, IGizmoToolbarService } from "./services/gizmoToolbarService"; import { AsyncLock } from "core/Misc/asyncLock"; import { Logger } from "core/Misc/logger"; @@ -13,7 +15,7 @@ import { DefaultInspectorExtensionFeed } from "./extensibility/defaultInspectorE import { LegacyInspectableObjectPropertiesServiceDefinition } from "./legacy/inspectableCustomPropertiesService"; import { MakeModularTool } from "./modularTool"; import { GizmoServiceDefinition } from "./services/gizmoService"; -import { GizmoToolbarServiceDefinition } from "./services/gizmoToolbarService"; +import { GizmoToolbarServiceDefinition, GizmoToolbarServiceIdentity } from "./services/gizmoToolbarService"; import { MiniStatsServiceDefinition } from "./services/miniStatsService"; import { DebugServiceDefinition } from "./services/panes/debugService"; import { AnimationGroupPropertiesServiceDefinition } from "./services/panes/properties/animationGroupPropertiesService"; @@ -56,22 +58,77 @@ import { StatsServiceDefinition } from "./services/panes/statsService"; import { ToolsServiceDefinition } from "./services/panes/toolsService"; import { PickingServiceDefinition } from "./services/pickingService"; import { SceneContextIdentity } from "./services/sceneContext"; -import { SelectionServiceDefinition } from "./services/selectionService"; +import { SelectionServiceDefinition, SelectionServiceIdentity } from "./services/selectionService"; import { ShellServiceIdentity } from "./services/shellService"; import { UserFeedbackServiceDefinition } from "./services/userFeedbackService"; export type InspectorOptions = Omit & { autoResizeEngine?: boolean }; +/** + * Handle returned by ShowInspector that provides control over the inspector. + */ +export interface IInspectorHandle extends IDisposable { + /** + * Gets the current gizmo mode, or null if no gizmo is active. + * Returns null if the inspector is not yet fully initialized. + */ + getGizmoMode(): Nullable; + + /** + * Sets the active gizmo mode. Pass null to disable all gizmos. + * @param mode The gizmo mode to activate, or null to disable. + */ + setGizmoMode(mode: Nullable): void; + + /** + * Gets the current coordinates mode for gizmos. + * Returns "world" if the inspector is not yet fully initialized. + */ + getCoordinatesMode(): CoordinatesMode; + + /** + * Sets the coordinates mode for gizmos (local or world space). + * @param mode The coordinates mode to use. + */ + setCoordinatesMode(mode: CoordinatesMode): void; + + /** + * Gets the currently selected entity in the inspector. + * Returns null if nothing is selected or if the inspector is not yet fully initialized. + */ + getSelectedEntity(): Nullable; + + /** + * Sets the selected entity in the inspector. + * @param entity The entity to select, or null to clear selection. + */ + setSelectedEntity(entity: Nullable): void; + + /** + * Sets up keyboard hotkeys for gizmo control. + * Hotkeys are only active when the target element has focus or is hovered. + * - W: Translate mode + * - E: Rotate mode + * - R: Scale mode + * - T: Bounding box mode + * - Q: Disable gizmo + * - X: Toggle local/world coordinates + * @param targetElement The element to monitor for focus/hover. Typically the canvas. + * @returns A dispose function to remove the hotkey listeners. + */ + setupGizmoHotkeys(targetElement: HTMLElement): IDisposable; +} + // TODO: The key should probably be the Canvas, because we only want to show one inspector instance per canvas. // If it is called for a different scene that is rendering to the same canvas, then we should probably // switch the inspector instance to that scene (once this is supported). -const InspectorTokens = new WeakMap(); +const InspectorTokens = new WeakMap(); // This async lock is used to sequentialize all calls to ShowInspector and dispose of existing inspectors. // This is needed because each time Inspector is shown or hidden, it is potentially mutating the same DOM element. const InspectorLock = new AsyncLock(); -export function ShowInspector(scene: Scene, options: Partial = {}): IDisposable { +export function ShowInspector(scene: Scene, options: Partial = {}): IInspectorHandle { // Dispose of any existing inspector for this scene. InspectorTokens.get(scene)?.dispose(); @@ -79,19 +136,89 @@ export function ShowInspector(scene: Scene, options: Partial = // to show the Inspector and there will be cleanup work to do. let disposeAsync = async () => await Promise.resolve(); - // Create an inspector dispose token. The dispose will use the same async lock to - // make sure async dispose (hide) does not actually start until async show is finished. - const inspectorToken = { + // Service references - populated when services are ready + const serviceRefs: { gizmoToolbar: IGizmoToolbarService | null; selection: ISelectionService | null } = { + gizmoToolbar: null, + selection: null, + }; + + // Create an inspector handle with dispose and control methods. + const inspectorHandle: IInspectorHandle = { dispose: () => { // eslint-disable-next-line @typescript-eslint/no-floating-promises InspectorLock.lockAsync(async () => { await disposeAsync(); }); }, - } as const; + getGizmoMode: () => serviceRefs.gizmoToolbar?.gizmoMode ?? null, + setGizmoMode: (mode: Nullable) => { + if (serviceRefs.gizmoToolbar) { + serviceRefs.gizmoToolbar.gizmoMode = mode; + } + }, + getCoordinatesMode: () => serviceRefs.gizmoToolbar?.coordinatesMode ?? "world", + setCoordinatesMode: (mode: CoordinatesMode) => { + if (serviceRefs.gizmoToolbar) { + serviceRefs.gizmoToolbar.coordinatesMode = mode; + } + }, + getSelectedEntity: () => serviceRefs.selection?.selectedEntity ?? null, + setSelectedEntity: (entity: Nullable) => { + if (serviceRefs.selection) { + serviceRefs.selection.selectedEntity = entity; + } + }, + setupGizmoHotkeys: (targetElement: HTMLElement) => { + const handler = (e: KeyboardEvent) => { + // Only handle hotkeys when target element has focus or is hovered + const activeElement = document.activeElement; + const isFocused = activeElement === targetElement || targetElement.contains(activeElement); + const isHovered = targetElement.matches(":hover"); + + if (!isFocused && !isHovered) { + return; + } - // Track the inspector token for the scene. - InspectorTokens.set(scene, inspectorToken); + // Ignore if modifier keys are pressed + if (e.ctrlKey || e.metaKey || e.altKey) { + return; + } + + switch (e.key.toLowerCase()) { + case "w": + inspectorHandle.setGizmoMode("translate"); + break; + case "e": + inspectorHandle.setGizmoMode("rotate"); + break; + case "r": + inspectorHandle.setGizmoMode("scale"); + break; + case "t": + inspectorHandle.setGizmoMode("boundingBox"); + break; + case "q": + inspectorHandle.setGizmoMode(null); + break; + case "x": + inspectorHandle.setCoordinatesMode(inspectorHandle.getCoordinatesMode() === "local" ? "world" : "local"); + break; + default: + return; + } + + e.preventDefault(); + }; + + window.addEventListener("keydown", handler); + return { + dispose: () => window.removeEventListener("keydown", handler), + }; + }, + }; + + // Track the inspector handle for the scene. + InspectorTokens.set(scene, inspectorHandle); // Set default options. options = { @@ -271,9 +398,19 @@ export function ShowInspector(scene: Scene, options: Partial = // Tracks entity selection state (e.g. which Mesh or Material or other entity is currently selected in scene explorer and bound to the properties pane, etc.). SelectionServiceDefinition, - // Gizmos for manipulating objects in the scene. + // Gizmo toolbar for manipulating objects in the scene. GizmoToolbarServiceDefinition, + // Captures service references for external control via IInspectorHandle. + { + friendlyName: "Inspector Handle Wiring", + consumes: [GizmoToolbarServiceIdentity, SelectionServiceIdentity], + factory: (gizmoToolbar: IGizmoToolbarService, selection: ISelectionService) => { + serviceRefs.gizmoToolbar = gizmoToolbar; + serviceRefs.selection = selection; + }, + } as ServiceDefinition<[], [IGizmoToolbarService, ISelectionService]>, + // Allows picking objects from the scene to select them. PickingServiceDefinition, @@ -299,7 +436,7 @@ export function ShowInspector(scene: Scene, options: Partial = disposeActions.push(() => modularTool.dispose()); const sceneDisposedObserver = scene.onDisposeObservable.addOnce(() => { - inspectorToken.dispose(); + inspectorHandle.dispose(); }); disposeActions.push(() => sceneDisposedObserver.remove()); @@ -309,5 +446,5 @@ export function ShowInspector(scene: Scene, options: Partial = }); }); - return inspectorToken; + return inspectorHandle; } diff --git a/packages/dev/inspector-v2/src/legacy/inspector.tsx b/packages/dev/inspector-v2/src/legacy/inspector.tsx index b4083f15f84..447837dfa89 100644 --- a/packages/dev/inspector-v2/src/legacy/inspector.tsx +++ b/packages/dev/inspector-v2/src/legacy/inspector.tsx @@ -1,5 +1,4 @@ import type { - IDisposable, IExplorerAdditionalChild, IInspectorContextMenuItem, IInspectorContextMenuType, @@ -9,7 +8,7 @@ import type { WritableObject, } from "core/index"; import type { EntityBase } from "../components/scene/sceneExplorer"; -import type { InspectorOptions as InspectorV2Options } from "../inspector"; +import type { IInspectorHandle, InspectorOptions as InspectorV2Options } from "../inspector"; import type { WeaklyTypedServiceDefinition } from "../modularity/serviceContainer"; import type { ServiceDefinition } from "../modularity/serviceDefinition"; import type { IGizmoService } from "../services/gizmoService"; @@ -288,7 +287,7 @@ export function ConvertOptions(v1Options: Partial): Partial< * @deprecated This class only exists for backward compatibility. Use the module-level ShowInspector function instead. */ export class Inspector { - private static _CurrentInstance: Nullable<{ scene: Scene; options: Partial; disposeToken: IDisposable }> = null; + private static _CurrentInstance: Nullable<{ scene: Scene; options: Partial; disposeToken: IInspectorHandle }> = null; private static _PopupToggler: Nullable<(side: "left" | "right") => void> = null; private static _SectionHighlighter: Nullable<(sectionIds: readonly string[]) => void> = null; private static _SidePaneOpenCounter: Nullable<() => number> = null; @@ -325,6 +324,14 @@ export class Inspector { return !!this._CurrentInstance; } + /** + * Gets the current inspector handle, or null if the inspector is not visible. + * This provides access to the IInspectorHandle API for external control (e.g., gizmo hotkeys). + */ + public static get CurrentHandle(): Nullable { + return this._CurrentInstance?.disposeToken ?? null; + } + public static Show(scene: Scene, userOptions: Partial) { this._Show(scene, userOptions); } diff --git a/packages/dev/inspector-v2/src/services/gizmoToolbarService.tsx b/packages/dev/inspector-v2/src/services/gizmoToolbarService.tsx index 7db110b37dd..dc903e3be83 100644 --- a/packages/dev/inspector-v2/src/services/gizmoToolbarService.tsx +++ b/packages/dev/inspector-v2/src/services/gizmoToolbarService.tsx @@ -1,9 +1,11 @@ -import type { ServiceDefinition } from "../modularity/serviceDefinition"; +import type { IDisposable, IReadonlyObservable, Nullable } from "core/index"; +import type { IService, ServiceDefinition } from "../modularity/serviceDefinition"; import type { ISceneContext } from "./sceneContext"; import type { ISelectionService } from "./selectionService"; import type { IShellService } from "./shellService"; import type { IGizmoService } from "./gizmoService"; +import { Observable } from "core/Misc/observable"; import { SceneContextIdentity } from "./sceneContext"; import { SelectionServiceIdentity } from "./selectionService"; import { ShellServiceIdentity } from "./shellService"; @@ -11,10 +13,67 @@ import { GizmoToolbar } from "../components/gizmoToolbar"; import { useObservableState } from "../hooks/observableHooks"; import { GizmoServiceIdentity } from "./gizmoService"; -export const GizmoToolbarServiceDefinition: ServiceDefinition<[], [ISceneContext, IShellService, ISelectionService, IGizmoService]> = { +/** + * The available gizmo modes. + */ +export type GizmoMode = "translate" | "rotate" | "scale" | "boundingBox"; + +/** + * The available coordinates modes for gizmos. + */ +export type CoordinatesMode = "local" | "world"; + +export const GizmoToolbarServiceIdentity = Symbol("GizmoToolbarService"); + +/** + * Internal service that manages gizmo toolbar state. + * External control is exposed via IInspectorHandle from ShowInspector(). + */ +export interface IGizmoToolbarService extends IService { + gizmoMode: Nullable; + coordinatesMode: CoordinatesMode; + readonly onGizmoModeChanged: IReadonlyObservable>; + readonly onCoordinatesModeChanged: IReadonlyObservable; +} + +export const GizmoToolbarServiceDefinition: ServiceDefinition<[IGizmoToolbarService], [ISceneContext, IShellService, ISelectionService, IGizmoService]> = { friendlyName: "Gizmo Toolbar", + produces: [GizmoToolbarServiceIdentity], consumes: [SceneContextIdentity, ShellServiceIdentity, SelectionServiceIdentity, GizmoServiceIdentity], factory: (sceneContext, shellService, selectionService, gizmoService) => { + let gizmoModeState: Nullable = null; + let coordinatesModeState: CoordinatesMode = "world"; + + const gizmoModeObservable = new Observable>(); + const coordinatesModeObservable = new Observable(); + + const gizmoToolbarService: IGizmoToolbarService & IDisposable = { + get gizmoMode() { + return gizmoModeState; + }, + set gizmoMode(mode: Nullable) { + if (mode !== gizmoModeState) { + gizmoModeState = mode; + gizmoModeObservable.notifyObservers(mode); + } + }, + get coordinatesMode() { + return coordinatesModeState; + }, + set coordinatesMode(mode: CoordinatesMode) { + if (mode !== coordinatesModeState) { + coordinatesModeState = mode; + coordinatesModeObservable.notifyObservers(mode); + } + }, + onGizmoModeChanged: gizmoModeObservable, + onCoordinatesModeChanged: coordinatesModeObservable, + dispose: () => { + gizmoModeObservable.clear(); + coordinatesModeObservable.clear(); + }, + }; + shellService.addToolbarItem({ key: "Gizmo Toolbar", verticalLocation: "top", @@ -23,8 +82,10 @@ export const GizmoToolbarServiceDefinition: ServiceDefinition<[], [ISceneContext component: () => { const scene = useObservableState(() => sceneContext.currentScene, sceneContext.currentSceneObservable); const selectedEntity = useObservableState(() => selectionService.selectedEntity, selectionService.onSelectedEntityChanged); - return scene ? : null; + return scene ? : null; }, }); + + return gizmoToolbarService; }, }; diff --git a/packages/tools/playground/src/components/rendererComponent.tsx b/packages/tools/playground/src/components/rendererComponent.tsx index 506d87a2108..e8e95d8d96c 100644 --- a/packages/tools/playground/src/components/rendererComponent.tsx +++ b/packages/tools/playground/src/components/rendererComponent.tsx @@ -9,7 +9,9 @@ import { DownloadManager } from "../tools/downloadManager"; import { AddFileRevision } from "../tools/localSession"; import { Engine, EngineStore, WebGPUEngine, LastCreatedAudioEngine, Logger } from "@dev/core"; -import type { IDisposable, Nullable, Scene, ThinEngine } from "@dev/core"; +import type { Nullable, Scene, ThinEngine } from "@dev/core"; + +import type { IInspectorHandle } from "inspector-v2/index"; import "../scss/rendering.scss"; @@ -35,7 +37,8 @@ export class RenderingComponent extends React.Component; - private _inspectorV2Token: Nullable = null; + private _inspectorV2Token: Nullable = null; + private _hotkeyDisposable: Nullable<{ dispose(): void }> = null; /** * Create the rendering component. @@ -136,6 +139,7 @@ export class RenderingComponent extends React.Component { // no-op placeholder retained for backward compatibility }; diff --git a/packages/tools/sandbox/src/components/renderingZone.tsx b/packages/tools/sandbox/src/components/renderingZone.tsx index c5ac2790eb1..d1c8ae876e6 100644 --- a/packages/tools/sandbox/src/components/renderingZone.tsx +++ b/packages/tools/sandbox/src/components/renderingZone.tsx @@ -178,15 +178,52 @@ export class RenderingZone extends React.Component { this.props.globalState.filesInput = filesInput; - window.addEventListener("keydown", (event) => { + window.addEventListener("keydown", async (event) => { + // Ignore if in an input field or if modifier keys are pressed + if ((event.target as HTMLElement).nodeName === "INPUT" || event.ctrlKey || event.metaKey || event.altKey) { + return; + } + // Press R to reload - if (event.keyCode === 82 && event.target && (event.target as HTMLElement).nodeName !== "INPUT" && this._scene) { + if (event.keyCode === 82 && this._scene) { if (this.props.globalState.assetUrl) { this.loadAssetFromUrl(this.props.globalState.assetUrl); } else { filesInput.reload(); } + return; + } + + // Gizmo hotkeys for inspector v2 (number keys to avoid camera control conflicts) + const handle = await this.props.globalState.getInspectorV2HandleAsync(); + if (!handle) { + return; } + + switch (event.key) { + case "1": + handle.setGizmoMode("translate"); + break; + case "2": + handle.setGizmoMode("rotate"); + break; + case "3": + handle.setGizmoMode("scale"); + break; + case "4": + handle.setGizmoMode("boundingBox"); + break; + case "0": + handle.setGizmoMode(null); + break; + case "`": + handle.setCoordinatesMode(handle.getCoordinatesMode() === "local" ? "world" : "local"); + break; + default: + return; + } + + event.preventDefault(); }); } diff --git a/packages/tools/sandbox/src/globalState.ts b/packages/tools/sandbox/src/globalState.ts index 758f8770525..fc2d6ab455c 100644 --- a/packages/tools/sandbox/src/globalState.ts +++ b/packages/tools/sandbox/src/globalState.ts @@ -57,6 +57,13 @@ export class GlobalState { this.onSceneLoaded.addOnce(async () => await this.refreshDebugLayerAsync()); } + public async getInspectorV2HandleAsync() { + if (!this.isDebugLayerEnabled || !this._isInspectorV2ModeEnabled) { + return null; + } + return (await this._inspectorV2ModulePromise)?.Inspector.CurrentHandle ?? null; + } + private async _showInspectorV1Async() { const inspectorV2Module = await this._inspectorV2ModulePromise; inspectorV2Module?.DetachInspectorGlobals();