export interface RangeOption { type: 'range' default: number | [number, number] min: number max: number step?: number label?: string /** Optional UI grouping (e.g. 'Trunk', 'Canopy'); ungrouped params land in 'General' */ group?: string } export interface IntegerOption { type: 'integer' default: number | [number, number] min: number max: number label?: string /** Optional UI grouping (e.g. 'Trunk', 'Canopy'); ungrouped params land in 'General' */ group?: string } export interface ColorOption { type: 'color' default: string label?: string /** Optional UI grouping (e.g. 'Trunk', 'Canopy'); ungrouped params land in 'General' */ group?: string /** Semantic color role ('bark', 'leaf', …) — lets a StyleProfile supply the default. */ role?: string } export interface ColorArrayOption { type: 'color-array' default: string[] min?: number max?: number label?: string /** Optional UI grouping (e.g. 'Trunk', 'Canopy'); ungrouped params land in 'General' */ group?: string /** Semantic color role ('bark', 'leaf', …) — lets a StyleProfile supply the default. */ role?: string } export interface BooleanOption { type: 'boolean' default: boolean label?: string /** Optional UI grouping (e.g. 'Trunk', 'Canopy'); ungrouped params land in 'General' */ group?: string } export interface SelectOption { type: 'select' default: string options: string[] label?: string /** Optional UI grouping (e.g. 'Trunk', 'Canopy'); ungrouped params land in 'General' */ group?: string } export type OptionDef = RangeOption | IntegerOption | ColorOption | ColorArrayOption | BooleanOption | SelectOption export type OptionSchema = Record export type OptionValues = { [K in keyof S]: S[K] extends RangeOption ? number : S[K] extends IntegerOption ? number : S[K] extends ColorOption ? string : S[K] extends ColorArrayOption ? string[] : S[K] extends BooleanOption ? boolean : S[K] extends SelectOption ? string : never } /** A value that can be fixed or a [min, max] range resolved at generation time */ export type Randomizable = T | [T, T] /** * The *input* shape for a schema: like {@link OptionValues}, but numeric options also * accept a `[min, max]` tuple (resolved to a number at generation time). Use this for a * generator's public options type; use {@link OptionValues} for the resolved result. */ export type OptionInput = { [K in keyof S]: S[K] extends RangeOption ? Randomizable : S[K] extends IntegerOption ? Randomizable : S[K] extends ColorOption ? string : S[K] extends ColorArrayOption ? string[] : S[K] extends BooleanOption ? boolean : S[K] extends SelectOption ? string : never } /** * Resolve options for a generator: apply preset, then overrides, then fill schema defaults. * Numeric values can be [min, max] tuples — resolved using rand() if provided. * * Precedence: explicit overrides > preset > style palette (for `role`-tagged * color options) > schema default. A style re-colors everything it has a role * for, but never beats an author's or preset's explicit choice. */ export function resolveOptions( schema: S, options: Record, presets?: Record>, rand?: () => number, style?: { palettes: Record }, ): OptionValues { const presetName = options.preset ?? 'default' const preset = presets?.[presetName] ?? {} const { preset: _, style: _s, ...overrides } = options const resolved: Record = {} for (const [key, def] of Object.entries(schema)) { const role = (def.type === 'color' || def.type === 'color-array') ? def.role : undefined const stylePal = role && style ? style.palettes[role] : undefined const styleVal = stylePal === undefined ? undefined : def.type === 'color' ? stylePal[0] : [...stylePal] let val = overrides[key] ?? preset[key] ?? styleVal ?? structuredClone(def.default) // Resolve [min, max] ranges for numeric types if (Array.isArray(val) && val.length === 2 && typeof val[0] === 'number' && typeof val[1] === 'number' && (def.type === 'range' || def.type === 'integer')) { const r = rand ? rand() : Math.random() val = val[0] + (val[1] - val[0]) * r if (def.type === 'integer') val = Math.round(val) } resolved[key] = val } return resolved as OptionValues }