diff --git a/newIDE/app/src/InstancesEditor/index.js b/newIDE/app/src/InstancesEditor/index.js index ee698210662d..5d815ad9a7b1 100644 --- a/newIDE/app/src/InstancesEditor/index.js +++ b/newIDE/app/src/InstancesEditor/index.js @@ -119,6 +119,8 @@ export type InstancesEditorPropsWithoutSizeAndScroll = {| instancesEditorShortcutsCallbacks: InstancesEditorShortcutsCallbacks, tileMapTileSelection: ?TileMapTileSelection, onSelectTileMapTile: (?TileMapTileSelection) => void, + zoomToFitContentOnSceneLoad?: boolean, + onZoomToFitContentDone?: () => void, |}; type Props = {| @@ -424,14 +426,38 @@ export default class InstancesEditor extends Component { }); this._mountEditorComponents(this.props); + this.fpsLimiter.forceNextUpdate(); this._renderScene(); if (this.props.onViewPositionChanged) { // Call it at the end, so that the top component knows the view position // is initialized. this.props.onViewPositionChanged(this.viewPosition); } + this.onCanvasAndRendererInitialized(); } + onCanvasAndRendererInitialized = () => { + if (this.props.zoomToFitContentOnSceneLoad) { + // Try zooming at multiple intervals to ensure we catch the content when it's ready + // Earlier attempts have a chance to have an incomplete AABB, (it's random depending on loading/rendering timing) + // so we do it several times. + setTimeout(() => { + this.zoomToFitContent(); + }, 100); + + setTimeout(() => { + this.zoomToFitContent(); + }, 200); + + setTimeout(() => { + this.zoomToFitContent(); + if (this.props.onZoomToFitContentDone) { + this.props.onZoomToFitContentDone(); + } + }, 500); + } + }; + /** * Force the internal InstancesRenderer to be destroyed and recreated * (as well as other components holding references to instances). Call @@ -1477,7 +1503,9 @@ export default class InstancesEditor extends Component { zoomToFitContent = () => { const contentAABB = this.getContentAABB(); - if (contentAABB) this.fitViewToRectangle(contentAABB, { adaptZoom: true }); + if (contentAABB) { + this.fitViewToRectangle(contentAABB, { adaptZoom: true }); + } }; _getAreaRectangle = (): Rectangle => { diff --git a/newIDE/app/src/MainFrame/EditorContainers/BaseEditor.js b/newIDE/app/src/MainFrame/EditorContainers/BaseEditor.js index fd928874f053..ce55d2574cca 100644 --- a/newIDE/app/src/MainFrame/EditorContainers/BaseEditor.js +++ b/newIDE/app/src/MainFrame/EditorContainers/BaseEditor.js @@ -33,6 +33,10 @@ export type EditorContainerExtraProps = {| // Ask AI mode?: 'chat' | 'agent', aiRequestId?: string | null, + + // Scene editor + zoomToFitContentOnSceneLoad?: boolean, + onZoomToFitContentDone?: () => void, |}; export type SceneEventsOutsideEditorChanges = {| diff --git a/newIDE/app/src/MainFrame/EditorContainers/SceneEditorContainer.js b/newIDE/app/src/MainFrame/EditorContainers/SceneEditorContainer.js index d900a7cb256e..2d55eb0ad49a 100644 --- a/newIDE/app/src/MainFrame/EditorContainers/SceneEditorContainer.js +++ b/newIDE/app/src/MainFrame/EditorContainers/SceneEditorContainer.js @@ -220,6 +220,16 @@ export class SceneEditorContainer extends React.Component {}} // Nothing to do as scenes are not events-based objects. onEventsBasedObjectChildrenEdited={() => {}} + zoomToFitContentOnSceneLoad={ + this.props.extraEditorProps + ? this.props.extraEditorProps.zoomToFitContentOnSceneLoad + : undefined + } + onZoomToFitContentDone={ + this.props.extraEditorProps + ? this.props.extraEditorProps.onZoomToFitContentDone + : undefined + } /> ); } diff --git a/newIDE/app/src/MainFrame/index.js b/newIDE/app/src/MainFrame/index.js index 031366733840..6dfc6df64d0e 100644 --- a/newIDE/app/src/MainFrame/index.js +++ b/newIDE/app/src/MainFrame/index.js @@ -203,6 +203,7 @@ import { useGamesPlatformFrame } from './EditorContainers/HomePage/PlaySection/U import { useExtensionLoadErrorDialog } from '../Utils/UseExtensionLoadErrorDialog'; import { PanesContainer } from './PanesContainer'; import StandaloneDialog from './StandAloneDialog'; +import { mapFor } from '../Utils/MapFor'; const GD_STARTUP_TIMES = global.GD_STARTUP_TIMES || []; @@ -557,6 +558,10 @@ const MainFrame = (props: Props) => { renderExtensionLoadErrorDialog, } = useExtensionLoadErrorDialog(); + const zoomToFitContentOnSceneLoadByName = React.useRef<{ [string]: boolean }>( + {} + ); + /** * This reference is useful to get the current opened project, * even in the callback of a hook/promise - without risking to read "stale" data. @@ -662,6 +667,15 @@ const MainFrame = (props: Props) => { ? { storageProviders: props.storageProviders } : kind === 'ask-ai' ? { mode, aiRequestId } + : kind === 'layout' + ? { + zoomToFitContentOnSceneLoad: + zoomToFitContentOnSceneLoadByName.current[name], + onZoomToFitContentDone: () => { + // Only zoom to fit once on scene load. + delete zoomToFitContentOnSceneLoadByName.current[name]; + }, + } : undefined; return { icon, @@ -854,6 +868,8 @@ const MainFrame = (props: Props) => { editorTabs: closeProjectTabs(state.editorTabs, currentProject), })); + zoomToFitContentOnSceneLoadByName.current = {}; + // Delete the project from memory. All references to it have been dropped previously // by the setState. console.info('Deleting project from memory...'); @@ -1177,6 +1193,17 @@ const MainFrame = (props: Props) => { openLeaderboardReplacerDialogIfNeeded(project, oldProjectId); configureMultiplayerLobbiesIfNeeded(project, oldProjectId); } + + // Assume that when a project is created, we always want to zoom to fit + // the content when opening a scene for the first time. + zoomToFitContentOnSceneLoadByName.current = {}; + const layoutNames = mapFor(0, project.getLayoutsCount(), i => { + return project.getLayoutAt(i).getName(); + }); + layoutNames.forEach(layoutName => { + zoomToFitContentOnSceneLoadByName.current[layoutName] = true; + }); + if (!options.dontOpenAnySceneOrProjectManager) { options.openAllScenes || options.openQuickCustomizationDialog ? openAllScenes({ @@ -3464,7 +3491,7 @@ const MainFrame = (props: Props) => { await createProjectFromTutorial(tutorialId, { storageProvider: emptyStorageProvider, saveAsLocation: null, - creationSource: 'in-app-tutorial', + creationSource: 'tutorial', // Remaining will be set by the template. }); } catch (error) { diff --git a/newIDE/app/src/ProjectCreation/NewProjectSetupDialog.js b/newIDE/app/src/ProjectCreation/NewProjectSetupDialog.js index 547dad8fb039..7d713c2e337b 100644 --- a/newIDE/app/src/ProjectCreation/NewProjectSetupDialog.js +++ b/newIDE/app/src/ProjectCreation/NewProjectSetupDialog.js @@ -89,7 +89,8 @@ export type NewProjectCreationSource = | 'quick-customization' | 'ai-agent-request' | 'course-chapter' - | 'in-app-tutorial'; + | 'in-app-tutorial' + | 'tutorial'; export type NewProjectSetup = {| storageProvider: StorageProvider, diff --git a/newIDE/app/src/SceneEditor/EditorsDisplay.flow.js b/newIDE/app/src/SceneEditor/EditorsDisplay.flow.js index bf7f80a7601b..877fc8069d44 100644 --- a/newIDE/app/src/SceneEditor/EditorsDisplay.flow.js +++ b/newIDE/app/src/SceneEditor/EditorsDisplay.flow.js @@ -135,6 +135,9 @@ export type SceneEditorsDisplayProps = {| instancesEditorShortcutsCallbacks: InstancesEditorShortcutsCallbacks, onOpenedEditorsChanged: () => void, + + zoomToFitContentOnSceneLoad?: boolean, + onZoomToFitContentDone?: () => void, |}; export type SceneEditorsDisplayInterface = {| diff --git a/newIDE/app/src/SceneEditor/MosaicEditorsDisplay/index.js b/newIDE/app/src/SceneEditor/MosaicEditorsDisplay/index.js index a07f59bd2d8b..df2817a7c575 100644 --- a/newIDE/app/src/SceneEditor/MosaicEditorsDisplay/index.js +++ b/newIDE/app/src/SceneEditor/MosaicEditorsDisplay/index.js @@ -377,6 +377,8 @@ const MosaicEditorsDisplay = React.forwardRef< pauseRendering={!props.isActive} tileMapTileSelection={props.tileMapTileSelection} onSelectTileMapTile={props.onSelectTileMapTile} + zoomToFitContentOnSceneLoad={props.zoomToFitContentOnSceneLoad} + onZoomToFitContentDone={props.onZoomToFitContentDone} /> ), }, diff --git a/newIDE/app/src/SceneEditor/SwipeableDrawerEditorsDisplay/index.js b/newIDE/app/src/SceneEditor/SwipeableDrawerEditorsDisplay/index.js index 4779ca4c2d66..2a64a6dc024f 100644 --- a/newIDE/app/src/SceneEditor/SwipeableDrawerEditorsDisplay/index.js +++ b/newIDE/app/src/SceneEditor/SwipeableDrawerEditorsDisplay/index.js @@ -303,6 +303,8 @@ const SwipeableDrawerEditorsDisplay = React.forwardRef< showBasicProfilingCounters={values.showBasicProfilingCounters} tileMapTileSelection={props.tileMapTileSelection} onSelectTileMapTile={props.onSelectTileMapTile} + zoomToFitContentOnSceneLoad={props.zoomToFitContentOnSceneLoad} + onZoomToFitContentDone={props.onZoomToFitContentDone} />
diff --git a/newIDE/app/src/SceneEditor/index.js b/newIDE/app/src/SceneEditor/index.js index 3dd04545a059..2de5352c75bb 100644 --- a/newIDE/app/src/SceneEditor/index.js +++ b/newIDE/app/src/SceneEditor/index.js @@ -151,6 +151,9 @@ type Props = {| // Preview: hotReloadPreviewButtonProps: HotReloadPreviewButtonProps, + + zoomToFitContentOnSceneLoad?: boolean, + onZoomToFitContentDone?: () => void, |}; type State = {| @@ -2152,6 +2155,10 @@ export default class SceneEditor extends React.Component { onOpenedEditorsChanged={this.updateToolbar} lastSelectionType={this.state.lastSelectionType} onExtensionInstalled={this.props.onExtensionInstalled} + zoomToFitContentOnSceneLoad={ + this.props.zoomToFitContentOnSceneLoad + } + onZoomToFitContentDone={this.props.onZoomToFitContentDone} /> {({ i18n }) => (