The texture field only offers pictures: each texture with its frames, no animations any more; an animation id left in an older project shows as a missing reference (#143)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5046ddb810
commit
c2fd536aaf
5 files changed
+72
-19
No files matched your search
@@ -38,7 +38,7 @@ npm run build # writes dist/ and regenerates index.html; the folder is wh
|
||||
The browser suite and the three example games need a machine with a GPU:
|
||||
|
||||
```bash
|
||||
node tools/seed-e2e.mjs && node tools/e2e.mjs # the editor, 1032 checks (GM_STEPS=20 for one step)
|
||||
node tools/seed-e2e.mjs && node tools/e2e.mjs # the editor, 1034 checks (GM_STEPS=20 for one step)
|
||||
node tools/seed-jumprun.mjs && node tools/smoke-jumprun.mjs
|
||||
node tools/seed-tetris.mjs && node tools/smoke-tetris.mjs
|
||||
node tools/seed-actor.mjs && node tools/smoke-actor.mjs
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AnimationAsset, TextureAsset, TextureFrame } from '../model/Asset';
|
||||
import type { TextureAsset, TextureFrame } from '../model/Asset';
|
||||
import { findAsset, type Project } from '../model/Project';
|
||||
import { findFrame } from '../model/texture';
|
||||
|
||||
@@ -7,38 +7,30 @@ export const NO_TEXTURE_LABEL = 'None';
|
||||
const FRAME_SEPARATOR = ' / ';
|
||||
|
||||
export interface TextureChoice {
|
||||
/** Asset id for the whole picture or an animation, frame id for one cut-out of a picture. */
|
||||
/** Asset id for the whole picture, frame id for one cut-out of it. */
|
||||
id: string;
|
||||
label: string;
|
||||
asset: TextureAsset | AnimationAsset;
|
||||
/** Null for the whole-picture entry and for an animation. */
|
||||
asset: TextureAsset;
|
||||
/** Null for the whole-picture entry. */
|
||||
frame: TextureFrame | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The texture's name, "Sheet / Frame" for a frame, "None" for the empty id, `<id>?` when it no
|
||||
* longer resolves – a dangling reference shows its id rather than passing for an empty field.
|
||||
* Any other asset counts as dangling, an animation included: a texture field only draws pictures.
|
||||
*/
|
||||
export function textureRefLabel(project: Project | null, id: string): string {
|
||||
if (id === '') return NO_TEXTURE_LABEL;
|
||||
if (!project) return `${id}?`;
|
||||
const asset = findAsset(project, id);
|
||||
if (asset) return asset.type === 'texture' || asset.type === 'animation' ? asset.name : `${id}?`;
|
||||
if (asset) return asset.type === 'texture' ? asset.name : `${id}?`;
|
||||
const found = findFrame(project, id);
|
||||
return found ? `${found.asset.name}${FRAME_SEPARATOR}${found.frame.name}` : `${id}?`;
|
||||
}
|
||||
|
||||
/** Everything a sprite may wear, in menu order: the animations first, a sheet's frames would bury them. */
|
||||
export function textureRefChoices(project: Project): TextureChoice[] {
|
||||
const animations = project.assets
|
||||
.filter((a): a is AnimationAsset => a.type === 'animation')
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map((asset) => ({ id: asset.id, label: `${asset.name} (animation)`, asset, frame: null }));
|
||||
return [...animations, ...pictureChoices(project)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Every still picture: each texture followed by its own frames. Textures are sorted by name so the
|
||||
* Every still picture, the choices of a texture field: each texture followed by its own frames. Textures are sorted by name so the
|
||||
* order does not depend on upload time; frames keep creation order, which for a sliced sheet is
|
||||
* reading order.
|
||||
*/
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
type PropertyDef,
|
||||
type StructFieldDef,
|
||||
} from '../../core/properties/PropertySchema';
|
||||
import { NO_TEXTURE_LABEL, textureRefChoices, textureRefLabel } from '../../core/properties/textureRef';
|
||||
import { NO_TEXTURE_LABEL, pictureChoices, textureRefLabel } from '../../core/properties/textureRef';
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
@@ -230,7 +230,7 @@ const REFERENCES: Record<ReferenceKind, Reference> = {
|
||||
label: assetChoiceLabel(asset),
|
||||
})),
|
||||
},
|
||||
texture: { none: NO_TEXTURE_LABEL, label: textureRefLabel, choices: textureRefChoices },
|
||||
texture: { none: NO_TEXTURE_LABEL, label: textureRefLabel, choices: pictureChoices },
|
||||
statemachine: {
|
||||
none: NO_ASSET_LABEL,
|
||||
label: assetRefLabel,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { CreateTextureCommand } from '../../src/core/commands/assetCommands';
|
||||
import { CreateAssetCommand, CreateTextureCommand } from '../../src/core/commands/assetCommands';
|
||||
import type { TextureAsset } from '../../src/core/model/Asset';
|
||||
import { findAsset } from '../../src/core/model/Project';
|
||||
import { findPropertyUsages, findUsages } from '../../src/core/model/usages';
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
textureRefIds,
|
||||
uniqueFrameName,
|
||||
} from '../../src/core/model/texture';
|
||||
import { pictureChoices, textureRefLabel } from '../../src/core/properties/textureRef';
|
||||
import { deserializeProject } from '../../src/core/store/serialize';
|
||||
import { makeSpriteScene, place, roundTrip } from '../helpers';
|
||||
|
||||
@@ -262,3 +263,29 @@ describe('frame names', () => {
|
||||
expect(nextFrameNames(sheet('Frame 2'), 3)).toEqual(['Frame 1', 'Frame 3', 'Frame 4']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('texture field references', () => {
|
||||
it('offers pictures and their frames, never an animation', () => {
|
||||
const fixture = makeSpriteScene();
|
||||
const texture = addTexture(fixture);
|
||||
texture.frames.push({ id: 'f1', name: 'Idle', x: 0, y: 0, w: 32, h: 24 });
|
||||
const animation = new CreateAssetCommand('animation', 'Walk');
|
||||
fixture.stack.execute(animation);
|
||||
|
||||
expect(pictureChoices(fixture.project).map((c) => [c.id, c.label])).toEqual([
|
||||
[texture.id, 'Bricks'],
|
||||
['f1', 'Bricks / Idle'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('shows an animation id left from an older project as a dangling reference', () => {
|
||||
const fixture = makeSpriteScene();
|
||||
const texture = addTexture(fixture);
|
||||
const animation = new CreateAssetCommand('animation', 'Walk');
|
||||
fixture.stack.execute(animation);
|
||||
|
||||
expect(textureRefLabel(fixture.project, texture.id)).toBe('Bricks');
|
||||
expect(textureRefLabel(fixture.project, animation.asset.id)).toBe(`${animation.asset.id}?`);
|
||||
expect(textureRefLabel(fixture.project, '')).toBe('None');
|
||||
});
|
||||
});
|
||||
@@ -267,6 +267,40 @@ export async function textures(t) {
|
||||
);
|
||||
}
|
||||
|
||||
// 12e-ii. A texture field offers pictures only (#143). Since #97 an animation is played, not worn,
|
||||
// so the Texture row of a sprite no longer lists one, and an animation id left in the
|
||||
// field by an older project reads as dangling rather than passing for a picture.
|
||||
await mutate(`
|
||||
__gm.state.project.assets.push(
|
||||
{ id: 'tex-anim', type: 'animation', name: 'Walk', props: {}, root: '', tracks: [] });
|
||||
const worn = __gm.state.project.assets.find((a) => a.id === 'tex-sprite').props;
|
||||
window.__texBefore = worn.texture;
|
||||
worn.texture = 'tex-anim';
|
||||
__gm.state.select({ kind: 'asset', assetId: 'tex-sprite' })`);
|
||||
await sleep(400);
|
||||
const textureField = await json("__gm.inspectorField('Texture', true)");
|
||||
const dangling = await json("__gm.locateAll('tex-anim?')");
|
||||
check(
|
||||
'an animation id in the Texture row reads as a dangling reference',
|
||||
Boolean(textureField) && dangling.length === 1 && Math.abs(dangling[0].y - textureField.y) < 12,
|
||||
JSON.stringify({ textureField, dangling }),
|
||||
);
|
||||
await click(textureField.x, textureField.y);
|
||||
await sleep(300);
|
||||
const offered = (await json('__gm.menuRows()')) ?? [];
|
||||
check(
|
||||
'the Texture row offers the picture and no animation',
|
||||
offered.includes(texture.name) && !offered.some((row) => row?.includes('(animation)') || row === 'Walk'),
|
||||
JSON.stringify(offered),
|
||||
);
|
||||
await press('Escape');
|
||||
await mutate(`
|
||||
const assets = __gm.state.project.assets;
|
||||
assets.splice(assets.findIndex((a) => a.id === 'tex-anim'), 1);
|
||||
assets.find((a) => a.id === 'tex-sprite').props.texture = window.__texBefore;
|
||||
__gm.state.select(null)`);
|
||||
await sleep(300);
|
||||
|
||||
// 12f. "+ → Texture" offers the pictures the server already holds, unreferenced ones included:
|
||||
// paste a second picture, undo the asset (the file stays), then pick that file in the dialog.
|
||||
await evaluate(PASTE_SPARE);
|
||||
|
||||
Reference in new issue
Block a user