Skip to content

Commit

Permalink
fix: Avoid showing the bounding box when the scene is still loading a…
Browse files Browse the repository at this point in the history
…ssets (#906)

* fix: Avoid showing the bounding box when the scene is still loading assets

* feat: add tests for EcsEntity class

* feat: Add a new method isOutOfBoundaries to EcsEntities

* feat: Add a new sceneMetrics rpc

* feat: Add a sceneMetrics state

* feat: Update Metrics component to use the new sceneMetrics state

* feat: Add tests

* feat: export scene-metrics rpc

* fix: Remove unused import

* refactor: get scene nodes
  • Loading branch information
cyaiox authored Feb 29, 2024
1 parent 2c5f6a7 commit 2e58941
Show file tree
Hide file tree
Showing 11 changed files with 373 additions and 40 deletions.
89 changes: 54 additions & 35 deletions packages/@dcl/inspector/src/components/Renderer/Metrics/Metrics.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import React, { useCallback, useEffect, useMemo } from 'react'
import cx from 'classnames'
import { IoGridOutline as SquaresGridIcon, IoAlertCircleOutline as AlertIcon } from 'react-icons/io5'
import { Material } from '@babylonjs/core'
import { CrdtMessageType } from '@dcl/ecs'

import { withSdk, WithSdkProps } from '../../../hoc/withSdk'
import { useChange } from '../../../hooks/sdk/useChange'
import { useOutsideClick } from '../../../hooks/useOutsideClick'
import { useAppDispatch, useAppSelector } from '../../../redux/hooks'
import { getMetrics, getLimits, setEntitiesOutOfBoundaries, setMetrics, setLimits } from '../../../redux/scene-metrics'
import { SceneMetrics } from '../../../redux/scene-metrics/types'
import type { Layout } from '../../../lib/utils/layout'
import { GROUND_MESH_PREFIX, PARCEL_SIZE } from '../../../lib/utils/scene'
import { Button } from '../../Button'
import { getSceneLimits } from './utils'
import type { Metrics } from './types'

import './Metrics.css'

Expand Down Expand Up @@ -49,35 +52,30 @@ const Metrics = withSdk<WithSdkProps>(({ sdk }) => {
const ROOT = sdk.engine.RootEntity
const PLAYER_ROOT = sdk.engine.PlayerEntity
const CAMERA_ROOT = sdk.engine.CameraEntity
const dispatch = useAppDispatch()
const metrics = useAppSelector(getMetrics)
const limits = useAppSelector(getLimits)
const [showMetrics, setShowMetrics] = React.useState(false)
const [metrics, setMetrics] = React.useState<Metrics>({
triangles: 0,
entities: 0,
bodies: 0,
materials: 0,
textures: 0
})
const [sceneLayout, setSceneLayout] = React.useState<Layout>({
base: { x: 0, y: 0 },
parcels: []
})

const getNodes = useCallback(
() =>
sdk.components.Nodes.getOrNull(ROOT)?.value.filter((node) => ![PLAYER_ROOT, CAMERA_ROOT].includes(node.entity)) ??
[],
[sdk]
)

const handleUpdateMetrics = useCallback(() => {
const meshes = sdk.scene.meshes.filter(
(mesh) =>
!(
IGNORE_MESHES.includes(mesh.id) ||
mesh.id.startsWith(GROUND_MESH_PREFIX) ||
mesh.id.startsWith('BoundingMesh')
)
!IGNORE_MESHES.includes(mesh.id) &&
!mesh.id.startsWith(GROUND_MESH_PREFIX) &&
!mesh.id.startsWith('BoundingMesh')
)
const triangles = meshes.reduce((acc, mesh) => acc + mesh.getTotalVertices(), 0)
const entities =
(
sdk.components.Nodes.getOrNull(ROOT)?.value.filter(
(node) => ![PLAYER_ROOT, CAMERA_ROOT].includes(node.entity)
) ?? [ROOT]
).length - 1
const uniqueTextures = new Set(
sdk.scene.textures
.filter((texture) => !IGNORE_TEXTURES.includes(texture.name))
Expand All @@ -86,30 +84,56 @@ const Metrics = withSdk<WithSdkProps>(({ sdk }) => {
const uniqueMaterials = new Set(
sdk.scene.materials.map((material) => material.id).filter((id) => !IGNORE_MATERIALS.includes(id))
)
setMetrics({
triangles: triangles,
entities: entities,
bodies: meshes.length,
materials: uniqueMaterials.size,
textures: uniqueTextures.size
})
}, [sdk])

dispatch(
setMetrics({
triangles,
entities: getNodes().length,
bodies: meshes.length,
materials: uniqueMaterials.size,
textures: uniqueTextures.size
})
)
}, [sdk, dispatch, getNodes, setMetrics])

const handleUpdateSceneLayout = useCallback(() => {
const scene = sdk.components.Scene.getOrNull(ROOT)
if (scene) {
setSceneLayout({ ...(scene.layout as Layout) })
setSceneLayout(scene.layout as Layout)
dispatch(setLimits(getSceneLimits(scene.layout.parcels.length)))
}
}, [sdk, setSceneLayout])

const handleSceneChange = useCallback(() => {
const nodes = getNodes()
const entitiesOutOfBoundaries = nodes.reduce((count, node) => {
const entity = sdk.sceneContext.getEntityOrNull(node.entity)
return entity && entity.isOutOfBoundaries() ? count + 1 : count
}, 0)

dispatch(setEntitiesOutOfBoundaries(entitiesOutOfBoundaries))
}, [sdk, dispatch, getNodes, setEntitiesOutOfBoundaries])

useEffect(() => {
const handleOutsideMaterialChange = (material: Material) => {
if (material.name === 'entity_outside_layout_multimaterial') {
handleSceneChange()
}
}

const addOutsideMaterialObservable = sdk.scene.onNewMultiMaterialAddedObservable.add(handleOutsideMaterialChange)
const removeOutsideMaterialObservable = sdk.scene.onMaterialRemovedObservable.add(handleOutsideMaterialChange)

sdk.scene.onDataLoadedObservable.add(handleUpdateMetrics)
sdk.scene.onMeshRemovedObservable.add(handleUpdateMetrics)

handleUpdateSceneLayout()

return () => {
sdk.scene.onDataLoadedObservable.removeCallback(handleUpdateMetrics)
sdk.scene.onMeshRemovedObservable.removeCallback(handleUpdateMetrics)
sdk.scene.onNewMultiMaterialAddedObservable.remove(addOutsideMaterialObservable)
sdk.scene.onMaterialRemovedObservable.remove(removeOutsideMaterialObservable)
}
}, [])

Expand All @@ -122,15 +146,10 @@ const Metrics = withSdk<WithSdkProps>(({ sdk }) => {
[handleUpdateSceneLayout]
)

const limits = useMemo<Metrics>(() => {
const parcels = sceneLayout.parcels.length
return getSceneLimits(parcels)
}, [sceneLayout])

const limitsExceeded = useMemo<Record<string, boolean>>(() => {
return Object.fromEntries(
Object.entries(metrics)
.map(([key, value]) => [key, value > limits[key as keyof Metrics]])
.map(([key, value]) => [key, value > limits[key as keyof SceneMetrics]])
.filter(([, value]) => value)
)
}, [metrics, limits])
Expand Down Expand Up @@ -177,7 +196,7 @@ const Metrics = withSdk<WithSdkProps>(({ sdk }) => {
<div className={cx('Description', { LimitExceeded: limitsExceeded[key] })}>
<span className="primary">{value}</span>
{'/'}
<span className="secondary">{limits[key as keyof Metrics]}</span>
<span className="secondary">{limits[key as keyof SceneMetrics]}</span>
</div>
</div>
))}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,4 +112,96 @@ describe('EcsEntity', () => {
expect(gltfPathLoading).toBe(filePath)
expect(entity.isGltfPathLoading()).toBe(false)
})

it('should dispose entity correctly', () => {
const Transform = components.Transform(engine)
entity.putComponent(Transform)
entity.boundingInfoMesh = new BABYLON.AbstractMesh('boundingInfoMesh', scene)
entity.dispose()
expect(entity.usedComponents.size).toBe(0)
expect(entity.boundingInfoMesh?.isDisposed()).toBe(true)
expect(scene.getTransformNodeByID(entity.id)).toBeNull()
})

it('should return true when gltfPathLoading is set', () => {
entity.setGltfPathLoading()
expect(entity.isGltfPathLoading()).toBe(true)
})

it('should return false when gltfPathLoading is not set', () => {
expect(entity.isGltfPathLoading()).toBe(false)
})

it('should resolve gltfPathLoading', async () => {
entity.setGltfPathLoading()
const filePath = 'some-path'
setTimeout(() => entity.resolveGltfPathLoading(filePath), 1)
expect(entity.isGltfPathLoading()).toBe(true)
const gltfPathLoading = await entity.getGltfPathLoading()
expect(gltfPathLoading).toBe(filePath)
expect(entity.isGltfPathLoading()).toBe(false)
})

it('should set gltfAssetContainer correctly', async () => {
const gltfAssetContainer = new BABYLON.AssetContainer(scene)
entity.setGltfAssetContainer(gltfAssetContainer)
expect(entity.gltfAssetContainer).toBe(gltfAssetContainer)
await expect(entity.onGltfContainerLoaded()).resolves.toBe(gltfAssetContainer)
})

it('should set gltfContainer correctly', async () => {
const gltfContainer = new BABYLON.AbstractMesh('gltfContainer', scene)
entity.setGltfContainer(gltfContainer)
expect(entity.gltfContainer).toBe(gltfContainer)
await expect(entity.onAssetLoaded()).resolves.toBe(gltfContainer)
})

it('should set meshRenderer correctly', async () => {
const meshRenderer = new BABYLON.AbstractMesh('meshRenderer', scene)
entity.setMeshRenderer(meshRenderer)
expect(entity.meshRenderer).toBe(meshRenderer)
await expect(entity.onAssetLoaded()).resolves.toBe(meshRenderer)
})

it('should set the visibility of the entity and its children', () => {
const gltfContainer = new BABYLON.AbstractMesh('gltfContainer', scene)
entity.setGltfContainer(gltfContainer)
const child = new EcsEntity(1 as Entity, context, scene)
const childMeshRenderer = new BABYLON.AbstractMesh('childMeshRenderer', scene)
child.parent = entity
child.setMeshRenderer(childMeshRenderer)
expect(entity.isHidden()).toBe(false)
expect(child.isHidden()).toBe(false)
entity.setVisibility(false)
expect(entity.isHidden()).toBe(true)
expect(child.isHidden()).toBe(true)
})

it('should set and get the lock state of the entity', () => {
expect(entity.isLocked()).toBe(false)
entity.setLock(true)
expect(entity.isLocked()).toBe(true)
})

it('should generate the bounding box correctly', () => {
const mesh1 = new BABYLON.Mesh('mesh1', scene)
const mesh2 = new BABYLON.Mesh('mesh2', scene)
mesh1.position = new BABYLON.Vector3(1, 1, 1)
mesh1.parent = entity
mesh2.position = new BABYLON.Vector3(2, 2, 2)
mesh2.parent = entity
entity.generateBoundingBox()
expect(entity.boundingInfoMesh).toBeDefined()
const boundingInfoMesh = entity.boundingInfoMesh!
expect(boundingInfoMesh.name).toBe(`BoundingMesh-${entity.id}`)
expect(boundingInfoMesh.position).toEqual(entity.absolutePosition)
expect(boundingInfoMesh.rotationQuaternion).toEqual(entity.absoluteRotationQuaternion)
expect(boundingInfoMesh.scaling).toEqual(entity.absoluteScaling)
expect(boundingInfoMesh.getBoundingInfo().boundingBox.minimumWorld).toEqual(
mesh1.getBoundingInfo().boundingBox.minimumWorld
)
expect(boundingInfoMesh.getBoundingInfo().boundingBox.maximumWorld).toEqual(
mesh2.getBoundingInfo().boundingBox.maximumWorld
)
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,10 @@ export class EcsEntity extends BABYLON.TransformNode {
})
}
}

isOutOfBoundaries() {
return !!this.boundingInfoMesh?.showBoundingBox
}
}

/**
Expand Down Expand Up @@ -244,22 +248,22 @@ async function validateEntityIsOutsideLayout(entity: EcsEntity) {

function updateMeshBoundingBoxVisibility(entity: EcsEntity, mesh: BABYLON.AbstractMesh) {
const scene = mesh.getScene()
if (scene.isLoading) return

const { isEntityOutsideLayout } = getLayoutManager(scene)

if (isEntityOutsideLayout(mesh)) {
if (mesh.showBoundingBox) return

mesh.showBoundingBox = true
for (const childMesh of entity.getChildMeshes(false)) {
addOutsideLayoutMaterial(childMesh, scene)
}
mesh.showBoundingBox = true
} else {
if (!mesh.showBoundingBox) return

mesh.showBoundingBox = false
for (const childMesh of entity.getChildMeshes(false)) {
removeOutsideLayoutMaterial(childMesh)
}
mesh.showBoundingBox = false
}
}

Expand Down
20 changes: 20 additions & 0 deletions packages/@dcl/inspector/src/lib/rpc/scene-metrics/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { RPC, Transport } from '@dcl/mini-rpc'
import { SceneMetricsRPC } from './types'

export class SceneMetricsClient extends RPC<SceneMetricsRPC.Method, SceneMetricsRPC.Params, SceneMetricsRPC.Result> {
constructor(transport: Transport) {
super(SceneMetricsRPC.name, transport)
}

getMetrics = () => {
return this.request(SceneMetricsRPC.Method.GET_METRICS, undefined)
}

getLimits = () => {
return this.request(SceneMetricsRPC.Method.GET_LIMITS, undefined)
}

getEntitiesOutOfBoundaries = () => {
return this.request(SceneMetricsRPC.Method.GET_ENTITIES_OUT_OF_BOUNDARIES, undefined)
}
}
Loading

0 comments on commit 2e58941

Please sign in to comment.