Skip to content

Commit

Permalink
Add gizmo centroid (#1029)
Browse files Browse the repository at this point in the history
* center gizmo when multiple entities are selected

* some refactor for Selection component
  • Loading branch information
nicoecheza authored Nov 13, 2024
1 parent 5502b7c commit 499b43b
Show file tree
Hide file tree
Showing 10 changed files with 155 additions and 42 deletions.
2 changes: 1 addition & 1 deletion packages/@dcl/inspector/src/components/Tree/Tree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ export function Tree<T>() {
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)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ const getContext = () => ({
componentPutOperations: {
[Transform.componentId]: jest.fn()
},
componentDeleteOperations: {
[Transform.componentId]: jest.fn()
},
engine: {
RootEntity: 0 as Entity
}
Expand Down Expand Up @@ -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', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,17 @@ export class EcsEntity extends BABYLON.TransformNode {
}

putComponent(component: ComponentDefinition<unknown>) {
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<unknown>) {
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)
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -77,6 +77,21 @@ export class SceneContext {
[this.editorComponents.Lock.componentId]: putLockComponent
}

readonly componentDeleteOperations: Record<number, ComponentOperation> = {
[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<void>()

Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
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) {
const context = entity.context.deref()!
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)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
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'

const highlightedMeshes = new Set<AbstractMesh>()

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

Expand Down Expand Up @@ -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()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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
Expand All @@ -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 }>()
Expand Down Expand Up @@ -180,6 +196,53 @@ export function createGizmoManager(context: SceneContext) {
}
}

// Map to store the original parent of each entity
const originalParents = new Map<Entity, TransformNode | null>()

// 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)
Expand Down Expand Up @@ -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
},
Expand Down
2 changes: 1 addition & 1 deletion packages/@dcl/inspector/src/lib/babylon/setup/input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
}

Expand Down

0 comments on commit 499b43b

Please sign in to comment.