Skip to content

feat(spx-gui): add configurable game size preview demo - #3477

Open
qingqing-ux wants to merge 2 commits into
goplus:uifrom
qingqing-ux:issue-3452
Open

qingqing-ux wants to merge 2 commits into
goplus:uifrom
qingqing-ux:issue-3452

Conversation

@qingqing-ux

@qingqing-ux qingqing-ux commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

Closes #3452

Changes

  • Let users choose Original 4:3, Landscape 16:9, Portrait 9:16, or a custom game size when creating a project.
  • Open the editor directly after the project is created.
  • Keep the Preview panel stable while only the internal game viewport changes ratio and remains centered.
  • Preserve the selected ratio across editing preview, run, and fullscreen states so sprites and backdrops are not distorted.
  • Add a demo switch for hiding the Sprites and Stage panels to evaluate a more focused Preview layout.

Design considerations

  • Keep the editor panel layout independent from the game viewport ratio.
  • Center landscape viewports with vertical whitespace and portrait viewports with horizontal whitespace.
  • Retain the original 4:3 size as the default while supporting landscape, portrait, and custom ratios.
  • Select the game canvas size and aspect ratio when creating a project, then use the same ratio in the editor preview, run, and fullscreen modes.
  • Reference the layout proportions from PR [Demo PR] Single-Sprite Tutorial Mode UI Design #3425 so the Preview can use the right-side workspace when the lower panels are hidden.

Validation path

  1. Open /docs/game-size-preview in the preview environment for this PR.
  2. Create projects with 4:3, 16:9, 9:16, and custom sizes.
  3. Confirm that each project opens directly in the editor after creation.
  4. Confirm that the Preview panel remains stable while the internal game viewport stays proportionally centered.
  5. Check preview, run, and fullscreen states and confirm that sprites and backdrops are not distorted.
  6. Open the profile menu and toggle Hide Sprites and Stage panels.
  7. Confirm that the Preview uses the right-side editor area in focused mode and that the default layout is restored after turning the option off.

This page is for design validation only. Each visitor creates a temporary demo project within their current page session. Demo projects are not saved, published, or shared between visitors.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7b337c6da7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

constructor(owner?: string, name?: string, inits?: SpxProjectInits) {
super()
const reactiveThis = reactive(this) as this
const viewportSize = inits?.viewportSize ?? defaultViewportSize

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Hydrate the viewport size when loading project files

When a custom-size project is imported or reopened, the production loaders construct new SpxProject() without initializers, so this fallback fixes viewportSize at 480×360; loadFiles() still destructures and ignores config.run. Consequently, a 16:9 or portrait project exported from the new demo is displayed at the wrong aspect ratio after loading, and its next export silently overwrites the original run dimensions. The load path needs to apply the persisted run dimensions to viewportSize rather than relying solely on constructor initialization.

Useful? React with 👍 / 👎.

@fennoai fennoai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: Configurable game size + demo page

This PR cleanly generalizes the hardcoded 4:3 viewport into a configurable viewportSize, threads it through the model, editor preview, runner surface, and fullscreen states, and adds a well-isolated /docs/game-size-preview demo. Model changes are covered by a good parametrized test, the defensive copy of viewportSize ({ ...viewportSize }) is a nice touch, and ProjectEditorDemo.vue handles lifecycle/disposal correctly. Security review found no issues — user-supplied width/height are strictly number | null (via UINumberInput) before reaching aspect-ratio/style strings, and the name regex /^[\w-]+$/ is anchored with no ReDoS risk.

A few points worth addressing, the first being the most important:

Load/export asymmetry for viewport size. exportGameFilesWithoutMemo now writes run: { width, height } from viewportSize, but loadFiles still explicitly ignores the loaded run config, and viewportSize is only ever set in the constructor. Since all real load paths do new SpxProject() then .load(serialized), a project saved with a non-default size (e.g. 720×405) will reload as the default 480×360 — and the next save silently overwrites the persisted run size back to default. This undermines the feature's goal of preserving the chosen ratio across sessions. The stale comment at project.ts:446-447 ("the fixed viewport / run size is used") should also be updated since the viewport is no longer fixed. If restoring on load is out of scope for this PR, please at least make the comment reflect the actual (intentional) behavior.

Remaining items are inline. Minor/non-blocking notes not inlined:

  • Consider rAF-coalescing writes in useContentSize (utils/dom.ts) and skipping no-op size updates; this PR adds two new resize-sensitive consumers (EditorPreview, ProjectRunnerSurface) whose computed styles mutate element size, so an un-throttled ResizeObserver can drive per-frame recompute during a drag-resize. Benefits existing call sites too.
  • New public-ish surface lacks docs: SpxProjectInits.viewportSize / readonly viewportSize (project.ts), the layout prop on ProjectEditor.vue, and fillContainer on EditorPreview.vue. A one-line JSDoc on each would help.

Findings without inline locations

  • spx-gui/src/models/spx/project.ts:448: run: runConfig is destructured but never applied, and viewportSize is only set from constructor inits — yet exportGameFilesWithoutMemo (line 533) now writes run: { width, height } from viewportSize. Real load paths (new SpxProject() + .load()) will therefore reload any custom-sized project as the default 480×360 and overwrite the saved size on next export. Restore viewportSize from runConfig here, and update the stale comment above ("the fixed viewport / run size is used") which no longer holds.

</template>

<script lang="ts">
type GameSize = {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GameSize = { width: number; height: number } structurally duplicates the exported ViewportSize type in models/spx/project.ts, and the parent (index.vue) already imports ViewportSize and treats the emitted gameSize as one. Consider importing and reusing ViewportSize for Preset and the created emit to drop the duplication and the gameSize/viewportSize naming mismatch.

const runnable = computed(() => project.value != null && !isLoading.value && error.value == null)
const projectAspectRatio = computed(() => {
const viewportSize = project.value?.viewportSize
if (viewportSize == null) return '4 / 3'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The '4 / 3' fallback re-introduces the hardcoded ratio this PR is removing and duplicates defaultViewportSize knowledge (same pattern in community/project.vue:77). Since viewportSize is non-optional on a loaded project, this branch only fires while project is null; deriving the fallback from defaultMapSize (${defaultMapSize.width} / ${defaultMapSize.height}) keeps a single source of truth.


const previewSprite = await addDemoSprite(project)
for (let i = project.sprites.length; i < demoSpriteCount; i += 1) {
project.addSprite(sourceSprite.clone())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sourceSprite.clone() deep-clones all costumes/animations up to demoSpriteCount (20) times, then populateDemoSprites immediately hides all but the preview sprite — so 19 fully-cloned, reactively-registered sprites are never displayed. Each addSprite also rebuilds zorder (this.zorder = [...this.zorder, id]), making this O(n²). Fine at 20, but if the count is ever raised consider cloning lazily or lowering it.

This branch was successfully deployed

1 active deployment
Preview – builder 7b337c6d Deployed Aug 28, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant