diff --git a/packages/@dcl/inspector/src/components/Toolbar/Gizmos/Gizmos.tsx b/packages/@dcl/inspector/src/components/Toolbar/Gizmos/Gizmos.tsx index e4e3c1adb..141d64049 100644 --- a/packages/@dcl/inspector/src/components/Toolbar/Gizmos/Gizmos.tsx +++ b/packages/@dcl/inspector/src/components/Toolbar/Gizmos/Gizmos.tsx @@ -27,9 +27,18 @@ export const Gizmos = withSdk(({ sdk }) => { const [selection, setSelection] = useComponentValue(entity || ROOT, sdk.components.Selection) - const handlePositionGizmo = useCallback(() => setSelection({ gizmo: GizmoType.POSITION }), [setSelection]) - const handleRotationGizmo = useCallback(() => setSelection({ gizmo: GizmoType.ROTATION }), [setSelection]) - const handleScaleGizmo = useCallback(() => setSelection({ gizmo: GizmoType.SCALE }), [setSelection]) + const handlePositionGizmo = useCallback( + () => setSelection({ gizmo: selection.gizmo !== GizmoType.POSITION ? GizmoType.POSITION : GizmoType.FREE }), + [selection, setSelection] + ) + const handleRotationGizmo = useCallback( + () => setSelection({ gizmo: selection.gizmo !== GizmoType.ROTATION ? GizmoType.ROTATION : GizmoType.FREE }), + [selection, setSelection] + ) + const handleScaleGizmo = useCallback( + () => setSelection({ gizmo: selection.gizmo !== GizmoType.SCALE ? GizmoType.SCALE : GizmoType.FREE }), + [selection, setSelection] + ) const { isPositionGizmoWorldAligned, diff --git a/packages/@dcl/inspector/src/hooks/editor/useSnap.ts b/packages/@dcl/inspector/src/hooks/editor/useSnap.ts index 15533194c..2c38c9841 100644 --- a/packages/@dcl/inspector/src/hooks/editor/useSnap.ts +++ b/packages/@dcl/inspector/src/hooks/editor/useSnap.ts @@ -6,6 +6,7 @@ import { GizmoType } from '../../lib/utils/gizmo' function getSnapValue(gizmo: GizmoType) { switch (gizmo) { case GizmoType.POSITION: + case GizmoType.FREE: return snapManager.getPositionSnap() case GizmoType.ROTATION: return Math.round(snapManager.getRotationSnap() * (180 / Math.PI)) diff --git a/packages/@dcl/inspector/src/lib/babylon/decentraland/gizmo-manager.spec.ts b/packages/@dcl/inspector/src/lib/babylon/decentraland/gizmo-manager.spec.ts index c80716a1e..85b5628ac 100644 --- a/packages/@dcl/inspector/src/lib/babylon/decentraland/gizmo-manager.spec.ts +++ b/packages/@dcl/inspector/src/lib/babylon/decentraland/gizmo-manager.spec.ts @@ -159,7 +159,12 @@ describe('GizmoManager', () => { }) describe('When getting the gizmo types', () => { it('should return the gizmo types', () => { - expect(gizmos.getGizmoTypes()).toEqual([GizmoType.POSITION, GizmoType.ROTATION, GizmoType.SCALE]) + expect(gizmos.getGizmoTypes()).toEqual([ + GizmoType.POSITION, + GizmoType.ROTATION, + GizmoType.SCALE, + GizmoType.FREE + ]) }) }) describe('When setting the gizmo type', () => { diff --git a/packages/@dcl/inspector/src/lib/babylon/decentraland/gizmo-manager.ts b/packages/@dcl/inspector/src/lib/babylon/decentraland/gizmo-manager.ts index b4b891b24..4b0f87057 100644 --- a/packages/@dcl/inspector/src/lib/babylon/decentraland/gizmo-manager.ts +++ b/packages/@dcl/inspector/src/lib/babylon/decentraland/gizmo-manager.ts @@ -1,5 +1,13 @@ import mitt from 'mitt' -import { IAxisDragGizmo, PickingInfo, Quaternion, Node } from '@babylonjs/core' +import { + IAxisDragGizmo, + PickingInfo, + Quaternion, + Node, + Vector3, + PointerDragBehavior, + AbstractMesh +} from '@babylonjs/core' import { Entity, TransformType } from '@dcl/ecs' import { Vector3 as DclVector3, Quaternion as DclQuaternion } from '@dcl/ecs-math' import { GizmoType } from '../../utils/gizmo' @@ -54,8 +62,12 @@ export function createGizmoManager(context: SceneContext) { let isEnabled = true const parentMapper: Map = new Map() + function getSelectedEntities() { + return context.operations.getSelectedEntities() + } + function areMultipleEntitiesSelected() { - return context.operations.getSelectedEntities().length > 1 + return getSelectedEntities().length > 1 } function fixRotationGizmoAlignment(value: TransformType) { @@ -142,7 +154,7 @@ export function createGizmoManager(context: SceneContext) { // Update entity transform for all the selected entities if (areMultipleEntitiesSelected()) { - for (const entityId of context.operations.getSelectedEntities()) { + for (const entityId of getSelectedEntities()) { if (entityId === lastEntity.entityId) continue const entity = context.getEntityOrNull(entityId)! const transform = getTransform(entity) @@ -154,7 +166,7 @@ export function createGizmoManager(context: SceneContext) { function initTransform() { if (lastEntity === null) return if (areMultipleEntitiesSelected()) { - for (const entityId of context.operations.getSelectedEntities()) { + for (const entityId of getSelectedEntities()) { if (entityId === lastEntity.entityId) continue const entity = context.getEntityOrNull(entityId)! parentMapper.set(entityId, entity.parent!) @@ -250,6 +262,25 @@ export function createGizmoManager(context: SceneContext) { } }) + const meshPointerDragBehavior = new PointerDragBehavior({ + dragPlaneNormal: new Vector3(0, 1, 0) + }) + + context.scene.onPointerDown = function (_e, pickResult) { + if (lastEntity === null || pickResult.pickedMesh === null || !gizmoManager.freeGizmoEnabled) return + const selectedEntities = getSelectedEntities().map((entityId) => context.getEntityOrNull(entityId)!) + if (selectedEntities.some((entity) => pickResult.pickedMesh!.isDescendantOf(entity))) { + initTransform() + meshPointerDragBehavior.attach(lastEntity as unknown as AbstractMesh) + } + } + + context.scene.onPointerUp = function () { + if (lastEntity === null || !gizmoManager.freeGizmoEnabled) return + updateTransform() + meshPointerDragBehavior.detach() + } + if (canvas) { canvas.addEventListener('pointerenter', () => { if (movingNode) { @@ -291,12 +322,13 @@ export function createGizmoManager(context: SceneContext) { unsetEntity() }, getGizmoTypes() { - return [GizmoType.POSITION, GizmoType.ROTATION, GizmoType.SCALE] as const + return [GizmoType.POSITION, GizmoType.ROTATION, GizmoType.SCALE, GizmoType.FREE] as const }, setGizmoType(type: GizmoType) { gizmoManager.positionGizmoEnabled = type === GizmoType.POSITION gizmoManager.rotationGizmoEnabled = type === GizmoType.ROTATION gizmoManager.scaleGizmoEnabled = type === GizmoType.SCALE + gizmoManager.freeGizmoEnabled = type === GizmoType.FREE events.emit('change') }, isPositionGizmoWorldAligned, diff --git a/packages/@dcl/inspector/src/lib/babylon/decentraland/gizmo-patch.ts b/packages/@dcl/inspector/src/lib/babylon/decentraland/gizmo-patch.ts index db4352a1f..495102511 100644 --- a/packages/@dcl/inspector/src/lib/babylon/decentraland/gizmo-patch.ts +++ b/packages/@dcl/inspector/src/lib/babylon/decentraland/gizmo-patch.ts @@ -362,6 +362,20 @@ export class PatchedRotationGizmo extends RotationGizmo { } export class PatchedGizmoManager extends GizmoManager { + protected _gizmosEnabled: { + positionGizmo: boolean + rotationGizmo: boolean + scaleGizmo: boolean + boundingBoxGizmo: boolean + freeGizmo: boolean + } = { + positionGizmo: false, + rotationGizmo: false, + scaleGizmo: false, + boundingBoxGizmo: false, + freeGizmo: false + } + set rotationGizmoEnabled(value: boolean) { if (value) { if (!this.gizmos.rotationGizmo) { @@ -388,4 +402,12 @@ export class PatchedGizmoManager extends GizmoManager { get rotationGizmoEnabled() { return this._gizmosEnabled.rotationGizmo } + + set freeGizmoEnabled(value: boolean) { + this._gizmosEnabled.freeGizmo = value + } + + get freeGizmoEnabled(): boolean { + return this._gizmosEnabled.freeGizmo + } } diff --git a/packages/@dcl/inspector/src/lib/babylon/setup/input.ts b/packages/@dcl/inspector/src/lib/babylon/setup/input.ts index d1d6d2567..1524043f1 100644 --- a/packages/@dcl/inspector/src/lib/babylon/setup/input.ts +++ b/packages/@dcl/inspector/src/lib/babylon/setup/input.ts @@ -6,6 +6,8 @@ import { keyState, Keys } from '../decentraland/keys' import { getAncestors, isAncestor, mapNodes } from '../../sdk/nodes' let isSnapEnabled = snapManager.isEnabled() +let clickStartTimer: ReturnType +let isDragging = false export function initKeyboard(canvas: HTMLCanvasElement, scene: BABYLON.Scene) { canvas.addEventListener('keydown', (e) => { @@ -25,6 +27,17 @@ export function initKeyboard(canvas: HTMLCanvasElement, scene: BABYLON.Scene) { keyState[e.keyCode] = false }) + // When the canvas lost the focus, clear the special keys state + canvas.addEventListener('blur', () => { + keyState[Keys.KEY_SHIFT] = false + keyState[Keys.KEY_CTRL] = false + }) + + // Event to store the ctrlKey when the canvas has lost the focus + canvas.addEventListener('contextmenu', (e) => { + keyState[Keys.KEY_CTRL] = e.ctrlKey + }) + scene.onPointerObservable.add((e) => { if (e.type === BABYLON.PointerEventTypes.POINTERDOWN) { const evt = e.event as PointerEvent @@ -69,6 +82,10 @@ export function interactWithScene( const entity = mesh && findParentEntity(mesh) if (entity && pointerEvent === 'pointerDown') { + clickStartTimer = setTimeout(() => { + isDragging = true + }, 150) // 150ms to detect if the user is dragging + } else if (entity && pointerEvent === 'pointerUp' && !isDragging) { const context = entity.context.deref()! const { operations, engine, editorComponents } = context const ancestors = getAncestors(engine, entity.entityId) @@ -77,4 +94,10 @@ export function interactWithScene( operations.updateSelectedEntity(entity.entityId, !!keyState[Keys.KEY_CTRL]) void operations.dispatch() } + + // Clear isDragging flag each pointerUp + if (pointerEvent === 'pointerUp') { + clearTimeout(clickStartTimer) + isDragging = false + } } diff --git a/packages/@dcl/inspector/src/lib/utils/gizmo.ts b/packages/@dcl/inspector/src/lib/utils/gizmo.ts index 730aeb6ff..afd076d0e 100644 --- a/packages/@dcl/inspector/src/lib/utils/gizmo.ts +++ b/packages/@dcl/inspector/src/lib/utils/gizmo.ts @@ -1,5 +1,6 @@ export enum GizmoType { POSITION = 0, ROTATION = 1, - SCALE = 2 + SCALE = 2, + FREE = 3 }