import type { VisualRegionDefinition } from "./regions" type RegionSet = readonly RegionName[] | "all" export type VisualInvariant = | { type: "required"; regions: readonly RegionName[] } | { type: "continuous-any"; regions: readonly RegionName[] } | { type: "unique"; regions: readonly RegionName[] } | { type: "stable"; regions: readonly RegionName[] } | { type: "fixed"; regions: readonly RegionName[]; tolerance?: number } | { type: "opacity"; regions: RegionSet; floor?: number } | { type: "motion" regions: RegionSet tolerance?: number maxReversals?: number maxPositionReversals?: number } | { type: "continuity"; regions: RegionSet } | { type: "label-stability"; regions: RegionSet } | { type: "flow"; regions: readonly RegionName[]; overlapTolerance?: number } | { type: "preserve-bottom-anchor" } | { type: "acquire-bottom-anchor" } export type VisualPlan = { regionNames?: readonly RegionName[] invariants: readonly VisualInvariant[] markerRequired?: readonly RegionName[] perMarker?: boolean aggregateMotion?: boolean } export type LegacyVisualStabilityOptions = { flow?: RegionName[] motionTolerance?: number opacityFloor?: number overlapTolerance?: number maxReversals?: number maxPositionReversals?: number stable?: RegionName[] fixed?: RegionName[] motion?: RegionName[] unique?: RegionName[] preserveBottomAnchor?: boolean acquireBottomAnchor?: boolean perMarker?: boolean continuousAny?: RegionName[][] required?: RegionName[] aggregateMotion?: boolean inferRequired?: boolean } export function visualPlan>( regions: Regions, invariants: readonly VisualInvariant>[], options: Omit>, "regionNames" | "invariants"> = {}, ): VisualPlan> { return { ...options, regionNames: Object.keys(regions) as Extract[], invariants } } export function legacyVisualPlan( options: LegacyVisualStabilityOptions = {}, ): VisualPlan { const inferred = options.inferRequired === false ? [] : [ ...(options.stable ?? []), ...(options.fixed ?? []), ...(options.unique ?? []), ...(options.motion ?? []), ...(options.flow ?? []), ] return { perMarker: options.perMarker, aggregateMotion: options.aggregateMotion, markerRequired: [ ...(options.required ?? []), ...(options.stable ?? []), ...(options.fixed ?? []), ...(options.unique ?? []), ...(options.motion ?? []), ...(options.flow ?? []), ], invariants: [ { type: "required", regions: [...(options.required ?? []), ...inferred] }, ...(options.continuousAny ?? []).map( (regions): VisualInvariant => ({ type: "continuous-any", regions }), ), ...(options.unique ? [{ type: "unique" as const, regions: options.unique }] : []), ...(options.stable ? [{ type: "stable" as const, regions: options.stable }] : []), ...(options.fixed ? [{ type: "fixed" as const, regions: options.fixed, tolerance: options.motionTolerance }] : []), { type: "opacity", regions: "all", floor: options.opacityFloor }, { type: "continuity", regions: "all" }, { type: "motion", regions: options.motion ?? "all", tolerance: options.motionTolerance, maxReversals: options.maxReversals, maxPositionReversals: options.maxPositionReversals, }, { type: "label-stability", regions: "all" }, ...(options.preserveBottomAnchor ? [{ type: "preserve-bottom-anchor" as const }] : []), ...(options.acquireBottomAnchor ? [{ type: "acquire-bottom-anchor" as const }] : []), ...(options.flow ? [{ type: "flow" as const, regions: options.flow, overlapTolerance: options.overlapTolerance }] : []), ], } }