From 499b43b62a9439b544aba4ca0917acb8863b899d Mon Sep 17 00:00:00 2001 From: Nicolas Echezarreta Date: Wed, 13 Nov 2024 04:49:27 -0800 Subject: [PATCH] Add gizmo centroid (#1029) * center gizmo when multiple entities are selected * some refactor for Selection component --- .../inspector/src/components/Tree/Tree.tsx | 2 +- .../babylon/decentraland/EcsEntity.spec.ts | 5 +- .../src/lib/babylon/decentraland/EcsEntity.ts | 8 +- .../lib/babylon/decentraland/SceneContext.ts | 17 +++- .../decentraland/editorComponents/hide.ts | 6 +- .../decentraland/editorComponents/lock.ts | 8 +- .../editorComponents/selection.ts | 53 +++++++---- .../lib/babylon/decentraland/gizmo-manager.ts | 89 +++++++++++++++++-- .../inspector/src/lib/babylon/setup/input.ts | 2 +- .../sdk/operations/update-selected-entity.ts | 7 +- 10 files changed, 155 insertions(+), 42 deletions(-) diff --git a/packages/@dcl/inspector/src/components/Tree/Tree.tsx b/packages/@dcl/inspector/src/components/Tree/Tree.tsx index 31125d285..f9123985d 100644 --- a/packages/@dcl/inspector/src/components/Tree/Tree.tsx +++ b/packages/@dcl/inspector/src/components/Tree/Tree.tsx @@ -155,7 +155,7 @@ export function Tree() { if (event.type === ClickType.CONTEXT_MENU && event.ctrlKey) { onSelect(value, true) } else if (event.type === ClickType.CLICK) { - onSelect(value) + onSelect(value, event.shiftKey) if (event.detail > 1 && onDoubleSelect) onDoubleSelect(value) } } diff --git a/packages/@dcl/inspector/src/lib/babylon/decentraland/EcsEntity.spec.ts b/packages/@dcl/inspector/src/lib/babylon/decentraland/EcsEntity.spec.ts index 256094d8c..75a77c6cc 100644 --- a/packages/@dcl/inspector/src/lib/babylon/decentraland/EcsEntity.spec.ts +++ b/packages/@dcl/inspector/src/lib/babylon/decentraland/EcsEntity.spec.ts @@ -9,6 +9,9 @@ const getContext = () => ({ componentPutOperations: { [Transform.componentId]: jest.fn() }, + componentDeleteOperations: { + [Transform.componentId]: jest.fn() + }, engine: { RootEntity: 0 as Entity } @@ -71,7 +74,7 @@ describe('EcsEntity', () => { const Transform = components.Transform(engine) entity.deleteComponent(Transform) expect(entity.usedComponents.size).toBe(0) - expect(mockedContext.componentPutOperations[Transform.componentId]).toBeCalledWith(entity, Transform) + expect(mockedContext.componentDeleteOperations[Transform.componentId]).toBeCalledWith(entity, Transform) }) it('components and native engine', () => { diff --git a/packages/@dcl/inspector/src/lib/babylon/decentraland/EcsEntity.ts b/packages/@dcl/inspector/src/lib/babylon/decentraland/EcsEntity.ts index 779235200..344020f8f 100644 --- a/packages/@dcl/inspector/src/lib/babylon/decentraland/EcsEntity.ts +++ b/packages/@dcl/inspector/src/lib/babylon/decentraland/EcsEntity.ts @@ -46,13 +46,17 @@ export class EcsEntity extends BABYLON.TransformNode { } putComponent(component: ComponentDefinition) { + const ctx = this.context.deref()! + const operation = ctx.componentPutOperations[component.componentId] this.usedComponents.set(component.componentId, component) - this.context.deref()!.componentPutOperations[component.componentId]?.call(null, this, component) + operation?.call(null, this, component) } deleteComponent(component: ComponentDefinition) { + const ctx = this.context.deref()! + const operation = ctx.componentDeleteOperations[component.componentId] this.usedComponents.delete(component.componentId) - this.context.deref()!.componentPutOperations[component.componentId]?.call(null, this, component) + operation?.call(null, this, component) } /** diff --git a/packages/@dcl/inspector/src/lib/babylon/decentraland/SceneContext.ts b/packages/@dcl/inspector/src/lib/babylon/decentraland/SceneContext.ts index 23fe803e2..41efa8b42 100644 --- a/packages/@dcl/inspector/src/lib/babylon/decentraland/SceneContext.ts +++ b/packages/@dcl/inspector/src/lib/babylon/decentraland/SceneContext.ts @@ -7,7 +7,7 @@ import future from 'fp-future' import { createEditorComponents } from '../../sdk/components' import { ComponentOperation } from './component-operations' import { EcsEntity } from './EcsEntity' -import { putEntitySelectedComponent } from './editorComponents/selection' +import { deleteEntitySelectedComponent, putEntitySelectedComponent } from './editorComponents/selection' import { putBillboardComponent } from './sdkComponents/billboard' import { putGltfContainerComponent } from './sdkComponents/gltf-container' import { putMeshRendererComponent } from './sdkComponents/mesh-renderer' @@ -77,6 +77,21 @@ export class SceneContext { [this.editorComponents.Lock.componentId]: putLockComponent } + readonly componentDeleteOperations: Record = { + [this.Transform.componentId]: putTransformComponent, + [this.MeshRenderer.componentId]: putMeshRendererComponent, + [this.Material.componentId]: putMaterialComponent, + [this.Billboard.componentId]: putBillboardComponent, + [this.GltfContainer.componentId]: putGltfContainerComponent, + [this.TextShape.componentId]: putTextShapeComponent, + [this.NftShape.componentId]: putNftShapeComponent, + [this.VideoPlayer.componentId]: putVideoPlayerComponent, + [this.editorComponents.Selection.componentId]: deleteEntitySelectedComponent, + [this.editorComponents.Scene.componentId]: putSceneComponent, + [this.editorComponents.Hide.componentId]: putHideComponent, + [this.editorComponents.Lock.componentId]: putLockComponent + } + // this future is resolved when the scene is disposed readonly stopped = future() diff --git a/packages/@dcl/inspector/src/lib/babylon/decentraland/editorComponents/hide.ts b/packages/@dcl/inspector/src/lib/babylon/decentraland/editorComponents/hide.ts index be699b353..4c63eb300 100644 --- a/packages/@dcl/inspector/src/lib/babylon/decentraland/editorComponents/hide.ts +++ b/packages/@dcl/inspector/src/lib/babylon/decentraland/editorComponents/hide.ts @@ -1,6 +1,6 @@ import { ComponentType } from '@dcl/ecs' import type { ComponentOperation } from '../component-operations' -import { updateGizmoManager } from './selection' +import { setGizmoManager, unsetGizmoManager } from './selection' export const putHideComponent: ComponentOperation = (entity, component) => { const container = entity.gltfContainer ?? entity.meshRenderer @@ -11,10 +11,10 @@ export const putHideComponent: ComponentOperation = (entity, component) => { const { value: isHidden } = (component.getOrNull(entity.entityId) as { value: boolean } | null) ?? {} container.setEnabled(!isHidden) if (isHidden) { - context.gizmos.unsetEntity() + unsetGizmoManager(entity) } else { const selectionValue = context.editorComponents.Selection.getOrNull(entity.entityId) - updateGizmoManager(entity, selectionValue) + if (selectionValue) setGizmoManager(entity, selectionValue) } } } diff --git a/packages/@dcl/inspector/src/lib/babylon/decentraland/editorComponents/lock.ts b/packages/@dcl/inspector/src/lib/babylon/decentraland/editorComponents/lock.ts index 14845f9a0..f5673613d 100644 --- a/packages/@dcl/inspector/src/lib/babylon/decentraland/editorComponents/lock.ts +++ b/packages/@dcl/inspector/src/lib/babylon/decentraland/editorComponents/lock.ts @@ -1,6 +1,6 @@ import { ComponentType } from '@dcl/ecs' import type { ComponentOperation } from '../component-operations' -import { toggleSelection, updateGizmoManager } from './selection' +import { setGizmoManager, unsetGizmoManager } from './selection' export const putLockComponent: ComponentOperation = (entity, component) => { if (component.componentType === ComponentType.LastWriteWinElementSet) { @@ -8,12 +8,10 @@ export const putLockComponent: ComponentOperation = (entity, component) => { const { value: isLocked } = (component.getOrNull(entity.entityId) as { value: boolean } | null) ?? {} entity.setLock(!!isLocked) if (isLocked) { - toggleSelection(entity, false) - context.gizmos.unsetEntity() + unsetGizmoManager(entity) } else { const selectionValue = context.editorComponents.Selection.getOrNull(entity.entityId) - toggleSelection(entity, !!selectionValue) - updateGizmoManager(entity, selectionValue) + if (selectionValue) setGizmoManager(entity, selectionValue) } } } diff --git a/packages/@dcl/inspector/src/lib/babylon/decentraland/editorComponents/selection.ts b/packages/@dcl/inspector/src/lib/babylon/decentraland/editorComponents/selection.ts index 9d4f9b77e..5ef0293db 100644 --- a/packages/@dcl/inspector/src/lib/babylon/decentraland/editorComponents/selection.ts +++ b/packages/@dcl/inspector/src/lib/babylon/decentraland/editorComponents/selection.ts @@ -1,6 +1,6 @@ import { AbstractMesh, Color3 } from '@babylonjs/core' import { ComponentType } from '@dcl/ecs' -import { CoreComponents } from '../../../sdk/components' +import { CoreComponents, EditorComponentsTypes } from '../../../sdk/components' import { EcsEntity } from '../EcsEntity' import type { ComponentOperation } from '../component-operations' @@ -8,9 +8,16 @@ const highlightedMeshes = new Set() export const putEntitySelectedComponent: ComponentOperation = (entity, component) => { if (component.componentType === ComponentType.LastWriteWinElementSet) { - const componentValue = entity.isLocked() ? null : (component.getOrNull(entity.entityId) as { gizmo: number } | null) - toggleSelection(entity, !!componentValue) - updateGizmoManager(entity, componentValue) + if (entity.isLocked()) return deleteEntitySelectedComponent(entity, component) + + const componentValue = component.get(entity.entityId) as unknown as EditorComponentsTypes['Selection'] + setGizmoManager(entity, componentValue) + } +} + +export const deleteEntitySelectedComponent: ComponentOperation = (entity, component) => { + if (component.componentType === ComponentType.LastWriteWinElementSet) { + unsetGizmoManager(entity) } } @@ -38,24 +45,36 @@ export const toggleSelection = (entity: EcsEntity, value: boolean) => { } } -export const updateGizmoManager = (entity: EcsEntity, value: { gizmo: number } | null) => { +export const setGizmoManager = (entity: EcsEntity, value: { gizmo: number }) => { const context = entity.context.deref()! - let processedSomeEntity = false - const Transform = context.engine.getComponent(CoreComponents.TRANSFORM) - for (const [_entity] of context.engine.getEntitiesWith(context.editorComponents.Selection)) { - processedSomeEntity = true - if (entity.entityId === _entity && Transform.has(_entity)) { - context.gizmos.setEntity(entity) - const types = context.gizmos.getGizmoTypes() - const type = types[value?.gizmo || 0] - context.gizmos.setGizmoType(type) - return - } + if (!Transform.has(entity.entityId)) return + + toggleSelection(entity, true) + + const selectedEntities = Array.from(context.engine.getEntitiesWith(context.editorComponents.Selection)) + const types = context.gizmos.getGizmoTypes() + const type = types[value?.gizmo || 0] + context.gizmos.setGizmoType(type) + + if (selectedEntities.length === 1) { + context.gizmos.setEntity(entity) + } else if (selectedEntities.length > 1) { + context.gizmos.repositionGizmoOnCentroid() } +} - if (!processedSomeEntity) { +export const unsetGizmoManager = (entity: EcsEntity) => { + const context = entity.context.deref()! + const selectedEntities = Array.from(context.engine.getEntitiesWith(context.editorComponents.Selection)) + const currentEntity = context.gizmos.getEntity() + + toggleSelection(entity, false) + + if (currentEntity?.entityId === entity.entityId || selectedEntities.length === 0) { context.gizmos.unsetEntity() + } else { + context.gizmos.repositionGizmoOnCentroid() } } 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 209551571..8da5fd707 100644 --- a/packages/@dcl/inspector/src/lib/babylon/decentraland/gizmo-manager.ts +++ b/packages/@dcl/inspector/src/lib/babylon/decentraland/gizmo-manager.ts @@ -6,7 +6,8 @@ import { Node, Vector3, PointerDragBehavior, - AbstractMesh + AbstractMesh, + TransformNode } from '@babylonjs/core' import { Entity, TransformType } from '@dcl/ecs' import { Vector3 as DclVector3, Quaternion as DclQuaternion } from '@dcl/ecs-math' @@ -17,6 +18,8 @@ import { SceneContext } from './SceneContext' import { PatchedGizmoManager } from './gizmo-patch' import { ROOT } from '../../sdk/tree' +const GIZMO_DUMMY_NODE = 'GIZMO_DUMMY_NODE' + interface GizmoAxis { xGizmo: IAxisDragGizmo yGizmo: IAxisDragGizmo @@ -39,6 +42,19 @@ function areQuaternionsEqual(a: DclQuaternion, b: DclQuaternion) { return a.x === b.x && a.y === b.y && a.z === b.z && a.w === b.w } +function calculateCenter(positions: Vector3[]): Vector3 { + if (positions.length === 0) throw new Error('No positions provided to calculate center') + + const sum = positions.reduce((acc, pos) => { + acc.x += pos.x + acc.y += pos.y + acc.z += pos.z + return acc + }, new Vector3(0, 0, 0)) + + return sum.scale(1 / positions.length) +} + export function createGizmoManager(context: SceneContext) { // events const events = mitt<{ change: void }>() @@ -180,6 +196,53 @@ export function createGizmoManager(context: SceneContext) { } } + // Map to store the original parent of each entity + const originalParents = new Map() + + // Check if a transform node for the gizmo already exists, or create one + function getDummyNode(): TransformNode { + let dummyNode = context.scene.getTransformNodeByName(GIZMO_DUMMY_NODE) as TransformNode + if (!dummyNode) dummyNode = new TransformNode(GIZMO_DUMMY_NODE, context.scene) as TransformNode + return dummyNode + } + + function repositionGizmoOnCentroid() { + const selectedEntities = getSelectedEntities().map((entityId) => context.getEntityOrNull(entityId)!) + const positions = selectedEntities.map((entity) => { + const { x, y, z } = getTransform(entity).position + return new Vector3(x, y, z) + }) + const centroidPosition = calculateCenter(positions) + const dummyNode = getDummyNode() + + // Set the dummy node position on centroid. This should be the first thing to do on the dummy node + // so everything aligns to the right position afterwards. + dummyNode.position = centroidPosition + + // Store the original parents and set the dummy node as parent for each selected entity + selectedEntities.forEach((entity) => { + const parent = entity.parent as TransformNode | null + originalParents.set(entity.entityId, parent) + entity.setParent(dummyNode) + }) + + // Attach the gizmo to the dummy node + gizmoManager.attachToNode(dummyNode) + } + + function restoreOriginalParents() { + originalParents.forEach((parent, entity) => { + const ecsEntity = context.getEntityOrNull(entity)! + ecsEntity.setParent(parent) + }) + + // Clear the stored parents as they're now restored + originalParents.clear() + + // Detach the gizmo from the dummy node if needed + gizmoManager.attachToNode(null) + } + gizmoManager.gizmos.scaleGizmo?.onDragStartObservable.add(initTransform) gizmoManager.gizmos.positionGizmo?.onDragStartObservable.add(initTransform) gizmoManager.gizmos.rotationGizmo?.onDragStartObservable.add(initTransform) @@ -309,25 +372,33 @@ export function createGizmoManager(context: SceneContext) { return isEnabled }, setEnabled, - setEntity(entity: EcsEntity | null) { + setEntity(entity: EcsEntity | null): void { if ( entity === lastEntity || !isEnabled || - areMultipleEntitiesSelected() || entity?.isHidden() || entity?.isLocked() || entity?.getRoot() !== ROOT ) { return } - gizmoManager.attachToNode(entity) - lastEntity = entity - // fix gizmo rotation/position if necessary - const transform = getTransform() - fixRotationGizmoAlignment(transform) - fixPositionGizmoAlignment(transform) + restoreOriginalParents() + if (areMultipleEntitiesSelected()) { + return repositionGizmoOnCentroid() + } else { + gizmoManager.attachToNode(entity) + lastEntity = entity + // fix gizmo rotation/position if necessary + const transform = getTransform() + fixRotationGizmoAlignment(transform) + fixPositionGizmoAlignment(transform) + } events.emit('change') }, + repositionGizmoOnCentroid() { + restoreOriginalParents() + return repositionGizmoOnCentroid() + }, getEntity() { return lastEntity }, diff --git a/packages/@dcl/inspector/src/lib/babylon/setup/input.ts b/packages/@dcl/inspector/src/lib/babylon/setup/input.ts index 87f79929a..f7fee93c8 100644 --- a/packages/@dcl/inspector/src/lib/babylon/setup/input.ts +++ b/packages/@dcl/inspector/src/lib/babylon/setup/input.ts @@ -96,7 +96,7 @@ export function interactWithScene( const ancestors = getAncestors(engine, entity.entityId) const nodes = mapNodes(engine, (node) => (isAncestor(ancestors, node.entity) ? { ...node, open: true } : node)) operations.updateValue(editorComponents.Nodes, engine.RootEntity, { value: nodes }) - operations.updateSelectedEntity(entity.entityId, !!keyState[Keys.KEY_CTRL]) + operations.updateSelectedEntity(entity.entityId, !!keyState[Keys.KEY_CTRL] || !!keyState[Keys.KEY_SHIFT]) void operations.dispatch() } diff --git a/packages/@dcl/inspector/src/lib/sdk/operations/update-selected-entity.ts b/packages/@dcl/inspector/src/lib/sdk/operations/update-selected-entity.ts index 159f50d5f..460cde85c 100644 --- a/packages/@dcl/inspector/src/lib/sdk/operations/update-selected-entity.ts +++ b/packages/@dcl/inspector/src/lib/sdk/operations/update-selected-entity.ts @@ -49,8 +49,11 @@ export function updateSelectedEntity(engine: IEngine) { } } - // then select new entity - if (!Selection.has(entity) || deletedSelection) { + // allow deselecting from a list of selected entities... + if (multiple && Selection.has(entity)) { + Selection.deleteFrom(entity) + } else if (!Selection.has(entity) || deletedSelection) { + // then select new entity Selection.createOrReplace(entity, { gizmo }) }