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

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

Merged
merged 10 commits into from
Feb 29, 2024
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 @@
})
}
}

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

Check warning on line 205 in packages/@dcl/inspector/src/lib/babylon/decentraland/EcsEntity.ts

View check run for this annotation

Codecov / codecov/patch

packages/@dcl/inspector/src/lib/babylon/decentraland/EcsEntity.ts#L204-L205

Added lines #L204 - L205 were not covered by tests
}

/**
Expand Down Expand Up @@ -244,22 +248,22 @@

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Condition added to avoid showing bounding boxes while the scene is still loading assets


Check warning on line 252 in packages/@dcl/inspector/src/lib/babylon/decentraland/EcsEntity.ts

View check run for this annotation

Codecov / codecov/patch

packages/@dcl/inspector/src/lib/babylon/decentraland/EcsEntity.ts#L251-L252

Added lines #L251 - L252 were not covered by tests
const { isEntityOutsideLayout } = getLayoutManager(scene)

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

mesh.showBoundingBox = true

Check warning on line 257 in packages/@dcl/inspector/src/lib/babylon/decentraland/EcsEntity.ts

View check run for this annotation

Codecov / codecov/patch

packages/@dcl/inspector/src/lib/babylon/decentraland/EcsEntity.ts#L257

Added line #L257 was not covered by tests
for (const childMesh of entity.getChildMeshes(false)) {
addOutsideLayoutMaterial(childMesh, scene)
}
mesh.showBoundingBox = true
} else {
if (!mesh.showBoundingBox) return

mesh.showBoundingBox = false

Check warning on line 263 in packages/@dcl/inspector/src/lib/babylon/decentraland/EcsEntity.ts

View check run for this annotation

Codecov / codecov/patch

packages/@dcl/inspector/src/lib/babylon/decentraland/EcsEntity.ts#L263

Added line #L263 was not covered by tests
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
Loading