import { ChannelName, ChannelNodeMap } from "./channels.js"; import { Sprite2D } from "../sprites/Sprite2D.js"; import { Texture } from "three"; import { Entity, Trait } from "koota"; import Node from "three/src/nodes/core/Node.js"; //#region src/materials/MaterialEffect.d.ts /** A single field value in an effect schema (type inferred from shape). */ type EffectSchemaValue = number | readonly [number, number] | readonly [number, number, number] | readonly [number, number, number, number] | (() => unknown); /** An effect schema — maps field names to their default values. */ type EffectSchema = Record; /** Keys whose schema value is a plain number or tuple (→ TSL uniform, settable at runtime). */ type UniformKeys = { [K in keyof S]: S[K] extends ((...args: never[]) => unknown) ? never : K; }[keyof S]; /** Keys whose schema value is a factory function (→ typed constant, read-only reference). */ type ConstantKeys = { [K in keyof S]: S[K] extends ((...args: never[]) => unknown) ? K : never; }[keyof S]; /** Derive JS value types from an effect schema (uniform fields only, used by property setters). */ type EffectValues = { -readonly [K in UniformKeys]: S[K] extends number ? number : S[K] extends readonly [number, number, number, number] ? [number, number, number, number] : S[K] extends readonly [number, number, number] ? [number, number, number] : S[K] extends readonly [number, number] ? [number, number] : never; }; /** * Read-only constants from factory function fields. * The reference is frozen at construction time and cannot be reassigned, but the * object's internals are freely mutable and mutations take effect immediately * (e.g. `this.forwardPlus.resize(w, h)` works live — no remove/re-add needed). */ type EffectConstants = { [K in ConstantKeys]: S[K] extends (() => infer R) ? R : never; }; /** Computed field metadata from schema. */ interface EffectField { /** Field name (unprefixed). */ name: string; /** Number of float components (1=float, 2=vec2, 3=vec3, 4=vec4). */ size: number; /** Default values as flat array. */ default: number[]; } /** Map a uniform schema value type to the corresponding parameterized Node type. */ type SchemaToNodeType = V extends ((...args: never[]) => unknown) ? never : V extends number ? Node<'float'> : V extends readonly [number, number, number, number] ? Node<'vec4'> : V extends readonly [number, number, number] ? Node<'vec3'> : V extends readonly [number, number] ? Node<'vec2'> : Node<'float'> | Node<'vec2'> | Node<'vec3'> | Node<'vec4'>; /** Context passed to an effect's TSL node builder. */ interface EffectNodeContext { /** The previous color in the effect chain (vec4 node). */ inputColor: Node<'vec4'>; /** Atlas UV coordinates (vec2 node). */ inputUV: Node<'vec2'>; /** TSL attribute nodes for each uniform schema field, keyed by unprefixed name. */ attrs: { [K in UniformKeys]: SchemaToNodeType; }; /** Read-only constants from factory function fields. */ constants: EffectConstants; } /** Context passed to a provider effect's channel node builder. */ interface ChannelNodeContext { /** Atlas UV coordinates (vec2 node). */ atlasUV: Node<'vec2'>; /** Read-only constants from factory function fields. */ constants: EffectConstants; /** TSL attribute nodes for each uniform schema field, keyed by unprefixed name. */ attrs: { [K in UniformKeys]: SchemaToNodeType; }; /** Base sprite texture (for auto-normal generation, etc.). Null if unavailable. */ baseTexture: Texture | null; } /** * Base class for per-sprite shader effects. * * Each MaterialEffect subclass defines: * - `effectName` — unique name for the effect * - `effectSchema` — per-sprite data schema with default values * - `buildNode()` — TSL node builder for the effect shader * * Each MaterialEffect instance: * - Has typed property accessors for each schema field * - Uses the snapshot pattern for pre-enrollment staging * - Dual-writes to ECS traits and packed GPU buffers * * @example Class-based definition: * ```typescript * class DissolveEffect extends MaterialEffect { * static readonly effectName = 'dissolve' * static readonly effectSchema = { progress: 0 } as const * declare progress: number * * static buildNode({ inputColor, attrs }: EffectNodeContext) { * return mix(inputColor, vec4(0, 0, 0, 0), attrs.progress) * } * } * ``` * * @example Factory definition: * ```typescript * const DissolveEffect = createMaterialEffect({ * name: 'dissolve', * schema: { progress: 0 }, * node({ inputColor, attrs }) { * return mix(inputColor, vec4(0, 0, 0, 0), attrs.progress) * }, * }) * ``` */ declare abstract class MaterialEffect { /** Unique effect name. Must be overridden by subclass. */ static readonly effectName: string; /** Per-sprite data schema with default values. Must be overridden by subclass. */ static readonly effectSchema: EffectSchema; /** Per-fragment channels this effect provides (e.g., ['normal']). */ static readonly provides: readonly ChannelName[]; /** Channel node builder — produces TSL nodes for declared channels. */ static channelNode: ((channelName: string, context: ChannelNodeContext) => Node) | null; /** @internal Auto-generated Koota trait from schema. */ static _trait: Trait; /** @internal Computed field metadata from schema. */ static _fields: EffectField[]; /** @internal Total float slots needed for this effect's data (excluding flags). */ static _totalFloats: number; /** @internal TSL node builder function. */ static _node: (context: EffectNodeContext) => Node<'vec4'>; /** @internal Whether static initialization has been performed. */ static _initialized: boolean; /** * TSL node builder. Must be overridden by subclass (class-based path). * The factory path sets this via static assignment. */ static buildNode(_context: EffectNodeContext): Node<'vec4'>; /** @internal Factory functions for constant fields (keyed by field name). */ static _constantFactories: Record unknown>; /** * Initialize static metadata from the schema (called once per subclass, lazily). * Computes field metadata, creates Koota trait, and sets up the node function. * @internal */ static _initialize(): void; /** Auto-incrementing unique ID for debugging. */ static _nextId: number; /** Unique instance ID (like Three.js object ids). */ readonly id: number; /** Effect name (from static). */ readonly name: string; /** @internal The sprite this effect is attached to. */ _sprite: Sprite2D | null; /** @internal The ECS entity for the parent sprite. */ _entity: Entity | null; /** @internal Snapshot defaults for pre-enrollment staging. Keyed by field name. */ _defaults: Record; /** * Per-instance constant values (from factory function schema fields). * References are frozen at construction time and cannot be reassigned. * Internal state is freely mutable and mutations apply immediately. * @internal */ _constants: Record; constructor(); /** * Attach this effect to a sprite. * @internal Called by Sprite2D.addEffect() */ _attach(sprite: Sprite2D): void; /** * Detach this effect from its sprite. * @internal Called by Sprite2D.removeEffect() */ _detach(): void; /** * Read a field value using the snapshot pattern. * If attached to an enrolled sprite, reads from ECS trait. * Otherwise, reads from the snapshot defaults. * @internal */ _getField(name: string): number | number[]; /** * Write a field value using the snapshot pattern. * If attached to an enrolled sprite, writes to ECS trait (systems sync to GPU). * Otherwise, writes to the snapshot defaults. * Only triggers immediate GPU buffer sync for standalone sprites. * @internal */ _setField(name: string, value: number | number[]): void; } /** Configuration passed to createMaterialEffect(). */ interface MaterialEffectConfig { /** Unique name for this effect. */ name: string; /** Per-sprite data schema — default values define types and initial values. */ schema: S; /** TSL node builder: receives input color, UV, and per-field attribute nodes. Optional for provider-only effects. */ node?: (context: EffectNodeContext) => Node<'vec4'>; /** * Per-fragment channels this effect provides (e.g., `['normal'] as const`). * The `as const` (or literal array) is required for return-type narrowing * of {@link channelNode}. */ provides?: C; /** * Channel node builder — produces TSL nodes for declared channels. * * The return type is narrowed by the declared `provides`: if * `provides: ['normal'] as const`, the channelNode must return * `ChannelNodeMap['normal']` (i.e. `Node<'vec3'>`). For multi-channel * providers, the return is the union of `ChannelNodeMap[C[number]]` and * the function must narrow on `channelName` before returning. * * Omitting `provides` leaves `C = readonly []`, which makes this field * uncallable by type — a `channelNode` without a `provides` is a * compile-time error. */ channelNode?: C extends readonly [] ? never : (channelName: C[number], context: ChannelNodeContext) => ChannelNodeMap[C[number]]; } /** * Type for a MaterialEffect class created by the factory. * Instances have typed properties matching the schema. */ type MaterialEffectClass = { new (): MaterialEffect & EffectValues & EffectConstants; readonly effectName: string; readonly effectSchema: S; readonly provides: readonly ChannelName[]; readonly channelNode: ((channelName: string, context: ChannelNodeContext) => Node) | null; readonly _trait: Trait; readonly _fields: EffectField[]; readonly _totalFloats: number; readonly _constantFactories: Record unknown>; readonly _node: (context: EffectNodeContext) => Node<'vec4'>; readonly _initialized: boolean; _nextId: number; _initialize(): void; buildNode(context: EffectNodeContext): Node<'vec4'>; }; /** * Create a MaterialEffect class from a configuration object. * * This is the simple factory path — for quick effect definitions without * writing a full class. Returns a class that extends MaterialEffect with * typed properties. * * @example * ```typescript * const DissolveEffect = createMaterialEffect({ * name: 'dissolve', * schema: { progress: 0 }, * node({ inputColor, attrs }) { * return mix(inputColor, vec4(0, 0, 0, 0), attrs.progress) * }, * }) * * const dissolve = new DissolveEffect() * dissolve.progress = 0.5 * sprite.addEffect(dissolve) * ``` */ declare function createMaterialEffect(config: MaterialEffectConfig): MaterialEffectClass; //#endregion export { ChannelNodeContext, ConstantKeys, EffectConstants, EffectField, EffectNodeContext, EffectSchema, EffectSchemaValue, EffectValues, MaterialEffect, MaterialEffectClass, SchemaToNodeType, UniformKeys, createMaterialEffect }; //# sourceMappingURL=MaterialEffect.d.ts.map