diff --git a/packages/@dcl/inspector/src/components/Renderer/Renderer.tsx b/packages/@dcl/inspector/src/components/Renderer/Renderer.tsx index 6c4697551..eb460fda2 100644 --- a/packages/@dcl/inspector/src/components/Renderer/Renderer.tsx +++ b/packages/@dcl/inspector/src/components/Renderer/Renderer.tsx @@ -34,11 +34,14 @@ import { ZOOM_IN_ALT, ZOOM_OUT_ALT, ZOOM_OUT, - RESET_CAMERA + RESET_CAMERA, + DUPLICATE, + DUPLICATE_ALT } from '../../hooks/useHotkey' import { analytics, Event } from '../../lib/logic/analytics' import { Warnings } from '../Warnings' import { CameraSpeed } from './CameraSpeed' +import { Shortcuts } from './Shortcuts' import './Renderer.css' @@ -86,6 +89,18 @@ const Renderer: React.FC = () => { void sdk.sceneContext.operations.dispatch() }, [sdk]) + const duplicateSelectedEntities = useCallback(() => { + if (!sdk) return + const camera = sdk.scene.activeCamera! + camera.detachControl() + const selectedEntitites = sdk.sceneContext.operations.getSelectedEntities() + selectedEntitites.forEach((entity) => sdk.sceneContext.operations.duplicateEntity(entity)) + void sdk.sceneContext.operations.dispatch() + setTimeout(() => { + camera.attachControl(canvasRef.current, true) + }, 100) + }, [sdk]) + const copySelectedEntities = useCallback(() => { if (!sdk) return const selectedEntitites = sdk.sceneContext.operations.getSelectedEntities() @@ -123,6 +138,7 @@ const Renderer: React.FC = () => { useHotkey([ZOOM_IN, ZOOM_IN_ALT], zoomIn, canvasRef.current) useHotkey([ZOOM_OUT, ZOOM_OUT_ALT], zoomOut, canvasRef.current) useHotkey([RESET_CAMERA], resetCamera, canvasRef.current) + useHotkey([DUPLICATE, DUPLICATE_ALT], duplicateSelectedEntities, canvasRef.current) const getDropPosition = async () => { const pointerCoords = await getPointerCoords(sdk!.scene) @@ -239,6 +255,7 @@ const Renderer: React.FC = () => { {isLoading && } + ) diff --git a/packages/@dcl/inspector/src/components/Renderer/Shortcuts/Shortcuts.css b/packages/@dcl/inspector/src/components/Renderer/Shortcuts/Shortcuts.css new file mode 100644 index 000000000..a0e34e682 --- /dev/null +++ b/packages/@dcl/inspector/src/components/Renderer/Shortcuts/Shortcuts.css @@ -0,0 +1,138 @@ +.Shortcuts { + --shortcuts-bottom: 8px; + --shortcuts-right: 8px; + --shortcuts-button-height: 30px; + --shortcuts-button-width: 30px; + + position: absolute; + bottom: 8px; + right: 8px; + z-index: 1; +} + +.Shortcuts .Buttons { + display: flex; + flex-direction: row; + gap: 8px; +} + +.Shortcuts .Buttons .Button { + display: flex; + align-items: center; + justify-content: center; + height: 30px; + width: 30px; + padding: 5px; + border-radius: 4px; + background-color: var(--base-16); +} + +.Shortcuts .Buttons .Button:hover { + background-color: var(--base-17); +} + +.Shortcuts .Buttons .Button.Active { + background-color: var(--base-02); +} + +.Shortcuts .Buttons .Button.Active svg { + color: var(--base-21); +} + +.Shortcuts .Buttons .Button svg { + color: var(--base-02); +} + +.Shortcuts .Buttons .ZoomButtons { + display: flex; +} + +.Shortcuts .Buttons .ZoomButtons .Button:first-of-type { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-right: 1.5px solid var(--base-19); +} + +.Shortcuts .Buttons .ZoomButtons .Button:last-of-type { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + border-left: 1.5px solid var(--base-19); +} + +.Shortcuts > .Overlay { + display: flex; + flex-direction: column; + position: absolute; + width: 298px; + overflow-y: auto; + right: 0; + bottom: calc(var(--shortcuts-bottom) + var(--shortcuts-button-height) + 8px); + background-color: var(--base-19); + padding: 13px 12px; + border-radius: 4px; + gap: 16px; +} + +.Shortcuts > .Overlay h2.Header { + font-size: 14px; + font-weight: 500; + line-height: 17px; + color: var(--base-01); + margin-bottom: 0; +} + +.Shortcuts > .Overlay h5.SubHeader, +.Shortcuts > .Overlay .Item .Title, +.Shortcuts > .Overlay .Item .Description, +.Shortcuts > .Overlay .Item .Description .Key { + font-size: 11px; + font-weight: 500; + color: var(--base-01); + margin-bottom: 0; +} + +.Shortcuts > .Overlay h5.SubHeader { + color: var(--base-09); + text-transform: uppercase; +} + +.Shortcuts > .Overlay .Items { + display: flex; + flex-direction: column; + gap: 8px; +} + +.Shortcuts > .Overlay .Items .Item { + display: flex; + gap: 4px; + padding: 8px 0; + border-bottom: 1px solid var(--base-18); +} + +.Shortcuts > .Overlay .Items .Item:last-of-type { + border-bottom: none; +} + +.Shortcuts > .Overlay .Item .Title { + display: flex; + flex: 1; + align-items: center; +} + +.Shortcuts > .Overlay .Item .Description { + display: flex; + flex: 1; + align-items: center; + gap: 4px; + line-height: 14px; +} + +.Shortcuts > .Overlay .Item .Description .Key { + display: flex; + align-items: center; + justify-content: center; + padding: 0 4px; + height: 18px; + border-radius: 4px; + background-color: var(--base-13); +} diff --git a/packages/@dcl/inspector/src/components/Renderer/Shortcuts/Shortcuts.tsx b/packages/@dcl/inspector/src/components/Renderer/Shortcuts/Shortcuts.tsx new file mode 100644 index 000000000..04aea09dc --- /dev/null +++ b/packages/@dcl/inspector/src/components/Renderer/Shortcuts/Shortcuts.tsx @@ -0,0 +1,167 @@ +import React, { useCallback, useMemo } from 'react' +import cx from 'classnames' +import { + MdOutlineZoomIn as ZoomInIcon, + MdOutlineZoomOut as ZoomOutIcon, + MdKeyboard as KeyboardIcon +} from 'react-icons/md' +import { HiOutlineViewfinderCircle as ResetCameraIcon } from 'react-icons/hi2' + +import { useContainerSize } from '../../../hooks/useContainerSize' +import { useOutsideClick } from '../../../hooks/useOutsideClick' +import { Button } from '../../Button' +import { InfoTooltip } from '../../ui' +import { Props } from './types' + +import './Shortcuts.css' + +const ICON_SIZE = 18 + +const Shortcuts: React.FC = ({ canvas, onResetCamera, onZoomIn, onZoomOut }) => { + const [showShortcuts, setShowShortcuts] = React.useState(false) + const { height } = useContainerSize(canvas) + + const maxOverlayHeight = useMemo(() => { + return (height ?? 600) - 60 + }, [height]) + + const handleToggleShortcutsOverlay = useCallback( + (e: React.MouseEvent | MouseEvent) => { + e.preventDefault() + e.stopPropagation() + setShowShortcuts((value) => !value) + }, + [showShortcuts, setShowShortcuts] + ) + + const overlayRef = useOutsideClick(handleToggleShortcutsOverlay) + + return ( +
+
+ +
+ + +
+ + + + } + openOnTriggerMouseEnter={!showShortcuts} + closeOnTriggerClick={true} + position="top center" + /> +
+ {showShortcuts && ( +
+

Shortcuts

+
+
General
+
+
Pan Camera
+
+ W + A + S + D +
+
+
+
Select Multiple Items
+
+ Holdctrland click +
+
+
+
Save
+
+ ctrl+S +
+
+
+
Undo
+
+ ctrl+Z +
+
+
+
Redo
+
+ ctrl+Y +
+
+
+
Copy
+
+ ctrl+C +
+
+
+
Paste
+
+ ctrl+V +
+
+
+
Reset Camera
+
+ space +
+
+
+
+
Item Selected
+
+
Snap to Grid
+
+ Holdshift +
+
+
+
Toggle Positioning
+
+ M +
+
+
+
Toggle Rotating
+
+ R +
+
+
+
Toggle Scaling
+
+ X +
+
+
+
Duplicate
+
+ ctrl+D +
+
+
+
Delete
+
+ delorbackspace +
+
+
+
+ )} +
+ ) +} + +export default React.memo(Shortcuts) diff --git a/packages/@dcl/inspector/src/components/Renderer/Shortcuts/index.ts b/packages/@dcl/inspector/src/components/Renderer/Shortcuts/index.ts new file mode 100644 index 000000000..37e497108 --- /dev/null +++ b/packages/@dcl/inspector/src/components/Renderer/Shortcuts/index.ts @@ -0,0 +1,2 @@ +import Shortcuts from './Shortcuts' +export { Shortcuts } diff --git a/packages/@dcl/inspector/src/components/Renderer/Shortcuts/types.ts b/packages/@dcl/inspector/src/components/Renderer/Shortcuts/types.ts new file mode 100644 index 000000000..4f01855af --- /dev/null +++ b/packages/@dcl/inspector/src/components/Renderer/Shortcuts/types.ts @@ -0,0 +1,6 @@ +export interface Props { + canvas: React.RefObject + onResetCamera: () => void + onZoomIn: () => void + onZoomOut: () => void +} diff --git a/packages/@dcl/inspector/src/components/Toolbar/Gizmos/Gizmos.tsx b/packages/@dcl/inspector/src/components/Toolbar/Gizmos/Gizmos.tsx index 141d64049..3e411cfd0 100644 --- a/packages/@dcl/inspector/src/components/Toolbar/Gizmos/Gizmos.tsx +++ b/packages/@dcl/inspector/src/components/Toolbar/Gizmos/Gizmos.tsx @@ -7,14 +7,15 @@ import { withSdk } from '../../../hoc/withSdk' import { useComponentValue } from '../../../hooks/sdk/useComponentValue' import { useSelectedEntity } from '../../../hooks/sdk/useSelectedEntity' import { useOutsideClick } from '../../../hooks/useOutsideClick' +import { useHotkey } from '../../../hooks/useHotkey' import { useSnapToggle } from '../../../hooks/editor/useSnap' +import { useGizmoAlignment } from '../../../hooks/editor/useGizmoAlignment' import { ROOT } from '../../../lib/sdk/tree' import { GizmoType } from '../../../lib/utils/gizmo' import { ToolbarButton } from '../ToolbarButton' import { Snap } from './Snap' import './Gizmos.css' -import { useGizmoAlignment } from '../../../hooks/editor/useGizmoAlignment' export const Gizmos = withSdk(({ sdk }) => { const [showPanel, setShowPanel] = useState(false) @@ -40,6 +41,10 @@ export const Gizmos = withSdk(({ sdk }) => { [selection, setSelection] ) + useHotkey(['M'], handlePositionGizmo) + useHotkey(['R'], handleRotationGizmo) + useHotkey(['X'], handleScaleGizmo) + const { isPositionGizmoWorldAligned, isRotationGizmoWorldAligned, diff --git a/packages/@dcl/inspector/src/hooks/useHotkey.ts b/packages/@dcl/inspector/src/hooks/useHotkey.ts index 412bf2443..664e90ea5 100644 --- a/packages/@dcl/inspector/src/hooks/useHotkey.ts +++ b/packages/@dcl/inspector/src/hooks/useHotkey.ts @@ -24,6 +24,8 @@ export const ZOOM_OUT_ALT = `_` export const RESET_CAMERA = 'space' export const SAVE = `${CTRL}+s` export const SAVE_ALT = `${COMMAND}+s` +export const DUPLICATE = `${CTRL}+d` +export const DUPLICATE_ALT = `${COMMAND}+d` /** * Hook that listens for key presses and triggers a callback function when the specified keys are pressed. diff --git a/packages/@dcl/inspector/src/hooks/useOutsideClick.ts b/packages/@dcl/inspector/src/hooks/useOutsideClick.ts index 9242ffa47..5935f180b 100644 --- a/packages/@dcl/inspector/src/hooks/useOutsideClick.ts +++ b/packages/@dcl/inspector/src/hooks/useOutsideClick.ts @@ -1,12 +1,12 @@ import { useEffect, useRef } from 'react' -export const useOutsideClick = (callback: () => void) => { +export const useOutsideClick = (callback: (event: MouseEvent) => void) => { const ref = useRef(null) useEffect(() => { const handleClick = (event: MouseEvent) => { if (ref.current && !ref.current.contains(event.target as Node)) { - callback() + callback(event) } }