diff --git a/newIDE/app/src/AiGeneration/UseSearchAndInstallAsset.js b/newIDE/app/src/AiGeneration/UseSearchAndInstallAsset.js index 7878d9efc6..20478dd934 100644 --- a/newIDE/app/src/AiGeneration/UseSearchAndInstallAsset.js +++ b/newIDE/app/src/AiGeneration/UseSearchAndInstallAsset.js @@ -76,11 +76,11 @@ export const useSearchAndInstallAsset = ({ } if (!assetShortHeader) { - if (!objectType && !exactOrPartialAssetId) { + if (!assetSearchOptions.searchTerms && !exactOrPartialAssetId) { return { status: 'error', message: - 'Cannot search for an asset without an object type. Specify either `object_type` or `asset_id`.', + 'Cannot search for an asset without either `searchTerms` or `exactOrPartialAssetId`.', createdObjects: [], assetShortHeader: null, isTheFirstOfItsTypeInProject: false, diff --git a/newIDE/app/src/EditorFunctions/EditorFunctions.spec.js b/newIDE/app/src/EditorFunctions/EditorFunctions.spec.js index 125de8be86..715aa82f00 100644 --- a/newIDE/app/src/EditorFunctions/EditorFunctions.spec.js +++ b/newIDE/app/src/EditorFunctions/EditorFunctions.spec.js @@ -113,7 +113,7 @@ describe('editorFunctions', () => { project.delete(); }); - it('creates a new object (from the asset store)', async () => { + it('creates a new object (from scratch, because only object_type was provided)', async () => { // $FlowFixMe[underconstrained-implicit-instantiation] const onObjectsModifiedOutsideEditor = jest.fn(); @@ -129,6 +129,33 @@ describe('editorFunctions', () => { } ); + expect(result.message).toMatchInlineSnapshot( + `"Created a new object (from scratch) called \\"MyNewTextObject\\" of type \\"TextObject::Text\\" in scene \\"TestScene\\". It has the following properties: bold: false (boolean), characterSize: 20 (Pixel), color: 0;0;0 (color), font: (resource), isOutlineEnabled: false (boolean), isShadowEnabled: false (boolean), italic: false (boolean), lineHeight: 0 (Pixel), outlineColor: 255;255;255 (color), outlineThickness: 2 (Pixel), shadowAngle: 90 (DegreeAngle), shadowBlurRadius: 2 (Pixel), shadowColor: 0;0;0 (color), shadowDistance: 4 (Pixel), shadowOpacity: 127 (Pixel), text: Text (multilinestring), textAlignment: left (choice, one of: [\\"left\\", \\"center\\", \\"right\\"]), verticalTextAlignment: top (choice, one of: [\\"top\\", \\"center\\", \\"bottom\\"])."` + ); + expect(result.success).toBe(true); + expect(onObjectsModifiedOutsideEditor).toHaveBeenCalledWith({ + scene: testScene, + isNewObjectTypeUsed: true, + }); + }); + + it('creates a new object (from the asset store, with search_terms and object_type provided)', async () => { + // $FlowFixMe[underconstrained-implicit-instantiation] + const onObjectsModifiedOutsideEditor = jest.fn(); + + const result: EditorFunctionGenericOutput = await editorFunctions.create_or_replace_object.launchFunction( + { + ...makeFakeLaunchFunctionOptionsWithProject(project), + args: { + scene_name: 'TestScene', + object_type: 'TextObject::Text', + object_name: 'MyNewTextObject', + search_terms: 'Very cool text object', + }, + onObjectsModifiedOutsideEditor, + } + ); + expect(result.message).toMatchInlineSnapshot( `"Created (from the asset store) object \\"MyNewTextObject\\" of type \\"TextObject::Text\\" in scene \\"TestScene\\". It has the following properties: bold: false (boolean), characterSize: 20 (Pixel), color: 0;0;0 (color), font: (resource), isOutlineEnabled: false (boolean), isShadowEnabled: false (boolean), italic: false (boolean), lineHeight: 0 (Pixel), outlineColor: 255;255;255 (color), outlineThickness: 2 (Pixel), shadowAngle: 90 (DegreeAngle), shadowBlurRadius: 2 (Pixel), shadowColor: 0;0;0 (color), shadowDistance: 4 (Pixel), shadowOpacity: 127 (Pixel), text: Text (multilinestring), textAlignment: left (choice, one of: [\\"left\\", \\"center\\", \\"right\\"]), verticalTextAlignment: top (choice, one of: [\\"top\\", \\"center\\", \\"bottom\\"])."` ); @@ -139,7 +166,33 @@ describe('editorFunctions', () => { }); }); - it('creates a new object (from scratch if not found in the asset store)', async () => { + it('creates a new object (from the asset store, with search_terms but without any specified object type)', async () => { + // $FlowFixMe[underconstrained-implicit-instantiation] + const onObjectsModifiedOutsideEditor = jest.fn(); + + const result: EditorFunctionGenericOutput = await editorFunctions.create_or_replace_object.launchFunction( + { + ...makeFakeLaunchFunctionOptionsWithProject(project), + args: { + scene_name: 'TestScene', + object_name: 'SomeNewObject', + search_terms: 'A very cool sprite for my player in my game', + }, + onObjectsModifiedOutsideEditor, + } + ); + + expect(result.message).toMatchInlineSnapshot( + `"Created (from the asset store) object \\"SomeNewObject\\" of type \\"Sprite\\" in scene \\"TestScene\\". It has the following properties: ."` + ); + expect(result.success).toBe(true); + expect(onObjectsModifiedOutsideEditor).toHaveBeenCalledWith({ + scene: testScene, + isNewObjectTypeUsed: false, + }); + }); + + it('creates a new object (from scratch, fallback if not found in the asset store)', async () => { // $FlowFixMe[underconstrained-implicit-instantiation] const onObjectsModifiedOutsideEditor = jest.fn(); @@ -556,7 +609,7 @@ describe('editorFunctions', () => { expect(testScene.getObjects().hasObjectNamed('MyAssetObject')).toBe(true); }); - it('fails when creating a new object without object_type nor asset_id', async () => { + it('fails when creating a new object without object_type, search_terms nor asset_id', async () => { const result: EditorFunctionGenericOutput = await editorFunctions.create_or_replace_object.launchFunction( { ...makeFakeLaunchFunctionOptionsWithProject(project), @@ -569,7 +622,7 @@ describe('editorFunctions', () => { expect(result.success).toBe(false); expect(result.message).toMatchInlineSnapshot( - `"Cannot create object \\"MyAssetObject\\": specify either \\"object_type\\" or \\"asset_id\\"."` + `"No search_terms or asset_id were provided to create the object \\"MyAssetObject\\". This object was not created."` ); }); diff --git a/newIDE/app/src/EditorFunctions/index.js b/newIDE/app/src/EditorFunctions/index.js index 0390169acb..410da432ab 100644 --- a/newIDE/app/src/EditorFunctions/index.js +++ b/newIDE/app/src/EditorFunctions/index.js @@ -811,98 +811,103 @@ const createOrReplaceObject: EditorFunction = { ); } - if (!candidateType && !asset_id) { - return makeGenericFailure( - `Cannot create object "${targetObjectName}": specify either "object_type" or "asset_id".` - ); - } - const targetObjectsContainer = target_object_scope === 'global' ? globalObjects : layoutObjects; - // First try to search and install an object from the asset store. - try { - const { - status, - message, - createdObjects, - assetShortHeader, - isTheFirstOfItsTypeInProject, - } = await searchAndInstallAsset({ - objectsContainer: targetObjectsContainer, - objectName: targetObjectName, - objectType: candidateType, - searchTerms: search_terms || '', - description: description || '', - twoDimensionalViewKind: two_dimensional_view_kind || '', - exactOrPartialAssetId: asset_id || null, - relatedAiRequestId, - ...getRelatedAiRequestLastMessages(), - }); - - if (status === 'error') { + if (candidateType && !search_terms && !asset_id) { + // Do nothing: there is nothing given apart from an object type, + // which we can still use to fallback to create from scratch. + } else { + if (!search_terms && !asset_id) { return makeGenericFailure( - `Unable to search and install object (${message}).` + `No search_terms or asset_id were provided to create the object "${targetObjectName}". This object was not created.` ); - } else if (status === 'asset-installed') { - // Update behaviors shared data for the scene where the object was created. - // Assets from the store can come with behaviors that have shared data. - if (target_object_scope === 'global') { - gd.WholeProjectRefactorer.updateBehaviorsSharedData(project); - } else { - layout.updateBehaviorsSharedData(project); - } + } - // /!\ Tell the editor that some objects have potentially been modified (and even removed). - // This will force the objects panel to refresh. - onObjectsModifiedOutsideEditor({ - scene: layout, - isNewObjectTypeUsed: isTheFirstOfItsTypeInProject, + // First try to search and install an object from the asset store. + try { + const { + status, + message, + createdObjects, + assetShortHeader, + isTheFirstOfItsTypeInProject, + } = await searchAndInstallAsset({ + objectsContainer: targetObjectsContainer, + objectName: targetObjectName, + objectType: candidateType, + searchTerms: search_terms || '', + description: description || '', + twoDimensionalViewKind: two_dimensional_view_kind || '', + exactOrPartialAssetId: asset_id || null, + relatedAiRequestId, + ...getRelatedAiRequestLastMessages(), }); - if (createdObjects.length === 1) { - const object = createdObjects[0]; - const result: EditorFunctionGenericOutput = { - success: true, - message: [ - `Created (from the asset store) object "${object.getName()}" of type "${object.getType()}" in scene "${scene_name}".`, - getPropertiesText(object), - ].join(' '), - objectSizeInfo: { - [object.getName()]: getObjectSizeInfo( - object, - project, - PixiResourcesLoader, - assetShortHeader - ), - }, - }; - return result; - } - - return makeGenericSuccess( - `Created (from the asset store) ${createdObjects - .map( - object => - `object "${object.getName()}" of type "${object.getType()}"` - ) - .join(', ')} in scene "${scene_name}".` - ); - } else { - if (asset_id) { + if (status === 'error') { return makeGenericFailure( - `No asset found with id "${asset_id}". The object was not created.` + `Unable to search and install object (${message}).` ); - } + } else if (status === 'asset-installed') { + // Update behaviors shared data for the scene where the object was created. + // Assets from the store can come with behaviors that have shared data. + if (target_object_scope === 'global') { + gd.WholeProjectRefactorer.updateBehaviorsSharedData(project); + } else { + layout.updateBehaviorsSharedData(project); + } - // No asset found - we'll create an object from scratch. + // /!\ Tell the editor that some objects have potentially been modified (and even removed). + // This will force the objects panel to refresh. + onObjectsModifiedOutsideEditor({ + scene: layout, + isNewObjectTypeUsed: isTheFirstOfItsTypeInProject, + }); + + if (createdObjects.length === 1) { + const object = createdObjects[0]; + const result: EditorFunctionGenericOutput = { + success: true, + message: [ + `Created (from the asset store) object "${object.getName()}" of type "${object.getType()}" in scene "${scene_name}".`, + getPropertiesText(object), + ].join(' '), + objectSizeInfo: { + [object.getName()]: getObjectSizeInfo( + object, + project, + PixiResourcesLoader, + assetShortHeader + ), + }, + }; + return result; + } + + return makeGenericSuccess( + `Created (from the asset store) ${createdObjects + .map( + object => + `object "${object.getName()}" of type "${object.getType()}"` + ) + .join(', ')} in scene "${scene_name}".` + ); + } else { + if (asset_id) { + return makeGenericFailure( + `No asset found with id "${asset_id}". The object was not created.` + ); + } + + // No asset found - we'll create an object from scratch. + } + } catch (error) { + return makeGenericFailure( + `An unexpected error happened while search and installing objects (${ + error.message + }).` + ); } - } catch (error) { - return makeGenericFailure( - `An unexpected error happened while search and installing objects (${ - error.message - }).` - ); } // Create an object from scratch: this requires a known object type.