@@ -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,27 @@ export default React.memo(
141145 [ isBasicViewEnabled , handleAddComponent , handleOpenModal ]
142146 )
143147
148+ const getComponentTooltip = useCallback (
149+ ( componentId : number , description : string , link ?: string ) => {
150+ const componentInfo = availableComponents . find ( ( c ) => c . id === componentId )
151+
152+ if ( componentInfo ?. isOnEntity ) {
153+ return {
154+ text : 'This component is already added. An entity can only have one copy of each component.'
155+ }
156+ }
157+
158+ if ( componentId === sdk . components . VisibilityComponent . componentId ) {
159+ return {
160+ text : 'You must have either a GLTF Container or a Mesh Collider component to use this component.'
161+ }
162+ }
163+
164+ return { text : description , ...( link && { link } ) }
165+ } ,
166+ [ isComponentDisabled , availableComponents , sdk . components . VisibilityComponent . componentId ]
167+ )
168+
144169 const componentOptions = useMemo ( ( ) => {
145170 const options = [
146171 { header : '3D Content' } ,
@@ -152,19 +177,23 @@ export default React.memo(
152177 sdk . components . GltfContainer . componentId ,
153178 sdk . components . GltfContainer . componentName
154179 ) ,
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- }
180+ disabled : isComponentDisabled ( sdk . components . GltfContainer . componentId ) ,
181+ tooltip : getComponentTooltip (
182+ sdk . components . GltfContainer . componentId ,
183+ "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."
184+ )
158185 } ,
159186 {
160187 id : sdk . components . Material . componentId ,
161188 value : 'Material' ,
162189 onClick : ( ) =>
163190 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- }
191+ disabled : isComponentDisabled ( sdk . components . Material . componentId ) ,
192+ tooltip : getComponentTooltip (
193+ sdk . components . Material . componentId ,
194+ 'Material determines the visual appearance of an object. It defines properties such as color, texture, and transparency' ,
195+ 'https://docs.decentraland.org/creator/development-guide/sdk7/materials/'
196+ )
168197 } ,
169198 {
170199 id : sdk . components . VisibilityComponent . componentId ,
@@ -174,92 +203,96 @@ export default React.memo(
174203 sdk . components . VisibilityComponent . componentId ,
175204 sdk . components . VisibilityComponent . componentName
176205 ) ,
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' )
206+ disabled : isComponentDisabled ( sdk . components . VisibilityComponent . componentId ) ,
207+ tooltip : getComponentTooltip (
208+ sdk . components . VisibilityComponent . componentId ,
209+ '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.'
210+ )
192211 } ,
193212 {
194213 id : sdk . components . MeshRenderer . componentId ,
195214 value : 'Mesh Renderer' ,
196215 onClick : ( ) =>
197216 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- }
217+ disabled : isComponentDisabled ( sdk . components . MeshRenderer . componentId ) ,
218+ tooltip : getComponentTooltip (
219+ sdk . components . MeshRenderer . componentId ,
220+ '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' ,
221+ 'https://docs.decentraland.org/creator/development-guide/sdk7/shape-components/'
222+ )
202223 } ,
203224 {
204225 id : sdk . components . MeshCollider . componentId ,
205226 value : 'Mesh Collider' ,
206227 onClick : ( ) =>
207228 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- }
229+ disabled : isComponentDisabled ( sdk . components . MeshCollider . componentId ) ,
230+ tooltip : getComponentTooltip (
231+ sdk . components . MeshCollider . componentId ,
232+ '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' ,
233+ 'https://docs.decentraland.org/creator/development-guide/sdk7/colliders/'
234+ )
212235 } ,
213236 { header : 'Interaction' } ,
214237 {
215238 id : sdk . components . States . componentId ,
216239 value : 'States' ,
217240 onClick : ( ) =>
218241 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- }
242+ disabled : isComponentDisabled ( sdk . components . States . componentId ) ,
243+ tooltip : getComponentTooltip (
244+ sdk . components . States . componentId ,
245+ 'States specify the status of entities. Use triggers to check or change states, and set actions accordingly.' ,
246+ 'https://docs.decentraland.org/creator/smart-items/#states'
247+ )
223248 } ,
224249 {
225250 id : sdk . components . Triggers . componentId ,
226251 value : 'Triggers' ,
227252 onClick : ( ) =>
228253 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- }
254+ disabled : isComponentDisabled ( sdk . components . Triggers . componentId ) ,
255+ tooltip : getComponentTooltip (
256+ sdk . components . Triggers . componentId ,
257+ 'Triggers activate actions based on player interactions like clicks, entering/exiting areas, or global events like "on spawn".' ,
258+ 'https://docs.decentraland.org/creator/smart-items/#triggers'
259+ )
233260 } ,
234261 {
235262 id : sdk . components . Actions . componentId ,
236263 value : 'Actions' ,
237264 onClick : ( ) =>
238265 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- }
266+ disabled : isComponentDisabled ( sdk . components . Actions . componentId ) ,
267+ tooltip : getComponentTooltip (
268+ sdk . components . Actions . componentId ,
269+ 'Actions list the capabilities of entities, from playing animations to changing visibility. Customize or add new actions, which are activated by triggers.' ,
270+ 'https://docs.decentraland.org/creator/smart-items/#actions'
271+ )
243272 } ,
244273 {
245274 id : sdk . components . AudioSource . componentId ,
246275 value : 'Audio Source' ,
247276 onClick : ( ) =>
248277 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- }
278+ disabled : isComponentDisabled ( sdk . components . AudioSource . componentId ) ,
279+ tooltip : getComponentTooltip (
280+ sdk . components . AudioSource . componentId ,
281+ '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' ,
282+ 'https://docs.decentraland.org/creator/development-guide/sdk7/sounds'
283+ )
253284 } ,
254285 {
255286 id : sdk . components . TextShape . componentId ,
256287 value : 'Text Shape' ,
257288 onClick : ( ) =>
258289 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- }
290+ disabled : isComponentDisabled ( sdk . components . TextShape . componentId ) ,
291+ tooltip : getComponentTooltip (
292+ sdk . components . TextShape . componentId ,
293+ 'Use TextShape to display text in the 3D space' ,
294+ 'https://docs.decentraland.org/creator/development-guide/sdk7/text'
295+ )
263296 } ,
264297 {
265298 id : sdk . components . PointerEvents . componentId ,
@@ -269,10 +302,12 @@ export default React.memo(
269302 sdk . components . PointerEvents . componentId ,
270303 sdk . components . PointerEvents . componentName
271304 ) ,
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- }
305+ disabled : isComponentDisabled ( sdk . components . PointerEvents . componentId ) ,
306+ tooltip : getComponentTooltip (
307+ sdk . components . PointerEvents . componentId ,
308+ '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' ,
309+ 'https://docs.decentraland.org/creator/development-guide/sdk7/click-events'
310+ )
276311 }
277312 ]
278313
@@ -283,23 +318,23 @@ export default React.memo(
283318 return set
284319 } , new Set < number > ( ) )
285320
286- const availableIds = availableComponents . reduce ( ( set , component ) => set . add ( component . id ) , new Set < number > ( ) )
287-
288321 if ( availableComponents . some ( ( component ) => ! optionIds . has ( component . id ) ) ) {
289322 options . push ( { header : 'Other' } )
290323 for ( const component of availableComponents ) {
291324 if ( ! optionIds . has ( component . id ) ) {
292325 options . push ( {
293326 id : component . id ,
294327 value : component . name ,
295- onClick : ( ) => handleClickAddComponent ( component . id , component . name )
296- } as any )
328+ onClick : ( ) => handleClickAddComponent ( component . id , component . name ) ,
329+ disabled : isComponentDisabled ( component . id ) ,
330+ tooltip : getComponentTooltip ( component . id , `${ component . name } component` )
331+ } )
297332 }
298333 }
299334 }
300335
301- return options . filter ( ( option ) => ! option . id || availableIds . has ( option . id ) )
302- } , [ sdk , availableComponents , isComponentDisabled , handleClickAddComponent ] )
336+ return options
337+ } , [ sdk , availableComponents , isComponentDisabled , handleClickAddComponent , getComponentTooltip ] )
303338
304339 const quitEditMode = useCallback ( ( ) => setEditMode ( false ) , [ ] )
305340 const enterEditMode = useCallback ( ( ) => setEditMode ( true ) , [ ] )
0 commit comments