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>
104 lines
4.1 KiB
TypeScript
104 lines
4.1 KiB
TypeScript
import { schemaForAsset } from '../properties/PropertySchema';
|
||
import { type Asset, type ContainerAsset, isSceneAsset, type SceneAsset } from './Asset';
|
||
import type { SceneObject } from './SceneObject';
|
||
|
||
/** Written into every document. Reserved for format migrations – the loader does not branch on it yet. */
|
||
export const PROJECT_FORMAT_VERSION = 1;
|
||
|
||
export interface Project {
|
||
formatVersion: number;
|
||
id: string;
|
||
name: string;
|
||
assets: Asset[];
|
||
/** Scene shown in the viewport and instantiated on Play. Empty string = none. */
|
||
activeSceneId: string;
|
||
}
|
||
|
||
export function createEmptyProject(id: string, name: string): Project {
|
||
return { formatVersion: PROJECT_FORMAT_VERSION, id, name, assets: [], activeSceneId: '' };
|
||
}
|
||
|
||
export function findAsset(project: Project, assetId: string): Asset | undefined {
|
||
return project.assets.find((a) => a.id === assetId);
|
||
}
|
||
|
||
/** The scene asset with that id – undefined for other asset types as well. */
|
||
export function findScene(project: Project, sceneId: string): SceneAsset | undefined {
|
||
const asset = findAsset(project, sceneId);
|
||
return asset && isSceneAsset(asset) ? asset : undefined;
|
||
}
|
||
|
||
export function listScenes(project: Project): SceneAsset[] {
|
||
return project.assets.filter(isSceneAsset);
|
||
}
|
||
|
||
/**
|
||
* Everything that holds placed objects. Container assets do so just like scenes: their
|
||
* `objects` are the template a placed container is instantiated from. Anything that walks
|
||
* "all placed objects" has to cover both, or it silently skips the templates.
|
||
*/
|
||
export type ObjectHolder = SceneAsset | ContainerAsset;
|
||
|
||
export function objectHolders(project: Project): ObjectHolder[] {
|
||
return project.assets.filter((a): a is ObjectHolder => a.type === 'scene' || a.type === 'container');
|
||
}
|
||
|
||
export interface SceneObjectRef {
|
||
holder: ObjectHolder;
|
||
object: SceneObject;
|
||
}
|
||
|
||
/** Objects of all scenes and container templates that satisfy `predicate`, each with its holder. */
|
||
export function objectsWhere(
|
||
project: Project,
|
||
predicate: (object: SceneObject) => boolean,
|
||
): SceneObjectRef[] {
|
||
const result: SceneObjectRef[] = [];
|
||
for (const holder of objectHolders(project)) {
|
||
for (const object of holder.objects) if (predicate(object)) result.push({ holder, object });
|
||
}
|
||
return result;
|
||
}
|
||
|
||
/** Every placed instance of an asset, in scenes and in container templates alike. */
|
||
export function findUsages(project: Project, assetId: string): SceneObjectRef[] {
|
||
return objectsWhere(project, (o) => o.assetId === assetId);
|
||
}
|
||
|
||
/** One place where an `asset`-kind property points at an asset. */
|
||
export type PropertyUsage =
|
||
| { on: 'asset'; asset: Asset; key: string }
|
||
| { on: 'object'; holder: ObjectHolder; object: SceneObject; key: string };
|
||
|
||
/**
|
||
* Every property that references `assetId`, on assets and on the overrides of placed objects.
|
||
*
|
||
* This is the other half of "where is this asset used": `findUsages` finds assets that were
|
||
* *placed*, which is meaningless for a texture – a texture is never placed, it is worn by
|
||
* sprites through their `texture` property. Deleting one therefore has to count both, or it
|
||
* reports "0 uses" for a picture that ten sprites are showing.
|
||
*
|
||
* A reference is deliberately left dangling rather than cleared when its asset goes: a
|
||
* dangling one resolves to "no picture" everywhere it is read, and it comes back by itself
|
||
* when the delete is undone.
|
||
*/
|
||
export function findPropertyUsages(project: Project, assetId: string): PropertyUsage[] {
|
||
if (!assetId) return [];
|
||
const result: PropertyUsage[] = [];
|
||
const assetKeys = new Map<string, string[]>();
|
||
for (const asset of project.assets) {
|
||
const keys = schemaForAsset(asset)
|
||
.filter((def) => def.kind === 'asset')
|
||
.map((def) => def.key);
|
||
assetKeys.set(asset.id, keys);
|
||
for (const key of keys) if (asset.props[key] === assetId) result.push({ on: 'asset', asset, key });
|
||
}
|
||
for (const holder of objectHolders(project)) {
|
||
for (const object of holder.objects) {
|
||
for (const key of assetKeys.get(object.assetId) ?? [])
|
||
if (object.overrides[key] === assetId) result.push({ on: 'object', holder, object, key });
|
||
}
|
||
}
|
||
return result;
|
||
}
|