import { CreateAssetCommand, SetActiveSceneCommand } from '../../core/commands/assetCommands'; import { nextAssetName } from '../../core/commands/naming'; import { ASSET_TYPE_LABELS, ASSET_TYPES, type AssetType, hasColorSwatch, isPlaceableType, } from '../../core/model/AssetTypes'; import { findAsset } from '../../core/model/Project'; import type { TextureStore } from '../../core/store/TextureStore'; import { Button, ListView, Menu, type MenuItem, Panel, ScrollContainer, startDrag } from '../../ui'; import { confirmDeleteAsset } from '../actions'; import type { EditorState } from '../state'; import { pickAndImport } from '../textureImport'; /** Lists all assets of the project; creates and deletes them. Placing is a drag into a view. */ export class AssetPanel extends Panel { private readonly list = new ListView(); private readonly scroll = new ScrollContainer(); constructor( private readonly state: EditorState, /** Where imported images go. Without it the Texture entry has nothing to upload to. */ private readonly textures: TextureStore | null = null, ) { super({ title: 'Assets', padding: 0, gap: 0 }); this.minWidth = 160; const add = new Button('+', { variant: 'ghost', width: 28 }); add.onClick = () => new Menu( ASSET_TYPES.map((type) => ({ label: ASSET_TYPE_LABELS[type], onSelect: () => (type === 'texture' ? this.importTextures() : this.createAsset(type)), })), ).openBelow(add); this.header.add(add); this.list.emptyText = 'No assets – press +'; this.list.onSelect = (id) => state.select(id ? { kind: 'asset', assetId: id } : null); this.list.onActivate = (id) => this.activate(id); this.list.onContextMenu = (id, x, y) => this.openContextMenu(id, x, y); this.list.onDragOut = (id) => this.startPlacementDrag(id); this.scroll.setContent(this.list); this.content.add(this.scroll, 1); state.on('project', () => this.refresh()); state.on('load', () => this.refresh()); state.on('selection', () => this.syncSelection()); this.refresh(); } private refresh(): void { const active = this.state.project.activeSceneId; this.list.setItems( this.state.project.assets.map((asset) => ({ id: asset.id, label: asset.name, hint: asset.type === 'scene' && asset.id === active ? 'scene ●' : asset.type, swatch: hasColorSwatch(asset.type) ? Number(asset.props.color ?? 0) : undefined, })), ); this.syncSelection(); this.scroll.relayoutContent(); } private syncSelection(): void { const s = this.state.selection; 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])); if (this.state.execute(command)) this.state.select({ kind: 'asset', assetId: command.asset.id }); } /** * A texture is an uploaded picture, so its menu entry opens the file dialog instead of * creating an empty asset. */ private importTextures(): void { if (!this.textures) { this.state.setNotice('Image upload is not available.'); return; } pickAndImport({ state: this.state, textures: this.textures }); } /** Double-click / Enter on a scene makes it the active one. Placing is a drag, not a click. */ private activate(id: string): void { const asset = findAsset(this.state.project, id); if (asset?.type === 'scene') this.state.execute(new SetActiveSceneCommand(id)); } /** Where an asset's row sits on screen, so a check can pick it up by id (test hook). */ rowGlobal(assetId: string): { x: number; y: number } | null { return this.list.rowGlobal(assetId); } /** Hands the asset to the viewport under the pointer; a scene has nowhere to be placed into. */ private startPlacementDrag(assetId: string): void { const asset = findAsset(this.state.project, assetId); if (!asset || !isPlaceableType(asset.type)) return; startDrag({ kind: 'asset', id: assetId }, asset.name); } private openContextMenu(id: string, x: number, y: number): Menu | undefined { const asset = findAsset(this.state.project, id); if (!asset) return undefined; const items: MenuItem[] = []; if (asset.type === 'scene') { items.push({ label: 'Set as active scene', onSelect: () => this.state.execute(new SetActiveSceneCommand(id)), disabled: this.state.project.activeSceneId === id, }); } items.push({ label: 'Delete…', danger: true, onSelect: () => confirmDeleteAsset(this.state, id) }); const menu = new Menu(items); menu.open(x, y); return menu; } }