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
+
+
+
+
+
+
+
+
+
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)
}
}