import { RemoveFrameCommand, RenameFrameCommand, SetFrameRectCommand, } from '../../core/commands/frameCommands'; import type { TextureAsset, TextureFrame } from '../../core/model/Asset'; import { findAssetOf, type Project } from '../../core/model/Project'; import { frameRect, type FrameRect, normalizeFrameRect, setFrameRect } from '../../core/model/texture'; import { Button } from '../../ui'; import { buildCutRows, refreshCutRows } from './cutSection'; import { createFieldRow, type FieldHandlers } from './fieldFactory'; import { addGap, addNameField, addNote, addTitle, type InspectorHost } from './host'; import { previewingHandlers } from './previewHandlers'; const RECT_COMPONENTS = [ { key: 'x', label: 'X' }, { key: 'y', label: 'Y' }, { key: 'w', label: 'Width' }, { key: 'h', label: 'Height' }, ] as const; /** Structure-defining facts of a frame selection: none – only the frame disappearing rebuilds. */ export function frameSignature(project: Project, assetId: string, frameId: string): string { return currentFrame(project, assetId, frameId) ? 'frame' : 'missing'; } /** Name, the rectangle it cuts out of its texture, and the way to delete it. */ export function buildFrameSection(host: InspectorHost, assetId: string, frameId: string): void { const asset = currentTexture(host.state.project, assetId); const frame = asset?.frames.find((f) => f.id === frameId); if (!asset || !frame) return; addTitle(host, 'TEXTURE FRAME'); addNameField(host, frame.name, { onChange: (name) => { // The command rejects a blank name but cannot put the old text back into the field, // so an empty one is not sent at all. if (name.trim()) host.state.execute(new RenameFrameCommand(assetId, frameId, name.trim())); else host.nameField?.setValue(frame.name); }, }); addNote(host, `Frame of the texture "${asset.name}".`); addTitle(host, 'RECTANGLE (IMAGE PIXELS)'); for (const component of RECT_COMPONENTS) rectRow(host, assetId, frameId, component.key, component.label, frame[component.key]); buildCutRows(host, { assetId, frameId }, 'IMAGE PIXELS'); addGap(host); host.body.add( new Button('Delete frame', { variant: 'danger', onClick: () => host.state.execute(new RemoveFrameCommand(assetId, frameId)), }), ); } /** Pushes the frame's current name and rectangle into the rows. False when it is gone (rebuild). */ export function refreshFrameSection(host: InspectorHost, assetId: string, frameId: string): boolean { const frame = currentFrame(host.state.project, assetId, frameId); if (!frame) return false; host.nameField?.setValue(frame.name); for (const component of RECT_COMPONENTS) host.fields.get(component.key)?.setValue(frame[component.key]); refreshCutRows(host, { assetId, frameId }); return true; } /** A number row editing one component of the rectangle. Whole pixels: a frame indexes a bitmap. */ function rectRow( host: InspectorHost, assetId: string, frameId: string, key: keyof FrameRect, label: string, value: number, ): void { const row = createFieldRow( host, { key, label, kind: 'number', default: 0, step: 1, decimals: 0 }, value, rectHandlers(host, assetId, frameId, key), ); host.fields.set(key, row); host.body.add(row.row); } /** * The whole rectangle with one component replaced; the other three are read from the model at * that moment, so a frame dragged in the Texture tab in between is not undone. Normalising the * preview with the same function the command uses keeps it on the sheet and identical to what * is committed. */ function rectHandlers( host: InspectorHost, assetId: string, frameId: string, key: keyof FrameRect, ): FieldHandlers { return previewingHandlers(host, { target: () => currentFrame(host.state.project, assetId, frameId), read: frameRect, write: setFrameRect, toModel: (input, frame) => { const asset = currentTexture(host.state.project, assetId); return normalizeFrameRect( { ...frameRect(frame), [key]: Number(input) }, asset?.width ?? 0, asset?.height ?? 0, ); }, commit: (rect) => new SetFrameRectCommand(assetId, frameId, rect), }); } /** * Looked up inside the selected texture rather than by frame id alone, so a selection left over * from a deleted texture cannot edit whichever frame later inherits the id. */ function currentFrame(project: Project, assetId: string, frameId: string): TextureFrame | undefined { return currentTexture(project, assetId)?.frames.find((f) => f.id === frameId); } function currentTexture(project: Project, assetId: string): TextureAsset | undefined { return findAssetOf(project, assetId, 'texture'); }