Let the project set the size of scene view symbols
game-maker / gate (push) Successful in 3m0s
game-maker / suite (push) Failing after 9m27s

Pixel art projects draw at very different sizes, so the fixed 18 px
icons of behaviors, containers and rigidbodies were too big in some
projects and too small in others. The size now lives in the project
settings and travels with the project.

- New field Symbol scale in the project settings dialog, below Play
  starts. It takes 0.1 to 4 with two decimals and defaults to 1.
- The value is stored as project.symbolScale. Older projects load
  with 1, and values out of range are clamped on load.
- Every change is its own undo step through a project command.
- The factor scales the icon size, the spacing of icons that share a
  spot, the label offset and the selection frame of icon-only symbols.
- The footprints in bounds.ts that Fit uses stay as they were. The
  hit area follows the icon, so clicks match what is drawn.
- uiHooks reports the icon size so e2e can measure it.

The e2e sum in the README is 1375. That is 1374 plus one new check,
and 1374 itself has not been confirmed by CI yet.

Tests:
- ./check.sh: 157 files, 2326 tests, CHECK EXIT 0 (before 2320).
- New tests/core/symbol-scale.test.ts with 5 tests (default, save
  and load, clamping, undo, icon frame), one more in
  project-settings.test.ts.
- e2e 46f: dialog opens with 1, 0.5 plus Apply gives a 9 px icon,
  undo gives 18 again. Module 46: ALL PASSED, 13 checks.
- Counter checks: without loading the value the save and load test
  fails. With the icon ignoring the factor exactly 46f fails. Both
  originals restored per cmp.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-09-24 10:55:42 +02:00
1 parent 5399ef17e5
commit 5afb70a92e
14 files changed
+233 -23

No files matched your search

+18
View File
@@ -1,5 +1,6 @@
import { collisionLayersOf } from '../model/collisionLayers';
import type { Project } from '../model/Project';
import { clampSymbolScale } from '../model/symbolScale';
import type { Command } from './Command';
export class RenameProjectCommand implements Command {
@@ -38,3 +39,20 @@ export class SetCollisionLayersCommand implements Command {
project.collisionLayers = this.previous;
}
}
/** Size of the scene view's symbol icons, kept inside its range like a loaded project's. */
export class SetSymbolScaleCommand implements Command {
readonly label = 'Symbol scale';
private previous = 1;
constructor(private readonly scale: number) {}
execute(project: Project): void {
this.previous = project.symbolScale;
project.symbolScale = clampSymbolScale(this.scale);
}
undo(project: Project): void {
project.symbolScale = this.previous;
}
}
+7
View File
@@ -7,6 +7,7 @@ import {
type SceneAsset,
} from './Asset';
import { DEFAULT_LAYER_NAME } from './collisionLayers';
import { DEFAULT_SYMBOL_SCALE } from './symbolScale';
/** 2: a placed container carries no copy of its template. */
export const PROJECT_FORMAT_VERSION = 2;
@@ -36,6 +37,11 @@ export interface Project {
* which is also what every collider starts on, so nothing collides differently than it did.
*/
collisionLayers: string[];
/**
* Size of the scene view's symbol icons as a factor, 0.1 to 4 (see `symbolScale.ts`). A project
* written before there was one reads as 1, the size the icons always had.
*/
symbolScale: number;
}
export function createEmptyProject(id: string, name: string): Project {
@@ -48,6 +54,7 @@ export function createEmptyProject(id: string, name: string): Project {
startSceneId: '',
plugins: [],
collisionLayers: [DEFAULT_LAYER_NAME],
symbolScale: DEFAULT_SYMBOL_SCALE,
};
}
+19
View File
@@ -0,0 +1,19 @@
/**
* How large the scene view draws its symbol icons, as a factor of their normal size. A project of
* 16-pixel art wants them smaller than one of 64-pixel art, which is why it belongs to the project
* and not to the browser.
*/
export const SYMBOL_SCALE_MIN = 0.1;
export const SYMBOL_SCALE_MAX = 4;
export const DEFAULT_SYMBOL_SCALE = 1;
/** Keeps a factor inside the range; anything that is not a finite number is the default. */
export function clampSymbolScale(value: unknown): number {
if (typeof value !== 'number' || !Number.isFinite(value)) return DEFAULT_SYMBOL_SCALE;
return Math.min(SYMBOL_SCALE_MAX, Math.max(SYMBOL_SCALE_MIN, value));
}
/** The factor as stored, read off untrusted JSON. A project written before there was one has 1. */
export function symbolScaleOf(raw: unknown): number {
return clampSymbolScale((raw as { symbolScale?: unknown } | null)?.symbolScale);
}
+16
View File
@@ -114,6 +114,22 @@ export function footprintCorners(
return boxCorners(placed, w + 2 * local, h + 2 * local, anchorOffset(type, props, w), 0);
}
/**
* The upright square of a symbol that is only its icon, centred on where the icon stands and grown
* with the project's symbol scale like the icon, plus `pad` world units on every side.
*/
export function iconCorners(type: AssetType, props: PropertyBag, at: Vec2, scale: number, pad = 0): Vec2[] {
const { w, h } = footprintOf(type, props);
const hw = (w * scale) / 2 + pad;
const hh = (h * scale) / 2 + pad;
return [
{ x: at.x - hw, y: at.y - hh },
{ x: at.x + hw, y: at.y - hh },
{ x: at.x + hw, y: at.y + hh },
{ x: at.x - hw, y: at.y + hh },
];
}
/** The world rectangle around the drawn footprint – what culling, framing and labels need. */
export function footprintExtent(
type: AssetType,
+2
View File
@@ -39,6 +39,7 @@ import type {
import { parseCellKey } from '../model/tilemap';
import { pluginsOf } from './projectPlugins';
import { collisionLayersOf } from '../model/collisionLayers';
import { symbolScaleOf } from '../model/symbolScale';
import { decodeCells, encodeCells } from './tilemapCells';
import { isAssetType } from '../model/AssetTypes';
import { isFontFile } from '../model/font';
@@ -248,6 +249,7 @@ export function deserializeProject(raw: unknown, fallbackId = ''): Project {
// is actually running.
plugins: pluginsOf(r),
collisionLayers: collisionLayersOf(r),
symbolScale: symbolScaleOf(r),
};
return project;
}