Images are uploaded to a content-addressed blob endpoint and stored as data/textures/<sha256>.<ext>; the project only keeps the file name. The sprite points at a texture asset through an ordinary property of the new kind `asset`, so instance overrides, "Apply to container", "Reset to asset", undo and `Set property` work without any extra code. The new kind also replaces the ad-hoc asset picker the graph nodes faked as an enum. Import goes through + -> Texture, drag & drop onto the canvas or Ctrl+V. Images wider than 2048px are downscaled client-side and re-encoded to WebP if needed, because nginx caps uploads at 1MB; an image that already fits is uploaded byte for byte so pixel art is never resampled. The sprite colour default changes from red to white, since colour tints the texture multiplicatively and a tint of white shows an image as it is. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
61 lines
3.0 KiB
TypeScript
61 lines
3.0 KiB
TypeScript
/**
|
||
* Editor actions that several places trigger the same way (inspector, hierarchy context
|
||
* menu, keyboard): they run the command and put the selection where the user expects it.
|
||
*/
|
||
import { DeleteAssetCommand } from '../core/commands/assetCommands';
|
||
import { AddColliderCommand } from '../core/commands/colliderCommands';
|
||
import { CreateContainerCommand } from '../core/commands/containerCommands';
|
||
import { RemoveSceneObjectsCommand } from '../core/commands/sceneObjectCommands';
|
||
import { findAsset, findPropertyUsages, findUsages } from '../core/model/Project';
|
||
import { Dialog } from '../ui';
|
||
import { type EditorState, objectSelection } from './state';
|
||
|
||
/** Groups the objects into a new container asset placed in their stead; selects the container. */
|
||
export function createContainer(state: EditorState, sceneId: string, objectIds: readonly string[]): boolean {
|
||
const command = new CreateContainerCommand(sceneId, objectIds);
|
||
if (!state.execute(command)) return false;
|
||
state.select(objectSelection(sceneId, [command.objectId]));
|
||
return true;
|
||
}
|
||
|
||
/** Gives the object a collider child sized like it; selects the collider. */
|
||
export function addCollider(state: EditorState, sceneId: string, objectId: string): boolean {
|
||
const command = new AddColliderCommand(sceneId, objectId);
|
||
if (!state.execute(command)) return false;
|
||
state.select(objectSelection(sceneId, [command.objectId]));
|
||
return true;
|
||
}
|
||
|
||
/** Removes the objects (with their subtrees) as one undo step. */
|
||
export function removeObjects(state: EditorState, sceneId: string, objectIds: readonly string[]): boolean {
|
||
return state.execute(new RemoveSceneObjectsCommand(sceneId, objectIds));
|
||
}
|
||
|
||
/**
|
||
* Asks before deleting an asset, spelling out what goes with it. Both ways of deleting (the
|
||
* asset list's context menu and the Delete key) go through here, so the warning cannot differ
|
||
* depending on how the user triggered it.
|
||
*/
|
||
export function confirmDeleteAsset(state: EditorState, assetId: string): void {
|
||
const asset = findAsset(state.project, assetId);
|
||
if (!asset) return;
|
||
const usages = findUsages(state.project, assetId).length;
|
||
// A texture is never placed – it is worn through a property – so counting placed instances
|
||
// would report "unused" for a picture a dozen sprites are showing.
|
||
const references = findPropertyUsages(state.project, assetId).length;
|
||
const detail =
|
||
asset.type === 'scene'
|
||
? `The scene and its ${asset.objects.length} object(s) are removed.`
|
||
: references > 0
|
||
? `${references} sprite(s) reference it and fall back to their plain colour.`
|
||
: usages === 0
|
||
? 'This cannot be undone with Ctrl+Z after reload.'
|
||
: asset.type === 'container'
|
||
? `${usages} placed container(s) are removed together with their content.`
|
||
: `${usages} placed instance(s) are removed as well (scenes and container templates).`;
|
||
new Dialog(`Delete "${asset.name}"?`, detail, [
|
||
{ label: 'Cancel' },
|
||
{ label: 'Delete', danger: true, onClick: () => state.execute(new DeleteAssetCommand(assetId)) },
|
||
]).openCentered();
|
||
}
|