import { findObject } from '../../core/model/hierarchy'; import { resolvedHolder } from '../../core/model/instance'; import type { PropertyBag } from '../../core/model/Properties'; import { findAsset, findObjectHolder, type Project } from '../../core/model/Project'; import { objectName } from '../../core/model/SceneObject'; import { findFrame, resolvePicture, resolveTexture, type TextureRef } from '../../core/model/texture'; import type { FlatDef } from '../../core/properties/PropertySchema'; import { resolveAssetProps, resolveSceneObjectProps } from '../../core/properties/resolve'; import { shaderOf } from '../../core/properties/shaders'; import type { Selection } from '../state'; /** Something the Shader tab's preview can show: a sprite, placed or not, a texture or one of its frames (#168). */ export type PictureSubject = | { kind: 'object'; holderId: string; objectId: string } | { kind: 'asset'; assetId: string } | { kind: 'frame'; frameId: string }; /** What the preview draws for a subject, resolved again on every redraw so an edit shows at once. */ export interface PreviewPicture { name: string; /** Null for a sprite without a picture: the preview shows its plain colour, as the scene does. */ texture: TextureRef | null; w: number; h: number; tint: number; /** The rows the subject carries; a sprite already wearing the shader starts the sliders at its values. */ props: PropertyBag; } export const TEST_IMAGE_SIZE = 64; const TEST_IMAGE_CELL = 8; /** The subject a selection names, if it has a picture; anything else leaves the preview where it was. */ export function pictureSubjectOf(project: Project, selection: Selection): PictureSubject | null { let subject: PictureSubject | null = null; if (selection?.kind === 'object') subject = { kind: 'object', holderId: selection.holderId, objectId: selection.objectId }; else if (selection?.kind === 'asset') subject = { kind: 'asset', assetId: selection.assetId }; else if (selection?.kind === 'frame') subject = { kind: 'frame', frameId: selection.frameId }; return subject && previewPicture(project, subject) ? subject : null; } /** Null once the subject is gone or no longer has a picture – the preview then falls back to the test image. */ export function previewPicture(project: Project, subject: PictureSubject): PreviewPicture | null { if (subject.kind === 'object') { const holder = findObjectHolder(project, subject.holderId); const object = holder && findObject(resolvedHolder(project, holder), subject.objectId); const asset = object && findAsset(project, object.assetId); if (!object || asset?.type !== 'sprite') return null; return spritePicture(project, objectName(object, asset), resolveSceneObjectProps(project, asset, object)); } if (subject.kind === 'frame') { const found = findFrame(project, subject.frameId); const ref = resolvePicture(project, subject.frameId); return found && ref ? texturePicture(`${found.asset.name} / ${found.frame.name}`, ref) : null; } const asset = findAsset(project, subject.assetId); if (!asset) return null; if (asset.type === 'sprite') return spritePicture(project, asset.name, resolveAssetProps(project, asset)); const ref = resolvePicture(project, asset.id); return ref ? texturePicture(asset.name, ref) : null; } function spritePicture(project: Project, name: string, props: PropertyBag): PreviewPicture { return { name, texture: resolveTexture(project, props), w: Number(props.width) || 1, h: Number(props.height) || 1, tint: Number(props.color ?? 0xffffff), props, }; } function texturePicture(name: string, ref: TextureRef): PreviewPicture { return { name, texture: ref, w: ref.rect?.w ?? ref.sourceWidth, h: ref.rect?.h ?? ref.sourceHeight, tint: 0xffffff, props: {}, }; } /** Where a slider starts: the subject's own value when it wears this shader, the declared default otherwise. */ export function startValues( defs: readonly FlatDef<'number'>[], props: PropertyBag, shaderId: string, ): Record { const wears = shaderOf(props) === shaderId; const values: Record = {}; for (const def of defs) { const own = Number(props[def.key]); values[def.key] = wears && Number.isFinite(own) ? own : def.default; } return values; } /** * A property declares its min, its max only when it is above the min. Without one the slider * reaches twice the default's distance, so the default sits in the middle. */ export function sliderRange(def: FlatDef<'number'>): { min: number; max: number } { const min = def.min ?? 0; if (def.max !== undefined && def.max > min) return { min, max: def.max }; return { min, max: def.default > min ? min + 2 * (def.default - min) : min + 1 }; } /** * The built-in picture for when nothing with a picture was selected: colour runs from left to right * and top to bottom, a checker shows where a UV lands, and the edge fades out so alpha has something * to act on. Straight RGBA, row by row. */ export function testImagePixels(size = TEST_IMAGE_SIZE): Uint8ClampedArray { const border = size / 8; const pixels = new Uint8ClampedArray(size * size * 4); for (let y = 0; y < size; y++) { for (let x = 0; x < size; x++) { const u = (x + 0.5) / size; const v = (y + 0.5) / size; const shade = (Math.floor(x / TEST_IMAGE_CELL) + Math.floor(y / TEST_IMAGE_CELL)) % 2 ? 0.55 : 1; const edge = Math.min(x, y, size - 1 - x, size - 1 - y); const i = (y * size + x) * 4; pixels[i] = Math.round(255 * u * shade); pixels[i + 1] = Math.round(255 * v * shade); pixels[i + 2] = Math.round(255 * (1 - u) * shade); pixels[i + 3] = edge >= border ? 255 : Math.round((255 * (edge + 0.5)) / border); } } return pixels; }