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:
|
The browser suite and the three example games need a machine with a GPU:
|
||||||
|
|
||||||
```bash
|
```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-jumprun.mjs && node tools/smoke-jumprun.mjs
|
||||||
node tools/seed-tetris.mjs && node tools/smoke-tetris.mjs
|
node tools/seed-tetris.mjs && node tools/smoke-tetris.mjs
|
||||||
node tools/seed-actor.mjs && node tools/smoke-actor.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 { findAsset, type Project } from '../model/Project';
|
||||||
import { findFrame } from '../model/texture';
|
import { findFrame } from '../model/texture';
|
||||||
|
|
||||||
@@ -7,38 +7,30 @@ export const NO_TEXTURE_LABEL = 'None';
|
|||||||
const FRAME_SEPARATOR = ' / ';
|
const FRAME_SEPARATOR = ' / ';
|
||||||
|
|
||||||
export interface TextureChoice {
|
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;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
asset: TextureAsset | AnimationAsset;
|
asset: TextureAsset;
|
||||||
/** Null for the whole-picture entry and for an animation. */
|
/** Null for the whole-picture entry. */
|
||||||
frame: TextureFrame | null;
|
frame: TextureFrame | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The texture's name, "Sheet / Frame" for a frame, "None" for the empty id, `<id>?` when it no
|
* 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.
|
* 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 {
|
export function textureRefLabel(project: Project | null, id: string): string {
|
||||||
if (id === '') return NO_TEXTURE_LABEL;
|
if (id === '') return NO_TEXTURE_LABEL;
|
||||||
if (!project) return `${id}?`;
|
if (!project) return `${id}?`;
|
||||||
const asset = findAsset(project, 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);
|
const found = findFrame(project, id);
|
||||||
return found ? `${found.asset.name}${FRAME_SEPARATOR}${found.frame.name}` : `${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
|
* order does not depend on upload time; frames keep creation order, which for a sliced sheet is
|
||||||
* reading order.
|
* reading order.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
type PropertyDef,
|
type PropertyDef,
|
||||||
type StructFieldDef,
|
type StructFieldDef,
|
||||||
} from '../../core/properties/PropertySchema';
|
} 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 {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
@@ -230,7 +230,7 @@ const REFERENCES: Record<ReferenceKind, Reference> = {
|
|||||||
label: assetChoiceLabel(asset),
|
label: assetChoiceLabel(asset),
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
texture: { none: NO_TEXTURE_LABEL, label: textureRefLabel, choices: textureRefChoices },
|
texture: { none: NO_TEXTURE_LABEL, label: textureRefLabel, choices: pictureChoices },
|
||||||
statemachine: {
|
statemachine: {
|
||||||
none: NO_ASSET_LABEL,
|
none: NO_ASSET_LABEL,
|
||||||
label: assetRefLabel,
|
label: assetRefLabel,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
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 type { TextureAsset } from '../../src/core/model/Asset';
|
||||||
import { findAsset } from '../../src/core/model/Project';
|
import { findAsset } from '../../src/core/model/Project';
|
||||||
import { findPropertyUsages, findUsages } from '../../src/core/model/usages';
|
import { findPropertyUsages, findUsages } from '../../src/core/model/usages';
|
||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
textureRefIds,
|
textureRefIds,
|
||||||
uniqueFrameName,
|
uniqueFrameName,
|
||||||
} from '../../src/core/model/texture';
|
} from '../../src/core/model/texture';
|
||||||
|
import { pictureChoices, textureRefLabel } from '../../src/core/properties/textureRef';
|
||||||
import { deserializeProject } from '../../src/core/store/serialize';
|
import { deserializeProject } from '../../src/core/store/serialize';
|
||||||
import { makeSpriteScene, place, roundTrip } from '../helpers';
|
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']);
|
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:
|
// 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.
|
// paste a second picture, undo the asset (the file stays), then pick that file in the dialog.
|
||||||
await evaluate(PASTE_SPARE);
|
await evaluate(PASTE_SPARE);
|
||||||
|
|||||||
Reference in new issue
Block a user