Create a sprite asset from a texture's context menu

The sprite takes the whole image at its pixel size and the texture's name,
numbered like any other clash. CreateAssetCommand now accepts starting
properties; they go through the schema in execute, so an unknown key or a
bad value is refused before the project is touched, and one undo removes
the asset.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
rootandClaude Opus 5 committed 2026-09-11 09:57:19 +02:00
1 parent 1e2285c545
commit 9212e61c4c
9 files changed
+137 -7

No files matched your search

+8 -1
View File
@@ -41,12 +41,19 @@ export class CreateAssetCommand implements Command {
readonly asset: Asset;
private previousActiveScene = '';
constructor(type: AssetType, name: string) {
/** `props` replace schema defaults and pass through the schema like any other edit. */
constructor(
type: AssetType,
name: string,
private readonly props: PropertyBag = {},
) {
this.asset = buildAsset(type, name);
this.label = `Create ${type} "${name}"`;
}
execute(project: Project): void {
for (const [key, value] of Object.entries(this.props))
this.asset.props[key] = coerceValue(requirePropDef(this.asset, key), value);
project.assets.push(this.asset);
this.previousActiveScene = project.activeSceneId;
if (this.asset.type === 'scene' && !project.activeSceneId) project.activeSceneId = this.asset.id;
+7
View File
@@ -22,6 +22,13 @@ export function texturePixelSize(project: Project, props: PropertyBag): PixelSiz
return w > 0 && h > 0 ? { w, h } : null;
}
/** A sprite wearing the texture at its own pixel size – or at the default size while that is unknown. */
export function spriteFromTexture(project: Project, textureId: string): PropertyBag {
const props: PropertyBag = { texture: textureId };
const size = texturePixelSize(project, props);
return size ? { ...props, width: size.w, height: size.h } : props;
}
/**
* What `axis` has to become for the sprite to carry the picture's proportions, leaving the
* other side where it is. Null when the other side is not a usable number.
+10 -2
View File
@@ -7,7 +7,9 @@ import {
hasColorSwatch,
isPlaceableType,
} from '../../core/model/AssetTypes';
import type { PropertyBag } from '../../core/model/Properties';
import { findAsset } from '../../core/model/Project';
import { spriteFromTexture } from '../../core/properties/aspect';
import { assetDeviates } from '../../core/properties/deviation';
import type { TextureStore } from '../../core/store/TextureStore';
import { Button, ListView, Menu, type MenuItem, Panel, ScrollContainer, startDrag } from '../../ui';
@@ -77,8 +79,8 @@ export class AssetPanel extends Panel {
this.list.select(s?.kind === 'asset' ? s.assetId : null, true);
}
private createAsset(type: AssetType): void {
const command = new CreateAssetCommand(type, nextAssetName(this.state.project, ASSET_TYPE_LABELS[type]));
private createAsset(type: AssetType, name = ASSET_TYPE_LABELS[type], props: PropertyBag = {}): void {
const command = new CreateAssetCommand(type, nextAssetName(this.state.project, name), props);
if (this.state.execute(command)) this.state.select({ kind: 'asset', assetId: command.asset.id });
}
@@ -128,6 +130,12 @@ export class AssetPanel extends Panel {
disabled: this.state.project.activeSceneId === id,
});
}
if (asset.type === 'texture') {
items.push({
label: 'Create sprite from texture',
onSelect: () => this.createAsset('sprite', asset.name, spriteFromTexture(this.state.project, id)),
});
}
items.push({ label: 'Delete…', danger: true, onSelect: () => confirmDeleteAsset(this.state, id) });
const menu = new Menu(items);
menu.open(x, y);