Skip to content

Commit daa4796

Browse files
committed
feat: show all components and disable the ones already added
1 parent fa1370b commit daa4796

2 files changed

Lines changed: 141 additions & 80 deletions

File tree

packages/@dcl/inspector/src/components/EntityInspector/EntityHeader/EntityHeader.tsx

Lines changed: 112 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { Entity } from '@dcl/ecs'
99
import { WithSdkProps, withSdk } from '../../../hoc/withSdk'
1010
import { useChange } from '../../../hooks/sdk/useChange'
1111
import { useComponentValue } from '../../../hooks/sdk/useComponentValue'
12-
import { isRoot, useEntityComponent } from '../../../hooks/sdk/useEntityComponent'
12+
import { isRoot, useEntityComponent, getComponentName } from '../../../hooks/sdk/useEntityComponent'
1313
import { useHasComponent } from '../../../hooks/sdk/useHasComponent'
1414
import { CAMERA, PLAYER, ROOT } from '../../../lib/sdk/tree'
1515
import { getAssetByModel, getAssetById } from '../../../lib/logic/catalog'
@@ -104,21 +104,25 @@ export default React.memo(
104104
[entity]
105105
)
106106

107+
const availableComponents = getAvailableComponents(entity)
108+
107109
const isComponentDisabled = useCallback(
108-
(component: string) => {
109-
switch (component) {
110-
case 'Visibility': {
111-
return !hasGltfContainer && !hasMeshCollider
112-
}
113-
default:
114-
return false
110+
(componentId: number) => {
111+
const componentInfo = availableComponents.find((comp) => comp.id === componentId)
112+
113+
if (componentInfo && componentInfo.isOnEntity) {
114+
return true
115115
}
116+
117+
if (componentId === sdk.components.VisibilityComponent.componentId) {
118+
return !hasGltfContainer && !hasMeshCollider
119+
}
120+
121+
return false
116122
},
117-
[entity, hasGltfContainer, hasMeshCollider]
123+
[availableComponents, hasGltfContainer, hasMeshCollider, sdk.components.VisibilityComponent.componentId]
118124
)
119125

120-
const availableComponents = getAvailableComponents(entity)
121-
122126
const handleOpenModal = useCallback(
123127
(cb?: () => void) => {
124128
setModal({ isOpen: true, cb })
@@ -141,6 +145,33 @@ export default React.memo(
141145
[isBasicViewEnabled, handleAddComponent, handleOpenModal]
142146
)
143147

148+
const getComponentTooltip = useCallback(
149+
(componentId: number, description: string, link?: string) => {
150+
const isDisabled = isComponentDisabled(componentId)
151+
152+
if (!isDisabled) {
153+
return { text: description, ...(link && { link }) }
154+
}
155+
156+
const componentInfo = availableComponents.find((c) => c.id === componentId)
157+
158+
if (componentInfo?.isOnEntity) {
159+
return {
160+
text: 'This component is already added. An entity can only have one copy of each component.'
161+
}
162+
}
163+
164+
if (componentId === sdk.components.VisibilityComponent.componentId) {
165+
return {
166+
text: 'You must have either a GLTF Container or a Mesh Collider component to use this component.'
167+
}
168+
}
169+
170+
return { text: description, ...(link && { link }) }
171+
},
172+
[isComponentDisabled, availableComponents, sdk.components.VisibilityComponent.componentId]
173+
)
174+
144175
const componentOptions = useMemo(() => {
145176
const options = [
146177
{ header: '3D Content' },
@@ -152,19 +183,23 @@ export default React.memo(
152183
sdk.components.GltfContainer.componentId,
153184
sdk.components.GltfContainer.componentName
154185
),
155-
tooltip: {
156-
text: "The GLTF assigns a 3D model file for the item's visible shape. It also handles collisions, to make an item clickable or block the player from walking through it."
157-
}
186+
disabled: isComponentDisabled(sdk.components.GltfContainer.componentId),
187+
tooltip: getComponentTooltip(
188+
sdk.components.GltfContainer.componentId,
189+
"The GLTF assigns a 3D model file for the item's visible shape. It also handles collisions, to make an item clickable or block the player from walking through it."
190+
)
158191
},
159192
{
160193
id: sdk.components.Material.componentId,
161194
value: 'Material',
162195
onClick: () =>
163196
handleClickAddComponent(sdk.components.Material.componentId, sdk.components.Material.componentName),
164-
tooltip: {
165-
text: 'Material determines the visual appearance of an object. It defines properties such as color, texture, and transparency',
166-
link: 'https://docs.decentraland.org/creator/development-guide/sdk7/materials/'
167-
}
197+
disabled: isComponentDisabled(sdk.components.Material.componentId),
198+
tooltip: getComponentTooltip(
199+
sdk.components.Material.componentId,
200+
'Material determines the visual appearance of an object. It defines properties such as color, texture, and transparency',
201+
'https://docs.decentraland.org/creator/development-guide/sdk7/materials/'
202+
)
168203
},
169204
{
170205
id: sdk.components.VisibilityComponent.componentId,
@@ -174,92 +209,96 @@ export default React.memo(
174209
sdk.components.VisibilityComponent.componentId,
175210
sdk.components.VisibilityComponent.componentName
176211
),
177-
tooltip: {
178-
className: 'EntityHeader',
179-
text: (
180-
<span className="VisibilityComponentTooltip">
181-
Visibility controls whether an object is visible or not to the player. Items marked as invisible are
182-
shown on the editor, but not to players running the scene.
183-
{isComponentDisabled('Visibility') && (
184-
<span className="ErrorMessage">
185-
You must have either a GLTF Container or a Mesh Collider component to use this component.
186-
</span>
187-
)}
188-
</span>
189-
)
190-
},
191-
disabled: isComponentDisabled('Visibility')
212+
disabled: isComponentDisabled(sdk.components.VisibilityComponent.componentId),
213+
tooltip: getComponentTooltip(
214+
sdk.components.VisibilityComponent.componentId,
215+
'Visibility controls whether an object is visible or not to the player. Items marked as invisible are shown on the editor, but not to players running the scene.'
216+
)
192217
},
193218
{
194219
id: sdk.components.MeshRenderer.componentId,
195220
value: 'Mesh Renderer',
196221
onClick: () =>
197222
handleClickAddComponent(sdk.components.MeshRenderer.componentId, sdk.components.MeshRenderer.componentName),
198-
tooltip: {
199-
text: 'Use MeshRenderer to assign a primitive 3D shape to the item. Instead of using a 3D file from GLTF, assign a simple cube, plane, sphere, or cylinder. These shapes can be used together with Materials',
200-
link: 'https://docs.decentraland.org/creator/development-guide/sdk7/shape-components/'
201-
}
223+
disabled: isComponentDisabled(sdk.components.MeshRenderer.componentId),
224+
tooltip: getComponentTooltip(
225+
sdk.components.MeshRenderer.componentId,
226+
'Use MeshRenderer to assign a primitive 3D shape to the item. Instead of using a 3D file from GLTF, assign a simple cube, plane, sphere, or cylinder. These shapes can be used together with Materials',
227+
'https://docs.decentraland.org/creator/development-guide/sdk7/shape-components/'
228+
)
202229
},
203230
{
204231
id: sdk.components.MeshCollider.componentId,
205232
value: 'Mesh Collider',
206233
onClick: () =>
207234
handleClickAddComponent(sdk.components.MeshCollider.componentId, sdk.components.MeshCollider.componentName),
208-
tooltip: {
209-
text: 'MeshCollider defines the collision properties of an item, based on its invisible collision geometry. Collisions serve to make an item clickable or to block the player from walking through an item',
210-
link: 'https://docs.decentraland.org/creator/development-guide/sdk7/colliders/'
211-
}
235+
disabled: isComponentDisabled(sdk.components.MeshCollider.componentId),
236+
tooltip: getComponentTooltip(
237+
sdk.components.MeshCollider.componentId,
238+
'MeshCollider defines the collision properties of an item, based on its invisible collision geometry. Collisions serve to make an item clickable or to block the player from walking through an item',
239+
'https://docs.decentraland.org/creator/development-guide/sdk7/colliders/'
240+
)
212241
},
213242
{ header: 'Interaction' },
214243
{
215244
id: sdk.components.States.componentId,
216245
value: 'States',
217246
onClick: () =>
218247
handleClickAddComponent(sdk.components.States.componentId, sdk.components.States.componentName),
219-
tooltip: {
220-
text: 'States specify the status of entities. Use triggers to check or change states, and set actions accordingly.',
221-
link: 'https://docs.decentraland.org/creator/smart-items/#states'
222-
}
248+
disabled: isComponentDisabled(sdk.components.States.componentId),
249+
tooltip: getComponentTooltip(
250+
sdk.components.States.componentId,
251+
'States specify the status of entities. Use triggers to check or change states, and set actions accordingly.',
252+
'https://docs.decentraland.org/creator/smart-items/#states'
253+
)
223254
},
224255
{
225256
id: sdk.components.Triggers.componentId,
226257
value: 'Triggers',
227258
onClick: () =>
228259
handleClickAddComponent(sdk.components.Triggers.componentId, sdk.components.Triggers.componentName),
229-
tooltip: {
230-
text: 'Triggers activate actions based on player interactions like clicks, entering/exiting areas, or global events like "on spawn".',
231-
link: 'https://docs.decentraland.org/creator/smart-items/#triggers'
232-
}
260+
disabled: isComponentDisabled(sdk.components.Triggers.componentId),
261+
tooltip: getComponentTooltip(
262+
sdk.components.Triggers.componentId,
263+
'Triggers activate actions based on player interactions like clicks, entering/exiting areas, or global events like "on spawn".',
264+
'https://docs.decentraland.org/creator/smart-items/#triggers'
265+
)
233266
},
234267
{
235268
id: sdk.components.Actions.componentId,
236269
value: 'Actions',
237270
onClick: () =>
238271
handleClickAddComponent(sdk.components.Actions.componentId, sdk.components.Actions.componentName),
239-
tooltip: {
240-
text: 'Actions list the capabilities of entities, from playing animations to changing visibility. Customize or add new actions, which are activated by triggers.',
241-
link: 'https://docs.decentraland.org/creator/smart-items/#actions'
242-
}
272+
disabled: isComponentDisabled(sdk.components.Actions.componentId),
273+
tooltip: getComponentTooltip(
274+
sdk.components.Actions.componentId,
275+
'Actions list the capabilities of entities, from playing animations to changing visibility. Customize or add new actions, which are activated by triggers.',
276+
'https://docs.decentraland.org/creator/smart-items/#actions'
277+
)
243278
},
244279
{
245280
id: sdk.components.AudioSource.componentId,
246281
value: 'Audio Source',
247282
onClick: () =>
248283
handleClickAddComponent(sdk.components.AudioSource.componentId, sdk.components.AudioSource.componentName),
249-
tooltip: {
250-
text: 'AudioSource enables the playback of sound in your scene. The item emits sound that originates from its location, from an .mp3 file in your scene project',
251-
link: 'https://docs.decentraland.org/creator/development-guide/sdk7/sounds'
252-
}
284+
disabled: isComponentDisabled(sdk.components.AudioSource.componentId),
285+
tooltip: getComponentTooltip(
286+
sdk.components.AudioSource.componentId,
287+
'AudioSource enables the playback of sound in your scene. The item emits sound that originates from its location, from an .mp3 file in your scene project',
288+
'https://docs.decentraland.org/creator/development-guide/sdk7/sounds'
289+
)
253290
},
254291
{
255292
id: sdk.components.TextShape.componentId,
256293
value: 'Text Shape',
257294
onClick: () =>
258295
handleClickAddComponent(sdk.components.TextShape.componentId, sdk.components.TextShape.componentName),
259-
tooltip: {
260-
text: 'Use TextShape to display text in the 3D space',
261-
link: 'https://docs.decentraland.org/creator/development-guide/sdk7/text'
262-
}
296+
disabled: isComponentDisabled(sdk.components.TextShape.componentId),
297+
tooltip: getComponentTooltip(
298+
sdk.components.TextShape.componentId,
299+
'Use TextShape to display text in the 3D space',
300+
'https://docs.decentraland.org/creator/development-guide/sdk7/text'
301+
)
263302
},
264303
{
265304
id: sdk.components.PointerEvents.componentId,
@@ -269,10 +308,12 @@ export default React.memo(
269308
sdk.components.PointerEvents.componentId,
270309
sdk.components.PointerEvents.componentName
271310
),
272-
tooltip: {
273-
text: 'Use PointerEvents to configure the hints shown to players when they hover the cursor over the item. Change the text, the button, the max distance, etc',
274-
link: 'https://docs.decentraland.org/creator/development-guide/sdk7/click-events'
275-
}
311+
disabled: isComponentDisabled(sdk.components.PointerEvents.componentId),
312+
tooltip: getComponentTooltip(
313+
sdk.components.PointerEvents.componentId,
314+
'Use PointerEvents to configure the hints shown to players when they hover the cursor over the item. Change the text, the button, the max distance, etc',
315+
'https://docs.decentraland.org/creator/development-guide/sdk7/click-events'
316+
)
276317
}
277318
]
278319

@@ -283,23 +324,23 @@ export default React.memo(
283324
return set
284325
}, new Set<number>())
285326

286-
const availableIds = availableComponents.reduce((set, component) => set.add(component.id), new Set<number>())
287-
288327
if (availableComponents.some((component) => !optionIds.has(component.id))) {
289328
options.push({ header: 'Other' })
290329
for (const component of availableComponents) {
291330
if (!optionIds.has(component.id)) {
292331
options.push({
293332
id: component.id,
294333
value: component.name,
295-
onClick: () => handleClickAddComponent(component.id, component.name)
296-
} as any)
334+
onClick: () => handleClickAddComponent(component.id, component.name),
335+
disabled: isComponentDisabled(component.id),
336+
tooltip: getComponentTooltip(component.id, `${component.name} component`)
337+
})
297338
}
298339
}
299340
}
300341

301-
return options.filter((option) => !option.id || availableIds.has(option.id))
302-
}, [sdk, availableComponents, isComponentDisabled, handleClickAddComponent])
342+
return options
343+
}, [sdk, availableComponents, isComponentDisabled, handleClickAddComponent, getComponentTooltip])
303344

304345
const quitEditMode = useCallback(() => setEditMode(false), [])
305346
const enterEditMode = useCallback(() => setEditMode(true), [])

packages/@dcl/inspector/src/hooks/sdk/useEntityComponent.ts

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ export const DISABLED_COMPONENTS: string[] = [
1717
CoreComponents.NETWORK_ENTITY,
1818
CoreComponents.TWEEN_SEQUENCE,
1919
ComponentName.ADMIN_TOOLS,
20-
ComponentName.REWARDS
20+
ComponentName.REWARDS,
21+
CoreComponents.AVATAR_ATTACH
2122
]
2223

2324
export const SMART_ITEM_COMPONENTS: string[] = [
@@ -62,16 +63,19 @@ const transformComponentName = (value: string): string => {
6263
case CoreComponents.GLTF_CONTAINER:
6364
return 'GLTF'
6465
default:
65-
return value
66+
return value.replace(/inspector::/, '')
6667
}
6768
}
69+
6870
export const getComponentName = (value: string) => (transformComponentName(value).match(/[^:]*$/) || [])[0] || '?'
6971

7072
export const useEntityComponent = () => {
7173
const sdk = useSdk()
72-
const [availableComponentsState, setAvailableComponentsState] = useState<Array<{ id: number; name: string }> | null>(
73-
null
74-
)
74+
const [availableComponentsState, setAvailableComponentsState] = useState<Array<{
75+
id: number
76+
name: string
77+
isOnEntity: boolean
78+
}> | null>(null)
7579

7680
useChange(({ component, operation }) => {
7781
if (!component) return
@@ -140,10 +144,26 @@ export const useEntityComponent = () => {
140144
const getAvailableComponents = useCallback(
141145
(entity: Entity) => {
142146
if (availableComponentsState === null) {
143-
const missing = getComponents(entity, true)
144-
let available = Array.from(missing.entries())
147+
const allSystemComponents = new Map<number, string>()
148+
if (sdk) {
149+
for (const component of sdk.engine.componentsIter()) {
150+
debugger
151+
allSystemComponents.set(component.componentId, component.componentName)
152+
}
153+
}
154+
155+
let available = Array.from(allSystemComponents.entries())
145156
.filter(([_, name]) => enabledComponents.has(name))
146-
.map(([id, name]) => ({ id, name: getComponentName(name) }))
157+
.map(([id, name]) => {
158+
const component = sdk?.engine.getComponentOrNull(id)
159+
const isOnEntity = component ? component.has(entity) : false
160+
161+
return {
162+
id,
163+
name: getComponentName(name),
164+
isOnEntity
165+
}
166+
})
147167
.sort((a, b) => a.name.localeCompare(b.name))
148168

149169
if (isRoot(entity)) {
@@ -160,7 +180,7 @@ export const useEntityComponent = () => {
160180

161181
return availableComponentsState
162182
},
163-
[getComponents, enabledComponents, availableComponentsState]
183+
[getComponents, enabledComponents, availableComponentsState, sdk]
164184
)
165185

166186
return { getComponents, addComponent, removeComponent, getAvailableComponents }

0 commit comments

Comments
 (0)