A source wears an icon without becoming decoration (#89)
game-maker / suite (push) Successful in 9m49s

Behavior and Container draw an icon because isSymbolType() is true for them,
but that predicate does not mean "draws an icon" — it means "is editor
decoration". Four places read it that way: collectWorldItems, GameViewport.
collectFrame and syncDrawOrder all drop symbols out of the running world and
the game picture, and placementOf() gives them an upright unscaled box.
Listing 'particles' there would have handed the source an icon and taken away
its particles, in the scene view as well as in the game.

So the two questions are two functions now. hasSceneIcon() says a type draws
an icon; isSymbolType() keeps meaning decoration. The icon itself is a
SceneViewport decoration, which is editor-only for free because GameViewport
never syncs decorations.

A ring (r 10, #c26a1f) with four sparks, upright and unscaled like the other
symbols, its middle left open — unlike Behavior and Container a source draws
something of its own, and the field is densest exactly there. The name sits
beside it and a dashed line runs to the parent, same colour.

Found while writing the tests: the source's grab box scaled with its transform
while the icon did not, so the two came apart. placementOf() returns only the
translation for 'particles', making the box the upright 24 x 24 that covers
its icon. The source's own transform still feeds every particle untouched.

824 checks (821 + 3).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
rootandClaude Opus 5 committed 2026-09-14 08:25:49 +02:00
1 parent 8ae20d044f
commit 78baf0c719
9 files changed
+177 -13

No files matched your search

+19
View File
@@ -7,6 +7,7 @@ import {
} from '../../src/core/commands/sceneObjectCommands';
import { worldPositionOf, worldTransformOf } from '../../src/core/graph';
import type { ContainerAsset } from '../../src/core/model/Asset';
import { hasSceneIcon, isSymbolType } from '../../src/core/model/AssetTypes';
import {
apply,
decompose,
@@ -180,5 +181,23 @@ describe('footprint under a transform', () => {
// A behavior's diamond keeps its size and stays upright: the frame does not scale by 2.
const symbol = footprintExtent('behavior', {}, m);
expect(symbol.right - symbol.left).toBe(28);
// Since a source wears an icon of its own (#89), the square that can be grabbed covers it
// instead of growing with the source's scale.
const source = footprintExtent('particles', {}, m);
expect(source.right - source.left).toBe(24);
expect(footprintContains('particles', {}, m, { x: 111, y: 100 })).toBe(true);
expect(footprintContains('particles', {}, m, { x: 121, y: 100 })).toBe(false);
});
it('a source wears an icon without becoming a symbol', () => {
expect(hasSceneIcon('particles')).toBe(true);
// The decisive one: `isSymbolType` does not mean "has an icon", it means "editor decoration".
// Were a source one, `collectWorldItems` and the Game tab would both drop it and the effect
// would never be seen outside the Scene tab.
expect(isSymbolType('particles')).toBe(false);
for (const type of ['camera', 'behavior', 'collider', 'container'] as const)
expect(hasSceneIcon(type)).toBe(true);
expect(hasSceneIcon('sprite')).toBe(false);
expect(hasSceneIcon('text')).toBe(false);
});
});