Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add gizmo centroid #1029

Merged
merged 4 commits into from
Nov 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading