@@ -9,7 +9,7 @@ import { Entity } from '@dcl/ecs'
99import { WithSdkProps , withSdk } from '../../../hoc/withSdk'
1010import { useChange } from '../../../hooks/sdk/useChange'
1111import { useComponentValue } from '../../../hooks/sdk/useComponentValue'
12- import { isRoot , useEntityComponent } from '../../../hooks/sdk/useEntityComponent'
12+ import { isRoot , useEntityComponent , getComponentName } from '../../../hooks/sdk/useEntityComponent'
1313import { useHasComponent } from '../../../hooks/sdk/useHasComponent'
1414import { CAMERA , PLAYER , ROOT } from '../../../lib/sdk/tree'
1515import { 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 ) , [ ] )
0 commit comments