@ikijs/editor 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/document.ts","../src/mesh-uv.ts","../src/commands.ts","../src/grid-keyform.ts","../src/reparent.ts","../src/alpha-bbox.ts","../src/binding-capture.ts","../src/atlas.ts","../src/factories.ts","../src/auto-rig.ts"],"sourcesContent":["import { parseIkiModel } from \"@ikijs/format\";\nimport type {\n IkiBinding,\n IkiDeformer,\n IkiDeformerBinding,\n IkiDeformerTransform,\n IkiMatrixDeformer,\n IkiModel,\n IkiPart,\n IkiPhysics,\n IkiTexture,\n IkiTransform,\n IkiUvRect,\n IkiWarpDeformer,\n} from \"@ikijs/format\";\n\nimport type { EditCommand } from \"./commands\";\nimport { remapMeshUvsToRect } from \"./mesh-uv\";\n\n/**\n * One part mapped to an imported atlas source. `index` is always 0 because the\n * atlas is a single page; only the `uv` sub-rectangle varies per part.\n */\nexport interface AtlasAssignment {\n partId: string;\n uv: IkiUvRect;\n}\n\n/** Input to {@link EditorDocument.applyAtlas}: the new atlas table plus the\n * per-part UV assignments into it. */\nexport interface ApplyAtlasInput {\n textures: IkiTexture[];\n partTextureAssignments: AtlasAssignment[];\n}\n\n/**\n * In-memory editing session over a single {@link IkiModel}. The model is held\n * directly (no superset) and mutated in place by invertible {@link EditCommand}s\n * pushed through {@link execute}; undo/redo invert/re-apply them.\n *\n * The constructor `structuredClone`s the input so the caller's model is never\n * mutated. Parts are addressed by stable `id`, never by array index.\n */\nexport class EditorDocument {\n private readonly model: IkiModel;\n private readonly undoStack: EditCommand[] = [];\n private readonly redoStack: EditCommand[] = [];\n /** Editor-only session state, never serialized; keyed by stable part id. The\n * unmodified BASE local uvs of every mesh part, captured once at construction\n * so atlas remaps always derive from the original (idempotent). */\n private readonly baseMeshUvs = new Map<string, number[]>();\n\n constructor(model: IkiModel) {\n this.model = structuredClone(model);\n // Capture-once base UVs. Scope: assumes the loaded model's mesh parts carry\n // original LOCAL 0..1 uvs (true for the sample). Restoring base UVs after\n // reloading an already-textured exported model is out of scope (deferred —\n // needs project-file persistence).\n for (const part of this.model.parts) {\n if (part.mesh) {\n this.baseMeshUvs.set(part.id, part.mesh.uvs.slice());\n }\n }\n }\n\n /** Live reference to the working model — for READ access. Mutate it only\n * through {@link execute}/{@link undo}/{@link redo}. */\n getModel(): IkiModel {\n return this.model;\n }\n\n /** Resolve a part by stable id. Throws a plain `Error` (NOT an\n * `IkiFormatError`) with a path-qualified message if the id is unknown. */\n findPart(id: string): IkiPart {\n const part = this.model.parts.find((p) => p.id === id);\n if (!part) {\n throw new Error(`parts: no part with id \"${id}\"`);\n }\n return part;\n }\n\n /** Resolve a warp deformer by stable id. Throws a path-qualified plain\n * `Error` if no deformer matches the id or the match is not a warp deformer.\n * READ/mutate-through accessor, consistent with {@link findPart}. */\n findWarpDeformer(id: string): IkiWarpDeformer {\n const deformer = this.model.deformers?.find((d) => d.id === id);\n if (!deformer || deformer.kind !== \"warp\") {\n throw new Error(`deformers: no warp deformer with id \"${id}\"`);\n }\n return deformer;\n }\n\n /** Resolve a matrix deformer by stable id. Throws a path-qualified plain\n * `Error` if no deformer matches the id or the match is a warp deformer\n * (`kind === \"warp\"`). A `kind` of `\"matrix\"` or `undefined` is a matrix\n * deformer. READ/mutate-through accessor, consistent with {@link findPart}. */\n findMatrixDeformer(id: string): IkiMatrixDeformer {\n const deformer = this.model.deformers?.find((d) => d.id === id);\n if (!deformer || deformer.kind === \"warp\") {\n throw new Error(`deformers: no matrix deformer with id \"${id}\"`);\n }\n return deformer;\n }\n\n /** Resolve any deformer (matrix or warp) by stable id. Throws a\n * path-qualified plain `Error` if no deformer matches the id.\n * READ/mutate-through accessor, consistent with {@link findPart}. */\n findDeformer(id: string): IkiDeformer {\n const deformer = this.model.deformers?.find((d) => d.id === id);\n if (!deformer) {\n throw new Error(`deformers: no deformer with id \"${id}\"`);\n }\n return deformer;\n }\n\n /** Resolve a physics rig by stable id. Throws a path-qualified plain `Error`\n * if no rig matches the id. The `physics` array is optional, so guard it.\n * READ/mutate-through accessor, consistent with {@link findDeformer}. */\n findPhysicsRig(id: string): IkiPhysics {\n const rig = this.model.physics?.find((r) => r.id === id);\n if (!rig) {\n throw new Error(`physics: no physics rig with id \"${id}\"`);\n }\n return rig;\n }\n\n /**\n * Record the base mesh UVs for a part inserted AFTER construction (e.g. by\n * {@link AddPart}). Returns the PRIOR entry for that id (or `undefined` if\n * none existed) so the caller can restore it on undo. No-op for meshless\n * parts (returns `undefined`). Must be called by any command that pushes a\n * mesh part into the model so the part joins the constructor-captured base-UV\n * side state required by {@link applyAtlas}.\n *\n * The returned prior value is what {@link restoreBaseMeshUvs} expects on\n * undo — pass it verbatim. Hazard guarded: DeletePart(X) → AddPart(X′) →\n * undo(add) → undo(delete); without restore, X's constructor-captured entry\n * would be permanently gone after undo of the add, causing applyAtlas to\n * fail when X is restored.\n */\n captureBaseMeshUvs(partId: string): number[] | undefined {\n const prev = this.baseMeshUvs.get(partId);\n const part = this.model.parts.find((p) => p.id === partId);\n if (part?.mesh) {\n this.baseMeshUvs.set(partId, part.mesh.uvs.slice());\n }\n return prev;\n }\n\n /**\n * Restore the base-UV side state to exactly what it was before a\n * {@link captureBaseMeshUvs} call. Called from {@link AddPart.invert}:\n * pass the value returned by captureBaseMeshUvs on first apply.\n * - `prev` is `number[]` → sets the entry (restores a prior mesh's base).\n * - `prev` is `undefined` → deletes the entry (no entry existed before the\n * add, so a later different-mesh part reusing the id must not inherit this\n * one's base).\n * DeletePart does NOT call this — the entry persists across delete/undo so\n * the restored part still has its base available for applyAtlas.\n */\n restoreBaseMeshUvs(partId: string, prev: number[] | undefined): void {\n if (prev !== undefined) {\n this.baseMeshUvs.set(partId, prev);\n } else {\n this.baseMeshUvs.delete(partId);\n }\n }\n\n /** The construction-captured base UVs for a mesh part. Throws a path-qualified\n * plain `Error` if absent. SINGLE accessor for both apply branches — never\n * read `baseMeshUvs` with a bare `!` elsewhere. */\n private requireBaseUvs(partId: string): number[] {\n const base = this.baseMeshUvs.get(partId);\n if (!base) {\n throw new Error(`parts: no base mesh uvs captured for part \"${partId}\"`);\n }\n return base;\n }\n\n /**\n * Replace the atlas table and rewrite every part's texture reference in a\n * single atomic step. For every part in `partTextureAssignments` set\n * `texture = { index: 0, uv }`; CLEAR `texture` (delete the key) on every\n * other part.\n *\n * Mesh parts are textured as a MATCHED PAIR: an assigned mesh part also has\n * its per-vertex `mesh.uvs` remapped (from the construction-captured base)\n * into the same `uv` rect; an unassigned mesh part has its `mesh.uvs` restored\n * to that base. Quad parts carry `texture.uv` only and are untouched here.\n *\n * Deliberately NON-undoable: it does NOT push to or clear the undo/redo\n * stacks (texture/atlas state is not undoable in 5b — the unified\n * editor-state superset is deferred to 5d). `canUndo()`/`canRedo()` are\n * unchanged after a call.\n *\n * Validate-all-then-apply: structural input validation, per-partId\n * resolution, and a base-UV preflight over every mesh part all run BEFORE any\n * mutation, so a bad input (wrong shape, duplicate partId, unknown partId, or\n * a mesh part with no captured base) throws a plain `Error` and leaves the\n * model exactly as it was — never a partial application.\n */\n applyAtlas(input: ApplyAtlasInput): void {\n // Step A — structural validation of the input shape.\n const { texture, assignmentsByPart } = this.normalizeAtlasInput(input);\n\n // Step B — resolve every partId before mutating anything.\n const resolved = new Map<IkiPart, IkiUvRect>();\n for (const [partId, uv] of assignmentsByPart) {\n resolved.set(this.findPart(partId), uv);\n }\n\n // Step C — preflight base UVs for EVERY mesh part (assigned AND unassigned):\n // the mutate loop's clear/restore branch also reads the base of unassigned\n // mesh parts, so this guard must cover them before any write so atomicity\n // (validate-all-then-apply) holds.\n for (const part of this.model.parts) {\n if (part.mesh) {\n this.requireBaseUvs(part.id);\n }\n }\n\n // Mutate — only reached when A + B + C all pass.\n this.model.textures =\n texture === undefined ? undefined : [{ source: texture.source }];\n for (const part of this.model.parts) {\n const uv = resolved.get(part);\n if (uv) {\n part.texture = {\n index: 0,\n uv: { x: uv.x, y: uv.y, width: uv.width, height: uv.height },\n };\n if (part.mesh) {\n // Replace the mesh object so each part owns its own uvs array —\n // de-aliases parts that shared the same mesh object in the input model.\n part.mesh = {\n ...part.mesh,\n uvs: remapMeshUvsToRect(this.requireBaseUvs(part.id), uv),\n };\n }\n } else {\n delete part.texture;\n if (part.mesh) {\n // Same spread here to break aliasing on restore as well.\n part.mesh = {\n ...part.mesh,\n uvs: this.requireBaseUvs(part.id).slice(),\n };\n }\n }\n }\n }\n\n /**\n * Clear a model-committed texture reference from a single part. Deliberately\n * NON-undoable, matching {@link applyAtlas} — texture/atlas state is not in\n * the undo model (unified editor-state is deferred). Does NOT push to or\n * clear the undo/redo stacks. Does NOT touch `mesh.uvs` — atlas-space UVs on\n * an untextured mesh are inert for color rendering; a later atlas import\n * remaps from the base UVs anyway.\n *\n * This is the single consistent texture/atlas undo boundary: by the time a\n * part is deletable (no texture), its DeletePart snapshot carries no texture\n * reference, so no stale index can resurface on undo after a later atlas\n * repack.\n *\n * Throws a path-qualified plain `Error` (NOT `IkiFormatError`) if the part\n * id is unknown — same contract as {@link findPart}.\n */\n clearPartTextureRef(partId: string): void {\n const part = this.findPart(partId);\n delete part.texture;\n }\n\n /**\n * Validate the structural shape of an {@link ApplyAtlasInput} without\n * touching the model, returning a known-good `{ texture?, assignmentsByPart }`\n * for {@link applyAtlas} to resolve and apply.\n */\n private normalizeAtlasInput(input: ApplyAtlasInput): {\n texture?: IkiTexture;\n assignmentsByPart: Map<string, IkiUvRect>;\n } {\n if (input.textures.length > 1) {\n throw new Error(\n `applyAtlas: textures must be a single atlas page (got ${input.textures.length})`,\n );\n }\n if (\n input.partTextureAssignments.length > 0 &&\n input.textures.length !== 1\n ) {\n throw new Error(\n \"applyAtlas: partTextureAssignments require exactly one texture\",\n );\n }\n\n const assignmentsByPart = new Map<string, IkiUvRect>();\n for (const { partId, uv } of input.partTextureAssignments) {\n if (assignmentsByPart.has(partId)) {\n throw new Error(\n `applyAtlas: duplicate partId \"${partId}\" in partTextureAssignments`,\n );\n }\n assignmentsByPart.set(partId, uv);\n }\n\n return { texture: input.textures[0], assignmentsByPart };\n }\n\n /**\n * Overwrite a part's whole transform with a fresh copy of `transform`.\n * Replacing the whole object (rather than individual channels) preserves any\n * optional keys already absent from the incoming value.\n *\n * Deliberately NON-undoable — used ONLY for an editor app's transient capture\n * pose. Does NOT push to or clear undoStack/redoStack (sibling to\n * {@link applyAtlas}'s non-undoable boundary). The caller is responsible for\n * restoring the exact prior snapshot when the capture pose ends.\n *\n * `IkiTransform` is a flat number map, so a shallow spread is a sufficient\n * deep copy — no aliasing of the caller's object remains.\n */\n setPartTransformEphemeral(partId: string, transform: IkiTransform): void {\n const part = this.findPart(partId);\n part.transform = { ...transform };\n }\n\n /**\n * Overwrite a matrix deformer's optional transform with a fresh copy, or\n * delete it when `transform` is `undefined`.\n * `undefined` deletes the key, restoring the deformer to the same state as\n * one that never had a transform (absent-vs-present matters for downstream\n * renderers).\n *\n * Deliberately NON-undoable — used ONLY for an editor app's transient capture\n * pose. Does NOT push to or clear undoStack/redoStack (sibling to\n * {@link applyAtlas}'s non-undoable boundary). The caller is responsible for\n * restoring the exact prior snapshot when the capture pose ends.\n *\n * `IkiDeformerTransform` is a flat number map, so a shallow spread is a\n * sufficient deep copy — no aliasing of the caller's object remains.\n */\n setDeformerTransformEphemeral(\n deformerId: string,\n transform: IkiDeformerTransform | undefined,\n ): void {\n const deformer = this.findMatrixDeformer(deformerId);\n if (transform === undefined) {\n delete deformer.transform;\n } else {\n deformer.transform = { ...transform };\n }\n }\n\n /**\n * Overwrite a part's whole bindings array with a fresh deep copy, or delete\n * the key when `bindings` is empty.\n *\n * Deliberately NON-undoable — used ONLY for an editor app's transient capture\n * neutralization (zeroing the row being recaptured so the preview reflects\n * base-only during posing). Does NOT push to or clear undoStack/redoStack.\n * The caller is responsible for restoring the original bindings when the\n * capture session ends.\n */\n setPartBindingsEphemeral(partId: string, bindings: IkiBinding[]): void {\n const part = this.findPart(partId);\n if (bindings.length > 0) {\n part.bindings = bindings.map((b) => ({ ...b }));\n } else {\n delete part.bindings;\n }\n }\n\n /**\n * Overwrite a matrix deformer's whole bindings array with a fresh deep copy,\n * or delete the key when `bindings` is empty.\n *\n * Deliberately NON-undoable — used ONLY for an editor app's transient capture\n * neutralization (zeroing the row being recaptured so the preview reflects\n * base-only during posing). Does NOT push to or clear undoStack/redoStack.\n * The caller is responsible for restoring the original bindings when the\n * capture session ends.\n */\n setDeformerBindingsEphemeral(\n deformerId: string,\n bindings: IkiDeformerBinding[],\n ): void {\n const deformer = this.findMatrixDeformer(deformerId);\n if (bindings.length > 0) {\n deformer.bindings = bindings.map((b) => ({ ...b }));\n } else {\n delete deformer.bindings;\n }\n }\n\n /** Apply a command and record it as one undo step. Clears the redo stack. */\n execute(cmd: EditCommand): void {\n cmd.apply(this);\n this.undoStack.push(cmd);\n this.redoStack.length = 0;\n }\n\n /** Invert the most recent command and move it onto the redo stack. */\n undo(): void {\n const cmd = this.undoStack.pop();\n if (!cmd) return;\n cmd.invert(this);\n this.redoStack.push(cmd);\n }\n\n /** Re-apply the most recently undone command and move it back onto undo. */\n redo(): void {\n const cmd = this.redoStack.pop();\n if (!cmd) return;\n cmd.apply(this);\n this.undoStack.push(cmd);\n }\n\n canUndo(): boolean {\n return this.undoStack.length > 0;\n }\n\n canRedo(): boolean {\n return this.redoStack.length > 0;\n }\n\n /**\n * Validate and export the current working model by running it through\n * {@link parseIkiModel}. Uses `structuredClone` so the validator's\n * normalized output cannot alias the working model. Propagates\n * `IkiFormatError` unchanged on failure — callers surface `.message`.\n */\n toIkiModel(): IkiModel {\n return parseIkiModel(structuredClone(this.model));\n }\n\n /**\n * Pretty-print the validated model as a `.iki` JSON string. Always\n * validates first — invalid documents never reach a file.\n */\n serialize(): string {\n return JSON.stringify(this.toIkiModel(), null, 2);\n }\n}\n","import type { IkiUvRect } from \"@ikijs/format\";\n\n/**\n * Affinely map a mesh's BASE local UVs into an atlas sub-rectangle.\n *\n * Per the `@ikijs/format` UV convention (top-left origin, +y down, 0..1; see\n * {@link IkiUvRect}/{@link IkiMesh}), the input is the part's BASE local uvs and\n * the output places each component into `rect` with NO flip — base UV space and\n * `rect` share the same orientation:\n * out[2i] = rect.x + baseUvs[2i] * rect.width\n * out[2i+1] = rect.y + baseUvs[2i+1] * rect.height\n *\n * The caller guarantees `rect` is inset/clamped (via `uvRectFor`) so every\n * output component stays within 0..1 and passes the format validator.\n *\n * Returns a fresh array; `baseUvs` is never mutated.\n */\nexport function remapMeshUvsToRect(\n baseUvs: number[],\n rect: IkiUvRect,\n): number[] {\n if (baseUvs.length % 2 !== 0) {\n throw new Error(\n `remapMeshUvsToRect: baseUvs must have an even length (u,v pairs), got ${baseUvs.length}`,\n );\n }\n\n const out = new Array<number>(baseUvs.length);\n for (let i = 0; i < baseUvs.length; i += 2) {\n out[i] = rect.x + baseUvs[i] * rect.width;\n out[i + 1] = rect.y + baseUvs[i + 1] * rect.height;\n }\n return out;\n}\n","import type {\n IkiBinding,\n IkiDeformer,\n IkiDeformerBinding,\n IkiDeformerTransform,\n IkiGridKeyform,\n IkiMatrixDeformer,\n IkiMesh,\n IkiModel,\n IkiPart,\n IkiPhysics,\n} from \"@ikijs/format\";\nimport {\n IKI_FORMAT_VERSION,\n IkiFormatError,\n parseIkiModel,\n} from \"@ikijs/format\";\n\nimport type { EditorDocument } from \"./document\";\nimport { upsertGridKeyform } from \"./grid-keyform\";\nimport { remapMeshUvsToRect } from \"./mesh-uv\";\nimport {\n validateDeformerDelete,\n validateDeformerReparent,\n validatePartAttach,\n} from \"./reparent\";\n\n/**\n * Scan the id-flat namespace (parts first, then deformers) and return which\n * array already holds `id`, or `undefined` if the id is free. Parts and\n * deformers share a single flat id namespace, so both arrays must be checked\n * to produce a source-qualified collision message.\n */\nfunction findIdCollision(\n model: IkiModel,\n id: string,\n): \"part\" | \"deformer\" | undefined {\n for (const p of model.parts) {\n if (p.id === id) return \"part\";\n }\n for (const d of model.deformers ?? []) {\n if (d.id === id) return \"deformer\";\n }\n return undefined;\n}\n\n/**\n * One invertible edit. The document is passed IN at apply/invert time — a\n * command is constructed from `(partId, value)` alone, before any document\n * exists — so the same command object can be applied, inverted, and re-applied\n * by the undo/redo stack.\n *\n * Prior-value capture happens exactly ONCE, on the first {@link apply}. `redo`\n * (a second `apply`) reuses that captured value rather than re-reading the\n * current field, so undo always restores the original target.\n */\nexport interface EditCommand {\n apply(doc: EditorDocument): void;\n invert(doc: EditorDocument): void;\n readonly label: string;\n}\n\n/** Channels of {@link IkiTransform} this editor can edit (object-field names,\n * NOT the binding `IkiTransformChannel` vocabulary). */\nexport type EditTransformChannel =\n | \"x\"\n | \"y\"\n | \"rotation\"\n | \"scaleX\"\n | \"scaleY\"\n | \"opacity\";\n\n/**\n * Generic single-field command: reads/writes one field of the resolved part via\n * a getter/setter closure, capturing the prior value on the first `apply` and\n * restoring it on `invert`. `T` is the captured value's type; for cloned values\n * (the color tuple) the getter/setter perform the copy.\n */\nclass FieldCommand<T> implements EditCommand {\n readonly label: string;\n private captured = false;\n private prevValue!: T;\n\n constructor(\n private readonly partId: string,\n private readonly newValue: T,\n label: string,\n private readonly get: (part: IkiPart) => T,\n private readonly set: (part: IkiPart, value: T) => void,\n ) {\n this.label = label;\n }\n\n apply(doc: EditorDocument): void {\n const part = doc.findPart(this.partId);\n if (!this.captured) {\n this.prevValue = this.get(part);\n this.captured = true;\n }\n this.set(part, this.newValue);\n }\n\n invert(doc: EditorDocument): void {\n const part = doc.findPart(this.partId);\n this.set(part, this.prevValue);\n }\n}\n\n/** Edit a part's RGBA fill. The 4-tuple is mutable, so the command clones on\n * construction (caller's array), on capture (part's current color), and on\n * assign (writing to the part) — it never retains the caller's or model's\n * array by reference. */\nexport class SetPartColor extends FieldCommand<\n [number, number, number, number]\n> {\n constructor(partId: string, rgba: [number, number, number, number]) {\n super(\n partId,\n [...rgba] as [number, number, number, number],\n \"Set color\",\n (part) => [...part.color],\n (part, value) => {\n part.color = [...value] as [number, number, number, number];\n },\n );\n }\n}\n\n/** Edit a part's width (model-space units). */\nexport class SetPartWidth extends FieldCommand<number> {\n constructor(partId: string, value: number) {\n super(\n partId,\n value,\n \"Set width\",\n (part) => part.width,\n (part, v) => {\n part.width = v;\n },\n );\n }\n}\n\n/** Edit a part's height (model-space units). */\nexport class SetPartHeight extends FieldCommand<number> {\n constructor(partId: string, value: number) {\n super(\n partId,\n value,\n \"Set height\",\n (part) => part.height,\n (part, v) => {\n part.height = v;\n },\n );\n }\n}\n\n/** Edit a part's paint order. */\nexport class SetPartOrder extends FieldCommand<number> {\n constructor(partId: string, value: number) {\n super(\n partId,\n value,\n \"Set order\",\n (part) => part.order,\n (part, v) => {\n part.order = v;\n },\n );\n }\n}\n\n/**\n * Edit one channel of a part's base transform. `x`/`y` are required; the rest\n * are optional and may be absent on the part. For an optional channel the\n * command captures the raw current value INCLUDING `undefined`, and restoring\n * `undefined` DELETES the key so undo returns the part to its original\n * (possibly-omitted) shape. Engine defaults (rotation 0 / scale 1 / opacity 1)\n * are NOT substituted here.\n */\nexport class SetPartTransform extends FieldCommand<number | undefined> {\n constructor(partId: string, channel: EditTransformChannel, value: number) {\n super(\n partId,\n value,\n \"Set transform\",\n (part) => part.transform[channel],\n (part, v) => {\n if (v === undefined) {\n delete part.transform[channel];\n } else {\n part.transform[channel] = v;\n }\n },\n );\n }\n}\n\n/**\n * Capture the warp deformer's grid as one keyform at the driving parameter\n * `value`, upserting `offsets` into `warps[0].keyforms`. The 4-tuple-style\n * mutable `offsets` array is cloned on construction so a later caller mutation\n * cannot corrupt apply/redo (mirrors {@link SetPartColor}).\n *\n * `apply` validates BEFORE mutating: the deformer must have a grid warp, the\n * offsets length must equal `grid.points.length`, and `value` must lie within\n * the driving parameter's declared `[min,max]` (fail fast, before\n * `parseIkiModel` would reject it). Prior keyforms are deep-copied once on the\n * first `apply` (capture-once like {@link FieldCommand}); `invert` restores\n * that deep copy so re-apply after undo never aliases.\n */\nexport class CaptureGridKeyform implements EditCommand {\n readonly label = \"Capture grid keyform\";\n private readonly offsets: number[];\n private captured = false;\n private prevKeyforms!: IkiGridKeyform[];\n\n constructor(\n private readonly deformerId: string,\n private readonly value: number,\n offsets: number[],\n ) {\n this.offsets = [...offsets];\n }\n\n apply(doc: EditorDocument): void {\n const deformer = doc.findWarpDeformer(this.deformerId);\n const warp = deformer.warps?.[0];\n if (!warp) {\n throw new Error(\n `deformers.\"${this.deformerId}\".warps: no grid warp to capture into`,\n );\n }\n if (this.offsets.length !== deformer.grid.points.length) {\n throw new Error(\n `deformers.\"${this.deformerId}\".warps[0].keyforms.offsets length ${this.offsets.length} must equal grid.points length ${deformer.grid.points.length}`,\n );\n }\n const param = doc\n .getModel()\n .parameters.find((p) => p.id === warp.parameter);\n if (!param) {\n throw new Error(\n `deformers.\"${this.deformerId}\".warps[0].parameter \"${warp.parameter}\" is not a declared parameter`,\n );\n }\n if (this.value < param.min || this.value > param.max) {\n throw new Error(\n `deformers.\"${this.deformerId}\".warps[0].keyforms.value ${this.value} is outside parameter \"${warp.parameter}\" range [${param.min},${param.max}]`,\n );\n }\n\n if (!this.captured) {\n this.prevKeyforms = structuredClone(warp.keyforms);\n this.captured = true;\n }\n warp.keyforms = upsertGridKeyform(warp.keyforms, this.value, [\n ...this.offsets,\n ]);\n }\n\n invert(doc: EditorDocument): void {\n const warp = doc.findWarpDeformer(this.deformerId).warps?.[0];\n if (!warp) {\n throw new Error(\n `deformers.\"${this.deformerId}\".warps: no grid warp to restore into`,\n );\n }\n warp.keyforms = structuredClone(this.prevKeyforms);\n }\n}\n\n/**\n * Generic single-field command targeting a matrix deformer, mirroring\n * {@link FieldCommand} but resolving via `doc.findMatrixDeformer` instead of\n * `doc.findPart`. Capture-once on first `apply`; restore on `invert`.\n */\nclass DeformerFieldCommand<T> implements EditCommand {\n readonly label: string;\n private captured = false;\n private prevValue!: T;\n\n constructor(\n private readonly deformerId: string,\n private readonly newValue: T,\n label: string,\n private readonly get: (deformer: IkiMatrixDeformer) => T,\n private readonly set: (deformer: IkiMatrixDeformer, value: T) => void,\n ) {\n this.label = label;\n }\n\n apply(doc: EditorDocument): void {\n const deformer = doc.findMatrixDeformer(this.deformerId);\n if (!this.captured) {\n this.prevValue = this.get(deformer);\n this.captured = true;\n }\n this.set(deformer, this.newValue);\n }\n\n invert(doc: EditorDocument): void {\n const deformer = doc.findMatrixDeformer(this.deformerId);\n this.set(deformer, this.prevValue);\n }\n}\n\n/** Edit a matrix deformer's pivot x. */\nexport class SetDeformerPivotX extends DeformerFieldCommand<number> {\n constructor(deformerId: string, value: number) {\n super(\n deformerId,\n value,\n \"Set pivot x\",\n (d) => d.pivot.x,\n (d, v) => {\n d.pivot.x = v;\n },\n );\n }\n}\n\n/** Edit a matrix deformer's pivot y. */\nexport class SetDeformerPivotY extends DeformerFieldCommand<number> {\n constructor(deformerId: string, value: number) {\n super(\n deformerId,\n value,\n \"Set pivot y\",\n (d) => d.pivot.y,\n (d, v) => {\n d.pivot.y = v;\n },\n );\n }\n}\n\n/**\n * Set a matrix deformer's pivot x and y atomically (one drag = one undo step).\n * {@link SetDeformerPivotX} and {@link SetDeformerPivotY} remain for the\n * Inspector's single-axis number inputs.\n */\nexport class SetDeformerPivot implements EditCommand {\n readonly label = \"Set pivot\";\n private captured = false;\n private prevPivot!: { x: number; y: number };\n private readonly pivot: { x: number; y: number };\n\n constructor(\n private readonly deformerId: string,\n pivot: { x: number; y: number },\n ) {\n // Fresh clone so a caller mutating their arg after construction cannot\n // corrupt apply/redo.\n this.pivot = { x: pivot.x, y: pivot.y };\n }\n\n apply(doc: EditorDocument): void {\n const deformer = doc.findMatrixDeformer(this.deformerId);\n if (!this.captured) {\n this.prevPivot = { x: deformer.pivot.x, y: deformer.pivot.y };\n this.captured = true;\n }\n deformer.pivot = { x: this.pivot.x, y: this.pivot.y };\n }\n\n invert(doc: EditorDocument): void {\n const deformer = doc.findMatrixDeformer(this.deformerId);\n // Fresh clone — never alias the captured object so repeated undo/redo\n // cycles cannot corrupt the saved prior value.\n deformer.pivot = { x: this.prevPivot.x, y: this.prevPivot.y };\n }\n}\n\n/** Channels of {@link IkiDeformerTransform} this editor can edit. */\nexport type DeformerTransformChannel =\n | \"x\"\n | \"y\"\n | \"rotation\"\n | \"scaleX\"\n | \"scaleY\";\n\n/**\n * Edit one channel of a matrix deformer's base transform. Because\n * {@link IkiDeformerTransform} REQUIRES finite `x` and `y`, this command\n * captures and restores the WHOLE prior `transform` object (present or absent)\n * rather than a single channel, so undo can delete the transform when it did\n * not previously exist, and redo never produces a partial object missing `x`/`y`.\n *\n * When no `transform` is present on the deformer, `apply` creates one from\n * the identity base `{ x: 0, y: 0 }` — the minimal valid shape the validator\n * accepts — then writes the edited channel. For example, editing `rotation`\n * on a transform-less deformer yields `{ x: 0, y: 0, rotation: <value> }`.\n */\nexport class SetDeformerTransform implements EditCommand {\n readonly label = \"Set deformer transform\";\n private captured = false;\n private prevTransform!: IkiDeformerTransform | undefined;\n\n constructor(\n private readonly deformerId: string,\n private readonly channel: DeformerTransformChannel,\n private readonly value: number,\n ) {}\n\n apply(doc: EditorDocument): void {\n const deformer = doc.findMatrixDeformer(this.deformerId);\n if (!this.captured) {\n // Shallow clone is sufficient — IkiDeformerTransform is a flat number map.\n this.prevTransform =\n deformer.transform === undefined\n ? undefined\n : { ...deformer.transform };\n this.captured = true;\n }\n // Start from the existing transform or the identity base. The identity base\n // is { x: 0, y: 0 } because the validator unconditionally requires finite\n // x and y whenever a transform object is present.\n const next: IkiDeformerTransform = {\n ...(deformer.transform ?? { x: 0, y: 0 }),\n };\n next[this.channel] = this.value;\n deformer.transform = next;\n }\n\n invert(doc: EditorDocument): void {\n const deformer = doc.findMatrixDeformer(this.deformerId);\n if (this.prevTransform === undefined) {\n delete deformer.transform;\n } else {\n // Assign a fresh clone — never alias the captured object so repeated\n // undo/redo cycles cannot corrupt the saved prior value.\n deformer.transform = { ...this.prevTransform };\n }\n }\n}\n\n/**\n * Replace a matrix deformer's `bindings` array wholesale. A single command\n * covers add, edit, and remove (pass the desired final array; pass `[]` to\n * remove all). Mirrors {@link CaptureGridKeyform}'s deep-copy discipline:\n * clone-on-construction, capture-once, fresh deep copy on invert.\n *\n * Each {@link IkiDeformerBinding} is a flat object so a per-element spread\n * `{ ...b }` is a sufficient deep copy.\n */\nexport class SetDeformerBindings implements EditCommand {\n readonly label = \"Set deformer bindings\";\n private readonly bindings: IkiDeformerBinding[];\n private captured = false;\n private prevBindings!: IkiDeformerBinding[] | undefined;\n\n constructor(\n private readonly deformerId: string,\n bindings: IkiDeformerBinding[],\n ) {\n // Clone the caller's array on construction — prevents post-execute mutation\n // of the caller's array from corrupting apply/redo.\n this.bindings = bindings.map((b) => ({ ...b }));\n }\n\n apply(doc: EditorDocument): void {\n const deformer = doc.findMatrixDeformer(this.deformerId);\n\n // Validate a NARROW synthetic candidate before capture/mutate, exactly as\n // {@link SetPartBindings} does and for the same reason: a full\n // structuredClone(doc.getModel()) would also validate parts and deformers\n // the user is not editing, and those routinely hold in-flight `NaN` from an\n // emptied numeric input — so an unrelated blank field would refuse this\n // edit, and report it against the wrong object. The Inspector offers every\n // parameter in the model, so an undeclared id, a non-finite endpoint, or a\n // non-matrix channel must still be caught here.\n //\n // NOT covered by this shape: a binding that feeds a physics chain's own\n // anchor. That rule needs the whole deformer hierarchy, and reproducing it\n // over a sanitized model would mean hand-maintaining a second model shape\n // beside the validator. It stays an export-time check in `toIkiModel()`.\n // Parts and deformers share one flat id namespace, so the two synthetic\n // objects below must not both be \"_\".\n const candidateDeformer: Record<string, unknown> = {\n kind: \"matrix\",\n id: \"_d\",\n pivot: { x: 0, y: 0 },\n };\n if (this.bindings.length > 0) {\n candidateDeformer.bindings = this.bindings.map((b) => ({ ...b }));\n }\n const candidate = {\n version: IKI_FORMAT_VERSION,\n name: \"_\",\n canvas: { width: 1, height: 1 },\n parameters: doc.getModel().parameters,\n parts: [\n {\n id: \"_\",\n color: [0, 0, 0, 1],\n width: 1,\n height: 1,\n transform: { x: 0, y: 0 },\n order: 0,\n },\n ],\n deformers: [candidateDeformer],\n };\n // The synthetic deformer is always at deformers[0]; rewrite that prefix so\n // the surfaced error names the real target.\n try {\n parseIkiModel(candidate);\n } catch (e) {\n if (e instanceof IkiFormatError) {\n throw new IkiFormatError(\n e.message.replace(\n /^deformers\\[0\\]/,\n `deformers.\"${this.deformerId}\"`,\n ),\n );\n }\n throw e;\n }\n\n if (!this.captured) {\n // Preserve the original absent-vs-empty distinction.\n this.prevBindings =\n deformer.bindings === undefined\n ? undefined\n : deformer.bindings.map((b) => ({ ...b }));\n this.captured = true;\n }\n if (this.bindings.length > 0) {\n // Assign a fresh deep copy — model must never alias the command's stored array.\n deformer.bindings = this.bindings.map((b) => ({ ...b }));\n } else {\n // Delete the key on empty: keeps the model shape minimal and correctly\n // represents \"no bindings\" as an absent key rather than an empty array.\n delete deformer.bindings;\n }\n }\n\n invert(doc: EditorDocument): void {\n const deformer = doc.findMatrixDeformer(this.deformerId);\n if (this.prevBindings === undefined) {\n delete deformer.bindings;\n } else {\n // Assign a fresh deep copy — never alias the captured array so re-apply\n // after undo cannot corrupt the saved prior value.\n deformer.bindings = this.prevBindings.map((b) => ({ ...b }));\n }\n }\n}\n\n/**\n * Reparent a deformer (matrix or warp) under a new parent, or promote it to\n * root (`newParentId === undefined`). Calls {@link validateDeformerReparent}\n * FIRST so invalid reparents (cycles, warp parent, unknown id) throw before any\n * capture or mutation — a throwing apply leaves the model and undo stack\n * untouched.\n *\n * Absent-vs-present distinction: captures both the prior `parent` value AND\n * whether the key was present on the object, so `invert` can delete the key\n * (restore \"absent\") rather than blindly assigning `undefined`.\n */\nexport class SetDeformerParent implements EditCommand {\n readonly label = \"Set deformer parent\";\n private captured = false;\n private prevParent: string | undefined = undefined;\n private prevHadParent = false;\n\n constructor(\n private readonly deformerId: string,\n private readonly newParentId: string | undefined,\n ) {}\n\n apply(doc: EditorDocument): void {\n // Validate FIRST — throws before capture/mutate on any violation.\n validateDeformerReparent(\n doc.getModel().deformers ?? [],\n this.deformerId,\n this.newParentId,\n );\n\n // A new ANCESTOR can still bind a parameter that a physics chain anchored\n // here emits, which the format rejects as feedback. That rule needs the\n // whole hierarchy, and validating a full clone of the document here refuses\n // the edit whenever any unrelated part or deformer holds in-flight `NaN`\n // from an emptied numeric input — reporting it against the wrong object.\n // So the feedback case stays an export-time check in `toIkiModel()`, where\n // it was before, rather than trading a rare corruption for a common block.\n //\n // Known cost of that trade: the host surfaces the export error (the editor\n // app re-exports on a ~200ms debounce), but the message names the CHAIN, not\n // the edit — reparent `headDeformer` and you get\n // `physicsChains[0].segments[1].output.parameter \"...\" feeds its own anchor\n // deformer chain (feedback)`, which reads as a complaint about a hair lock.\n // If a friendlier error surface ever lands, this is the message to special-case.\n const deformer = doc.findDeformer(this.deformerId);\n if (!this.captured) {\n this.prevHadParent = Object.prototype.hasOwnProperty.call(\n deformer,\n \"parent\",\n );\n this.prevParent = deformer.parent;\n this.captured = true;\n }\n if (this.newParentId !== undefined) {\n deformer.parent = this.newParentId;\n } else {\n delete deformer.parent;\n }\n }\n\n invert(doc: EditorDocument): void {\n const deformer = doc.findDeformer(this.deformerId);\n if (this.prevHadParent) {\n deformer.parent = this.prevParent;\n } else {\n delete deformer.parent;\n }\n }\n}\n\n/**\n * Add a new part to the model. Validates the candidate model with\n * {@link parseIkiModel} BEFORE mutating, so a structurally invalid part\n * (bad color tuple, missing required fields, id collision with a deformer)\n * throws an `IkiFormatError` and leaves the model untouched.\n *\n * Captures the prior base-UV entry for the part's id on the FIRST apply so\n * invert can restore the exact pre-add state. This guards the id-reuse hazard:\n * DeletePart(X) → AddPart(X′, different mesh) → undo(add) → undo(delete) —\n * without restore, X's constructor-captured base would be gone, causing\n * applyAtlas to fail when X is restored by the undo of the delete.\n */\nexport class AddPart implements EditCommand {\n readonly label = \"Add part\";\n private readonly part: IkiPart;\n private captured = false;\n private prevBaseMeshUvs: number[] | undefined = undefined;\n\n constructor(part: IkiPart) {\n // Clone on construction — prevents caller mutation from corrupting apply/redo.\n this.part = structuredClone(part);\n }\n\n apply(doc: EditorDocument): void {\n // (a) Cheap id-uniqueness pre-check with a source-qualified message.\n const hit = findIdCollision(doc.getModel(), this.part.id);\n if (hit === \"part\") {\n throw new Error(\n `parts: id \"${this.part.id}\" collides with an existing part id`,\n );\n }\n if (hit === \"deformer\") {\n throw new Error(\n `parts: id \"${this.part.id}\" collides with an existing deformer id`,\n );\n }\n\n // (b) Full structural validation on a candidate clone — propagate\n // IkiFormatError unchanged so the caller gets a path-qualified message.\n const candidate = structuredClone(doc.getModel());\n candidate.parts.push(structuredClone(this.part));\n parseIkiModel(candidate);\n\n // (c) All checks pass — mutate the real model with a fresh clone so the\n // model never aliases the command's stored part.\n doc.getModel().parts.push(structuredClone(this.part));\n\n // Register base mesh UVs so applyAtlas can remap this part. Capture the\n // prior entry on the FIRST apply only — redo must NOT re-capture or it\n // would clobber the saved prior and make invert unable to restore correctly.\n const prev = doc.captureBaseMeshUvs(this.part.id);\n if (!this.captured) {\n this.prevBaseMeshUvs = prev;\n this.captured = true;\n }\n }\n\n invert(doc: EditorDocument): void {\n const parts = doc.getModel().parts;\n const i = parts.findIndex((p) => p.id === this.part.id);\n if (i !== -1) parts.splice(i, 1);\n // Restore the exact prior base-UV state so a constructor-captured entry for\n // a deleted part with this id survives the undo of the add.\n doc.restoreBaseMeshUvs(this.part.id, this.prevBaseMeshUvs);\n }\n}\n\n/**\n * Add a new deformer (matrix or warp) to the model. Validates the candidate\n * model with {@link parseIkiModel} BEFORE mutating — this enforces the warp\n * rest-grid invariant, points length, pivot, parent, and bindings without\n * hand-rolling partial checks. Mirrors {@link AddPart} but also tracks whether\n * `model.deformers` was absent before the first apply, so `invert` can restore\n * the exact key-absence state (mirrors {@link SetDeformerBindings}).\n */\nexport class AddDeformer implements EditCommand {\n readonly label = \"Add deformer\";\n private readonly deformer: IkiDeformer;\n private captured = false;\n private prevDeformersAbsent = false;\n\n constructor(deformer: IkiDeformer) {\n // Clone on construction — prevents caller mutation from corrupting apply/redo.\n this.deformer = structuredClone(deformer);\n }\n\n apply(doc: EditorDocument): void {\n const model = doc.getModel();\n\n // (a) Cheap id-uniqueness pre-check with a source-qualified message.\n const hit = findIdCollision(model, this.deformer.id);\n if (hit === \"deformer\") {\n throw new Error(\n `deformers: id \"${this.deformer.id}\" collides with an existing deformer id`,\n );\n }\n if (hit === \"part\") {\n throw new Error(\n `deformers: id \"${this.deformer.id}\" collides with an existing part id`,\n );\n }\n\n // (b) Full structural validation on a candidate clone.\n const candidate = structuredClone(model);\n candidate.deformers = [\n ...(candidate.deformers ?? []),\n structuredClone(this.deformer),\n ];\n parseIkiModel(candidate);\n\n // (c) All checks pass — capture-once, then mutate.\n if (!this.captured) {\n this.prevDeformersAbsent = model.deformers === undefined;\n this.captured = true;\n }\n if (model.deformers === undefined) {\n model.deformers = [];\n }\n model.deformers.push(structuredClone(this.deformer));\n }\n\n invert(doc: EditorDocument): void {\n const model = doc.getModel();\n const arr = model.deformers;\n if (!arr) return;\n const i = arr.findIndex((d) => d.id === this.deformer.id);\n if (i !== -1) arr.splice(i, 1);\n // Restore the absent-vs-present distinction. If deformers did not exist\n // before apply, delete the key once the array is empty again.\n if (this.prevDeformersAbsent && arr.length === 0) {\n delete model.deformers;\n }\n }\n}\n\n/**\n * Delete a part by id, preserving its original array slot so `invert` restores\n * it at the same position. Slot position is cosmetic (the renderer uses the\n * `order` field for paint ordering), but restoring the index keeps undo\n * visually predictable.\n *\n * No `parseIkiModel` pre-check is needed: removing an element from an already-\n * valid model cannot introduce a structural violation — EXCEPT for clip-mask\n * references, the one part→part reference in the contract. `apply` refuses to\n * delete a part still used as another part's `clip.masks` entry (guard below),\n * so it can never leave a dangling mask ref that would fail `toIkiModel()`.\n *\n * Texture-reference safety (package invariant): `apply` refuses to delete a\n * part that still carries `part.texture`. Texture/atlas state is non-undoable\n * per the 5b boundary; clear the texture first via\n * {@link EditorDocument.clearPartTextureRef} (model-committed) or\n * {@link EditorDocument.applyAtlas} with no assignment (imported) — both are\n * non-undoable. By the time a part is deletable it carries no texture, so\n * `invert` can never restore a stale texture index that would render the wrong\n * atlas region after a later atlas repack. No transactional atlas capture is\n * needed in this command.\n */\nexport class DeletePart implements EditCommand {\n readonly label = \"Delete part\";\n private captured = false;\n private removed!: IkiPart;\n private index!: number;\n\n constructor(private readonly partId: string) {}\n\n apply(doc: EditorDocument): void {\n // Validate FIRST — throws with path-qualified message if unknown.\n const part = doc.findPart(this.partId);\n\n // Texture guard — enforced at the `@ikijs/editor` boundary so public callers\n // cannot bypass it (the example store adds a friendly pre-check, but this\n // is the real invariant). Throw before any capture or mutation.\n if (part.texture !== undefined) {\n throw new Error(\n `parts.\"${this.partId}\": cannot delete — part has a texture reference; clear its texture first`,\n );\n }\n\n const parts = doc.getModel().parts;\n\n // Clip-mask guard — `clip.masks` is the one part→part reference in the model\n // contract. Deleting a referenced mask would leave a dangling ref that fails\n // parseIkiModel on toIkiModel(). Refuse rather than corrupt (mirrors the\n // texture guard above). Throw before any capture or mutation.\n const masker = parts.find(\n (p) => p.id !== this.partId && p.clip?.masks.includes(this.partId),\n );\n if (masker) {\n throw new Error(\n `parts.\"${this.partId}\": cannot delete — used as a clip mask by part \"${masker.id}\"; remove its clip first`,\n );\n }\n\n const i = parts.indexOf(part);\n if (!this.captured) {\n this.removed = structuredClone(part);\n this.index = i;\n this.captured = true;\n }\n parts.splice(i, 1);\n }\n\n invert(doc: EditorDocument): void {\n // Restore at the original slot — exact deep restore including bindings and\n // mesh. No texture key is present (apply enforced that invariant before\n // capture), so the snapshot is always atlas-safe on restore.\n doc.getModel().parts.splice(this.index, 0, structuredClone(this.removed));\n }\n}\n\n/**\n * Delete a deformer by id. Calls {@link validateDeformerDelete} FIRST so the\n * delete is refused while anything still references it — a child deformer, an\n * attached part, or a physics chain anchored to it — enforcing the same\n * referential safety as {@link SetDeformerParent} and {@link SetPartDeformer}.\n *\n * `invert` re-inserts the deformer at its original index.\n */\nexport class DeleteDeformer implements EditCommand {\n readonly label = \"Delete deformer\";\n private captured = false;\n private removed!: IkiDeformer;\n private index!: number;\n\n constructor(private readonly deformerId: string) {}\n\n apply(doc: EditorDocument): void {\n const model = doc.getModel();\n // Validate FIRST — throws before capture/mutate on any referential violation.\n validateDeformerDelete(\n model.deformers ?? [],\n model.parts,\n model.physicsChains ?? [],\n this.deformerId,\n );\n // validateDeformerDelete guarantees the deformer (and thus the array) exists.\n const arr = model.deformers!;\n const i = arr.findIndex((d) => d.id === this.deformerId);\n if (!this.captured) {\n this.removed = structuredClone(arr[i]);\n this.index = i;\n this.captured = true;\n }\n arr.splice(i, 1);\n }\n\n invert(doc: EditorDocument): void {\n // Re-insert at the original slot. `apply` only captures after\n // validateDeformerDelete passed, so the array it spliced out of is present:\n // fabricating one here would hide a broken undo stack and silently restore\n // just this node instead of surfacing the loss of the rest of the hierarchy.\n doc\n .getModel()\n .deformers!.splice(this.index, 0, structuredClone(this.removed));\n }\n}\n\n/**\n * Replace a part's `bindings` array wholesale. A single command covers add,\n * edit, and remove (pass the desired final array; pass `[]` to remove all).\n * Mirrors {@link SetDeformerBindings}'s deep-copy and absent-vs-empty discipline:\n * clone-on-construction, capture-once, fresh deep copy on every assign/invert.\n *\n * Each {@link IkiBinding} is a flat object, so a per-element spread `{ ...b }`\n * is a sufficient deep copy.\n *\n * Validates the WRITTEN bindings against the declared parameters via a narrow\n * synthetic {@link parseIkiModel} candidate (so unrelated in-flight invalid\n * editor state — e.g. a NaN width on another part — cannot false-reject a\n * binding edit). Validation runs BEFORE any mutation; on failure the model and\n * undo stack are left untouched.\n *\n * Empty bindings → omit the `bindings` key on the candidate AND delete\n * `part.bindings` on apply (keeps the model shape minimal; represents \"no\n * bindings\" as an absent key rather than an empty array).\n */\nexport class SetPartBindings implements EditCommand {\n readonly label = \"Set part bindings\";\n private readonly bindings: IkiBinding[];\n private captured = false;\n private prevBindings!: IkiBinding[] | undefined;\n\n constructor(\n private readonly partId: string,\n bindings: IkiBinding[],\n ) {\n // Clone the caller's array on construction — prevents post-execute mutation\n // of the caller's array from corrupting apply/redo.\n this.bindings = bindings.map((b) => ({ ...b }));\n }\n\n apply(doc: EditorDocument): void {\n // Build a narrow synthetic candidate carrying only the validation-relevant\n // context. A full structuredClone(doc.getModel()) is deliberately avoided\n // because unrelated parts may carry NaN in-flight values (e.g. from\n // NumberField.valueAsNumber), which would cause false-positive validation\n // failures. The synthetic model is the minimal shape parseIkiModel accepts.\n const candidatePart: Record<string, unknown> = {\n id: \"_\",\n color: [0, 0, 0, 1],\n width: 1,\n height: 1,\n transform: { x: 0, y: 0 },\n order: 0,\n };\n // Omit the bindings key entirely when empty — \"no bindings\" is represented\n // as key absence; an empty array is not a valid value in the format contract.\n if (this.bindings.length > 0) {\n candidatePart.bindings = this.bindings.map((b) => ({ ...b }));\n }\n const candidate = {\n version: IKI_FORMAT_VERSION,\n name: \"_\",\n canvas: { width: 1, height: 1 },\n parameters: doc.getModel().parameters,\n parts: [candidatePart],\n };\n // Validation before any mutation. The synthetic candidate always places the\n // part at parts[0], so the validator emits paths like \"parts[0].bindings[i]\".\n // Rewrite that prefix to name the real target so the surfaced error is\n // actionable (\"parts.\"part-a\".bindings[i]\" rather than \"parts[0].bindings[i]\").\n try {\n parseIkiModel(candidate);\n } catch (e) {\n if (e instanceof IkiFormatError) {\n throw new IkiFormatError(\n e.message.replace(/^parts\\[0\\]/, `parts.\"${this.partId}\"`),\n );\n }\n throw e;\n }\n\n // Resolution after validation so an unknown partId throws with a\n // path-qualified message (findPart throws) but only after the bindings\n // themselves are confirmed structurally valid.\n const part = doc.findPart(this.partId);\n if (!this.captured) {\n // Preserve the original absent-vs-empty distinction.\n this.prevBindings =\n part.bindings === undefined\n ? undefined\n : part.bindings.map((b) => ({ ...b }));\n this.captured = true;\n }\n if (this.bindings.length > 0) {\n // Assign a fresh deep copy — model must never alias the command's stored array.\n part.bindings = this.bindings.map((b) => ({ ...b }));\n } else {\n // Delete the key on empty: keeps the model shape minimal and correctly\n // represents \"no bindings\" as an absent key rather than an empty array.\n delete part.bindings;\n }\n }\n\n invert(doc: EditorDocument): void {\n const part = doc.findPart(this.partId);\n if (this.prevBindings === undefined) {\n delete part.bindings;\n } else {\n // Assign a fresh deep copy — never alias the captured array so re-apply\n // after undo cannot corrupt the saved prior value.\n part.bindings = this.prevBindings.map((b) => ({ ...b }));\n }\n }\n}\n\n/**\n * Attach a part to a deformer, or detach it (`newDeformerId === undefined`).\n * Calls {@link validatePartAttach} FIRST so invalid attachments (warp without\n * mesh, unknown ids) throw before any capture or mutation.\n *\n * Absent-vs-present distinction mirrors {@link SetDeformerParent}: captures\n * both the prior `deformer` value and whether the key was present, so `invert`\n * can delete the key rather than assigning `undefined`.\n */\nexport class SetPartDeformer implements EditCommand {\n readonly label = \"Set part deformer\";\n private captured = false;\n private prevDeformer: string | undefined = undefined;\n private prevHadDeformer = false;\n\n constructor(\n private readonly partId: string,\n private readonly newDeformerId: string | undefined,\n ) {}\n\n apply(doc: EditorDocument): void {\n // Validate FIRST — throws before capture/mutate on any violation.\n validatePartAttach(\n doc.getModel().deformers ?? [],\n this.partId,\n doc.getModel().parts,\n this.newDeformerId,\n );\n const part = doc.findPart(this.partId);\n if (!this.captured) {\n this.prevHadDeformer = Object.prototype.hasOwnProperty.call(\n part,\n \"deformer\",\n );\n this.prevDeformer = part.deformer;\n this.captured = true;\n }\n if (this.newDeformerId !== undefined) {\n part.deformer = this.newDeformerId;\n } else {\n delete part.deformer;\n }\n }\n\n invert(doc: EditorDocument): void {\n const part = doc.findPart(this.partId);\n if (this.prevHadDeformer) {\n part.deformer = this.prevDeformer;\n } else {\n delete part.deformer;\n }\n }\n}\n\n/**\n * Return true if the deformer identified by `deformerId` exists in the model\n * and has `kind === \"warp\"`. Used by SetPartMesh to detect warp-deformer\n * attachment without importing reparent.ts internals.\n */\nfunction isWarpDeformer(model: IkiModel, deformerId: string): boolean {\n const d = (model.deformers ?? []).find((x) => x.id === deformerId);\n return d?.kind === \"warp\";\n}\n\n/**\n * Add, regenerate, or remove the triangle mesh on a part.\n *\n * - `mesh !== undefined` → add or replace the mesh, registering the\n * unit-square base UVs in the side-table so {@link EditorDocument.applyAtlas}\n * can remap them later.\n * - `mesh === undefined` → delete `part.mesh` and remove the side-table entry.\n *\n * Fails fast (BEFORE any mutation) on warp-topology violations:\n * - REMOVE while `part.warps` is present (even empty) or the part is\n * attached to a warp deformer — the format rejects any `warps` key once\n * the mesh is gone.\n * - ADD/REPLACE while `part.warps` has authored offsets (`length > 0`) —\n * regenerating the mesh invalidates offset positions silently; the user\n * must remove the warps first.\n *\n * The remove guard checks PRESENCE of `part.warps` (not length) while the\n * add/replace guard checks LENGTH > 0. This asymmetry is intentional: the\n * format allows `warps: []` only when a mesh exists, so any present key\n * (even empty) would become invalid after mesh removal; but replacing a mesh\n * under an empty `warps: []` is harmless because there are no authored offsets.\n */\nexport class SetPartMesh implements EditCommand {\n readonly label = \"Set part mesh\";\n private readonly mesh: IkiMesh | undefined;\n private captured = false;\n private prevHadMesh = false;\n private prevMesh: IkiMesh | undefined;\n private prevBaseMeshUvs: number[] | undefined;\n\n constructor(\n private readonly partId: string,\n mesh: IkiMesh | undefined,\n ) {\n // Clone on construction — prevents caller mutation from corrupting apply/redo.\n this.mesh = mesh === undefined ? undefined : structuredClone(mesh);\n }\n\n apply(doc: EditorDocument): void {\n // (a) Resolve the live part up front — unlike SetPartBindings, which resolves\n // after parseIkiModel, we need the live part here because the warp-topology\n // guards at step (c) inspect part.warps / part.deformer before any mutation.\n const part = doc.findPart(this.partId);\n\n // (b) Structural validation — only needed when adding or replacing a mesh.\n // Build a NARROW synthetic candidate carrying only the new mesh so that\n // unrelated in-flight parts with NaN values cannot cause false failures.\n if (this.mesh !== undefined) {\n const candidatePart = {\n id: \"_\",\n color: [0, 0, 0, 1],\n width: 1,\n height: 1,\n transform: { x: 0, y: 0 },\n order: 0,\n mesh: structuredClone(this.mesh),\n };\n const candidate = {\n version: IKI_FORMAT_VERSION,\n name: \"_\",\n canvas: { width: 1, height: 1 },\n parameters: doc.getModel().parameters,\n parts: [candidatePart],\n };\n try {\n parseIkiModel(candidate);\n } catch (e) {\n if (e instanceof IkiFormatError) {\n throw new IkiFormatError(\n e.message.replace(/^parts\\[0\\]/, `parts.\"${this.partId}\"`),\n );\n }\n throw e;\n }\n }\n\n // (c) Warp-topology fail-fast — BEFORE any mutation, after structural validation.\n // The two paths key on DIFFERENT predicates; this asymmetry is intentional\n // (see class JSDoc above).\n const attachedToWarp =\n part.deformer !== undefined &&\n isWarpDeformer(doc.getModel(), part.deformer);\n\n if (this.mesh === undefined) {\n // REMOVE: guard on warps PRESENCE (even empty) OR warp-deformer attachment.\n // The format rejects ANY present `warps` key once the mesh is gone, so even\n // an empty array would make toIkiModel() throw.\n if (part.warps !== undefined || attachedToWarp) {\n throw new IkiFormatError(\n `parts.\"${this.partId}\": cannot remove mesh — part has warps or is attached to a warp deformer; remove its warps / detach from the warp deformer first`,\n );\n }\n // Clip-mask guard — the format requires a clip mask to be a mesh part, so\n // stripping the mesh from a referenced mask would dangle the ref and fail\n // toIkiModel(). Mirrors the DeletePart clip-mask guard.\n const masker = doc\n .getModel()\n .parts.find(\n (p) => p.id !== this.partId && p.clip?.masks.includes(this.partId),\n );\n if (masker) {\n throw new IkiFormatError(\n `parts.\"${this.partId}\": cannot remove mesh — used as a clip mask by part \"${masker.id}\" (masks require a mesh); remove its clip first`,\n );\n }\n } else {\n // ADD/REPLACE: guard on authored offsets (length > 0). An empty warps: []\n // has no offsets to invalidate, so replacing the mesh under it is harmless.\n if ((part.warps?.length ?? 0) > 0) {\n throw new IkiFormatError(\n `parts.\"${this.partId}\": cannot regenerate mesh — part has warps whose offsets are bound to the current rest mesh; remove its warps first`,\n );\n }\n }\n\n // (d) Mutate + side-table maintenance.\n\n // (i) Single first-apply capture block — captures BOTH prevMesh and\n // prevBaseMeshUvs together, BEFORE any mutation. captureBaseMeshUvs has\n // a side effect (registers current mesh.uvs); step (iii) overwrites it.\n if (!this.captured) {\n this.prevHadMesh = part.mesh !== undefined;\n this.prevMesh = part.mesh ? structuredClone(part.mesh) : undefined;\n this.prevBaseMeshUvs = doc.captureBaseMeshUvs(this.partId);\n this.captured = true;\n }\n // Redo: do NOT re-capture (would clobber the saved prior). The mutation\n // below is fully reconstructable from this.mesh + this.partId.\n\n // (ii) Apply the model mutation.\n if (this.mesh !== undefined) {\n // Compute stored UVs: if the part has an active texture, remap the\n // unit-square base UVs into the texture's atlas sub-rectangle.\n const storedUvs =\n part.texture !== undefined\n ? remapMeshUvsToRect(this.mesh.uvs, part.texture.uv)\n : this.mesh.uvs.slice();\n part.mesh = {\n vertices: this.mesh.vertices.slice(),\n uvs: storedUvs,\n indices: this.mesh.indices.slice(),\n };\n } else {\n delete part.mesh;\n }\n\n // (iii) Re-register the side-table to the correct final state, overwriting\n // the (i) read's incidental re-registration. Write ONLY via\n // restoreBaseMeshUvs — never via a new setBaseMeshUvs API.\n if (this.mesh !== undefined) {\n // Register the UNIT-SQUARE base (not the texture-remapped storedUvs) so\n // applyAtlas can always derive correct atlas-space UVs from the base.\n doc.restoreBaseMeshUvs(this.partId, this.mesh.uvs.slice());\n } else {\n doc.restoreBaseMeshUvs(this.partId, undefined);\n }\n }\n\n invert(doc: EditorDocument): void {\n const part = doc.findPart(this.partId);\n // Restore the mesh, preserving the absent-vs-present distinction.\n // applyAtlas (and texture changes generally) are NON-undoable and do NOT\n // clear the undo stack. So by the time undo() is called, part.texture may\n // have been changed by a later applyAtlas that this command never saw. To\n // avoid restoring stale texture-space UVs, rebuild uvs from the captured\n // unit-square base against the CURRENT texture rect rather than trusting\n // prevMesh.uvs verbatim.\n if (this.prevHadMesh) {\n // A mesh part ALWAYS has a registered unit-square base under the invariant\n // (applyAtlas preflights requireBaseUvs for every mesh part). If the base is\n // absent here the side-table invariant is broken — fail fast rather than\n // silently restoring stale texture-space uvs that would corrupt rendering.\n if (this.prevBaseMeshUvs === undefined) {\n throw new Error(\n `parts.\"${this.partId}\": cannot invert SetPartMesh — no base mesh uvs captured for a mesh part (broken side-table invariant)`,\n );\n }\n const restored = structuredClone(this.prevMesh!);\n // Remap the unit-square base into the current texture rect (if any).\n restored.uvs =\n part.texture !== undefined\n ? remapMeshUvsToRect(this.prevBaseMeshUvs, part.texture.uv)\n : this.prevBaseMeshUvs.slice();\n part.mesh = restored;\n } else {\n delete part.mesh;\n }\n // Restore the side-table to the exact prior state (unit-square base).\n doc.restoreBaseMeshUvs(this.partId, this.prevBaseMeshUvs);\n }\n}\n\n/**\n * Validate a candidate `physics` array against the model's declared parameters\n * via a NARROW synthetic model — full `parameters` + the full candidate physics\n * array + one trivial part. Mirrors {@link SetPartBindings}' synthetic-candidate\n * (avoids false-positives from unrelated in-flight NaN part numerics) but carries\n * the FULL physics array so the cross-rig rules (dup id / dup output / feedback)\n * run. On `IkiFormatError`, rewrite the failing `physics[n]` path to name the rig\n * AT THE FAILING INDEX — a cross-rig failure can surface at a different index than\n * the edited rig, so the rewrite follows the message index, not the command target.\n */\nfunction validatePhysicsCandidate(\n doc: EditorDocument,\n candidatePhysics: IkiPhysics[],\n): void {\n const candidatePart: Record<string, unknown> = {\n id: \"_\",\n color: [0, 0, 0, 1],\n width: 1,\n height: 1,\n transform: { x: 0, y: 0 },\n order: 0,\n };\n const candidate = {\n version: IKI_FORMAT_VERSION,\n name: \"_\",\n canvas: { width: 1, height: 1 },\n parameters: doc.getModel().parameters,\n parts: [candidatePart],\n deformers: doc.getModel().deformers,\n physics: candidatePhysics,\n physicsChains: doc.getModel().physicsChains,\n };\n try {\n parseIkiModel(candidate);\n } catch (e) {\n if (e instanceof IkiFormatError) {\n const m = e.message.match(/^physics\\[(\\d+)\\]/);\n const rigId = m ? candidatePhysics[Number(m[1])]?.id : undefined;\n if (rigId !== undefined) {\n throw new IkiFormatError(\n e.message.replace(/^physics\\[\\d+\\]/, `physics.\"${rigId}\"`),\n );\n }\n }\n throw e;\n }\n}\n\n/**\n * Add a physics rig to `model.physics`. Mirrors {@link AddDeformer}: clone on\n * construction, cheap friendly duplicate-id pre-check, full synthetic-candidate\n * validation before mutating, capture the absent-vs-present `physics` state once,\n * and on invert delete the key when the array empties back to its prior absence.\n */\nexport class AddPhysicsRig implements EditCommand {\n readonly label = \"Add physics rig\";\n private readonly rig: IkiPhysics;\n private captured = false;\n private prevPhysicsAbsent = false;\n\n constructor(rig: IkiPhysics) {\n // Deep clone — input/output are nested objects a shallow spread would alias.\n this.rig = structuredClone(rig);\n }\n\n apply(doc: EditorDocument): void {\n const model = doc.getModel();\n // (a) Friendly duplicate-id pre-check (the format check from validate.ts is\n // the real guard; this surfaces an actionable message before validate).\n if ((model.physics ?? []).some((r) => r.id === this.rig.id)) {\n throw new Error(\n `physics: id \"${this.rig.id}\" collides with an existing physics rig id`,\n );\n }\n // (b) Full-physics synthetic validation with the rig appended.\n validatePhysicsCandidate(doc, [\n ...(model.physics ?? []),\n structuredClone(this.rig),\n ]);\n // (c) Capture-once, then mutate with a fresh clone.\n if (!this.captured) {\n this.prevPhysicsAbsent = model.physics === undefined;\n this.captured = true;\n }\n if (model.physics === undefined) {\n model.physics = [];\n }\n model.physics.push(structuredClone(this.rig));\n }\n\n invert(doc: EditorDocument): void {\n const model = doc.getModel();\n const arr = model.physics;\n if (!arr) return;\n const i = arr.findIndex((r) => r.id === this.rig.id);\n if (i !== -1) arr.splice(i, 1);\n if (this.prevPhysicsAbsent && arr.length === 0) {\n delete model.physics;\n }\n }\n}\n\n/**\n * Delete a physics rig by id. Mirrors {@link DeleteDeformer}: resolve/validate\n * first, capture the removed rig + its index once, splice. Removing the LAST rig\n * deletes the `physics` key to keep the exported shape minimal; invert re-inserts\n * at the original index (recreating the array if it was deleted).\n */\nexport class DeletePhysicsRig implements EditCommand {\n readonly label = \"Delete physics rig\";\n private captured = false;\n private removed!: IkiPhysics;\n private index!: number;\n\n constructor(private readonly rigId: string) {}\n\n apply(doc: EditorDocument): void {\n const rig = doc.findPhysicsRig(this.rigId); // throws if unknown\n const arr = doc.getModel().physics!;\n const i = arr.indexOf(rig);\n if (!this.captured) {\n this.removed = structuredClone(rig);\n this.index = i;\n this.captured = true;\n }\n arr.splice(i, 1);\n // Minimal exported shape: drop the key once the last rig is gone.\n if (arr.length === 0) {\n delete doc.getModel().physics;\n }\n }\n\n invert(doc: EditorDocument): void {\n const model = doc.getModel();\n (model.physics ??= []).splice(this.index, 0, structuredClone(this.removed));\n }\n}\n\n/**\n * Replace a physics rig in place (tuning). Mirrors {@link SetDeformerBindings}\n * but DEEP-clones the nested `input`/`output` (a shallow spread would alias them).\n * Forbids rename (`rig.id` must equal the target `rigId`) — this command tunes a\n * rig, never re-keys it. Validates the whole candidate (the edited rig swapped in\n * at its index) so cross-rig rules still run, then captures the prior rig once.\n */\nexport class SetPhysicsRig implements EditCommand {\n readonly label = \"Set physics rig\";\n private readonly rig: IkiPhysics;\n private captured = false;\n private prevRig!: IkiPhysics;\n\n constructor(\n private readonly rigId: string,\n rig: IkiPhysics,\n ) {\n this.rig = structuredClone(rig);\n }\n\n apply(doc: EditorDocument): void {\n // No-rename guard — fail before mutating.\n if (this.rig.id !== this.rigId) {\n throw new Error(\n `physics.\"${this.rigId}\": cannot change rig id to \"${this.rig.id}\" (rename unsupported)`,\n );\n }\n const model = doc.getModel();\n const i = (model.physics ?? []).findIndex((r) => r.id === this.rigId);\n if (i === -1) {\n throw new Error(`physics: no physics rig with id \"${this.rigId}\"`);\n }\n // Validate a candidate with the edited rig swapped in at its index.\n const candidate = model.physics!.map((r, idx) =>\n idx === i ? structuredClone(this.rig) : r,\n );\n validatePhysicsCandidate(doc, candidate);\n if (!this.captured) {\n this.prevRig = structuredClone(model.physics![i]);\n this.captured = true;\n }\n model.physics![i] = structuredClone(this.rig);\n }\n\n invert(doc: EditorDocument): void {\n const arr = doc.getModel().physics;\n if (!arr) return;\n const i = arr.findIndex((r) => r.id === this.rigId);\n if (i !== -1) arr[i] = structuredClone(this.prevRig);\n }\n}\n","import type { IkiGridKeyform } from \"@ikijs/format\";\n\n/**\n * Pure, grid-size-agnostic keyform/offset math for authoring a warp-deformer\n * grid by dragging. No DOM, no `@ikijs/engine` — the load-bearing testable core.\n * Constraints derive only from the input array lengths, never the sample grid.\n */\n\n/**\n * Interpolate the grid offsets at `value`, mirroring the engine's\n * `accumulateKeyformOffsets` clamp+lerp semantics: clamp to the first/last\n * keyform (NO extrapolation) and linearly interpolate the bracketing pair.\n * Returns a NEW array of length `keyforms[0].offsets.length`. Throws on empty.\n */\nexport function interpolateGridOffsets(\n keyforms: { value: number; offsets: number[] }[],\n value: number,\n): number[] {\n if (keyforms.length === 0) {\n throw new Error(\"interpolateGridOffsets: keyforms must be non-empty\");\n }\n\n if (value <= keyforms[0].value) {\n return [...keyforms[0].offsets];\n }\n const last = keyforms[keyforms.length - 1];\n if (value >= last.value) {\n return [...last.offsets];\n }\n\n // Find the bracketing pair (keyforms are small in practice).\n let lo = keyforms[0];\n let hi = keyforms[1];\n for (let k = 1; k < keyforms.length - 1; k++) {\n if (keyforms[k].value <= value) {\n lo = keyforms[k];\n hi = keyforms[k + 1];\n }\n }\n const t = (value - lo.value) / (hi.value - lo.value);\n return lo.offsets.map((loOff, i) => loOff + (hi.offsets[i] - loOff) * t);\n}\n\n/**\n * Per-control-point delta of the dragged grid from the rest grid: for each\n * point `i`, `(draggedX_i - restX_i, draggedY_i - restY_i)`. The DOM layer\n * assembles the full `restFrameDraggedPoints` (including untouched points), so\n * this is a straight subtract — no prior-offset blending.\n *\n * Both arrays must have the SAME length and that length must be even (x,y\n * pairs). Returns a NEW array of length `restPoints.length`.\n */\nexport function computeGridOffsets(\n restPoints: number[],\n restFrameDraggedPoints: number[],\n): number[] {\n if (restPoints.length !== restFrameDraggedPoints.length) {\n throw new Error(\n `computeGridOffsets: restFrameDraggedPoints length ${restFrameDraggedPoints.length} must equal restPoints length ${restPoints.length}`,\n );\n }\n if (restPoints.length % 2 !== 0) {\n throw new Error(\n `computeGridOffsets: restPoints length ${restPoints.length} must be even (x,y pairs)`,\n );\n }\n return restPoints.map((rest, i) => restFrameDraggedPoints[i] - rest);\n}\n\n/**\n * Insert or replace the keyform at `value`, returning a NEW array. If a keyform\n * already exists with an exact-match `value`, REPLACE its offsets (with a copy);\n * otherwise INSERT `{ value, offsets: [...offsets] }` at the position that keeps\n * the array strictly ascending by value. The input array and its keyform objects\n * are never mutated, and `offsets` is copied so the result never aliases the\n * caller's array.\n *\n * Deliberately RANGE-FREE — a generic, reusable array op. Value-range\n * enforcement is the command's job, not this helper's.\n */\nexport function upsertGridKeyform(\n keyforms: IkiGridKeyform[],\n value: number,\n offsets: number[],\n): IkiGridKeyform[] {\n const result = keyforms.map((kf) => ({\n value: kf.value,\n offsets: [...kf.offsets],\n }));\n const existing = result.findIndex((kf) => kf.value === value);\n if (existing !== -1) {\n result[existing] = { value, offsets: [...offsets] };\n return result;\n }\n const insertAt = result.findIndex((kf) => kf.value > value);\n const entry: IkiGridKeyform = { value, offsets: [...offsets] };\n if (insertAt === -1) {\n result.push(entry);\n } else {\n result.splice(insertAt, 0, entry);\n }\n return result;\n}\n","import type { IkiDeformer, IkiPart, IkiPhysicsChain } from \"@ikijs/format\";\n\n/**\n * Pure, DOM-free validation helpers for deformer reparenting and part attachment.\n * Each function throws a path-qualified plain Error on rejection and returns void\n * on success. Neither function mutates the input arrays or objects.\n */\n\nfunction kindOf(d: IkiDeformer): \"warp\" | \"matrix\" {\n return d.kind === \"warp\" ? \"warp\" : \"matrix\";\n}\n\n/**\n * Validate that reparenting `deformerId` under `newParentId` keeps the deformer\n * hierarchy valid. Pass `newParentId === undefined` to move to root (always legal).\n * Checks: existence, self-reference, undeclared parent, kind constraint (warp\n * deformers cannot be parents), and cycle detection via the proposed edge.\n */\nexport function validateDeformerReparent(\n deformers: IkiDeformer[],\n deformerId: string,\n newParentId: string | undefined,\n): void {\n // (1) Target deformer must exist.\n const target = deformers.find((d) => d.id === deformerId);\n if (target === undefined) {\n throw new Error(`deformers: no deformer with id \"${deformerId}\"`);\n }\n\n // (2) Root is always legal.\n if (newParentId === undefined) return;\n\n // (3) Self-reference.\n if (newParentId === deformerId) {\n throw new Error(\n `deformers.\"${deformerId}\".parent \"${newParentId}\" is a self-reference`,\n );\n }\n\n // (4) Parent must be declared.\n const parent = deformers.find((d) => d.id === newParentId);\n if (parent === undefined) {\n throw new Error(\n `deformers.\"${deformerId}\".parent \"${newParentId}\" is not a declared deformer`,\n );\n }\n\n // (5) Parent must be a matrix deformer.\n if (kindOf(parent) === \"warp\") {\n throw new Error(\n `deformers.\"${deformerId}\".parent \"${newParentId}\" must be a matrix deformer (warp deformers cannot be parents)`,\n );\n }\n\n // (6) Cycle detection: build parentOf from current state, then override with\n // the proposed edge, and walk from deformerId following the chain.\n const parentOf = new Map<string, string>();\n for (const d of deformers) {\n if (d.parent !== undefined) parentOf.set(d.id, d.parent);\n }\n // Override with the proposed edge.\n parentOf.set(deformerId, newParentId);\n\n const visited = new Set<string>();\n let cur: string | undefined = deformerId;\n while (cur !== undefined) {\n if (visited.has(cur)) {\n throw new Error(\n `deformers: reparenting \"${deformerId}\" under \"${newParentId}\" would create a cycle`,\n );\n }\n visited.add(cur);\n cur = parentOf.get(cur);\n }\n}\n\n/**\n * Validate that deleting `deformerId` is safe. Throws when the deformer does\n * not exist, when another deformer is parented to it (must be reparented or\n * detached first), when a part is attached to it (must be detached first), or\n * when a physics chain anchors to it (must be re-anchored or deleted first).\n *\n * Every id that can reference a deformer must be covered here: the format\n * validator rejects a dangling reference at export, so a delete this function\n * lets through does not fail now — it strands the document in a state\n * `toIkiModel()` refuses.\n *\n * Note: there is no validatePartDelete here, but NOT because parts are\n * unreferenced — `clip.masks` names parts by id. That invariant is enforced\n * closer to the edits that could break it: `DeletePart` refuses to remove a\n * part still used as a mask, and `SetPartMesh` refuses to strip the mesh off\n * one (masks must be mesh parts). No command creates or edits `clip`.\n */\nexport function validateDeformerDelete(\n deformers: IkiDeformer[],\n parts: IkiPart[],\n physicsChains: IkiPhysicsChain[],\n deformerId: string,\n): void {\n // (1) Target deformer must exist.\n const target = deformers.find((d) => d.id === deformerId);\n if (target === undefined) {\n throw new Error(`deformers: no deformer with id \"${deformerId}\"`);\n }\n\n // (2) No other deformer may be parented to it.\n const childDeformer = deformers.find((d) => d.parent === deformerId);\n if (childDeformer !== undefined) {\n throw new Error(\n `deformers.\"${deformerId}\": cannot delete — deformer \"${childDeformer.id}\" is parented to it; reparent or detach it first`,\n );\n }\n\n // (3) No part may be attached to it.\n const attachedPart = parts.find((p) => p.deformer === deformerId);\n if (attachedPart !== undefined) {\n throw new Error(\n `deformers.\"${deformerId}\": cannot delete — part \"${attachedPart.id}\" is attached to it; detach it first`,\n );\n }\n\n // (4) No physics chain may anchor to it.\n const anchoredChain = physicsChains.find(\n (c) => c.anchorDeformer === deformerId,\n );\n if (anchoredChain !== undefined) {\n throw new Error(\n `deformers.\"${deformerId}\": cannot delete — physics chain \"${anchoredChain.id}\" anchors to it; re-anchor or delete the chain first`,\n );\n }\n}\n\n/**\n * Validate that attaching part `partId` to deformer `newDeformerId` is valid.\n * Pass `newDeformerId === undefined` to detach (always legal).\n * Checks: part existence, undeclared deformer, and mesh-required-for-warp.\n */\nexport function validatePartAttach(\n deformers: IkiDeformer[],\n partId: string,\n parts: IkiPart[],\n newDeformerId: string | undefined,\n): void {\n // (1) Part must exist.\n const part = parts.find((p) => p.id === partId);\n if (part === undefined) {\n throw new Error(`parts: no part with id \"${partId}\"`);\n }\n\n // (2) Detach is always legal.\n if (newDeformerId === undefined) return;\n\n // (3) Deformer must be declared.\n const deformer = deformers.find((d) => d.id === newDeformerId);\n if (deformer === undefined) {\n throw new Error(\n `parts.\"${partId}\".deformer \"${newDeformerId}\" is not a declared deformer`,\n );\n }\n\n // (4) Warp deformer requires a mesh on the part.\n if (kindOf(deformer) === \"warp\" && part.mesh === undefined) {\n throw new Error(\n `parts.\"${partId}\".deformer \"${newDeformerId}\" is a warp deformer and requires a mesh`,\n );\n }\n}\n","/**\n * Alpha bounding-box scan shared by every auto-rig ingestion path.\n *\n * The scan itself is environment-free — it only needs indexable RGBA bytes — so\n * a browser editor (canvas `ImageData`) and the Node MCP server (a `sharp`\n * raw buffer) run the SAME code instead of two copies that have to be kept\n * byte-identical by hand. Only decoding differs between them.\n */\n\n/** Alpha at or above this counts as coverage; below it is treated as empty. */\nexport const ALPHA_BBOX_THRESHOLD = 8;\n\n/** Top-left origin, +y down — image space, not model space. */\nexport interface AlphaBbox {\n x: number;\n y: number;\n w: number;\n h: number;\n}\n\n/**\n * Tight bounding box of every pixel with alpha >= {@link ALPHA_BBOX_THRESHOLD},\n * expanded 1px on each side (clamped to the image) for AA / extrude margin.\n *\n * Returns `null` when no pixel passes the threshold, leaving the \"empty layer\"\n * error to the caller: each ingestion path reports it with its own error type\n * and message.\n */\nexport function detectAlphaBbox(\n rgba: ArrayLike<number>,\n width: number,\n height: number,\n): AlphaBbox | null {\n let minX = width;\n let maxX = -1;\n let minY = height;\n let maxY = -1;\n\n for (let y = 0; y < height; y++) {\n for (let x = 0; x < width; x++) {\n const alpha = rgba[(y * width + x) * 4 + 3];\n if (alpha >= ALPHA_BBOX_THRESHOLD) {\n if (x < minX) minX = x;\n if (x > maxX) maxX = x;\n if (y < minY) minY = y;\n if (y > maxY) maxY = y;\n }\n }\n }\n\n if (maxX === -1) return null;\n\n // Expand 1px each side, clamped to image bounds; the x2/y2 clamps make w/h\n // implicitly in-bounds (no separate w/h clamp needed).\n const x = Math.max(0, minX - 1);\n const y = Math.max(0, minY - 1);\n const x2 = Math.min(width - 1, maxX + 1);\n const y2 = Math.min(height - 1, maxY + 1);\n\n return { x, y, w: x2 - x + 1, h: y2 - y + 1 };\n}\n","import type { IkiTransformChannel } from \"@ikijs/format\";\n\n/**\n * Pure, binding-value logic for computing an endpoint (rest-to-posed delta or\n * ratio) when capturing a transform channel binding. No DOM, no @ikijs/engine —\n * the single home of the additive-vs-multiplicative rule, reused by both the\n * part and deformer capture paths.\n *\n * Additive channels (translateX, translateY, rotate, scaleX, scaleY) return the\n * delta: `posedValue - restValue`. The binding will multiply that delta across\n * the driven range.\n *\n * Opacity is multiplicative: returns the ratio `posedValue / restValue`. The\n * binding will multiply by that ratio across the driven range. When `restValue`\n * is 0 (a degenerate case: base opacity cannot be represented multiplicatively\n * since 0 * x ≡ 0), this returns 0 as a documented fallback — it does NOT\n * recover `posedValue` and is NOT unit-tested as an identity. The store layer\n * additionally skips an opacity capture when rest opacity is 0, surfacing an\n * editError.\n *\n * Deformer channels never include opacity (they use `IkiMatrixChannel`), so the\n * opacity branch is reached only for parts.\n *\n * Finiteness of the captured value is NOT validated here; the store's\n * `captureEndpoint` finite-value guard is the appropriate layer for that check.\n */\nexport function captureBindingEndpoint(\n channel: IkiTransformChannel,\n restValue: number,\n posedValue: number,\n): number {\n if (channel === \"opacity\") {\n return restValue === 0 ? 0 : posedValue / restValue;\n }\n return posedValue - restValue;\n}\n","import type { IkiUvRect } from \"@ikijs/format\";\n\nexport const ATLAS_PADDING = 2;\nexport const UV_INSET_PX = 0.5;\n\n/** Intrinsic pixel size of one decoded image; id is an editor-only stable key. */\nexport interface AtlasSource {\n id: string;\n width: number;\n height: number;\n}\n\n/** Sub-image pixel rect within the page, top-left origin, EXCLUDING the gutter. */\nexport interface AtlasPlacement {\n id: string;\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\nexport interface AtlasLayout {\n pageWidth: number;\n pageHeight: number;\n placements: AtlasPlacement[];\n padding: number;\n}\n\n/**\n * Deterministic shelf/row packer. Sources are sorted by id for stability so\n * identical inputs always produce an identical layout.\n *\n * Padding is one-sided: each placement reserves `padding` px on its RIGHT and\n * BOTTOM only. Page left/top edges need no gutter.\n *\n * pageWidth/pageHeight = tight bound (max x+width+padding, max y+height+padding).\n * Empty sources → { pageWidth: 0, pageHeight: 0, placements: [], padding }.\n * Throws a plain Error naming the offending source id on a non-finite or <= 0 dimension.\n */\nexport function packAtlas(\n sources: AtlasSource[],\n padding = ATLAS_PADDING,\n): AtlasLayout {\n for (const src of sources) {\n if (!isFinite(src.width) || src.width <= 0) {\n throw new Error(\n `packAtlas: source \"${src.id}\" has invalid width ${src.width}`,\n );\n }\n if (!isFinite(src.height) || src.height <= 0) {\n throw new Error(\n `packAtlas: source \"${src.id}\" has invalid height ${src.height}`,\n );\n }\n }\n\n if (sources.length === 0) {\n return { pageWidth: 0, pageHeight: 0, placements: [], padding };\n }\n\n // Stable sort by id so identical inputs always produce the same layout.\n const sorted = sources\n .slice()\n .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));\n\n // Target page width: ceil(sqrt(total padded area)), raised to a sane minimum.\n const totalArea = sorted.reduce(\n (sum, s) => sum + (s.width + padding) * (s.height + padding),\n 0,\n );\n const targetWidth = Math.max(\n Math.ceil(Math.sqrt(totalArea)),\n // Ensure at least the widest single source fits.\n Math.max(...sorted.map((s) => s.width + padding)),\n );\n\n const placements: AtlasPlacement[] = [];\n let shelfX = 0;\n let shelfY = 0;\n let shelfHeight = 0; // tallest padded item in the current row\n\n for (const src of sorted) {\n const paddedW = src.width + padding;\n const paddedH = src.height + padding;\n\n // Wrap to next row when the item doesn't fit on the current shelf.\n if (shelfX > 0 && shelfX + paddedW > targetWidth) {\n shelfY += shelfHeight;\n shelfX = 0;\n shelfHeight = 0;\n }\n\n placements.push({\n id: src.id,\n x: shelfX,\n y: shelfY,\n width: src.width,\n height: src.height,\n });\n\n shelfX += paddedW;\n if (paddedH > shelfHeight) shelfHeight = paddedH;\n }\n\n // Tight page bounds: max right/bottom padded edge across all placements.\n let pageWidth = 0;\n let pageHeight = 0;\n for (const p of placements) {\n const right = p.x + p.width + padding;\n const bottom = p.y + p.height + padding;\n if (right > pageWidth) pageWidth = right;\n if (bottom > pageHeight) pageHeight = bottom;\n }\n\n return { pageWidth, pageHeight, placements, padding };\n}\n\n/**\n * Convert a pixel placement into a UV rect, inset by `insetPx` on all four\n * edges and clamped to [0, 1] so the validator's bounds check always passes.\n */\nexport function uvRectFor(\n placement: AtlasPlacement,\n page: { width: number; height: number },\n insetPx = UV_INSET_PX,\n): IkiUvRect {\n const px = placement.x + insetPx;\n const py = placement.y + insetPx;\n const pw = placement.width - insetPx * 2;\n const ph = placement.height - insetPx * 2;\n\n const x = Math.max(0, px / page.width);\n const y = Math.max(0, py / page.height);\n const width = Math.min(1 - x, Math.max(0, pw / page.width));\n const height = Math.min(1 - y, Math.max(0, ph / page.height));\n\n return { x, y, width, height };\n}\n","import type {\n IkiMatrixDeformer,\n IkiMesh,\n IkiModel,\n IkiPart,\n IkiWarpDeformer,\n} from \"@ikijs/format\";\n\n/**\n * Pure, DOM-free factory helpers for creating new model objects from scratch.\n * Kept separate from `commands.ts` (which is edit-only) so that the \"add item\"\n * use-case has a dedicated, testable home with no mutation concerns.\n *\n * All factories accept the live model so they can derive a collision-free id and\n * a sensible draw-order / grid span without hard-coding sample assumptions.\n */\n\n/**\n * Return the first collision-free id in the shared part+deformer namespace.\n * Parts and deformers share a flat id namespace (validate.ts:692-706), so we\n * scan both arrays. Returns `base` if unused; otherwise tries `${base}_2`,\n * `${base}_3`, … until a free slot is found.\n *\n * Parameters are a separate namespace and are NOT included in the scan.\n */\nfunction generateUniqueId(model: IkiModel, base: string): string {\n const used = new Set<string>();\n for (const p of model.parts) {\n used.add(p.id);\n }\n for (const d of model.deformers ?? []) {\n used.add(d.id);\n }\n\n if (!used.has(base)) return base;\n\n let n = 2;\n while (true) {\n const candidate = `${base}_${n}`;\n if (!used.has(candidate)) return candidate;\n n++;\n }\n}\n\n/**\n * Generate the flat `[x0,y0, x1,y1, …]` rest-grid control points for a\n * regular axis-aligned lattice with `(cols+1)*(rows+1)` points, row-major.\n *\n * Row 0 is the TOP (y = maxY); y strictly decreases with row index.\n * Column 0 is left (x = minX); x strictly increases with column index.\n * This ordering satisfies the `IkiWarpGrid` rest-grid invariant\n * (packages/format/src/types.ts:178-196) required by the validator and\n * the engine's grid-sampling code.\n */\nfunction generateRegularGridPoints(\n cols: number,\n rows: number,\n minX: number,\n maxX: number,\n minY: number,\n maxY: number,\n): number[] {\n const pts: number[] = [];\n for (let row = 0; row <= rows; row++) {\n const t = row / rows;\n const y = maxY - t * (maxY - minY); // maxY at row 0, minY at row `rows`\n for (let col = 0; col <= cols; col++) {\n const s = col / cols;\n const x = minX + s * (maxX - minX); // minX at col 0, maxX at col `cols`\n pts.push(x, y);\n }\n }\n return pts;\n}\n\n/**\n * Create a minimal valid part with a collision-free id and a paint order one\n * above the current top-most part so it is immediately visible in the viewport.\n * Uses a distinct non-white blue tint so it is distinguishable from the canvas\n * background without requiring a texture.\n *\n * No optional keys (`texture`, `mesh`, `deformer`, `bindings`, `warps`) are set\n * — the format treats all of them as absent by default.\n */\nexport function createDefaultPart(model: IkiModel): IkiPart {\n const order = model.parts.length\n ? Math.max(...model.parts.map((p) => p.order)) + 1\n : 0;\n\n return {\n id: generateUniqueId(model, \"part\"),\n color: [0.45, 0.6, 0.85, 1],\n width: 150,\n height: 150,\n transform: { x: 0, y: 0 },\n order,\n };\n}\n\n/**\n * Create a minimal valid matrix deformer rooted at the canvas origin.\n * `kind` is omitted because the format treats its absence as \"matrix\" (the\n * default), keeping the serialised model compact.\n */\nexport function createDefaultMatrixDeformer(\n model: IkiModel,\n): IkiMatrixDeformer {\n return {\n id: generateUniqueId(model, \"deformer\"),\n pivot: { x: 0, y: 0 },\n };\n}\n\n/**\n * Create a regular grid mesh in part LOCAL space (±0.5 unit frame).\n *\n * Vertices span x ∈ [-0.5, 0.5] and y ∈ [-0.5, 0.5] (+y up, engine convention).\n * Row 0 is the TOP of the grid (y = +0.5); row index increases downward.\n * UVs are unit-square base coordinates: u = col/cols (0..1 left→right),\n * v = row/rows (0..1 top→bottom). The top row maps to v=0 because v and y\n * run in opposite directions — keeps textures upright without a post-flip.\n * The UV-to-texture remap (atlas rect) is applied later in SetPartMesh, not here.\n *\n * Index winding per cell: [BL, BR, TL] then [TL, BR, TR], matching the engine's\n * implicit-quad convention (see examples/editor/src/mesh-generator.ts).\n *\n * Bounds are validated BEFORE any array allocation because this factory runs\n * before SetPartMesh's parseIkiModel — an unbounded count would freeze the\n * editor before the format-level 65536 limit is ever reached.\n */\nexport function createGridMesh(cols: number, rows: number): IkiMesh {\n // Guard: cols/rows must be integers ≥1 and the vertex count must fit within\n // the format limit of 65536 vertices (packages/format/src/validate.ts parseMesh).\n if (\n !Number.isInteger(cols) ||\n cols < 1 ||\n !Number.isInteger(rows) ||\n rows < 1 ||\n (cols + 1) * (rows + 1) > 65536\n ) {\n throw new Error(\n \"createGridMesh: cols and rows must be integers >= 1 with (cols+1)*(rows+1) <= 65536\",\n );\n }\n\n const colVerts = cols + 1;\n const rowVerts = rows + 1;\n\n const vertices: number[] = [];\n const uvs: number[] = [];\n\n // Row 0 is TOP (y = +0.5). Row `rows` is BOTTOM (y = -0.5).\n // Column 0 is left (x = -0.5). Column `cols` is right (x = +0.5).\n for (let row = 0; row < rowVerts; row++) {\n const t = row / rows;\n const y = 0.5 - t; // +0.5 at row 0, -0.5 at row `rows`\n const v = t; // 0 at top row, 1 at bottom row\n\n for (let col = 0; col < colVerts; col++) {\n const s = col / cols;\n const x = -0.5 + s; // -0.5 at col 0, +0.5 at col `cols`\n const u = s; // 0 at left col, 1 at right col\n\n vertices.push(x, y);\n uvs.push(u, v);\n }\n }\n\n // Two triangles per cell: [BL, BR, TL] then [TL, BR, TR].\n const indices: number[] = [];\n for (let row = 0; row < rows; row++) {\n for (let col = 0; col < cols; col++) {\n const tl = row * colVerts + col;\n const tr = row * colVerts + col + 1;\n const bl = (row + 1) * colVerts + col;\n const br = (row + 1) * colVerts + col + 1;\n\n indices.push(bl, br, tl);\n indices.push(tl, br, tr);\n }\n }\n\n return { vertices, uvs, indices };\n}\n\n/**\n * Create a 4×4-cell warp deformer whose rest grid spans a quarter of the\n * canvas in each direction (`±canvas.width/4` × `±canvas.height/4`). For the\n * 1000-unit sample canvas this gives x,y ∈ [−250, 250], which is large enough\n * to cover a typical face part without spilling to the edge.\n *\n * `warps` is omitted — the format treats its absence as \"rest grid only\", so\n * the deformer is immediately usable without authored keyforms.\n * `parent` is omitted — the deformer is placed at root; the caller may\n * reparent it via `SetDeformerParent` after creation.\n */\nexport function createDefaultWarpDeformer(model: IkiModel): IkiWarpDeformer {\n const hx = model.canvas.width / 4;\n const hy = model.canvas.height / 4;\n\n return {\n kind: \"warp\",\n id: generateUniqueId(model, \"warp\"),\n grid: {\n cols: 4,\n rows: 4,\n points: generateRegularGridPoints(4, 4, -hx, hx, -hy, hy),\n },\n };\n}\n","/**\n * Role table, role parsing, bbox→transform math, and model assembly for the\n * AI auto-rig generator. All pure functions — no DOM, no canvas, no\n * crypto.randomUUID.\n *\n * L/R = CHARACTER frame: *_L is the character's left = screen right.\n */\n\nimport {\n IKI_FORMAT_VERSION,\n StandardParameter,\n parseIkiModel,\n type IkiBinding,\n type IkiGrid2DWarp,\n type IkiGridWarp,\n type IkiMesh,\n type IkiModel,\n type IkiParameter,\n type IkiPart,\n type IkiWarp,\n type IkiWarpGrid,\n} from \"@ikijs/format\";\n\n// ── Types ────────────────────────────────────────────────────────────────────\n\nexport interface RoleSpec {\n /**\n * Which deformer the part is attached to in the generated rig. `\"none\"`\n * attaches it to nothing, so it holds still in world space; no shipped\n * role uses it.\n */\n deformer: \"faceWarp\" | \"headDeformer\" | \"bodyDeformer\" | \"none\";\n /** Back-to-front draw order. Higher = in front. */\n order: number;\n /** Whether this part gets a warp mesh (true) or is a static quad (false). */\n mesh: boolean;\n /** Present only for eye-family roles. */\n eyeSide?: \"L\" | \"R\";\n}\n\n/**\n * Input contract from the host app to this package's auto-rig functions.\n * Passed in after the host has decoded PNGs, computed alpha bboxes, and\n * mapped filenames to canonical roles.\n */\nexport interface LayerInput {\n /** Canonical role, e.g. \"eye_L\". */\n role: string;\n /** Original file name — used in error messages and as a stable id. */\n fileName: string;\n /** Shared canvas width (all layers have the same canvas size). */\n canvasW: number;\n /** Shared canvas height. */\n canvasH: number;\n /** Alpha-tight bounding box, top-left origin, +y down (image coords). */\n bbox: { x: number; y: number; w: number; h: number };\n /** Cropped image width = bbox.w. */\n cropW: number;\n /** Cropped image height = bbox.h. */\n cropH: number;\n}\n\n// ── Role table ───────────────────────────────────────────────────────────────\n\n/**\n * Single source of truth for every role the auto-rig generator understands.\n * Order values define back-to-front compositing (higher = in front).\n *\n * Eye-family roles use eyeSide to pair blink/gaze bindings correctly:\n * eye_L = character's left eye = screen right side.\n * eye_R = character's right eye = screen left side.\n */\nexport const ROLE_TABLE: Record<string, RoleSpec> = {\n // The silhouette behind the face. It rides the rigid head (not faceWarp,\n // whose grid it would stretch across the shoulders) as a MESH: it bends on\n // the turn through its own part warp (bakeHairBackTurnWarp) and swings its\n // ends on the hair-sway warps.\n hair_back: { deformer: \"headDeformer\", order: 0, mesh: true },\n // The torso. It rides its own rigid deformer, not the head's: the head turns\n // about the neck pivot while the shoulders follow at BODY_TURN_FOLLOW and\n // follow the head's breath bob, which is what keeps the character from\n // reading as a floating head. Drawn over the back hair so long hair falls\n // behind the shoulders.\n body: { deformer: \"bodyDeformer\", order: 5, mesh: false },\n face: { deformer: \"faceWarp\", order: 10, mesh: true },\n nose: { deformer: \"faceWarp\", order: 15, mesh: true },\n blush_L: { deformer: \"faceWarp\", order: 20, mesh: true },\n blush_R: { deformer: \"faceWarp\", order: 20, mesh: true },\n mouth: { deformer: \"faceWarp\", order: 25, mesh: true },\n // An OPTIONAL second mouth drawing, open. When present the two cross-fade on\n // MouthOpen instead of the closed one being stretched, which is the\n // difference between a portrait rig and one that can lip-sync.\n mouth_open: { deformer: \"faceWarp\", order: 26, mesh: true },\n eye_L: { deformer: \"faceWarp\", order: 30, mesh: true, eyeSide: \"L\" },\n eye_R: { deformer: \"faceWarp\", order: 30, mesh: true, eyeSide: \"R\" },\n iris_L: { deformer: \"faceWarp\", order: 31, mesh: true, eyeSide: \"L\" },\n iris_R: { deformer: \"faceWarp\", order: 31, mesh: true, eyeSide: \"R\" },\n pupil_L: { deformer: \"faceWarp\", order: 32, mesh: true, eyeSide: \"L\" },\n pupil_R: { deformer: \"faceWarp\", order: 32, mesh: true, eyeSide: \"R\" },\n highlight_L: { deformer: \"faceWarp\", order: 33, mesh: true, eyeSide: \"L\" },\n highlight_R: { deformer: \"faceWarp\", order: 33, mesh: true, eyeSide: \"R\" },\n // Upper lashes: an OPTIONAL separate layer ABOVE the iris that folds down to\n // the closed-eye seam (the same crease the white folds to), covering the cut\n // eyeball cleanly. When absent, the white's own fold is the only closed line.\n lash_L: { deformer: \"faceWarp\", order: 34, mesh: true, eyeSide: \"L\" },\n lash_R: { deformer: \"faceWarp\", order: 34, mesh: true, eyeSide: \"R\" },\n brow_L: { deformer: \"faceWarp\", order: 40, mesh: true },\n brow_R: { deformer: \"faceWarp\", order: 40, mesh: true },\n // Front hair rides faceWarp (mesh) so it follows the head-turn curvature with\n // the face instead of detaching as a rigid blob; its bbox joins the faceWarp\n // grid union so the grid covers it.\n hair_front: { deformer: \"faceWarp\", order: 50, mesh: true },\n};\n\n/**\n * Roles the generator requires. BOTH eyes are mandatory because the rig pairs\n * left/right blink parameters — a one-eyed rig would produce mismatched bindings.\n */\nexport const REQUIRED_ROLES = [\"face\", \"eye_L\", \"eye_R\", \"mouth\"] as const;\n\n// ── Alias map ────────────────────────────────────────────────────────────────\n\n/**\n * Minimal spelling-variant aliases → canonical role.\n * Only covers real-world variants; grow this only when you have evidence.\n */\nconst ALIAS_MAP: Record<string, string> = {\n eyebrow_L: \"brow_L\",\n eyebrow_R: \"brow_R\",\n eye_white_L: \"eye_L\",\n eye_white_R: \"eye_R\",\n};\n\n// ── normalizeRole ─────────────────────────────────────────────────────────────\n\n/**\n * Convert a raw filename to a canonical role key:\n * 1. Strip file extension.\n * 2. Lowercase.\n * 3. Collapse hyphens and spaces to underscores.\n * 4. Uppercase a trailing `_l` or `_r` side suffix → `_L` / `_R`.\n * 5. Apply alias map for known spelling variants.\n *\n * Examples:\n * \"Eye-L.png\" → \"eye_L\"\n * \"Brow_R.png\" → \"brow_R\"\n * \"eyebrow_L.png\" → \"brow_L\"\n */\nexport function normalizeRole(raw: string): string {\n // Strip extension\n const noExt = raw.replace(/\\.[^.]+$/, \"\");\n // Lowercase, then collapse hyphens/spaces → underscores\n const collapsed = noExt.toLowerCase().replace(/[-\\s]+/g, \"_\");\n // Uppercase trailing _l / _r side suffix\n const sided = collapsed.replace(\n /_([lr])$/,\n (_, s: string) => `_${s.toUpperCase()}`,\n );\n // Alias map\n return ALIAS_MAP[sided] ?? sided;\n}\n\n// ── assertRoleSet ─────────────────────────────────────────────────────────────\n\n/**\n * Single home for the unknown/duplicate/required role contract. Takes CANONICAL\n * role names (already normalized). Throws on:\n * - Unknown role (not in ROLE_TABLE)\n * - Duplicate role\n * - Missing required role\n *\n * The \"unknown role\" message from this function is fileName-free. That is\n * intentional: callers that lack a fileName (e.g. a future\n * `validateLayerInputs` that receives pre-normalized roles) get a useful error\n * without needing to pre-check. Callers that DO have the original fileName\n * (e.g. `parseLayerRoles`) pre-check unknown roles themselves so they can embed\n * the fileName in the message — but that pre-check is an enrichment, not a\n * requirement for correctness.\n */\nexport function assertRoleSet(roles: string[]): void {\n const seen = new Set<string>();\n for (const role of roles) {\n if (!(role in ROLE_TABLE)) {\n throw new Error(`auto-rig: unknown role \"${role}\"`);\n }\n if (seen.has(role)) {\n throw new Error(`auto-rig: duplicate role \"${role}\"`);\n }\n seen.add(role);\n }\n for (const required of REQUIRED_ROLES) {\n if (!seen.has(required)) {\n throw new Error(`auto-rig: missing required role \"${required}\"`);\n }\n }\n}\n\n// ── parseLayerRoles ───────────────────────────────────────────────────────────\n\n/**\n * Map an array of raw filenames to canonical `{ role, fileName }` pairs.\n *\n * Steps:\n * 1. Normalize each filename → role (normalizeRole).\n * 2. Eagerly check each role against ROLE_TABLE — unknown roles throw early\n * with the offending fileName included in the message.\n * 3. Call assertRoleSet to check duplicates + required roles.\n *\n * Throws a path-qualified Error on any contract violation.\n */\nexport function parseLayerRoles(\n fileNames: string[],\n): { role: string; fileName: string }[] {\n const pairs = fileNames.map((fileName) => {\n const role = normalizeRole(fileName);\n if (!(role in ROLE_TABLE)) {\n throw new Error(\n `auto-rig: unknown role \"${role}\" from file \"${fileName}\"`,\n );\n }\n return { role, fileName };\n });\n\n assertRoleSet(pairs.map((p) => p.role));\n return pairs;\n}\n\n// ── bboxToTransform ───────────────────────────────────────────────────────────\n\n/**\n * Convert an alpha bounding box (image coordinates, +y down, top-left origin)\n * to a model-space translation (model coordinates, +y up, canvas-center origin).\n *\n * x = bbox.x + bbox.w/2 - canvasW/2 (center of bbox relative to canvas center)\n * y = canvasH/2 - (bbox.y + bbox.h/2) (flip axis: image +y down → model +y up)\n *\n * Result is NOT rounded — fractional .5 values must be preserved to avoid\n * sub-pixel jitter in blink/gaze animations when the eye center falls between\n * two canvas pixels.\n *\n * @param partLabel Optional label for error messages (role or part id).\n */\nexport function bboxToTransform(\n bbox: { x: number; y: number; w: number; h: number },\n canvasW: number,\n canvasH: number,\n partLabel?: string,\n): { x: number; y: number } {\n if (bbox.w <= 0 || bbox.h <= 0) {\n throw new Error(`auto-rig: empty bbox for ${partLabel ?? \"layer\"}`);\n }\n const x = bbox.x + bbox.w / 2 - canvasW / 2;\n const y = canvasH / 2 - (bbox.y + bbox.h / 2); // flip: image +y-down → model +y-up\n return { x, y };\n}\n\n// ── validateLayerInputs ───────────────────────────────────────────────────────\n\n/**\n * Validate a LayerInput array before assembly. Called first inside\n * `generateIkiFromLayerSet` — the public API validates before deriving anything.\n *\n * Checks (in order):\n * 1. Non-empty layer list.\n * 2. Unknown/duplicate/required role contract via `assertRoleSet` (single home).\n * 3. Non-positive bbox.w, bbox.h, cropW, cropH per layer.\n * 4. Per-layer canvas size vs. the supplied `canvas` argument.\n * Matching every layer to the `canvas` arg inherently guarantees all layers\n * agree with each other — no separate peer-comparison loop is needed.\n *\n * Validates `layer.role` DIRECTLY (not via fileName). A caller could supply\n * `fileName:\"face.png\"` with `role:\"bad_role\"` — a filename check would miss it.\n *\n * Throws a plain `Error` with a path-qualified message on the first violation.\n */\nexport function validateLayerInputs(\n layers: LayerInput[],\n canvas: { width: number; height: number },\n): void {\n if (layers.length === 0) {\n throw new Error(\"auto-rig: validateLayerInputs: layers must not be empty\");\n }\n\n // Unknown / duplicate / required — single home for this contract\n assertRoleSet(layers.map((l) => l.role));\n\n for (const layer of layers) {\n const { role, bbox, cropW, cropH, canvasW, canvasH } = layer;\n if (bbox.w <= 0) {\n throw new Error(\n `auto-rig: validateLayerInputs: role \"${role}\" has non-positive bbox.w (${bbox.w})`,\n );\n }\n if (bbox.h <= 0) {\n throw new Error(\n `auto-rig: validateLayerInputs: role \"${role}\" has non-positive bbox.h (${bbox.h})`,\n );\n }\n if (cropW <= 0) {\n throw new Error(\n `auto-rig: validateLayerInputs: role \"${role}\" has non-positive cropW (${cropW})`,\n );\n }\n if (cropH <= 0) {\n throw new Error(\n `auto-rig: validateLayerInputs: role \"${role}\" has non-positive cropH (${cropH})`,\n );\n }\n if (canvasW !== canvas.width || canvasH !== canvas.height) {\n throw new Error(\n `auto-rig: validateLayerInputs: role \"${role}\" canvas size (${canvasW}×${canvasH}) does not match canvas arg (${canvas.width}×${canvas.height})`,\n );\n }\n }\n}\n\n// ── generateGridPoints ────────────────────────────────────────────────────────\n\n/**\n * Generate the flat `[x0,y0, x1,y1, …]` rest-grid control points for a\n * regular axis-aligned lattice with `(cols+1)*(rows+1)` points, row-major.\n *\n * Row 0 is the TOP (y = maxY); y strictly decreases with row index.\n * Column 0 is left (x = minX); x strictly increases with column index.\n * This ordering satisfies `checkGridRegularity` in the format validator.\n *\n * Local copy — do NOT import the private `generateRegularGridPoints` from\n * factories.ts; that helper is private to this package's factory layer.\n */\nexport function generateGridPoints(\n cols: number,\n rows: number,\n minX: number,\n maxX: number,\n minY: number,\n maxY: number,\n): number[] {\n const pts: number[] = [];\n for (let row = 0; row <= rows; row++) {\n const t = row / rows;\n const y = maxY - t * (maxY - minY); // maxY at row 0, minY at row `rows`\n for (let col = 0; col <= cols; col++) {\n const s = col / cols;\n const x = minX + s * (maxX - minX); // minX at col 0, maxX at col `cols`\n pts.push(x, y);\n }\n }\n return pts;\n}\n\n// ── createPixelGridMesh ───────────────────────────────────────────────────────\n\n/**\n * Create a regular grid mesh in PIXEL space, with the local origin at the\n * crop center (matching the `feature(...)` convention in sample-model.ts).\n *\n * Callers set `part.width=1, part.height=1` so the engine's scale pipeline is\n * bypassed — the pixel coordinates ARE the final geometry, positioned only by\n * `part.transform`. scaleX/scaleY bindings then scale about each part's own\n * center without an additional unit-to-pixel conversion step.\n *\n * Vertices span x ∈ [-w/2, w/2] and y ∈ [-h/2, h/2] (+y up, engine convention).\n * Row 0 is the TOP of the grid (y = +h/2); row index increases downward.\n *\n * UVs are base unit-square coordinates: u = col/cols (0..1 left→right),\n * v = row/rows (0..1 top→bottom). Top row maps to v=0 (v and y run in\n * opposite directions — keeps textures upright). Atlas remapping is the\n * caller's responsibility (e.g. applyAtlas), not done here.\n *\n * Index winding per cell: [BL, BR, TL] then [TL, BR, TR] — same as\n * `createGridMesh` in factories.ts so the engine's implicit-quad convention\n * is preserved.\n */\nexport function createPixelGridMesh(\n cols: number,\n rows: number,\n w: number,\n h: number,\n): IkiMesh {\n const colVerts = cols + 1;\n const rowVerts = rows + 1;\n\n const vertices: number[] = [];\n const uvs: number[] = [];\n\n // Row 0 = TOP (y = +h/2). Row `rows` = BOTTOM (y = -h/2).\n // Col 0 = left (x = -w/2). Col `cols` = right (x = +w/2).\n for (let row = 0; row < rowVerts; row++) {\n const t = row / rows;\n const y = h / 2 - t * h; // +h/2 at row 0, -h/2 at row `rows`\n const v = t; // 0 at top, 1 at bottom\n\n for (let col = 0; col < colVerts; col++) {\n const s = col / cols;\n const x = -w / 2 + s * w; // -w/2 at col 0, +w/2 at col `cols`\n const u = s; // 0 at left, 1 at right\n\n vertices.push(x, y);\n uvs.push(u, v);\n }\n }\n\n // Two triangles per cell: [BL, BR, TL] then [TL, BR, TR]\n const indices: number[] = [];\n for (let row = 0; row < rows; row++) {\n for (let col = 0; col < cols; col++) {\n const tl = row * colVerts + col;\n const tr = row * colVerts + col + 1;\n const bl = (row + 1) * colVerts + col;\n const br = (row + 1) * colVerts + col + 1;\n\n indices.push(bl, br, tl);\n indices.push(tl, br, tr);\n }\n }\n\n return { vertices, uvs, indices };\n}\n\n// ── Mesh density ─────────────────────────────────────────────────────────────\n\n/** Target size of one mesh cell. A part's vertices sample the face-warp grid\n * (and its own per-vertex warps) at this spacing; the GPU is linear between\n * them, so the spacing bounds how smooth a bend can render. 64 px puts two\n * vertices per FACE_GRID_CELLS cell on the hero and eight rows on the back\n * hair for the sway curl. */\nconst MESH_CELL_PX = 64;\n/** Cell-count floor / ceiling per axis: small parts stay at 4×4 (their\n * bakes fold/collapse them, they never bend), a canvas-spanning layer is\n * capped at 81 vertices so per-vertex warps stay cheap. */\nconst MESH_CELLS_MIN = 4;\nconst MESH_CELLS_MAX = 8;\n\n/** Cell counts for a pixel-grid mesh of a `w`×`h` crop: `round(size / MESH_CELL_PX)`\n * per axis, clamped to [MESH_CELLS_MIN, MESH_CELLS_MAX]. Exported for tests. */\nexport function meshCellsFor(\n w: number,\n h: number,\n): { cols: number; rows: number } {\n const cells = (px: number) =>\n Math.max(\n MESH_CELLS_MIN,\n Math.min(MESH_CELLS_MAX, Math.round(px / MESH_CELL_PX)),\n );\n return { cols: cells(w), rows: cells(h) };\n}\n\n/** Cells per axis of the face-warp grid. Bilinear cells render the cylinder\n * bend as a chord: on the hero (821 px across) 4 cells = 205 px chords with\n * a 12 px worst-case sag at full turn on the bangs, 6 cells halve it for\n * ~10 KB of keyforms. The fold guard is radius-from-reach and does not\n * depend on this. */\nconst FACE_GRID_CELLS = 6;\n\n// ── Head-turn cylinder constants ─────────────────────────────────────────────\n\n/** Cylinder radius as a multiple of the warp grid's half-width. The 0.6/0.5\n * margin ratio matches the local mesh bake and keeps asin clear of +/-1. */\nconst HEAD_CYLINDER_RADIUS_FACTOR = 0.6 / 0.5;\n/** Outer keyform stop of the head-turn bake, matching ParamAngleX's range. */\nconst HEAD_TURN_MAX_DEG = 30;\n\n/** Keyform stops of the head-turn / nod bakes, degrees. The engine blends\n * parameter-LINEARLY between stops but the cylinder bend is sin-based, so the\n * stops must be dense enough that the chord stays near the arc: at 15° spacing\n * the worst mid-stop error on the hero's grid is ≈4 px at the outer column\n * (3.7 px at 7.5°, 4.0 px at 22.5°) against 15.5 px with stops at ±30 only.\n * The bake is analytic, so extra stops cost only model bytes. Ascending,\n * symmetric, includes 0 (the rest cell). */\nconst HEAD_TURN_STOPS = [-30, -15, 0, 15, 30] as const;\n\n/**\n * Sideways slide, at full head turn, of a surface sitting one full cylinder\n * radius in front of the rotation axis — the unit of depth parallax.\n *\n * This is exactly the bulk `axisShift` the cylinder bake subtracts back out of\n * the face warp. Pinning it there was right: applied to the face it shoved the\n * head off the shoulders. But it is also the whole depth cue, so layers that\n * do NOT sit on the cylinder's axis have to get their own share of it back,\n * scaled by how far in front of (or behind) the axis they sit — the hair\n * (HAIR_FRONT_DEPTH, HAIR_BACK_DEPTH) and the features on the face\n * (FEATURE_DEPTH). `headNodParallaxUnit` is the same quantity on the nod axis.\n */\nexport function headTurnParallaxUnit(gridHalfWidth: number): number {\n return (\n gridHalfWidth *\n HEAD_CYLINDER_RADIUS_FACTOR *\n Math.sin(HEAD_TURN_MAX_DEG * (Math.PI / 180))\n );\n}\n\n// ── bakeHeadTurnGridWarpCentered ──────────────────────────────────────────────\n\n/**\n * Bake a cylinder head-turn grid warp for ParamAngleX, center-relative.\n *\n * WHY a cylinder: rotating a flat face mesh looks right head-on but the\n * silhouette doesn't narrow at the sides; projecting each point onto a\n * cylinder and rotating makes the face foreshorten naturally as it turns.\n *\n * HOW (center-relative): the cylinder axis sits at `centerX` (the face center\n * in model space). Each grid point at absolute x has local x = x - centerX,\n * which maps onto the cylinder. After rotating by theta, the new absolute x is:\n * xPrime = centerX + RADIUS * sin(asin(localX/RADIUS) + theta)\n * dx = xPrime - x, dy = 0\n *\n * At theta=0 the center keyform is all-zero (xPrime === x by identity).\n *\n * The axis column is pinned: the bulk sideways slide a cylinder rotation\n * produces is subtracted out, leaving only the foreshortening. See the comment\n * at the subtraction for why.\n *\n * RADIUS is derived from the grid's reach about `centerX` (same 0.6/0.5 margin\n * ratio as bakeHeadTurnGridWarp in the example app) so asin stays clear of ±1\n * whether or not the grid is symmetric about the axis.\n *\n * No production caller since faceWarp moved to the 2D bake; kept as the 1D\n * reference that the 2D bake's tests compare their AngleY = 0 row against,\n * keyed on the same `HEAD_TURN_STOPS`.\n */\nexport function bakeHeadTurnGridWarpCentered(\n grid: IkiWarpGrid,\n parameter: string,\n centerX: number,\n): IkiGridWarp {\n // Keyform stops (degrees), matching ParamAngleX's −30..30 range.\n const ANGLES = HEAD_TURN_STOPS;\n // Cylinder radius in MODEL units, from the grid's reach about the axis.\n const RADIUS = gridReach(grid, 0, centerX) * HEAD_CYLINDER_RADIUS_FACTOR;\n\n const pointCount = grid.points.length / 2;\n const DEG_TO_RAD = Math.PI / 180;\n\n const keyforms = ANGLES.map((angleDeg) => {\n const theta = angleDeg * DEG_TO_RAD;\n const offsets: number[] = [];\n for (let i = 0; i < pointCount; i++) {\n const dx = pinnedCylinderBend(\n grid.points[i * 2] - centerX,\n RADIUS,\n theta,\n );\n // dy is zero — cylinder bend only deforms horizontal position.\n offsets.push(dx, 0);\n }\n return { value: angleDeg, offsets };\n });\n\n // keyforms are sorted ascending by construction (HEAD_TURN_STOPS).\n return { parameter, keyforms };\n}\n\n/** Farthest grid point from `center` along one axis (0 = x, 1 = y): the\n * cylinder radius scales from this so no point's |local|/radius exceeds 1/1.2. */\nfunction gridReach(grid: IkiWarpGrid, axis: 0 | 1, center: number): number {\n let reach = 0;\n for (let i = axis; i < grid.points.length; i += 2) {\n reach = Math.max(reach, Math.abs(grid.points[i] - center));\n }\n return reach;\n}\n\n/**\n * Displacement of a point at signed distance `local` from the cylinder axis\n * after the cylinder rotates by `theta`, with the axis column pinned.\n *\n * Rotating a cylinder slides its whole visible surface sideways by\n * RADIUS*sin(theta) — 170px on a 430px-wide face at 30 degrees — on top of the\n * foreshortening. That bulk slide is what shoves the head off the shoulders and\n * detaches the back hair; the foreshortening alone is what reads as a turn.\n * Subtracting it leaves the differential and keeps the offsets monotonic, so no\n * cell folds. Deliberate bulk head motion stays on headDeformer's own translate\n * bindings, where it can be tuned independently, and the hair layers get their\n * depth-scaled share of it back through `headTurnParallaxUnit`.\n */\nfunction pinnedCylinderBend(\n local: number,\n radius: number,\n theta: number,\n): number {\n // Clamp local/radius to [-1,1] to keep asin defined at boundary points.\n const alpha = Math.asin(Math.max(-1, Math.min(1, local / radius)));\n return radius * Math.sin(alpha + theta) - local - radius * Math.sin(theta);\n}\n\n/**\n * Bake the head turn AND nod as one 2D grid warp over AngleX × AngleY.\n *\n * The same pinned cylinder bend as `bakeHeadTurnGridWarpCentered`, applied per\n * axis: dx from the horizontal bend at valuesX[i], dy from a vertical bend at\n * valuesY[j] about `centerY`. The axes are independent (dx depends only on x\n * and the yaw, dy only on y and the pitch), which is the same separable\n * convention the playground's 2D bake ships; the row at AngleY=0 is exactly the\n * 1D bake.\n *\n * Each axis takes its radius from the grid's reach about its own centre, with\n * the same margin factor, so the no-fold guarantee holds on both and does not\n * depend on the grid being symmetric about the axis: |local|/radius stays\n * ≤ 1/1.2 and asin(1/1.2) + 30° < 90°. The pitch itself is scaled by\n * NOD_BEND — see that constant for why a full nod is not a full 30° bend.\n *\n * Layout is the format's row-major `k(i, j) = j * valuesX.length + i`.\n */\nexport function bakeHeadTurnGridWarp2DCentered(\n grid: IkiWarpGrid,\n parameterX: string,\n parameterY: string,\n centerX: number,\n centerY: number,\n): IkiGrid2DWarp {\n const STOPS = [...HEAD_TURN_STOPS];\n const RADIUS_X = gridReach(grid, 0, centerX) * HEAD_CYLINDER_RADIUS_FACTOR;\n const RADIUS_Y = gridReach(grid, 1, centerY) * HEAD_CYLINDER_RADIUS_FACTOR;\n const pointCount = grid.points.length / 2;\n\n const DEG_TO_RAD = Math.PI / 180;\n const keyforms2d: { offsets: number[] }[] = [];\n for (const angleY of STOPS) {\n const thetaY = angleY * NOD_BEND * DEG_TO_RAD;\n for (const angleX of STOPS) {\n const thetaX = angleX * DEG_TO_RAD;\n const offsets: number[] = [];\n for (let i = 0; i < pointCount; i++) {\n offsets.push(\n pinnedCylinderBend(grid.points[i * 2] - centerX, RADIUS_X, thetaX),\n pinnedCylinderBend(\n grid.points[i * 2 + 1] - centerY,\n RADIUS_Y,\n thetaY,\n ),\n );\n }\n keyforms2d.push({ offsets });\n }\n }\n\n return {\n parameter: parameterX,\n parameterY,\n valuesX: STOPS,\n valuesY: STOPS,\n keyforms2d,\n };\n}\n\n// ── bindingsForRole ───────────────────────────────────────────────────────────\n\n// Role prefixes that belong to the eye stack (blink + optional gaze bindings).\n// Hoisted to module scope so it is not reallocated on every bindingsForRole call.\nconst EYE_STACK_PREFIXES = [\"eye_\", \"iris_\", \"pupil_\", \"highlight_\"] as const;\n\n/** Signed depth of each hair layer from the head cylinder's axis, as a fraction\n * of the cylinder radius; positive is toward the viewer. Tuned by eye against\n * the rendered turn, not derived.\n *\n * The bangs lead the face by a little — HAIR_FRONT_DEPTH is the FRINGE TIPS'\n * lead specifically, not the whole sheet's: the crown is root-pinned by the\n * warp that applies it, not a binding (see bindingsForRole's doc for why).\n * 0.16 was right while the shift was rigid and the back hair stood still, the\n * lead the only depth cue; once the back hair bent and bulged on the turn the\n * same lead read as the bangs running ahead of the head, so it came down. At\n * 0.06 the layering all but vanishes. 0.1 is settled with the lead\n * root-pinned: the fringe tips get it, the crown gets none.\n *\n * hair_back follows the head at about 60% of its travel (its counter-shift\n * takes ~20px off headDeformer's +50px). A deeper value that held the back of\n * the head still in world space read as the face sliding over a backdrop; the\n * turn cue for the back hair comes from its bend (bakeHairBackTurnWarp), not\n * from lagging the head. */\nconst HAIR_FRONT_DEPTH = 0.1;\nconst HAIR_BACK_DEPTH = -0.08;\n\n/** Rigid vertical travel of the head at full nod (px at AngleY = ±30). */\nconst NOD_TRAVEL = 30;\n/** Geometric pitch per degree of ParamAngleY: a full ±30 nod bends the face\n * cylinder by ±15°. The face-warp grid reaches up to the hair crown, which\n * sits near 45° on the vertical cylinder; a full 30° pitch there drops the\n * bangs' crown ~140px while the rigid back hair stays put, and the back\n * hair's crown pops out above it as a second silhouette. At half pitch the\n * drop stays under the back hair and the nod still foreshortens visibly. */\nconst NOD_BEND = 0.5;\n/** Vertical share of the nod for hair_back, as a fraction of\n * `headNodParallaxUnit`. The back hair gets no vertical bend of its own, so\n * it only needs to follow the bent crown down to stay tucked beneath it.\n * Tuned against renders as −0.07 of a turn-sized unit (sin 30°), before the\n * nod had its own; the same pixels in the nod unit (sin 15°). */\nconst HAIR_BACK_NOD_DEPTH = -0.135;\n\n/**\n * The nod's counterpart of `headTurnParallaxUnit`: the bulk vertical slide the\n * 2D bake pins out of the face warp at full nod. The nod bends at NOD_BEND of\n * the angle, so this is RADIUS_Y · sin(30° · NOD_BEND), not RADIUS_Y · sin(30°).\n */\nexport function headNodParallaxUnit(gridHalfHeight: number): number {\n return (\n gridHalfHeight *\n HEAD_CYLINDER_RADIUS_FACTOR *\n Math.sin(HEAD_TURN_MAX_DEG * NOD_BEND * (Math.PI / 180))\n );\n}\n\n/** Depth of the features in front of the head cylinder's axis, as a fraction\n * of its radius — the share of the pinned bulk slide each part gets back on\n * the turn (`headTurnParallaxUnit`) and on the nod (`headNodParallaxUnit`).\n *\n * The face contour is a cylinder rotating about its own axis: its silhouette\n * stays put and only foreshortens, which is what the pinned bake gives it.\n * The features painted on that surface do not stay put — they slide across\n * it toward the far side, and a nose that stands off the surface slides\n * further. Without their share they sat still while the outline squeezed\n * around them, and the head read as a flat sheet bending rather than turning.\n *\n * The nose is the anchor of the whole read, so the parallax exists only when\n * it is a layer of its own (the composer cuts it out of the face). With the\n * nose painted on the face plate, any lead on the eyes and mouth inverts the\n * 3/4 view — the nose lands nearer the NEAR eye and the mouth hangs off its\n * far side — and a blind review ranked that below no parallax at all.\n *\n * Keyed by role family (`eye_L` → `eye`). The whole eye stack shares one\n * depth because iris/pupil/highlight clip to the white, and the lashes fold\n * onto it. The magnitudes are small on purpose. Blind reviews that matched a\n * 3/4 reference asked for a nose lead of 0.34 of the radius, and at that the\n * features read as sliding across a face that itself does not turn: the\n * contour is pinned, so a 30° parameter cannot carry a true 3/4 view's\n * feature layout. Judged by eye across a ladder of rigs, the nose at twice\n * the surface's share and the mouth on the surface with the eyes is where\n * the turn stopped looking like a plate without looking like a slide. */\nconst FEATURE_DEPTH: Readonly<Record<string, number>> = {\n eye: 0.04,\n iris: 0.04,\n pupil: 0.04,\n highlight: 0.04,\n lash: 0.04,\n brow: 0.04,\n blush: 0.04,\n mouth: 0.04,\n mouth_open: 0.04,\n nose: 0.08,\n};\n\n/** The bangs' share of the nod: the brows'. They hang from the hairline, which\n * sits on the same surface as the brows, so they slide with them — held still\n * while the brows slid, the fringe closed onto the brows looking up and left\n * a bare forehead looking down. (Before the brows slid, a lead here lifted\n * the bangs clear off them, which is why they had none.) The turn lead stays\n * a root-pinned warp: on the turn the crown sits on the axis and must not\n * slide. */\nconst HAIR_FRONT_NOD_DEPTH = FEATURE_DEPTH.brow;\n\n/** `eye_L` → `eye`; an unsided role is its own family. */\nfunction roleFamily(role: string): string {\n return role.replace(/_[LR]$/, \"\");\n}\n\n/**\n * The depth-parallax translate bindings of a facial feature, or [] for any\n * other role, whenever the units are absent (a unit-less call is a rig without\n * a face warp to slide across), and whenever the rig has no nose layer to lead\n * (see FEATURE_DEPTH). Symmetric about zero, so the rest pose — the one every\n * proportion is judged on — is untouched.\n */\nfunction featureParallaxBindings(\n role: string,\n options: { parallaxUnit?: number; parallaxUnitY?: number; hasNose?: boolean },\n): IkiBinding[] {\n if (!options.hasNose) return [];\n const depth = FEATURE_DEPTH[roleFamily(role)];\n if (depth === undefined) return [];\n const bindings: IkiBinding[] = [];\n const shiftX = depth * (options.parallaxUnit ?? 0);\n if (shiftX !== 0) {\n bindings.push({\n parameter: StandardParameter.AngleX,\n channel: \"translateX\",\n from: -shiftX,\n to: shiftX,\n });\n }\n const shiftY = depth * (options.parallaxUnitY ?? 0);\n if (shiftY !== 0) {\n bindings.push({\n parameter: StandardParameter.AngleY,\n channel: \"translateY\",\n from: -shiftY,\n to: shiftY,\n });\n }\n return bindings;\n}\n\n/**\n * Derive the IkiBinding[] for a part from its role spec and crop dimensions.\n *\n * - face → no bindings: the contour is the cylinder itself, pinned by the bake\n * - every feature on that contour — eye stack, lashes, brows, both mouths,\n * nose, blush — carries the AngleX translateX / AngleY translateY depth\n * parallax of FEATURE_DEPTH (needs `parallaxUnit` / `parallaxUnitY` and\n * `hasNose`), on top of whatever its role adds below\n * - hair_front: an AngleY translateY nod follow at the brows' depth\n * (HAIR_FRONT_NOD_DEPTH), with the same `hasNose` gate as the brows it\n * follows, and nothing on the turn. Both its sway AND its\n * AngleX turn lead are root-pinned warps attached in\n * generateIkiFromLayerSet — a rigid translate/rotate would carry the whole\n * sheet (crown included) with the fringe tips, instead of leading from them.\n * - hair_back: the AngleX depth-parallax translateX, and an AngleY translateY\n * that tucks its crown under the bent front hair (needs `parallaxUnitY`)\n * - brow_L/R: BrowLeftY/RightY translateY (raise/lower) + BrowLeftAngle/RightAngle rotate\n * (each brow rotates its own, CCW-positive)\n * - eye-stack:\n * iris_/pupil_/highlight_ → gaze translateX + translateY (no blink binding)\n * eye_ (white) → none here; its blink is a fold warp attached in assembly,\n * and iris/pupil/highlight clip to it so the closing white CUTS them away\n * - mouth: MouthForm scaleX (-0.2 to 0.4), plus MouthOpen as scaleY (0 to 3)\n * without a mouth_open layer, or as an opacity fade-out with one\n * - mouth_open: MouthOpen opacity fade-in + the same MouthForm scaleX\n *\n * Returns [] when the role has no bindings (callers skip the bindings key when\n * the array is empty).\n */\nexport function bindingsForRole(\n spec: RoleSpec,\n role: string,\n cropW: number,\n cropH: number,\n options: {\n hasMouthOpen?: boolean;\n parallaxUnit?: number;\n parallaxUnitY?: number;\n /** Whether the layer set has a `nose` role — the feature parallax's gate. */\n hasNose?: boolean;\n } = {},\n): IkiBinding[] {\n return [\n ...roleOwnBindings(spec, role, cropW, cropH, options),\n ...featureParallaxBindings(role, options),\n ];\n}\n\n/** The bindings a role has for its own expression or motion — everything in\n * `bindingsForRole`'s list except the shared feature parallax. */\nfunction roleOwnBindings(\n spec: RoleSpec,\n role: string,\n cropW: number,\n cropH: number,\n options: {\n hasMouthOpen?: boolean;\n parallaxUnit?: number;\n parallaxUnitY?: number;\n hasNose?: boolean;\n },\n): IkiBinding[] {\n const isEyeStack = EYE_STACK_PREFIXES.some((p) => role.startsWith(p));\n\n if (isEyeStack && spec.eyeSide !== undefined) {\n // Only iris/pupil/highlight move with gaze; the white (eye_) gets nothing.\n const isGazeRole =\n role.startsWith(\"iris_\") ||\n role.startsWith(\"pupil_\") ||\n role.startsWith(\"highlight_\");\n if (!isGazeRole) return [];\n\n // Gaze range: proportional to crop size, capped to avoid over-travel.\n const gx = Math.min(cropW * 0.18, 22);\n const gy = Math.min(cropH * 0.18, 16);\n return [\n {\n parameter: StandardParameter.EyeballX,\n channel: \"translateX\",\n from: -gx,\n to: gx,\n },\n {\n parameter: StandardParameter.EyeballY,\n channel: \"translateY\",\n from: -gy,\n to: gy,\n },\n ];\n }\n\n // Mouth form applies to whichever mouth drawing is showing.\n const mouthForm = {\n // Mouth form: scaleX from -0.2 (pursed, param=-1) to 0.4 (wide, param=1).\n parameter: StandardParameter.MouthForm,\n channel: \"scaleX\" as const,\n from: -0.2,\n to: 0.4,\n };\n\n if (role === \"mouth\") {\n // With an open-mouth drawing present, the closed one fades out rather than\n // being stretched. Stretching a closed mouth is what produced a smear: the\n // art is a ~15px-tall line and scaleY 3 blows it up to a blurred band.\n if (options.hasMouthOpen) {\n return [\n {\n parameter: StandardParameter.MouthOpen,\n channel: \"opacity\",\n from: 1,\n to: 0,\n },\n mouthForm,\n ];\n }\n return [\n // Mouth open: scaleY from 0 (closed, param=0) to 3 (wide open, param=1).\n // Only a fallback — it distorts, but it is better than a mouth that\n // cannot open at all when the layer set has no open drawing.\n {\n parameter: StandardParameter.MouthOpen,\n channel: \"scaleY\",\n from: 0,\n to: 3,\n },\n mouthForm,\n ];\n }\n\n if (role === \"mouth_open\") {\n return [\n {\n parameter: StandardParameter.MouthOpen,\n channel: \"opacity\",\n from: 0,\n to: 1,\n },\n mouthForm,\n ];\n }\n\n if (role === \"brow_L\" || role === \"brow_R\") {\n // Raise/lower capped to avoid over-travel; tilt is a fixed ±12° range.\n const ty = Math.min(cropH * 0.8, 18);\n const deg = 12;\n if (role === \"brow_L\") {\n return [\n {\n parameter: StandardParameter.BrowLeftY,\n channel: \"translateY\",\n from: -ty,\n to: ty,\n },\n {\n parameter: StandardParameter.BrowLeftAngle,\n channel: \"rotate\",\n from: -deg,\n to: deg,\n },\n ];\n } else {\n return [\n {\n parameter: StandardParameter.BrowRightY,\n channel: \"translateY\",\n from: -ty,\n to: ty,\n },\n {\n parameter: StandardParameter.BrowRightAngle,\n channel: \"rotate\",\n from: -deg,\n to: deg,\n },\n ];\n }\n }\n\n if (role === \"hair_front\" || role === \"hair_back\") {\n const isFront = role === \"hair_front\";\n // Depth parallax on the head turn, for hair_back only: it hangs rigid off\n // headDeformer, so without a shift on top of the face turn it stayed flat\n // while the face beneath it foreshortened, reading as a cutout sliding.\n // hair_front leads the same way but as a root-pinned warp attached in\n // generateIkiFromLayerSet, not a binding here — see the doc above for why.\n const parallax: IkiBinding[] = [];\n if (!isFront) {\n const shiftX = HAIR_BACK_DEPTH * (options.parallaxUnit ?? 0);\n if (shiftX !== 0) {\n parallax.push({\n parameter: StandardParameter.AngleX,\n channel: \"translateX\",\n from: -shiftX,\n to: shiftX,\n });\n }\n }\n // On the nod the bangs slide with the brows they hang over — only when the\n // brows slide, i.e. with a nose to lead them — and the back hair follows\n // the bent crown down; see the two NOD_DEPTH constants.\n const frontDepth = options.hasNose ? HAIR_FRONT_NOD_DEPTH : 0;\n const shiftY =\n (isFront ? frontDepth : HAIR_BACK_NOD_DEPTH) *\n (options.parallaxUnitY ?? 0);\n if (shiftY !== 0) {\n parallax.push({\n parameter: StandardParameter.AngleY,\n channel: \"translateY\",\n from: -shiftY,\n to: shiftY,\n });\n }\n return parallax;\n }\n\n // face, blush_*, nose → nothing of their own\n return [];\n}\n\n// ── bakeEyelidFoldWarp ─────────────────────────────────────────────────────\n\n/** Crease sits this fraction of the eye height BELOW the white's center. */\nconst EYELID_FOLD_CREASE = 0.15;\n/** Without separate lashes, retain a thin band of the eye's own line art. */\nconst EYELID_FOLD_K = 0.04;\n/** The lash keeps a thicker band than the white when closed, so it reads as a\n * visible dark closed-eye line and covers the cut eyeball/seam. */\nconst LASH_FOLD_K = 0.2;\n\n/**\n * Live2D-style eyelid FOLD blink for the eye-white. Two EyeOpen keyforms collapse\n * the white toward a crease line `creaseOffsetY` below its center while scaling\n * its height by `k`: as EyeOpen → 0 the white folds shut. Because iris/pupil/\n * highlight CLIP to the white, the closing clip region CUTS the (static, round)\n * iris away instead of squashing it — unlike the old scaleY-collapse blink.\n * EyeOpen=1 → rest (zero offsets = the authored open art); =0 → folded.\n *\n * Offsets are authored in the mesh's own pixel frame (+y up, centered), matching\n * `createPixelGridMesh`, so the SAME mesh must be passed that the part renders.\n */\nexport function bakeEyelidFoldWarp(\n mesh: IkiMesh,\n parameter: string,\n creaseOffsetY: number,\n k: number,\n): IkiWarp {\n const closed: number[] = [];\n const zeros: number[] = [];\n for (let i = 0; i < mesh.vertices.length; i += 2) {\n const vy = mesh.vertices[i + 1];\n // closed y = creaseOffsetY + vy*k → dy added to the rest vertex vy.\n closed.push(0, creaseOffsetY - (1 - k) * vy);\n zeros.push(0, 0);\n }\n return {\n parameter,\n keyforms: [\n { value: 0, offsets: closed },\n { value: 1, offsets: zeros },\n ],\n };\n}\n\n// ── bakeHairBackTurnWarp ──────────────────────────────────────────────────────\n\n/** Cylinder radius for the back hair's turn bend, as a multiple of the part's\n * own half-width. Much flatter than the face's 1.2: the back hair spans the\n * whole head, and at the face's curvature its near edge would fold in by\n * ~180px. At 2.5 the near side tucks behind the face and the far side fills\n * out, which is the whole cue. */\nconst HAIR_BACK_BEND_RADIUS_FACTOR = 2.5;\n/** How far the back hair's far edge bulges OUT at full turn, as a fraction of\n * the part's half-width. A cylinder bend barely moves the far edge (it just\n * stops compressing), but on a real head the hair volume hidden behind the\n * far side swings into view and the silhouette fills out. Grows linearly\n * from the centre column to the far edge. Judged from renders: at ~0.32 the\n * far strands look pulled thin. */\nconst HAIR_BACK_FAR_BULGE = 0.22;\n\n/**\n * The back hair's share of the head turn, as a per-vertex warp on AngleX.\n *\n * hair_back hangs from the rigid headDeformer, not faceWarp, so without this it\n * turned as a flat sheet: the face foreshortened and slid while the silhouette\n * behind it kept its rest outline. Over the part's own columns, at a flatter\n * radius than the face: the NEAR side takes the same pinned cylinder bend the\n * face uses, so it compresses behind the face; the FAR side takes the chord of\n * its own ±30 keyform, linear in the angle, plus a bulge (HAIR_BACK_FAR_BULGE),\n * so it swings out and fills.\n *\n * Keyed on `HEAD_TURN_STOPS` — see there for why the stops sit 15° apart. The\n * near side needs that density as much as the face does: on the hero the\n * three-stop chord diverged from the analytic bend by 12.5 model units at the\n * near outer column at half turn (chord −47.8 against −35.3), enough that its\n * silhouette disagreed with the face's.\n *\n * The far side is deliberately not analytic. A cylinder's far column reverses\n * once the turn passes |asin(x/radius)|/2 (≈12° at the hero's far edge), so\n * net of the linear bulge that edge moved 52 model units through the first\n * 15° and only 24 through the second — it swung out and then stalled, where a\n * sheet revealed from behind the head should swing out at one speed. Linear in\n * the angle it is 38 and 38. The ±30 keyforms and the rest keyform are\n * unchanged by construction: at |deg| = 30 the two branches coincide, at 0\n * nothing is far, and the centre column is pinned on both.\n */\nexport function bakeHairBackTurnWarp(\n mesh: IkiMesh,\n parameter: string,\n): IkiWarp {\n let left = Infinity;\n let right = -Infinity;\n for (let i = 0; i < mesh.vertices.length; i += 2) {\n left = Math.min(left, mesh.vertices[i]);\n right = Math.max(right, mesh.vertices[i]);\n }\n const halfWidth = (right - left) / 2;\n const radius = halfWidth * HAIR_BACK_BEND_RADIUS_FACTOR;\n const bulge = HAIR_BACK_FAR_BULGE * halfWidth;\n const DEG_TO_RAD = Math.PI / 180;\n const keyforms = HEAD_TURN_STOPS.map((deg) => {\n const theta = deg * DEG_TO_RAD;\n // Turning right (s = +1) the far side is x < 0; the bulge pushes it further\n // left, i.e. outward. It grows LINEARLY with the turn — unlike the near\n // side's analytic bend — so each stop takes its own share of it: at full\n // bulge on every stop the mid stops would step it to full at 15° instead\n // of ramping it. Zero at rest.\n const s = Math.sign(deg);\n const turnFraction = Math.abs(deg) / HEAD_TURN_MAX_DEG;\n const fullTheta = s * HEAD_TURN_MAX_DEG * DEG_TO_RAD;\n const bulgeAtStop = (bulge * deg) / HEAD_TURN_MAX_DEG;\n const offsets: number[] = [];\n for (let i = 0; i < mesh.vertices.length; i += 2) {\n const x = mesh.vertices[i];\n const far = Math.max(0, (-s * x) / halfWidth);\n // The far side is the chord of its own ±30 keyform — see the doc comment.\n const bend =\n far > 0\n ? turnFraction * pinnedCylinderBend(x, radius, fullTheta)\n : pinnedCylinderBend(x, radius, theta);\n offsets.push(bend - bulgeAtStop * far, 0);\n }\n return { value: deg, offsets };\n });\n return { parameter, keyforms };\n}\n\n// ── bakeHairSwayWarp ───────────────────────────────────────────────────────────\n\n/** Tip travel of a swaying hair part at full sway, as a fraction of its own\n * height — \"the ends swing 9% of the hair's length\". Long back hair therefore\n * swings further than the bangs in pixels, as it should. */\nconst HAIR_SWAY_TIP_FRACTION = 0.09;\n/** Exponent on distance-from-root. 1 would be a hinge (straight shear); hair\n * bends, so the swing grows faster toward the ends. */\nconst HAIR_SWAY_CURL = 1.5;\n/** Range of the HairSwayX / HairSwayZ output parameters. */\nconst HAIR_SWAY_RANGE = 20;\n/** How far into that range one spring actually swings, as a fraction: the\n * hair rigs (mass 1, stiffness 80, damping 10 → ζ ≈ 0.56) overshoot a step\n * by ~12% on their ±10 steady state, peaking near 11.2. Used to size the\n * bangs' swing against the face-warp grid. */\nconst HAIR_SWAY_PEAK_FRACTION = 0.56;\n\n/**\n * Root-pinned sideways sway for a hair part, as a per-vertex warp.\n *\n * Parts have no pivot — a `rotate` binding turns a part about its crop centre.\n * Used for sway that swung the bangs' ROOTS off the hairline as much as their\n * ends, which is exactly how hair does not move. This warp instead leaves the\n * top row of the mesh where it is and displaces each row sideways by\n * `tipShift · u^HAIR_SWAY_CURL`, with `u` the row's distance from the top as a\n * fraction of the height: 0 at the roots, 1 at the ends.\n *\n * Keyforms sit at ±range with the rest pose in between, so a zero parameter is\n * zero offsets. Offsets are in the mesh's own pixel frame (+y up, centered),\n * matching `createPixelGridMesh` — pass the SAME mesh the part renders.\n *\n * Also reused for the bangs' AngleX turn lead: same root-pinned shape, driven\n * by the turn instead of a sway spring.\n */\nexport function bakeHairSwayWarp(\n mesh: IkiMesh,\n parameter: string,\n tipShift: number,\n range: number,\n): IkiWarp {\n let top = -Infinity;\n let bottom = Infinity;\n for (let i = 1; i < mesh.vertices.length; i += 2) {\n top = Math.max(top, mesh.vertices[i]);\n bottom = Math.min(bottom, mesh.vertices[i]);\n }\n const height = top - bottom;\n const swing: number[] = [];\n for (let i = 0; i < mesh.vertices.length; i += 2) {\n const u = height > 0 ? (top - mesh.vertices[i + 1]) / height : 0;\n swing.push(tipShift * Math.pow(u, HAIR_SWAY_CURL), 0);\n }\n return {\n parameter,\n keyforms: [\n // `0 - v`, not `-v`: the pinned root row must stay a plain +0.\n { value: -range, offsets: swing.map((v) => 0 - v) },\n { value: range, offsets: swing },\n ],\n };\n}\n\n// ── Rigid head travel + body follow ─────────────────────────────────────────\n\n/** Rigid sideways travel of the head at full turn (px at AngleX = ±30). */\nconst HEAD_TURN_TRAVEL = 50;\n/** The torso's share of that travel. Below ~0.2 the shoulders still read as\n * bolted down; at 1 the neck stops articulating. 0.3 keeps 70 % of the turn\n * in the neck while the shoulders visibly come along. Judged in the\n * playground, not derived. */\nconst BODY_TURN_FOLLOW = 0.3;\n/** Vertical travel of the head at full breath (px, +y up; negative = down). */\nconst HEAD_BREATH_BOB = -12;\n/** The torso's share of the head's breath bob, SAME direction. In phase with\n * the head (not the sample model's counter-phase +6/−12, which reads as a\n * shrug: chest up while the head settles) so the neck compresses by 6 px, not\n * 18. Moving DOWN also keeps the torso's flat canvas-bottom cut off-canvas —\n * a rise would lift that hard edge into view every breath. */\nconst BODY_BREATH_FOLLOW = 0.5;\n\n// ── generateIkiFromLayerSet ───────────────────────────────────────────────────\n\n/**\n * Auto-rig: given decoded layer inputs and the shared canvas size, produce a\n * valid IkiModel ready for parseIkiModel.\n *\n * - Validate all inputs before deriving anything.\n * - Place parts at source-derived positions (bboxToTransform, unshifted).\n * - Emit the standard parameters (same ids/ranges as sample-model.ts), plus a\n * conditional HairSwayX descriptor + hair-sway physics rig when a hair_front\n * layer is present.\n * - Build headDeformer (matrix, neck pivot, AngleX+Breath bindings),\n * bodyDeformer when a body layer is present (matrix, torso-base pivot,\n * AngleX follow + Breath follow), and faceWarp (warp, FACE_GRID_CELLS²,\n * baked cylinder warp center-relative on faceCenterX).\n * - Mesh parts (spec.mesh===true) → width:1, height:1, pixel grid mesh sized by\n * meshCellsFor + role bindings.\n * - Static parts (spec.mesh===false) → width:cropW, height:cropH, no mesh.\n * - Part ids equal the role string (deterministic, no crypto.randomUUID).\n * - Return parseIkiModel(structuredClone(model)) — every caller gets a\n * validated model; bad assembly fails loudly.\n */\nexport function generateIkiFromLayerSet(\n layers: LayerInput[],\n canvas: { width: number; height: number },\n): IkiModel {\n // Validate first — never derive anything from unchecked input.\n validateLayerInputs(layers, canvas);\n\n // Hair-sway secondary motion is gated on a front-hair layer being present.\n const hasHair = layers.some((l) => l.role === \"hair_front\");\n const hasMouthOpen = layers.some((l) => l.role === \"mouth_open\");\n // The feature parallax's gate — see FEATURE_DEPTH.\n const hasNose = layers.some((l) => l.role === \"nose\");\n const bodyLayer = layers.find((l) => l.role === \"body\");\n\n // ── Standard parameters — verbatim from sample-model.ts ──────────────────\n const parameters: IkiParameter[] = [\n {\n id: StandardParameter.MouthOpen,\n name: \"Mouth Open\",\n min: 0,\n max: 1,\n default: 0,\n },\n {\n id: StandardParameter.MouthForm,\n name: \"Mouth Form\",\n min: -1,\n max: 1,\n default: 0,\n },\n {\n id: StandardParameter.EyeOpenLeft,\n name: \"Eye L\",\n min: 0,\n max: 1,\n default: 1,\n },\n {\n id: StandardParameter.EyeOpenRight,\n name: \"Eye R\",\n min: 0,\n max: 1,\n default: 1,\n },\n {\n id: StandardParameter.EyeballX,\n name: \"Gaze X\",\n min: -1,\n max: 1,\n default: 0,\n },\n {\n id: StandardParameter.EyeballY,\n name: \"Gaze Y\",\n min: -1,\n max: 1,\n default: 0,\n },\n {\n id: StandardParameter.AngleX,\n name: \"Head Angle\",\n min: -30,\n max: 30,\n default: 0,\n },\n {\n id: StandardParameter.AngleY,\n name: \"Head Angle Y\",\n min: -30,\n max: 30,\n default: 0,\n },\n {\n id: StandardParameter.AngleZ,\n name: \"Head Angle Z\",\n min: -30,\n max: 30,\n default: 0,\n },\n {\n id: StandardParameter.Breath,\n name: \"Breath\",\n min: 0,\n max: 1,\n default: 0,\n },\n {\n id: StandardParameter.BrowLeftY,\n name: \"Brow L Y\",\n min: -1,\n max: 1,\n default: 0,\n },\n {\n id: StandardParameter.BrowRightY,\n name: \"Brow R Y\",\n min: -1,\n max: 1,\n default: 0,\n },\n {\n id: StandardParameter.BrowLeftAngle,\n name: \"Brow L Angle\",\n min: -1,\n max: 1,\n default: 0,\n },\n {\n id: StandardParameter.BrowRightAngle,\n name: \"Brow R Angle\",\n min: -1,\n max: 1,\n default: 0,\n },\n ];\n\n // Hair-sway output params (physics-driven), declared only when there is front\n // hair to drive — keeps no-hair models free of unused parameters.\n if (hasHair) {\n parameters.push(\n {\n id: StandardParameter.HairSwayX,\n name: \"Hair Sway X\",\n min: -HAIR_SWAY_RANGE,\n max: HAIR_SWAY_RANGE,\n default: 0,\n },\n {\n id: StandardParameter.HairSwayZ,\n name: \"Hair Sway Z\",\n min: -HAIR_SWAY_RANGE,\n max: HAIR_SWAY_RANGE,\n default: 0,\n },\n );\n }\n\n // ── Face layer: derive center and crop for pivot + grid ───────────────────\n const faceLayers = layers.filter((l) => l.role === \"face\");\n // validateLayerInputs guarantees \"face\" is present — safe to assert here.\n const faceLayer = faceLayers[0]!;\n const faceTransform = bboxToTransform(\n faceLayer.bbox,\n faceLayer.canvasW,\n faceLayer.canvasH,\n \"face\",\n );\n // faceCenterX: source-placed face center in model space (unshifted).\n const faceCenterX = faceTransform.x;\n const faceCropH = faceLayer.cropH;\n\n // ── Union bbox of all faceWarp-child layers (model space) ─────────────────\n // All faceWarp-assigned roles have spec.mesh===true (validated by ROLE_TABLE).\n // Each child's model-space extent: transform.{x,y} ± cropW/2, cropH/2\n // (centered pixel mesh convention — part.transform is the crop center).\n const faceWarpLayers = layers.filter(\n (l) => ROLE_TABLE[l.role].deformer === \"faceWarp\",\n );\n\n // Fall back to a full-canvas box only when no faceWarp layers exist (shouldn't\n // happen given required roles, but guards against future role-table changes).\n let unionMinX = -canvas.width / 2;\n let unionMaxX = canvas.width / 2;\n let unionMinY = -canvas.height / 2;\n let unionMaxY = canvas.height / 2;\n\n if (faceWarpLayers.length > 0) {\n const transforms = faceWarpLayers.map((l) =>\n bboxToTransform(l.bbox, l.canvasW, l.canvasH, l.role),\n );\n\n unionMinX = Math.min(\n ...transforms.map((t, i) => t.x - faceWarpLayers[i].cropW / 2),\n );\n unionMaxX = Math.max(\n ...transforms.map((t, i) => t.x + faceWarpLayers[i].cropW / 2),\n );\n unionMinY = Math.min(\n ...transforms.map((t, i) => t.y - faceWarpLayers[i].cropH / 2),\n );\n unionMaxY = Math.max(\n ...transforms.map((t, i) => t.y + faceWarpLayers[i].cropH / 2),\n );\n\n // Expand by 12% margin on each side so no child vertex lands on the grid\n // boundary and gets clamped by bindPointToRestGrid.\n const spanX = unionMaxX - unionMinX;\n const spanY = unionMaxY - unionMinY;\n const MARGIN = 0.12;\n unionMinX -= spanX * MARGIN;\n unionMaxX += spanX * MARGIN;\n unionMinY -= spanY * MARGIN;\n unionMaxY += spanY * MARGIN;\n }\n\n // ── faceWarp grid: symmetric about faceCenterX, spanning the margined union ─\n // Symmetric x so the cylinder axis aligns exactly with the face center.\n // halfW is the larger of the two distances from faceCenterX to the union edges,\n // ensuring the symmetric range [faceCenterX-halfW, faceCenterX+halfW] encloses\n // every child. y-range uses the margined union directly (not symmetric).\n const halfW = Math.max(faceCenterX - unionMinX, unionMaxX - faceCenterX);\n const faceGridMinX = faceCenterX - halfW;\n const faceGridMaxX = faceCenterX + halfW;\n\n const faceGrid = {\n cols: FACE_GRID_CELLS,\n rows: FACE_GRID_CELLS,\n points: generateGridPoints(\n FACE_GRID_CELLS,\n FACE_GRID_CELLS,\n faceGridMinX,\n faceGridMaxX,\n unionMinY,\n unionMaxY,\n ),\n };\n\n // Depth-parallax units for the hair and feature layers, from the same\n // extents the cylinder bake derives its radii from: half-width about the\n // face center for the turn, the larger vertical reach about it for the nod.\n const faceCenterY = faceTransform.y;\n const halfH = Math.max(faceCenterY - unionMinY, unionMaxY - faceCenterY);\n const parallaxUnit = headTurnParallaxUnit(halfW);\n const parallaxUnitY = headNodParallaxUnit(halfH);\n\n // ── headDeformer pivot (neck): slightly below the face bottom ─────────────\n // faceBottom is the model-space y of the bottom edge of the face crop.\n // The neck pivot sits 15% of the face crop height below the face bottom.\n const faceBottom = faceTransform.y - faceCropH / 2;\n const neckPivot = {\n x: faceCenterX,\n y: faceBottom - faceCropH * 0.15, // 15% below face bottom = neck\n };\n\n // ── Bake the center-relative turn × nod cylinder warp ─────────────────────\n const faceWarp2d = bakeHeadTurnGridWarp2DCentered(\n faceGrid,\n StandardParameter.AngleX,\n StandardParameter.AngleY,\n faceCenterX,\n faceCenterY,\n );\n\n // ── Deformers ─────────────────────────────────────────────────────────────\n const deformers = [\n // headDeformer: rigid matrix rotating/translating the whole head about the\n // neck pivot; bindings mirror sample-model.ts exactly.\n {\n id: \"headDeformer\",\n pivot: neckPivot,\n bindings: [\n // The turn is a pure yaw: no roll rides on AngleX. The sample model's\n // ±6° \"lean into the turn\" was tried here and dropped — rotating about\n // the neck pivot swings the crown (~500px above it) far more than the\n // chin, so the top of the head appeared to lunge ahead of the face on\n // every turn. Roll is AngleZ's job, below.\n {\n parameter: StandardParameter.AngleX,\n channel: \"translateX\" as const,\n from: -HEAD_TURN_TRAVEL,\n to: HEAD_TURN_TRAVEL,\n },\n // Nod: a vertical translate only. No rotate — a pitch expressed as a\n // rigid rotation would sum with the AngleZ roll below at diagonal\n // poses, collapsing pitch into roll.\n {\n parameter: StandardParameter.AngleY,\n channel: \"translateY\" as const,\n from: -NOD_TRAVEL,\n to: NOD_TRAVEL,\n },\n // Tilt: the whole head rolls about the neck pivot, one degree per\n // degree. Positive AngleZ is clockwise on screen (engine rotate is\n // CCW-positive, hence the flipped from/to) — Live2D's convention: in\n // its own sample motions AngleZ carries the sign of AngleX 214 times\n // out of 222, i.e. a head that leans into its turn tilts clockwise on\n // a turn to the viewer's right. The only rotate binding on the head.\n {\n parameter: StandardParameter.AngleZ,\n channel: \"rotate\" as const,\n from: 30,\n to: -30,\n },\n {\n parameter: StandardParameter.Breath,\n channel: \"translateY\" as const,\n from: 0,\n to: HEAD_BREATH_BOB,\n },\n ],\n },\n // faceWarp: cylinder-bend warp parented to headDeformer; grid is symmetric\n // about faceCenterX so the bake's cylinder axis aligns with the face center.\n // One 2D warp carries both the turn and the nod (a deformer holds either\n // `warps` or `warp2d`, never both).\n {\n kind: \"warp\" as const,\n id: \"faceWarp\",\n parent: \"headDeformer\",\n grid: faceGrid,\n warp2d: faceWarp2d,\n },\n ];\n\n // bodyDeformer: the torso's own rigid deformer. A sibling of headDeformer, not\n // its parent, so the head's own bindings stay untouched; reparent the head\n // under it once the torso gains a rotation. Translate-only, so the pivot is\n // inert today; it sits at the torso base, where a future lean would rock from.\n if (bodyLayer) {\n const bt = bboxToTransform(\n bodyLayer.bbox,\n bodyLayer.canvasW,\n bodyLayer.canvasH,\n \"body\",\n );\n deformers.push({\n id: \"bodyDeformer\",\n pivot: { x: bt.x, y: bt.y - bodyLayer.cropH / 2 },\n bindings: [\n // Low-weight follow of the turn, same direction as the head.\n {\n parameter: StandardParameter.AngleX,\n channel: \"translateX\" as const,\n from: -BODY_TURN_FOLLOW * HEAD_TURN_TRAVEL,\n to: BODY_TURN_FOLLOW * HEAD_TURN_TRAVEL,\n },\n // Breath: follow the head's bob, same direction, half amplitude.\n {\n parameter: StandardParameter.Breath,\n channel: \"translateY\" as const,\n from: 0,\n to: BODY_BREATH_FOLLOW * HEAD_BREATH_BOB,\n },\n ],\n });\n }\n\n // ── Shared closed-eye crease per side ─────────────────────────────────────\n // The white AND the lash fold to the SAME seam (derived from the eye-white\n // center), so the lash lands on top of the cut eyeball and covers it.\n const eyeCreaseBySide: Partial<Record<\"L\" | \"R\", number>> = {};\n for (const layer of layers) {\n const side = ROLE_TABLE[layer.role].eyeSide;\n if ((layer.role === \"eye_L\" || layer.role === \"eye_R\") && side) {\n const ey = bboxToTransform(\n layer.bbox,\n layer.canvasW,\n layer.canvasH,\n layer.role,\n ).y;\n eyeCreaseBySide[side] = ey - EYELID_FOLD_CREASE * layer.cropH;\n }\n }\n\n // ── Parts ─────────────────────────────────────────────────────────────────\n const parts: IkiPart[] = layers.map((layer) => {\n const { role, bbox, cropW, cropH, canvasW, canvasH } = layer;\n const spec = ROLE_TABLE[role];\n const t = bboxToTransform(bbox, canvasW, canvasH, role);\n const roleBindings = bindingsForRole(spec, role, cropW, cropH, {\n hasMouthOpen,\n parallaxUnit,\n parallaxUnitY,\n hasNose,\n });\n // `IkiPart.deformer` is optional, so a \"none\" role states its detachment by\n // leaving the field off rather than naming a deformer that must exist.\n const deformerId = spec.deformer === \"none\" ? undefined : spec.deformer;\n\n if (spec.mesh) {\n // Warp-deformer child: width:1, height:1 with a pixel grid mesh centered\n // at the crop center. The engine applies the part transform to position it.\n const { cols, rows } = meshCellsFor(cropW, cropH);\n const mesh = createPixelGridMesh(cols, rows, cropW, cropH);\n const part: IkiPart = {\n id: role,\n color: [1, 1, 1, 1] as [number, number, number, number],\n width: 1,\n height: 1,\n order: spec.order,\n transform: t,\n deformer: deformerId,\n mesh,\n };\n if (roleBindings.length > 0) {\n part.bindings = roleBindings;\n }\n // Hair sway and turn lead: the PhysicsMotion springs lag AngleX / AngleZ\n // onto HairSwayX / HairSwayZ (rigs and params exist only with front\n // hair), and both hair parts swing their ends on them with the roots\n // pinned. The back hair is longer, so the same fraction of its height is\n // a bigger swing.\n if ((role === \"hair_front\" || role === \"hair_back\") && hasHair) {\n let tipShift = HAIR_SWAY_TIP_FRACTION * cropH;\n let leadWarp: IkiWarp | undefined;\n if (role === \"hair_front\") {\n // hair_front is a faceWarp child: its vertices are swayed and\n // parallax-shifted BEFORE they bind to the rest grid, and past the\n // grid's edge they clamp to the edge column — the tips flatten into\n // a vertical line. Cap the swing so one spring at its peak stays\n // inside. Both springs peaking together (a hard turn and tilt at\n // once) may still clamp the outermost tips a little; widening the\n // grid instead would flatten the face's own bend, whose radius\n // scales with the grid.\n const shift = HAIR_FRONT_DEPTH * parallaxUnit;\n const headroom =\n Math.min(\n faceGridMaxX - (t.x + cropW / 2),\n t.x - cropW / 2 - faceGridMinX,\n ) - shift;\n // The lead alone can never exhaust the headroom (tips get\n // 0.06·halfW against a ≥0.12·span margin); Math.max(0, headroom)\n // only guards the sway, which can still turn it negative.\n tipShift = Math.min(\n tipShift,\n Math.max(0, headroom) / HAIR_SWAY_PEAK_FRACTION,\n );\n // The bangs' turn lead: root-pinned like the sway, keyed to\n // HEAD_TURN_MAX_DEG instead of a sway spring's range — see\n // bindingsForRole's doc for why this is a warp, not a binding.\n // Warp order in the array doesn't matter; they sum.\n leadWarp = bakeHairSwayWarp(\n mesh,\n StandardParameter.AngleX,\n shift,\n HEAD_TURN_MAX_DEG,\n );\n }\n part.warps = [\n bakeHairSwayWarp(\n mesh,\n StandardParameter.HairSwayX,\n tipShift,\n HAIR_SWAY_RANGE,\n ),\n bakeHairSwayWarp(\n mesh,\n StandardParameter.HairSwayZ,\n tipShift,\n HAIR_SWAY_RANGE,\n ),\n ...(leadWarp ? [leadWarp] : []),\n ];\n }\n // The back hair's turn: it gets no cylinder bend from a deformer, so it\n // bends on its own. Part warps sum, so this sits beside the sway.\n if (role === \"hair_back\") {\n part.warps = [\n ...(part.warps ?? []),\n bakeHairBackTurnWarp(mesh, StandardParameter.AngleX),\n ];\n }\n // Eye blink = fold: the white (eye_) and the lash (lash_) fold shut via a\n // warp toward the shared crease; iris/pupil/highlight clip to the white, so\n // the closing white CUTS them away (round, not squashed) and the lash lands\n // on top to cover the seam. The white is a required role (clip mask exists).\n if (spec.eyeSide !== undefined) {\n const isLash = role.startsWith(\"lash_\");\n if (role.startsWith(\"eye_\") || isLash) {\n const openParam =\n spec.eyeSide === \"L\"\n ? StandardParameter.EyeOpenLeft\n : StandardParameter.EyeOpenRight;\n const creaseWorldY =\n eyeCreaseBySide[spec.eyeSide] ?? t.y - EYELID_FOLD_CREASE * cropH;\n // Separate lashes supply the closed-eye line. Collapse their sclera\n // clip completely so a strip of iris cannot show underneath.\n const hasLash = layers.some((l) => l.role === `lash_${spec.eyeSide}`);\n part.warps = [\n bakeEyelidFoldWarp(\n mesh,\n openParam,\n creaseWorldY - t.y,\n isLash ? LASH_FOLD_K : hasLash ? 0 : EYELID_FOLD_K,\n ),\n ];\n } else {\n part.clip = { masks: [`eye_${spec.eyeSide}`] };\n }\n }\n return part;\n } else {\n // Static quad: no mesh, sized to the crop. It still takes bindings: no\n // static role carries any today, but this branch once dropped them\n // silently, and the next static role with a binding must not hit that.\n const part: IkiPart = {\n id: role,\n color: [1, 1, 1, 1] as [number, number, number, number],\n width: cropW,\n height: cropH,\n order: spec.order,\n transform: t,\n deformer: deformerId,\n };\n if (roleBindings.length > 0) {\n part.bindings = roleBindings;\n }\n return part;\n }\n });\n\n const model = {\n version: IKI_FORMAT_VERSION,\n name: \"Auto-Rigged Model\",\n canvas: { width: canvas.width, height: canvas.height },\n textures: [],\n parameters,\n deformers,\n parts,\n // Secondary motion: one spring lags AngleX onto HairSwayX so front hair\n // sways behind the head turn (same constants as the hand-authored sample),\n // and a second lags AngleZ onto HairSwayZ so it swings behind a tilt. Two\n // rigs because a rig has one input and one output (validator-enforced).\n // Both hair parts read the outputs through root-pinned sway warps.\n // Omitted when there is no front hair to drive.\n physics: hasHair\n ? [\n {\n id: \"hairSway\",\n input: { parameter: StandardParameter.AngleX, weight: 1 },\n output: { parameter: StandardParameter.HairSwayX, scale: -10 },\n mass: 1,\n stiffness: 80,\n damping: 10,\n },\n {\n id: \"hairTilt\",\n input: { parameter: StandardParameter.AngleZ, weight: 1 },\n output: { parameter: StandardParameter.HairSwayZ, scale: -10 },\n mass: 1,\n stiffness: 80,\n damping: 10,\n },\n ]\n : undefined,\n };\n\n // Gate: run through parseIkiModel so bad assembly fails loudly at the source.\n // structuredClone prevents the validator's normalizing output from aliasing\n // the local object, and ensures the returned model is fully independent.\n return parseIkiModel(structuredClone(model));\n}\n"],"mappings":"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","names":["parseIkiModel","parseIkiModel","y","x","IKI_FORMAT_VERSION","parseIkiModel","IKI_FORMAT_VERSION","parseIkiModel"]}
1
+ {"version":3,"sources":["../src/document.ts","../src/mesh-uv.ts","../src/commands.ts","../src/grid-keyform.ts","../src/reparent.ts","../src/alpha-bbox.ts","../src/binding-capture.ts","../src/atlas.ts","../src/factories.ts","../src/auto-rig.ts"],"sourcesContent":["import { parseIkiModel } from \"@ikijs/format\";\nimport type {\n IkiBinding,\n IkiDeformer,\n IkiDeformerBinding,\n IkiDeformerTransform,\n IkiMatrixDeformer,\n IkiModel,\n IkiPart,\n IkiPhysics,\n IkiTexture,\n IkiTransform,\n IkiUvRect,\n IkiWarpDeformer,\n} from \"@ikijs/format\";\n\nimport type { EditCommand } from \"./commands\";\nimport { remapMeshUvsToRect } from \"./mesh-uv\";\n\n/**\n * One part mapped to an imported atlas source. `index` is always 0 because the\n * atlas is a single page; only the `uv` sub-rectangle varies per part.\n */\nexport interface AtlasAssignment {\n partId: string;\n uv: IkiUvRect;\n}\n\n/** Input to {@link EditorDocument.applyAtlas}: the new atlas table plus the\n * per-part UV assignments into it. */\nexport interface ApplyAtlasInput {\n textures: IkiTexture[];\n partTextureAssignments: AtlasAssignment[];\n}\n\n/**\n * In-memory editing session over a single {@link IkiModel}. The model is held\n * directly (no superset) and mutated in place by invertible {@link EditCommand}s\n * pushed through {@link execute}; undo/redo invert/re-apply them.\n *\n * The constructor `structuredClone`s the input so the caller's model is never\n * mutated. Parts are addressed by stable `id`, never by array index.\n */\nexport class EditorDocument {\n private readonly model: IkiModel;\n private readonly undoStack: EditCommand[] = [];\n private readonly redoStack: EditCommand[] = [];\n /** Editor-only session state, never serialized; keyed by stable part id. The\n * unmodified BASE local uvs of every mesh part, captured once at construction\n * so atlas remaps always derive from the original (idempotent). */\n private readonly baseMeshUvs = new Map<string, number[]>();\n\n constructor(model: IkiModel) {\n this.model = structuredClone(model);\n // Capture-once base UVs. Scope: assumes the loaded model's mesh parts carry\n // original LOCAL 0..1 uvs (true for the sample). Restoring base UVs after\n // reloading an already-textured exported model is out of scope (deferred —\n // needs project-file persistence).\n for (const part of this.model.parts) {\n if (part.mesh) {\n this.baseMeshUvs.set(part.id, part.mesh.uvs.slice());\n }\n }\n }\n\n /** Live reference to the working model — for READ access. Mutate it only\n * through {@link execute}/{@link undo}/{@link redo}. */\n getModel(): IkiModel {\n return this.model;\n }\n\n /** Resolve a part by stable id. Throws a plain `Error` (NOT an\n * `IkiFormatError`) with a path-qualified message if the id is unknown. */\n findPart(id: string): IkiPart {\n const part = this.model.parts.find((p) => p.id === id);\n if (!part) {\n throw new Error(`parts: no part with id \"${id}\"`);\n }\n return part;\n }\n\n /** Resolve a warp deformer by stable id. Throws a path-qualified plain\n * `Error` if no deformer matches the id or the match is not a warp deformer.\n * READ/mutate-through accessor, consistent with {@link findPart}. */\n findWarpDeformer(id: string): IkiWarpDeformer {\n const deformer = this.model.deformers?.find((d) => d.id === id);\n if (!deformer || deformer.kind !== \"warp\") {\n throw new Error(`deformers: no warp deformer with id \"${id}\"`);\n }\n return deformer;\n }\n\n /** Resolve a matrix deformer by stable id. Throws a path-qualified plain\n * `Error` if no deformer matches the id or the match is a warp deformer\n * (`kind === \"warp\"`). A `kind` of `\"matrix\"` or `undefined` is a matrix\n * deformer. READ/mutate-through accessor, consistent with {@link findPart}. */\n findMatrixDeformer(id: string): IkiMatrixDeformer {\n const deformer = this.model.deformers?.find((d) => d.id === id);\n if (!deformer || deformer.kind === \"warp\") {\n throw new Error(`deformers: no matrix deformer with id \"${id}\"`);\n }\n return deformer;\n }\n\n /** Resolve any deformer (matrix or warp) by stable id. Throws a\n * path-qualified plain `Error` if no deformer matches the id.\n * READ/mutate-through accessor, consistent with {@link findPart}. */\n findDeformer(id: string): IkiDeformer {\n const deformer = this.model.deformers?.find((d) => d.id === id);\n if (!deformer) {\n throw new Error(`deformers: no deformer with id \"${id}\"`);\n }\n return deformer;\n }\n\n /** Resolve a physics rig by stable id. Throws a path-qualified plain `Error`\n * if no rig matches the id. The `physics` array is optional, so guard it.\n * READ/mutate-through accessor, consistent with {@link findDeformer}. */\n findPhysicsRig(id: string): IkiPhysics {\n const rig = this.model.physics?.find((r) => r.id === id);\n if (!rig) {\n throw new Error(`physics: no physics rig with id \"${id}\"`);\n }\n return rig;\n }\n\n /**\n * Record the base mesh UVs for a part inserted AFTER construction (e.g. by\n * {@link AddPart}). Returns the PRIOR entry for that id (or `undefined` if\n * none existed) so the caller can restore it on undo. No-op for meshless\n * parts (returns `undefined`). Must be called by any command that pushes a\n * mesh part into the model so the part joins the constructor-captured base-UV\n * side state required by {@link applyAtlas}.\n *\n * The returned prior value is what {@link restoreBaseMeshUvs} expects on\n * undo — pass it verbatim. Hazard guarded: DeletePart(X) → AddPart(X′) →\n * undo(add) → undo(delete); without restore, X's constructor-captured entry\n * would be permanently gone after undo of the add, causing applyAtlas to\n * fail when X is restored.\n */\n captureBaseMeshUvs(partId: string): number[] | undefined {\n const prev = this.baseMeshUvs.get(partId);\n const part = this.model.parts.find((p) => p.id === partId);\n if (part?.mesh) {\n this.baseMeshUvs.set(partId, part.mesh.uvs.slice());\n }\n return prev;\n }\n\n /**\n * Restore the base-UV side state to exactly what it was before a\n * {@link captureBaseMeshUvs} call. Called from {@link AddPart.invert}:\n * pass the value returned by captureBaseMeshUvs on first apply.\n * - `prev` is `number[]` → sets the entry (restores a prior mesh's base).\n * - `prev` is `undefined` → deletes the entry (no entry existed before the\n * add, so a later different-mesh part reusing the id must not inherit this\n * one's base).\n * DeletePart does NOT call this — the entry persists across delete/undo so\n * the restored part still has its base available for applyAtlas.\n */\n restoreBaseMeshUvs(partId: string, prev: number[] | undefined): void {\n if (prev !== undefined) {\n this.baseMeshUvs.set(partId, prev);\n } else {\n this.baseMeshUvs.delete(partId);\n }\n }\n\n /** The construction-captured base UVs for a mesh part. Throws a path-qualified\n * plain `Error` if absent. SINGLE accessor for both apply branches — never\n * read `baseMeshUvs` with a bare `!` elsewhere. */\n private requireBaseUvs(partId: string): number[] {\n const base = this.baseMeshUvs.get(partId);\n if (!base) {\n throw new Error(`parts: no base mesh uvs captured for part \"${partId}\"`);\n }\n return base;\n }\n\n /**\n * Replace the atlas table and rewrite every part's texture reference in a\n * single atomic step. For every part in `partTextureAssignments` set\n * `texture = { index: 0, uv }`; CLEAR `texture` (delete the key) on every\n * other part.\n *\n * Mesh parts are textured as a MATCHED PAIR: an assigned mesh part also has\n * its per-vertex `mesh.uvs` remapped (from the construction-captured base)\n * into the same `uv` rect; an unassigned mesh part has its `mesh.uvs` restored\n * to that base. Quad parts carry `texture.uv` only and are untouched here.\n *\n * Deliberately NON-undoable: it does NOT push to or clear the undo/redo\n * stacks (texture/atlas state is not undoable in 5b — the unified\n * editor-state superset is deferred to 5d). `canUndo()`/`canRedo()` are\n * unchanged after a call.\n *\n * Validate-all-then-apply: structural input validation, per-partId\n * resolution, and a base-UV preflight over every mesh part all run BEFORE any\n * mutation, so a bad input (wrong shape, duplicate partId, unknown partId, or\n * a mesh part with no captured base) throws a plain `Error` and leaves the\n * model exactly as it was — never a partial application.\n */\n applyAtlas(input: ApplyAtlasInput): void {\n // Step A — structural validation of the input shape.\n const { texture, assignmentsByPart } = this.normalizeAtlasInput(input);\n\n // Step B — resolve every partId before mutating anything.\n const resolved = new Map<IkiPart, IkiUvRect>();\n for (const [partId, uv] of assignmentsByPart) {\n resolved.set(this.findPart(partId), uv);\n }\n\n // Step C — preflight base UVs for EVERY mesh part (assigned AND unassigned):\n // the mutate loop's clear/restore branch also reads the base of unassigned\n // mesh parts, so this guard must cover them before any write so atomicity\n // (validate-all-then-apply) holds.\n for (const part of this.model.parts) {\n if (part.mesh) {\n this.requireBaseUvs(part.id);\n }\n }\n\n // Mutate — only reached when A + B + C all pass.\n this.model.textures =\n texture === undefined ? undefined : [{ source: texture.source }];\n for (const part of this.model.parts) {\n const uv = resolved.get(part);\n if (uv) {\n part.texture = {\n index: 0,\n uv: { x: uv.x, y: uv.y, width: uv.width, height: uv.height },\n };\n if (part.mesh) {\n // Replace the mesh object so each part owns its own uvs array —\n // de-aliases parts that shared the same mesh object in the input model.\n part.mesh = {\n ...part.mesh,\n uvs: remapMeshUvsToRect(this.requireBaseUvs(part.id), uv),\n };\n }\n } else {\n delete part.texture;\n if (part.mesh) {\n // Same spread here to break aliasing on restore as well.\n part.mesh = {\n ...part.mesh,\n uvs: this.requireBaseUvs(part.id).slice(),\n };\n }\n }\n }\n }\n\n /**\n * Clear a model-committed texture reference from a single part. Deliberately\n * NON-undoable, matching {@link applyAtlas} — texture/atlas state is not in\n * the undo model (unified editor-state is deferred). Does NOT push to or\n * clear the undo/redo stacks. Does NOT touch `mesh.uvs` — atlas-space UVs on\n * an untextured mesh are inert for color rendering; a later atlas import\n * remaps from the base UVs anyway.\n *\n * This is the single consistent texture/atlas undo boundary: by the time a\n * part is deletable (no texture), its DeletePart snapshot carries no texture\n * reference, so no stale index can resurface on undo after a later atlas\n * repack.\n *\n * Throws a path-qualified plain `Error` (NOT `IkiFormatError`) if the part\n * id is unknown — same contract as {@link findPart}.\n */\n clearPartTextureRef(partId: string): void {\n const part = this.findPart(partId);\n delete part.texture;\n }\n\n /**\n * Validate the structural shape of an {@link ApplyAtlasInput} without\n * touching the model, returning a known-good `{ texture?, assignmentsByPart }`\n * for {@link applyAtlas} to resolve and apply.\n */\n private normalizeAtlasInput(input: ApplyAtlasInput): {\n texture?: IkiTexture;\n assignmentsByPart: Map<string, IkiUvRect>;\n } {\n if (input.textures.length > 1) {\n throw new Error(\n `applyAtlas: textures must be a single atlas page (got ${input.textures.length})`,\n );\n }\n if (\n input.partTextureAssignments.length > 0 &&\n input.textures.length !== 1\n ) {\n throw new Error(\n \"applyAtlas: partTextureAssignments require exactly one texture\",\n );\n }\n\n const assignmentsByPart = new Map<string, IkiUvRect>();\n for (const { partId, uv } of input.partTextureAssignments) {\n if (assignmentsByPart.has(partId)) {\n throw new Error(\n `applyAtlas: duplicate partId \"${partId}\" in partTextureAssignments`,\n );\n }\n assignmentsByPart.set(partId, uv);\n }\n\n return { texture: input.textures[0], assignmentsByPart };\n }\n\n /**\n * Overwrite a part's whole transform with a fresh copy of `transform`.\n * Replacing the whole object (rather than individual channels) preserves any\n * optional keys already absent from the incoming value.\n *\n * Deliberately NON-undoable — used ONLY for an editor app's transient capture\n * pose. Does NOT push to or clear undoStack/redoStack (sibling to\n * {@link applyAtlas}'s non-undoable boundary). The caller is responsible for\n * restoring the exact prior snapshot when the capture pose ends.\n *\n * `IkiTransform` is a flat number map, so a shallow spread is a sufficient\n * deep copy — no aliasing of the caller's object remains.\n */\n setPartTransformEphemeral(partId: string, transform: IkiTransform): void {\n const part = this.findPart(partId);\n part.transform = { ...transform };\n }\n\n /**\n * Overwrite a matrix deformer's optional transform with a fresh copy, or\n * delete it when `transform` is `undefined`.\n * `undefined` deletes the key, restoring the deformer to the same state as\n * one that never had a transform (absent-vs-present matters for downstream\n * renderers).\n *\n * Deliberately NON-undoable — used ONLY for an editor app's transient capture\n * pose. Does NOT push to or clear undoStack/redoStack (sibling to\n * {@link applyAtlas}'s non-undoable boundary). The caller is responsible for\n * restoring the exact prior snapshot when the capture pose ends.\n *\n * `IkiDeformerTransform` is a flat number map, so a shallow spread is a\n * sufficient deep copy — no aliasing of the caller's object remains.\n */\n setDeformerTransformEphemeral(\n deformerId: string,\n transform: IkiDeformerTransform | undefined,\n ): void {\n const deformer = this.findMatrixDeformer(deformerId);\n if (transform === undefined) {\n delete deformer.transform;\n } else {\n deformer.transform = { ...transform };\n }\n }\n\n /**\n * Overwrite a part's whole bindings array with a fresh deep copy, or delete\n * the key when `bindings` is empty.\n *\n * Deliberately NON-undoable — used ONLY for an editor app's transient capture\n * neutralization (zeroing the row being recaptured so the preview reflects\n * base-only during posing). Does NOT push to or clear undoStack/redoStack.\n * The caller is responsible for restoring the original bindings when the\n * capture session ends.\n */\n setPartBindingsEphemeral(partId: string, bindings: IkiBinding[]): void {\n const part = this.findPart(partId);\n if (bindings.length > 0) {\n part.bindings = bindings.map((b) => ({ ...b }));\n } else {\n delete part.bindings;\n }\n }\n\n /**\n * Overwrite a matrix deformer's whole bindings array with a fresh deep copy,\n * or delete the key when `bindings` is empty.\n *\n * Deliberately NON-undoable — used ONLY for an editor app's transient capture\n * neutralization (zeroing the row being recaptured so the preview reflects\n * base-only during posing). Does NOT push to or clear undoStack/redoStack.\n * The caller is responsible for restoring the original bindings when the\n * capture session ends.\n */\n setDeformerBindingsEphemeral(\n deformerId: string,\n bindings: IkiDeformerBinding[],\n ): void {\n const deformer = this.findMatrixDeformer(deformerId);\n if (bindings.length > 0) {\n deformer.bindings = bindings.map((b) => ({ ...b }));\n } else {\n delete deformer.bindings;\n }\n }\n\n /** Apply a command and record it as one undo step. Clears the redo stack. */\n execute(cmd: EditCommand): void {\n cmd.apply(this);\n this.undoStack.push(cmd);\n this.redoStack.length = 0;\n }\n\n /** Invert the most recent command and move it onto the redo stack. */\n undo(): void {\n const cmd = this.undoStack.pop();\n if (!cmd) return;\n cmd.invert(this);\n this.redoStack.push(cmd);\n }\n\n /** Re-apply the most recently undone command and move it back onto undo. */\n redo(): void {\n const cmd = this.redoStack.pop();\n if (!cmd) return;\n cmd.apply(this);\n this.undoStack.push(cmd);\n }\n\n canUndo(): boolean {\n return this.undoStack.length > 0;\n }\n\n canRedo(): boolean {\n return this.redoStack.length > 0;\n }\n\n /**\n * Validate and export the current working model by running it through\n * {@link parseIkiModel}. Uses `structuredClone` so the validator's\n * normalized output cannot alias the working model. Propagates\n * `IkiFormatError` unchanged on failure — callers surface `.message`.\n */\n toIkiModel(): IkiModel {\n return parseIkiModel(structuredClone(this.model));\n }\n\n /**\n * Pretty-print the validated model as a `.iki` JSON string. Always\n * validates first — invalid documents never reach a file.\n */\n serialize(): string {\n return JSON.stringify(this.toIkiModel(), null, 2);\n }\n}\n","import type { IkiUvRect } from \"@ikijs/format\";\n\n/**\n * Affinely map a mesh's BASE local UVs into an atlas sub-rectangle.\n *\n * Per the `@ikijs/format` UV convention (top-left origin, +y down, 0..1; see\n * {@link IkiUvRect}/{@link IkiMesh}), the input is the part's BASE local uvs and\n * the output places each component into `rect` with NO flip — base UV space and\n * `rect` share the same orientation:\n * out[2i] = rect.x + baseUvs[2i] * rect.width\n * out[2i+1] = rect.y + baseUvs[2i+1] * rect.height\n *\n * The caller guarantees `rect` is inset/clamped (via `uvRectFor`) so every\n * output component stays within 0..1 and passes the format validator.\n *\n * Returns a fresh array; `baseUvs` is never mutated.\n */\nexport function remapMeshUvsToRect(\n baseUvs: number[],\n rect: IkiUvRect,\n): number[] {\n if (baseUvs.length % 2 !== 0) {\n throw new Error(\n `remapMeshUvsToRect: baseUvs must have an even length (u,v pairs), got ${baseUvs.length}`,\n );\n }\n\n const out = new Array<number>(baseUvs.length);\n for (let i = 0; i < baseUvs.length; i += 2) {\n out[i] = rect.x + baseUvs[i] * rect.width;\n out[i + 1] = rect.y + baseUvs[i + 1] * rect.height;\n }\n return out;\n}\n","import type {\n IkiBinding,\n IkiDeformer,\n IkiDeformerBinding,\n IkiDeformerTransform,\n IkiGridKeyform,\n IkiMatrixDeformer,\n IkiMesh,\n IkiModel,\n IkiPart,\n IkiPhysics,\n} from \"@ikijs/format\";\nimport {\n IKI_FORMAT_VERSION,\n IkiFormatError,\n parseIkiModel,\n} from \"@ikijs/format\";\n\nimport type { EditorDocument } from \"./document\";\nimport { upsertGridKeyform } from \"./grid-keyform\";\nimport { remapMeshUvsToRect } from \"./mesh-uv\";\nimport {\n validateDeformerDelete,\n validateDeformerReparent,\n validatePartAttach,\n} from \"./reparent\";\n\n/**\n * Scan the id-flat namespace (parts first, then deformers) and return which\n * array already holds `id`, or `undefined` if the id is free. Parts and\n * deformers share a single flat id namespace, so both arrays must be checked\n * to produce a source-qualified collision message.\n */\nfunction findIdCollision(\n model: IkiModel,\n id: string,\n): \"part\" | \"deformer\" | undefined {\n for (const p of model.parts) {\n if (p.id === id) return \"part\";\n }\n for (const d of model.deformers ?? []) {\n if (d.id === id) return \"deformer\";\n }\n return undefined;\n}\n\n/**\n * One invertible edit. The document is passed IN at apply/invert time — a\n * command is constructed from `(partId, value)` alone, before any document\n * exists — so the same command object can be applied, inverted, and re-applied\n * by the undo/redo stack.\n *\n * Prior-value capture happens exactly ONCE, on the first {@link apply}. `redo`\n * (a second `apply`) reuses that captured value rather than re-reading the\n * current field, so undo always restores the original target.\n */\nexport interface EditCommand {\n apply(doc: EditorDocument): void;\n invert(doc: EditorDocument): void;\n readonly label: string;\n}\n\n/** Channels of {@link IkiTransform} this editor can edit (object-field names,\n * NOT the binding `IkiTransformChannel` vocabulary). */\nexport type EditTransformChannel =\n | \"x\"\n | \"y\"\n | \"rotation\"\n | \"scaleX\"\n | \"scaleY\"\n | \"opacity\";\n\n/**\n * Generic single-field command: reads/writes one field of the resolved part via\n * a getter/setter closure, capturing the prior value on the first `apply` and\n * restoring it on `invert`. `T` is the captured value's type; for cloned values\n * (the color tuple) the getter/setter perform the copy.\n */\nclass FieldCommand<T> implements EditCommand {\n readonly label: string;\n private captured = false;\n private prevValue!: T;\n\n constructor(\n private readonly partId: string,\n private readonly newValue: T,\n label: string,\n private readonly get: (part: IkiPart) => T,\n private readonly set: (part: IkiPart, value: T) => void,\n ) {\n this.label = label;\n }\n\n apply(doc: EditorDocument): void {\n const part = doc.findPart(this.partId);\n if (!this.captured) {\n this.prevValue = this.get(part);\n this.captured = true;\n }\n this.set(part, this.newValue);\n }\n\n invert(doc: EditorDocument): void {\n const part = doc.findPart(this.partId);\n this.set(part, this.prevValue);\n }\n}\n\n/** Edit a part's RGBA fill. The 4-tuple is mutable, so the command clones on\n * construction (caller's array), on capture (part's current color), and on\n * assign (writing to the part) — it never retains the caller's or model's\n * array by reference. */\nexport class SetPartColor extends FieldCommand<\n [number, number, number, number]\n> {\n constructor(partId: string, rgba: [number, number, number, number]) {\n super(\n partId,\n [...rgba] as [number, number, number, number],\n \"Set color\",\n (part) => [...part.color],\n (part, value) => {\n part.color = [...value] as [number, number, number, number];\n },\n );\n }\n}\n\n/** Edit a part's width (model-space units). */\nexport class SetPartWidth extends FieldCommand<number> {\n constructor(partId: string, value: number) {\n super(\n partId,\n value,\n \"Set width\",\n (part) => part.width,\n (part, v) => {\n part.width = v;\n },\n );\n }\n}\n\n/** Edit a part's height (model-space units). */\nexport class SetPartHeight extends FieldCommand<number> {\n constructor(partId: string, value: number) {\n super(\n partId,\n value,\n \"Set height\",\n (part) => part.height,\n (part, v) => {\n part.height = v;\n },\n );\n }\n}\n\n/** Edit a part's paint order. */\nexport class SetPartOrder extends FieldCommand<number> {\n constructor(partId: string, value: number) {\n super(\n partId,\n value,\n \"Set order\",\n (part) => part.order,\n (part, v) => {\n part.order = v;\n },\n );\n }\n}\n\n/**\n * Edit one channel of a part's base transform. `x`/`y` are required; the rest\n * are optional and may be absent on the part. For an optional channel the\n * command captures the raw current value INCLUDING `undefined`, and restoring\n * `undefined` DELETES the key so undo returns the part to its original\n * (possibly-omitted) shape. Engine defaults (rotation 0 / scale 1 / opacity 1)\n * are NOT substituted here.\n */\nexport class SetPartTransform extends FieldCommand<number | undefined> {\n constructor(partId: string, channel: EditTransformChannel, value: number) {\n super(\n partId,\n value,\n \"Set transform\",\n (part) => part.transform[channel],\n (part, v) => {\n if (v === undefined) {\n delete part.transform[channel];\n } else {\n part.transform[channel] = v;\n }\n },\n );\n }\n}\n\n/**\n * Capture the warp deformer's grid as one keyform at the driving parameter\n * `value`, upserting `offsets` into `warps[0].keyforms`. The 4-tuple-style\n * mutable `offsets` array is cloned on construction so a later caller mutation\n * cannot corrupt apply/redo (mirrors {@link SetPartColor}).\n *\n * `apply` validates BEFORE mutating: the deformer must have a grid warp, the\n * offsets length must equal `grid.points.length`, and `value` must lie within\n * the driving parameter's declared `[min,max]` (fail fast, before\n * `parseIkiModel` would reject it). Prior keyforms are deep-copied once on the\n * first `apply` (capture-once like {@link FieldCommand}); `invert` restores\n * that deep copy so re-apply after undo never aliases.\n */\nexport class CaptureGridKeyform implements EditCommand {\n readonly label = \"Capture grid keyform\";\n private readonly offsets: number[];\n private captured = false;\n private prevKeyforms!: IkiGridKeyform[];\n\n constructor(\n private readonly deformerId: string,\n private readonly value: number,\n offsets: number[],\n ) {\n this.offsets = [...offsets];\n }\n\n apply(doc: EditorDocument): void {\n const deformer = doc.findWarpDeformer(this.deformerId);\n const warp = deformer.warps?.[0];\n if (!warp) {\n throw new Error(\n `deformers.\"${this.deformerId}\".warps: no grid warp to capture into`,\n );\n }\n if (this.offsets.length !== deformer.grid.points.length) {\n throw new Error(\n `deformers.\"${this.deformerId}\".warps[0].keyforms.offsets length ${this.offsets.length} must equal grid.points length ${deformer.grid.points.length}`,\n );\n }\n const param = doc\n .getModel()\n .parameters.find((p) => p.id === warp.parameter);\n if (!param) {\n throw new Error(\n `deformers.\"${this.deformerId}\".warps[0].parameter \"${warp.parameter}\" is not a declared parameter`,\n );\n }\n if (this.value < param.min || this.value > param.max) {\n throw new Error(\n `deformers.\"${this.deformerId}\".warps[0].keyforms.value ${this.value} is outside parameter \"${warp.parameter}\" range [${param.min},${param.max}]`,\n );\n }\n\n if (!this.captured) {\n this.prevKeyforms = structuredClone(warp.keyforms);\n this.captured = true;\n }\n warp.keyforms = upsertGridKeyform(warp.keyforms, this.value, [\n ...this.offsets,\n ]);\n }\n\n invert(doc: EditorDocument): void {\n const warp = doc.findWarpDeformer(this.deformerId).warps?.[0];\n if (!warp) {\n throw new Error(\n `deformers.\"${this.deformerId}\".warps: no grid warp to restore into`,\n );\n }\n warp.keyforms = structuredClone(this.prevKeyforms);\n }\n}\n\n/**\n * Generic single-field command targeting a matrix deformer, mirroring\n * {@link FieldCommand} but resolving via `doc.findMatrixDeformer` instead of\n * `doc.findPart`. Capture-once on first `apply`; restore on `invert`.\n */\nclass DeformerFieldCommand<T> implements EditCommand {\n readonly label: string;\n private captured = false;\n private prevValue!: T;\n\n constructor(\n private readonly deformerId: string,\n private readonly newValue: T,\n label: string,\n private readonly get: (deformer: IkiMatrixDeformer) => T,\n private readonly set: (deformer: IkiMatrixDeformer, value: T) => void,\n ) {\n this.label = label;\n }\n\n apply(doc: EditorDocument): void {\n const deformer = doc.findMatrixDeformer(this.deformerId);\n if (!this.captured) {\n this.prevValue = this.get(deformer);\n this.captured = true;\n }\n this.set(deformer, this.newValue);\n }\n\n invert(doc: EditorDocument): void {\n const deformer = doc.findMatrixDeformer(this.deformerId);\n this.set(deformer, this.prevValue);\n }\n}\n\n/** Edit a matrix deformer's pivot x. */\nexport class SetDeformerPivotX extends DeformerFieldCommand<number> {\n constructor(deformerId: string, value: number) {\n super(\n deformerId,\n value,\n \"Set pivot x\",\n (d) => d.pivot.x,\n (d, v) => {\n d.pivot.x = v;\n },\n );\n }\n}\n\n/** Edit a matrix deformer's pivot y. */\nexport class SetDeformerPivotY extends DeformerFieldCommand<number> {\n constructor(deformerId: string, value: number) {\n super(\n deformerId,\n value,\n \"Set pivot y\",\n (d) => d.pivot.y,\n (d, v) => {\n d.pivot.y = v;\n },\n );\n }\n}\n\n/**\n * Set a matrix deformer's pivot x and y atomically (one drag = one undo step).\n * {@link SetDeformerPivotX} and {@link SetDeformerPivotY} remain for the\n * Inspector's single-axis number inputs.\n */\nexport class SetDeformerPivot implements EditCommand {\n readonly label = \"Set pivot\";\n private captured = false;\n private prevPivot!: { x: number; y: number };\n private readonly pivot: { x: number; y: number };\n\n constructor(\n private readonly deformerId: string,\n pivot: { x: number; y: number },\n ) {\n // Fresh clone so a caller mutating their arg after construction cannot\n // corrupt apply/redo.\n this.pivot = { x: pivot.x, y: pivot.y };\n }\n\n apply(doc: EditorDocument): void {\n const deformer = doc.findMatrixDeformer(this.deformerId);\n if (!this.captured) {\n this.prevPivot = { x: deformer.pivot.x, y: deformer.pivot.y };\n this.captured = true;\n }\n deformer.pivot = { x: this.pivot.x, y: this.pivot.y };\n }\n\n invert(doc: EditorDocument): void {\n const deformer = doc.findMatrixDeformer(this.deformerId);\n // Fresh clone — never alias the captured object so repeated undo/redo\n // cycles cannot corrupt the saved prior value.\n deformer.pivot = { x: this.prevPivot.x, y: this.prevPivot.y };\n }\n}\n\n/** Channels of {@link IkiDeformerTransform} this editor can edit. */\nexport type DeformerTransformChannel =\n | \"x\"\n | \"y\"\n | \"rotation\"\n | \"scaleX\"\n | \"scaleY\";\n\n/**\n * Edit one channel of a matrix deformer's base transform. Because\n * {@link IkiDeformerTransform} REQUIRES finite `x` and `y`, this command\n * captures and restores the WHOLE prior `transform` object (present or absent)\n * rather than a single channel, so undo can delete the transform when it did\n * not previously exist, and redo never produces a partial object missing `x`/`y`.\n *\n * When no `transform` is present on the deformer, `apply` creates one from\n * the identity base `{ x: 0, y: 0 }` — the minimal valid shape the validator\n * accepts — then writes the edited channel. For example, editing `rotation`\n * on a transform-less deformer yields `{ x: 0, y: 0, rotation: <value> }`.\n */\nexport class SetDeformerTransform implements EditCommand {\n readonly label = \"Set deformer transform\";\n private captured = false;\n private prevTransform!: IkiDeformerTransform | undefined;\n\n constructor(\n private readonly deformerId: string,\n private readonly channel: DeformerTransformChannel,\n private readonly value: number,\n ) {}\n\n apply(doc: EditorDocument): void {\n const deformer = doc.findMatrixDeformer(this.deformerId);\n if (!this.captured) {\n // Shallow clone is sufficient — IkiDeformerTransform is a flat number map.\n this.prevTransform =\n deformer.transform === undefined\n ? undefined\n : { ...deformer.transform };\n this.captured = true;\n }\n // Start from the existing transform or the identity base. The identity base\n // is { x: 0, y: 0 } because the validator unconditionally requires finite\n // x and y whenever a transform object is present.\n const next: IkiDeformerTransform = {\n ...(deformer.transform ?? { x: 0, y: 0 }),\n };\n next[this.channel] = this.value;\n deformer.transform = next;\n }\n\n invert(doc: EditorDocument): void {\n const deformer = doc.findMatrixDeformer(this.deformerId);\n if (this.prevTransform === undefined) {\n delete deformer.transform;\n } else {\n // Assign a fresh clone — never alias the captured object so repeated\n // undo/redo cycles cannot corrupt the saved prior value.\n deformer.transform = { ...this.prevTransform };\n }\n }\n}\n\n/**\n * Replace a matrix deformer's `bindings` array wholesale. A single command\n * covers add, edit, and remove (pass the desired final array; pass `[]` to\n * remove all). Mirrors {@link CaptureGridKeyform}'s deep-copy discipline:\n * clone-on-construction, capture-once, fresh deep copy on invert.\n *\n * Each {@link IkiDeformerBinding} is a flat object so a per-element spread\n * `{ ...b }` is a sufficient deep copy.\n */\nexport class SetDeformerBindings implements EditCommand {\n readonly label = \"Set deformer bindings\";\n private readonly bindings: IkiDeformerBinding[];\n private captured = false;\n private prevBindings!: IkiDeformerBinding[] | undefined;\n\n constructor(\n private readonly deformerId: string,\n bindings: IkiDeformerBinding[],\n ) {\n // Clone the caller's array on construction — prevents post-execute mutation\n // of the caller's array from corrupting apply/redo.\n this.bindings = bindings.map((b) => ({ ...b }));\n }\n\n apply(doc: EditorDocument): void {\n const deformer = doc.findMatrixDeformer(this.deformerId);\n\n // Validate a NARROW synthetic candidate before capture/mutate, exactly as\n // {@link SetPartBindings} does and for the same reason: a full\n // structuredClone(doc.getModel()) would also validate parts and deformers\n // the user is not editing, and those routinely hold in-flight `NaN` from an\n // emptied numeric input — so an unrelated blank field would refuse this\n // edit, and report it against the wrong object. The Inspector offers every\n // parameter in the model, so an undeclared id, a non-finite endpoint, or a\n // non-matrix channel must still be caught here.\n //\n // NOT covered by this shape: a binding that feeds a physics chain's own\n // anchor. That rule needs the whole deformer hierarchy, and reproducing it\n // over a sanitized model would mean hand-maintaining a second model shape\n // beside the validator. It stays an export-time check in `toIkiModel()`.\n // Parts and deformers share one flat id namespace, so the two synthetic\n // objects below must not both be \"_\".\n const candidateDeformer: Record<string, unknown> = {\n kind: \"matrix\",\n id: \"_d\",\n pivot: { x: 0, y: 0 },\n };\n if (this.bindings.length > 0) {\n candidateDeformer.bindings = this.bindings.map((b) => ({ ...b }));\n }\n const candidate = {\n version: IKI_FORMAT_VERSION,\n name: \"_\",\n canvas: { width: 1, height: 1 },\n parameters: doc.getModel().parameters,\n parts: [\n {\n id: \"_\",\n color: [0, 0, 0, 1],\n width: 1,\n height: 1,\n transform: { x: 0, y: 0 },\n order: 0,\n },\n ],\n deformers: [candidateDeformer],\n };\n // The synthetic deformer is always at deformers[0]; rewrite that prefix so\n // the surfaced error names the real target.\n try {\n parseIkiModel(candidate);\n } catch (e) {\n if (e instanceof IkiFormatError) {\n throw new IkiFormatError(\n e.message.replace(\n /^deformers\\[0\\]/,\n `deformers.\"${this.deformerId}\"`,\n ),\n );\n }\n throw e;\n }\n\n if (!this.captured) {\n // Preserve the original absent-vs-empty distinction.\n this.prevBindings =\n deformer.bindings === undefined\n ? undefined\n : deformer.bindings.map((b) => ({ ...b }));\n this.captured = true;\n }\n if (this.bindings.length > 0) {\n // Assign a fresh deep copy — model must never alias the command's stored array.\n deformer.bindings = this.bindings.map((b) => ({ ...b }));\n } else {\n // Delete the key on empty: keeps the model shape minimal and correctly\n // represents \"no bindings\" as an absent key rather than an empty array.\n delete deformer.bindings;\n }\n }\n\n invert(doc: EditorDocument): void {\n const deformer = doc.findMatrixDeformer(this.deformerId);\n if (this.prevBindings === undefined) {\n delete deformer.bindings;\n } else {\n // Assign a fresh deep copy — never alias the captured array so re-apply\n // after undo cannot corrupt the saved prior value.\n deformer.bindings = this.prevBindings.map((b) => ({ ...b }));\n }\n }\n}\n\n/**\n * Reparent a deformer (matrix or warp) under a new parent, or promote it to\n * root (`newParentId === undefined`). Calls {@link validateDeformerReparent}\n * FIRST so invalid reparents (cycles, warp parent, unknown id) throw before any\n * capture or mutation — a throwing apply leaves the model and undo stack\n * untouched.\n *\n * Absent-vs-present distinction: captures both the prior `parent` value AND\n * whether the key was present on the object, so `invert` can delete the key\n * (restore \"absent\") rather than blindly assigning `undefined`.\n */\nexport class SetDeformerParent implements EditCommand {\n readonly label = \"Set deformer parent\";\n private captured = false;\n private prevParent: string | undefined = undefined;\n private prevHadParent = false;\n\n constructor(\n private readonly deformerId: string,\n private readonly newParentId: string | undefined,\n ) {}\n\n apply(doc: EditorDocument): void {\n // Validate FIRST — throws before capture/mutate on any violation.\n validateDeformerReparent(\n doc.getModel().deformers ?? [],\n this.deformerId,\n this.newParentId,\n );\n\n // A new ANCESTOR can still bind a parameter that a physics chain anchored\n // here emits, which the format rejects as feedback. That rule needs the\n // whole hierarchy, and validating a full clone of the document here refuses\n // the edit whenever any unrelated part or deformer holds in-flight `NaN`\n // from an emptied numeric input — reporting it against the wrong object.\n // So the feedback case stays an export-time check in `toIkiModel()`, where\n // it was before, rather than trading a rare corruption for a common block.\n //\n // Known cost of that trade: the host surfaces the export error (the editor\n // app re-exports on a ~200ms debounce), but the message names the CHAIN, not\n // the edit — reparent `headDeformer` and you get\n // `physicsChains[0].segments[1].output.parameter \"...\" feeds its own anchor\n // deformer chain (feedback)`, which reads as a complaint about a hair lock.\n // If a friendlier error surface ever lands, this is the message to special-case.\n const deformer = doc.findDeformer(this.deformerId);\n if (!this.captured) {\n this.prevHadParent = Object.prototype.hasOwnProperty.call(\n deformer,\n \"parent\",\n );\n this.prevParent = deformer.parent;\n this.captured = true;\n }\n if (this.newParentId !== undefined) {\n deformer.parent = this.newParentId;\n } else {\n delete deformer.parent;\n }\n }\n\n invert(doc: EditorDocument): void {\n const deformer = doc.findDeformer(this.deformerId);\n if (this.prevHadParent) {\n deformer.parent = this.prevParent;\n } else {\n delete deformer.parent;\n }\n }\n}\n\n/**\n * Add a new part to the model. Validates the candidate model with\n * {@link parseIkiModel} BEFORE mutating, so a structurally invalid part\n * (bad color tuple, missing required fields, id collision with a deformer)\n * throws an `IkiFormatError` and leaves the model untouched.\n *\n * Captures the prior base-UV entry for the part's id on the FIRST apply so\n * invert can restore the exact pre-add state. This guards the id-reuse hazard:\n * DeletePart(X) → AddPart(X′, different mesh) → undo(add) → undo(delete) —\n * without restore, X's constructor-captured base would be gone, causing\n * applyAtlas to fail when X is restored by the undo of the delete.\n */\nexport class AddPart implements EditCommand {\n readonly label = \"Add part\";\n private readonly part: IkiPart;\n private captured = false;\n private prevBaseMeshUvs: number[] | undefined = undefined;\n\n constructor(part: IkiPart) {\n // Clone on construction — prevents caller mutation from corrupting apply/redo.\n this.part = structuredClone(part);\n }\n\n apply(doc: EditorDocument): void {\n // (a) Cheap id-uniqueness pre-check with a source-qualified message.\n const hit = findIdCollision(doc.getModel(), this.part.id);\n if (hit === \"part\") {\n throw new Error(\n `parts: id \"${this.part.id}\" collides with an existing part id`,\n );\n }\n if (hit === \"deformer\") {\n throw new Error(\n `parts: id \"${this.part.id}\" collides with an existing deformer id`,\n );\n }\n\n // (b) Full structural validation on a candidate clone — propagate\n // IkiFormatError unchanged so the caller gets a path-qualified message.\n const candidate = structuredClone(doc.getModel());\n candidate.parts.push(structuredClone(this.part));\n parseIkiModel(candidate);\n\n // (c) All checks pass — mutate the real model with a fresh clone so the\n // model never aliases the command's stored part.\n doc.getModel().parts.push(structuredClone(this.part));\n\n // Register base mesh UVs so applyAtlas can remap this part. Capture the\n // prior entry on the FIRST apply only — redo must NOT re-capture or it\n // would clobber the saved prior and make invert unable to restore correctly.\n const prev = doc.captureBaseMeshUvs(this.part.id);\n if (!this.captured) {\n this.prevBaseMeshUvs = prev;\n this.captured = true;\n }\n }\n\n invert(doc: EditorDocument): void {\n const parts = doc.getModel().parts;\n const i = parts.findIndex((p) => p.id === this.part.id);\n if (i !== -1) parts.splice(i, 1);\n // Restore the exact prior base-UV state so a constructor-captured entry for\n // a deleted part with this id survives the undo of the add.\n doc.restoreBaseMeshUvs(this.part.id, this.prevBaseMeshUvs);\n }\n}\n\n/**\n * Add a new deformer (matrix or warp) to the model. Validates the candidate\n * model with {@link parseIkiModel} BEFORE mutating — this enforces the warp\n * rest-grid invariant, points length, pivot, parent, and bindings without\n * hand-rolling partial checks. Mirrors {@link AddPart} but also tracks whether\n * `model.deformers` was absent before the first apply, so `invert` can restore\n * the exact key-absence state (mirrors {@link SetDeformerBindings}).\n */\nexport class AddDeformer implements EditCommand {\n readonly label = \"Add deformer\";\n private readonly deformer: IkiDeformer;\n private captured = false;\n private prevDeformersAbsent = false;\n\n constructor(deformer: IkiDeformer) {\n // Clone on construction — prevents caller mutation from corrupting apply/redo.\n this.deformer = structuredClone(deformer);\n }\n\n apply(doc: EditorDocument): void {\n const model = doc.getModel();\n\n // (a) Cheap id-uniqueness pre-check with a source-qualified message.\n const hit = findIdCollision(model, this.deformer.id);\n if (hit === \"deformer\") {\n throw new Error(\n `deformers: id \"${this.deformer.id}\" collides with an existing deformer id`,\n );\n }\n if (hit === \"part\") {\n throw new Error(\n `deformers: id \"${this.deformer.id}\" collides with an existing part id`,\n );\n }\n\n // (b) Full structural validation on a candidate clone.\n const candidate = structuredClone(model);\n candidate.deformers = [\n ...(candidate.deformers ?? []),\n structuredClone(this.deformer),\n ];\n parseIkiModel(candidate);\n\n // (c) All checks pass — capture-once, then mutate.\n if (!this.captured) {\n this.prevDeformersAbsent = model.deformers === undefined;\n this.captured = true;\n }\n if (model.deformers === undefined) {\n model.deformers = [];\n }\n model.deformers.push(structuredClone(this.deformer));\n }\n\n invert(doc: EditorDocument): void {\n const model = doc.getModel();\n const arr = model.deformers;\n if (!arr) return;\n const i = arr.findIndex((d) => d.id === this.deformer.id);\n if (i !== -1) arr.splice(i, 1);\n // Restore the absent-vs-present distinction. If deformers did not exist\n // before apply, delete the key once the array is empty again.\n if (this.prevDeformersAbsent && arr.length === 0) {\n delete model.deformers;\n }\n }\n}\n\n/**\n * Delete a part by id, preserving its original array slot so `invert` restores\n * it at the same position. Slot position is cosmetic (the renderer uses the\n * `order` field for paint ordering), but restoring the index keeps undo\n * visually predictable.\n *\n * No `parseIkiModel` pre-check is needed: removing an element from an already-\n * valid model cannot introduce a structural violation — EXCEPT for clip-mask\n * references, the one part→part reference in the contract. `apply` refuses to\n * delete a part still used as another part's `clip.masks` entry (guard below),\n * so it can never leave a dangling mask ref that would fail `toIkiModel()`.\n *\n * Texture-reference safety (package invariant): `apply` refuses to delete a\n * part that still carries `part.texture`. Texture/atlas state is non-undoable\n * per the 5b boundary; clear the texture first via\n * {@link EditorDocument.clearPartTextureRef} (model-committed) or\n * {@link EditorDocument.applyAtlas} with no assignment (imported) — both are\n * non-undoable. By the time a part is deletable it carries no texture, so\n * `invert` can never restore a stale texture index that would render the wrong\n * atlas region after a later atlas repack. No transactional atlas capture is\n * needed in this command.\n */\nexport class DeletePart implements EditCommand {\n readonly label = \"Delete part\";\n private captured = false;\n private removed!: IkiPart;\n private index!: number;\n\n constructor(private readonly partId: string) {}\n\n apply(doc: EditorDocument): void {\n // Validate FIRST — throws with path-qualified message if unknown.\n const part = doc.findPart(this.partId);\n\n // Texture guard — enforced at the `@ikijs/editor` boundary so public callers\n // cannot bypass it (the example store adds a friendly pre-check, but this\n // is the real invariant). Throw before any capture or mutation.\n if (part.texture !== undefined) {\n throw new Error(\n `parts.\"${this.partId}\": cannot delete — part has a texture reference; clear its texture first`,\n );\n }\n\n const parts = doc.getModel().parts;\n\n // Clip-mask guard — `clip.masks` is the one part→part reference in the model\n // contract. Deleting a referenced mask would leave a dangling ref that fails\n // parseIkiModel on toIkiModel(). Refuse rather than corrupt (mirrors the\n // texture guard above). Throw before any capture or mutation.\n const masker = parts.find(\n (p) => p.id !== this.partId && p.clip?.masks.includes(this.partId),\n );\n if (masker) {\n throw new Error(\n `parts.\"${this.partId}\": cannot delete — used as a clip mask by part \"${masker.id}\"; remove its clip first`,\n );\n }\n\n const i = parts.indexOf(part);\n if (!this.captured) {\n this.removed = structuredClone(part);\n this.index = i;\n this.captured = true;\n }\n parts.splice(i, 1);\n }\n\n invert(doc: EditorDocument): void {\n // Restore at the original slot — exact deep restore including bindings and\n // mesh. No texture key is present (apply enforced that invariant before\n // capture), so the snapshot is always atlas-safe on restore.\n doc.getModel().parts.splice(this.index, 0, structuredClone(this.removed));\n }\n}\n\n/**\n * Delete a deformer by id. Calls {@link validateDeformerDelete} FIRST so the\n * delete is refused while anything still references it — a child deformer, an\n * attached part, or a physics chain anchored to it — enforcing the same\n * referential safety as {@link SetDeformerParent} and {@link SetPartDeformer}.\n *\n * `invert` re-inserts the deformer at its original index.\n */\nexport class DeleteDeformer implements EditCommand {\n readonly label = \"Delete deformer\";\n private captured = false;\n private removed!: IkiDeformer;\n private index!: number;\n\n constructor(private readonly deformerId: string) {}\n\n apply(doc: EditorDocument): void {\n const model = doc.getModel();\n // Validate FIRST — throws before capture/mutate on any referential violation.\n validateDeformerDelete(\n model.deformers ?? [],\n model.parts,\n model.physicsChains ?? [],\n this.deformerId,\n );\n // validateDeformerDelete guarantees the deformer (and thus the array) exists.\n const arr = model.deformers!;\n const i = arr.findIndex((d) => d.id === this.deformerId);\n if (!this.captured) {\n this.removed = structuredClone(arr[i]);\n this.index = i;\n this.captured = true;\n }\n arr.splice(i, 1);\n }\n\n invert(doc: EditorDocument): void {\n // Re-insert at the original slot. `apply` only captures after\n // validateDeformerDelete passed, so the array it spliced out of is present:\n // fabricating one here would hide a broken undo stack and silently restore\n // just this node instead of surfacing the loss of the rest of the hierarchy.\n doc\n .getModel()\n .deformers!.splice(this.index, 0, structuredClone(this.removed));\n }\n}\n\n/**\n * Replace a part's `bindings` array wholesale. A single command covers add,\n * edit, and remove (pass the desired final array; pass `[]` to remove all).\n * Mirrors {@link SetDeformerBindings}'s deep-copy and absent-vs-empty discipline:\n * clone-on-construction, capture-once, fresh deep copy on every assign/invert.\n *\n * Each {@link IkiBinding} is a flat object, so a per-element spread `{ ...b }`\n * is a sufficient deep copy.\n *\n * Validates the WRITTEN bindings against the declared parameters via a narrow\n * synthetic {@link parseIkiModel} candidate (so unrelated in-flight invalid\n * editor state — e.g. a NaN width on another part — cannot false-reject a\n * binding edit). Validation runs BEFORE any mutation; on failure the model and\n * undo stack are left untouched.\n *\n * Empty bindings → omit the `bindings` key on the candidate AND delete\n * `part.bindings` on apply (keeps the model shape minimal; represents \"no\n * bindings\" as an absent key rather than an empty array).\n */\nexport class SetPartBindings implements EditCommand {\n readonly label = \"Set part bindings\";\n private readonly bindings: IkiBinding[];\n private captured = false;\n private prevBindings!: IkiBinding[] | undefined;\n\n constructor(\n private readonly partId: string,\n bindings: IkiBinding[],\n ) {\n // Clone the caller's array on construction — prevents post-execute mutation\n // of the caller's array from corrupting apply/redo.\n this.bindings = bindings.map((b) => ({ ...b }));\n }\n\n apply(doc: EditorDocument): void {\n // Build a narrow synthetic candidate carrying only the validation-relevant\n // context. A full structuredClone(doc.getModel()) is deliberately avoided\n // because unrelated parts may carry NaN in-flight values (e.g. from\n // NumberField.valueAsNumber), which would cause false-positive validation\n // failures. The synthetic model is the minimal shape parseIkiModel accepts.\n const candidatePart: Record<string, unknown> = {\n id: \"_\",\n color: [0, 0, 0, 1],\n width: 1,\n height: 1,\n transform: { x: 0, y: 0 },\n order: 0,\n };\n // Omit the bindings key entirely when empty — \"no bindings\" is represented\n // as key absence; an empty array is not a valid value in the format contract.\n if (this.bindings.length > 0) {\n candidatePart.bindings = this.bindings.map((b) => ({ ...b }));\n }\n const candidate = {\n version: IKI_FORMAT_VERSION,\n name: \"_\",\n canvas: { width: 1, height: 1 },\n parameters: doc.getModel().parameters,\n parts: [candidatePart],\n };\n // Validation before any mutation. The synthetic candidate always places the\n // part at parts[0], so the validator emits paths like \"parts[0].bindings[i]\".\n // Rewrite that prefix to name the real target so the surfaced error is\n // actionable (\"parts.\"part-a\".bindings[i]\" rather than \"parts[0].bindings[i]\").\n try {\n parseIkiModel(candidate);\n } catch (e) {\n if (e instanceof IkiFormatError) {\n throw new IkiFormatError(\n e.message.replace(/^parts\\[0\\]/, `parts.\"${this.partId}\"`),\n );\n }\n throw e;\n }\n\n // Resolution after validation so an unknown partId throws with a\n // path-qualified message (findPart throws) but only after the bindings\n // themselves are confirmed structurally valid.\n const part = doc.findPart(this.partId);\n if (!this.captured) {\n // Preserve the original absent-vs-empty distinction.\n this.prevBindings =\n part.bindings === undefined\n ? undefined\n : part.bindings.map((b) => ({ ...b }));\n this.captured = true;\n }\n if (this.bindings.length > 0) {\n // Assign a fresh deep copy — model must never alias the command's stored array.\n part.bindings = this.bindings.map((b) => ({ ...b }));\n } else {\n // Delete the key on empty: keeps the model shape minimal and correctly\n // represents \"no bindings\" as an absent key rather than an empty array.\n delete part.bindings;\n }\n }\n\n invert(doc: EditorDocument): void {\n const part = doc.findPart(this.partId);\n if (this.prevBindings === undefined) {\n delete part.bindings;\n } else {\n // Assign a fresh deep copy — never alias the captured array so re-apply\n // after undo cannot corrupt the saved prior value.\n part.bindings = this.prevBindings.map((b) => ({ ...b }));\n }\n }\n}\n\n/**\n * Attach a part to a deformer, or detach it (`newDeformerId === undefined`).\n * Calls {@link validatePartAttach} FIRST so invalid attachments (warp without\n * mesh, unknown ids) throw before any capture or mutation.\n *\n * Absent-vs-present distinction mirrors {@link SetDeformerParent}: captures\n * both the prior `deformer` value and whether the key was present, so `invert`\n * can delete the key rather than assigning `undefined`.\n */\nexport class SetPartDeformer implements EditCommand {\n readonly label = \"Set part deformer\";\n private captured = false;\n private prevDeformer: string | undefined = undefined;\n private prevHadDeformer = false;\n\n constructor(\n private readonly partId: string,\n private readonly newDeformerId: string | undefined,\n ) {}\n\n apply(doc: EditorDocument): void {\n // Validate FIRST — throws before capture/mutate on any violation.\n validatePartAttach(\n doc.getModel().deformers ?? [],\n this.partId,\n doc.getModel().parts,\n this.newDeformerId,\n );\n const part = doc.findPart(this.partId);\n if (!this.captured) {\n this.prevHadDeformer = Object.prototype.hasOwnProperty.call(\n part,\n \"deformer\",\n );\n this.prevDeformer = part.deformer;\n this.captured = true;\n }\n if (this.newDeformerId !== undefined) {\n part.deformer = this.newDeformerId;\n } else {\n delete part.deformer;\n }\n }\n\n invert(doc: EditorDocument): void {\n const part = doc.findPart(this.partId);\n if (this.prevHadDeformer) {\n part.deformer = this.prevDeformer;\n } else {\n delete part.deformer;\n }\n }\n}\n\n/**\n * Return true if the deformer identified by `deformerId` exists in the model\n * and has `kind === \"warp\"`. Used by SetPartMesh to detect warp-deformer\n * attachment without importing reparent.ts internals.\n */\nfunction isWarpDeformer(model: IkiModel, deformerId: string): boolean {\n const d = (model.deformers ?? []).find((x) => x.id === deformerId);\n return d?.kind === \"warp\";\n}\n\n/**\n * Add, regenerate, or remove the triangle mesh on a part.\n *\n * - `mesh !== undefined` → add or replace the mesh, registering the\n * unit-square base UVs in the side-table so {@link EditorDocument.applyAtlas}\n * can remap them later.\n * - `mesh === undefined` → delete `part.mesh` and remove the side-table entry.\n *\n * Fails fast (BEFORE any mutation) on warp-topology violations:\n * - REMOVE while `part.warps` is present (even empty) or the part is\n * attached to a warp deformer — the format rejects any `warps` key once\n * the mesh is gone.\n * - ADD/REPLACE while `part.warps` has authored offsets (`length > 0`) —\n * regenerating the mesh invalidates offset positions silently; the user\n * must remove the warps first.\n *\n * The remove guard checks PRESENCE of `part.warps` (not length) while the\n * add/replace guard checks LENGTH > 0. This asymmetry is intentional: the\n * format allows `warps: []` only when a mesh exists, so any present key\n * (even empty) would become invalid after mesh removal; but replacing a mesh\n * under an empty `warps: []` is harmless because there are no authored offsets.\n */\nexport class SetPartMesh implements EditCommand {\n readonly label = \"Set part mesh\";\n private readonly mesh: IkiMesh | undefined;\n private captured = false;\n private prevHadMesh = false;\n private prevMesh: IkiMesh | undefined;\n private prevBaseMeshUvs: number[] | undefined;\n\n constructor(\n private readonly partId: string,\n mesh: IkiMesh | undefined,\n ) {\n // Clone on construction — prevents caller mutation from corrupting apply/redo.\n this.mesh = mesh === undefined ? undefined : structuredClone(mesh);\n }\n\n apply(doc: EditorDocument): void {\n // (a) Resolve the live part up front — unlike SetPartBindings, which resolves\n // after parseIkiModel, we need the live part here because the warp-topology\n // guards at step (c) inspect part.warps / part.deformer before any mutation.\n const part = doc.findPart(this.partId);\n\n // (b) Structural validation — only needed when adding or replacing a mesh.\n // Build a NARROW synthetic candidate carrying only the new mesh so that\n // unrelated in-flight parts with NaN values cannot cause false failures.\n if (this.mesh !== undefined) {\n const candidatePart = {\n id: \"_\",\n color: [0, 0, 0, 1],\n width: 1,\n height: 1,\n transform: { x: 0, y: 0 },\n order: 0,\n mesh: structuredClone(this.mesh),\n };\n const candidate = {\n version: IKI_FORMAT_VERSION,\n name: \"_\",\n canvas: { width: 1, height: 1 },\n parameters: doc.getModel().parameters,\n parts: [candidatePart],\n };\n try {\n parseIkiModel(candidate);\n } catch (e) {\n if (e instanceof IkiFormatError) {\n throw new IkiFormatError(\n e.message.replace(/^parts\\[0\\]/, `parts.\"${this.partId}\"`),\n );\n }\n throw e;\n }\n }\n\n // (c) Warp-topology fail-fast — BEFORE any mutation, after structural validation.\n // The two paths key on DIFFERENT predicates; this asymmetry is intentional\n // (see class JSDoc above).\n const attachedToWarp =\n part.deformer !== undefined &&\n isWarpDeformer(doc.getModel(), part.deformer);\n\n if (this.mesh === undefined) {\n // REMOVE: guard on warps PRESENCE (even empty) OR warp-deformer attachment.\n // The format rejects ANY present `warps` key once the mesh is gone, so even\n // an empty array would make toIkiModel() throw.\n if (part.warps !== undefined || attachedToWarp) {\n throw new IkiFormatError(\n `parts.\"${this.partId}\": cannot remove mesh — part has warps or is attached to a warp deformer; remove its warps / detach from the warp deformer first`,\n );\n }\n // Clip-mask guard — the format requires a clip mask to be a mesh part, so\n // stripping the mesh from a referenced mask would dangle the ref and fail\n // toIkiModel(). Mirrors the DeletePart clip-mask guard.\n const masker = doc\n .getModel()\n .parts.find(\n (p) => p.id !== this.partId && p.clip?.masks.includes(this.partId),\n );\n if (masker) {\n throw new IkiFormatError(\n `parts.\"${this.partId}\": cannot remove mesh — used as a clip mask by part \"${masker.id}\" (masks require a mesh); remove its clip first`,\n );\n }\n } else {\n // ADD/REPLACE: guard on authored offsets (length > 0). An empty warps: []\n // has no offsets to invalidate, so replacing the mesh under it is harmless.\n if ((part.warps?.length ?? 0) > 0) {\n throw new IkiFormatError(\n `parts.\"${this.partId}\": cannot regenerate mesh — part has warps whose offsets are bound to the current rest mesh; remove its warps first`,\n );\n }\n }\n\n // (d) Mutate + side-table maintenance.\n\n // (i) Single first-apply capture block — captures BOTH prevMesh and\n // prevBaseMeshUvs together, BEFORE any mutation. captureBaseMeshUvs has\n // a side effect (registers current mesh.uvs); step (iii) overwrites it.\n if (!this.captured) {\n this.prevHadMesh = part.mesh !== undefined;\n this.prevMesh = part.mesh ? structuredClone(part.mesh) : undefined;\n this.prevBaseMeshUvs = doc.captureBaseMeshUvs(this.partId);\n this.captured = true;\n }\n // Redo: do NOT re-capture (would clobber the saved prior). The mutation\n // below is fully reconstructable from this.mesh + this.partId.\n\n // (ii) Apply the model mutation.\n if (this.mesh !== undefined) {\n // Compute stored UVs: if the part has an active texture, remap the\n // unit-square base UVs into the texture's atlas sub-rectangle.\n const storedUvs =\n part.texture !== undefined\n ? remapMeshUvsToRect(this.mesh.uvs, part.texture.uv)\n : this.mesh.uvs.slice();\n part.mesh = {\n vertices: this.mesh.vertices.slice(),\n uvs: storedUvs,\n indices: this.mesh.indices.slice(),\n };\n } else {\n delete part.mesh;\n }\n\n // (iii) Re-register the side-table to the correct final state, overwriting\n // the (i) read's incidental re-registration. Write ONLY via\n // restoreBaseMeshUvs — never via a new setBaseMeshUvs API.\n if (this.mesh !== undefined) {\n // Register the UNIT-SQUARE base (not the texture-remapped storedUvs) so\n // applyAtlas can always derive correct atlas-space UVs from the base.\n doc.restoreBaseMeshUvs(this.partId, this.mesh.uvs.slice());\n } else {\n doc.restoreBaseMeshUvs(this.partId, undefined);\n }\n }\n\n invert(doc: EditorDocument): void {\n const part = doc.findPart(this.partId);\n // Restore the mesh, preserving the absent-vs-present distinction.\n // applyAtlas (and texture changes generally) are NON-undoable and do NOT\n // clear the undo stack. So by the time undo() is called, part.texture may\n // have been changed by a later applyAtlas that this command never saw. To\n // avoid restoring stale texture-space UVs, rebuild uvs from the captured\n // unit-square base against the CURRENT texture rect rather than trusting\n // prevMesh.uvs verbatim.\n if (this.prevHadMesh) {\n // A mesh part ALWAYS has a registered unit-square base under the invariant\n // (applyAtlas preflights requireBaseUvs for every mesh part). If the base is\n // absent here the side-table invariant is broken — fail fast rather than\n // silently restoring stale texture-space uvs that would corrupt rendering.\n if (this.prevBaseMeshUvs === undefined) {\n throw new Error(\n `parts.\"${this.partId}\": cannot invert SetPartMesh — no base mesh uvs captured for a mesh part (broken side-table invariant)`,\n );\n }\n const restored = structuredClone(this.prevMesh!);\n // Remap the unit-square base into the current texture rect (if any).\n restored.uvs =\n part.texture !== undefined\n ? remapMeshUvsToRect(this.prevBaseMeshUvs, part.texture.uv)\n : this.prevBaseMeshUvs.slice();\n part.mesh = restored;\n } else {\n delete part.mesh;\n }\n // Restore the side-table to the exact prior state (unit-square base).\n doc.restoreBaseMeshUvs(this.partId, this.prevBaseMeshUvs);\n }\n}\n\n/**\n * Validate a candidate `physics` array against the model's declared parameters\n * via a NARROW synthetic model — full `parameters` + the full candidate physics\n * array + one trivial part. Mirrors {@link SetPartBindings}' synthetic-candidate\n * (avoids false-positives from unrelated in-flight NaN part numerics) but carries\n * the FULL physics array so the cross-rig rules (dup id / dup output / feedback)\n * run. On `IkiFormatError`, rewrite the failing `physics[n]` path to name the rig\n * AT THE FAILING INDEX — a cross-rig failure can surface at a different index than\n * the edited rig, so the rewrite follows the message index, not the command target.\n */\nfunction validatePhysicsCandidate(\n doc: EditorDocument,\n candidatePhysics: IkiPhysics[],\n): void {\n const candidatePart: Record<string, unknown> = {\n id: \"_\",\n color: [0, 0, 0, 1],\n width: 1,\n height: 1,\n transform: { x: 0, y: 0 },\n order: 0,\n };\n const candidate = {\n version: IKI_FORMAT_VERSION,\n name: \"_\",\n canvas: { width: 1, height: 1 },\n parameters: doc.getModel().parameters,\n parts: [candidatePart],\n deformers: doc.getModel().deformers,\n physics: candidatePhysics,\n physicsChains: doc.getModel().physicsChains,\n };\n try {\n parseIkiModel(candidate);\n } catch (e) {\n if (e instanceof IkiFormatError) {\n const m = e.message.match(/^physics\\[(\\d+)\\]/);\n const rigId = m ? candidatePhysics[Number(m[1])]?.id : undefined;\n if (rigId !== undefined) {\n throw new IkiFormatError(\n e.message.replace(/^physics\\[\\d+\\]/, `physics.\"${rigId}\"`),\n );\n }\n }\n throw e;\n }\n}\n\n/**\n * Add a physics rig to `model.physics`. Mirrors {@link AddDeformer}: clone on\n * construction, cheap friendly duplicate-id pre-check, full synthetic-candidate\n * validation before mutating, capture the absent-vs-present `physics` state once,\n * and on invert delete the key when the array empties back to its prior absence.\n */\nexport class AddPhysicsRig implements EditCommand {\n readonly label = \"Add physics rig\";\n private readonly rig: IkiPhysics;\n private captured = false;\n private prevPhysicsAbsent = false;\n\n constructor(rig: IkiPhysics) {\n // Deep clone — input/output are nested objects a shallow spread would alias.\n this.rig = structuredClone(rig);\n }\n\n apply(doc: EditorDocument): void {\n const model = doc.getModel();\n // (a) Friendly duplicate-id pre-check (the format check from validate.ts is\n // the real guard; this surfaces an actionable message before validate).\n if ((model.physics ?? []).some((r) => r.id === this.rig.id)) {\n throw new Error(\n `physics: id \"${this.rig.id}\" collides with an existing physics rig id`,\n );\n }\n // (b) Full-physics synthetic validation with the rig appended.\n validatePhysicsCandidate(doc, [\n ...(model.physics ?? []),\n structuredClone(this.rig),\n ]);\n // (c) Capture-once, then mutate with a fresh clone.\n if (!this.captured) {\n this.prevPhysicsAbsent = model.physics === undefined;\n this.captured = true;\n }\n if (model.physics === undefined) {\n model.physics = [];\n }\n model.physics.push(structuredClone(this.rig));\n }\n\n invert(doc: EditorDocument): void {\n const model = doc.getModel();\n const arr = model.physics;\n if (!arr) return;\n const i = arr.findIndex((r) => r.id === this.rig.id);\n if (i !== -1) arr.splice(i, 1);\n if (this.prevPhysicsAbsent && arr.length === 0) {\n delete model.physics;\n }\n }\n}\n\n/**\n * Delete a physics rig by id. Mirrors {@link DeleteDeformer}: resolve/validate\n * first, capture the removed rig + its index once, splice. Removing the LAST rig\n * deletes the `physics` key to keep the exported shape minimal; invert re-inserts\n * at the original index (recreating the array if it was deleted).\n */\nexport class DeletePhysicsRig implements EditCommand {\n readonly label = \"Delete physics rig\";\n private captured = false;\n private removed!: IkiPhysics;\n private index!: number;\n\n constructor(private readonly rigId: string) {}\n\n apply(doc: EditorDocument): void {\n const rig = doc.findPhysicsRig(this.rigId); // throws if unknown\n const arr = doc.getModel().physics!;\n const i = arr.indexOf(rig);\n if (!this.captured) {\n this.removed = structuredClone(rig);\n this.index = i;\n this.captured = true;\n }\n arr.splice(i, 1);\n // Minimal exported shape: drop the key once the last rig is gone.\n if (arr.length === 0) {\n delete doc.getModel().physics;\n }\n }\n\n invert(doc: EditorDocument): void {\n const model = doc.getModel();\n (model.physics ??= []).splice(this.index, 0, structuredClone(this.removed));\n }\n}\n\n/**\n * Replace a physics rig in place (tuning). Mirrors {@link SetDeformerBindings}\n * but DEEP-clones the nested `input`/`output` (a shallow spread would alias them).\n * Forbids rename (`rig.id` must equal the target `rigId`) — this command tunes a\n * rig, never re-keys it. Validates the whole candidate (the edited rig swapped in\n * at its index) so cross-rig rules still run, then captures the prior rig once.\n */\nexport class SetPhysicsRig implements EditCommand {\n readonly label = \"Set physics rig\";\n private readonly rig: IkiPhysics;\n private captured = false;\n private prevRig!: IkiPhysics;\n\n constructor(\n private readonly rigId: string,\n rig: IkiPhysics,\n ) {\n this.rig = structuredClone(rig);\n }\n\n apply(doc: EditorDocument): void {\n // No-rename guard — fail before mutating.\n if (this.rig.id !== this.rigId) {\n throw new Error(\n `physics.\"${this.rigId}\": cannot change rig id to \"${this.rig.id}\" (rename unsupported)`,\n );\n }\n const model = doc.getModel();\n const i = (model.physics ?? []).findIndex((r) => r.id === this.rigId);\n if (i === -1) {\n throw new Error(`physics: no physics rig with id \"${this.rigId}\"`);\n }\n // Validate a candidate with the edited rig swapped in at its index.\n const candidate = model.physics!.map((r, idx) =>\n idx === i ? structuredClone(this.rig) : r,\n );\n validatePhysicsCandidate(doc, candidate);\n if (!this.captured) {\n this.prevRig = structuredClone(model.physics![i]);\n this.captured = true;\n }\n model.physics![i] = structuredClone(this.rig);\n }\n\n invert(doc: EditorDocument): void {\n const arr = doc.getModel().physics;\n if (!arr) return;\n const i = arr.findIndex((r) => r.id === this.rigId);\n if (i !== -1) arr[i] = structuredClone(this.prevRig);\n }\n}\n","import type { IkiGridKeyform } from \"@ikijs/format\";\n\n/**\n * Pure, grid-size-agnostic keyform/offset math for authoring a warp-deformer\n * grid by dragging. No DOM, no `@ikijs/engine` — the load-bearing testable core.\n * Constraints derive only from the input array lengths, never the sample grid.\n */\n\n/**\n * Interpolate the grid offsets at `value`, mirroring the engine's\n * `accumulateKeyformOffsets` clamp+lerp semantics: clamp to the first/last\n * keyform (NO extrapolation) and linearly interpolate the bracketing pair.\n * Returns a NEW array of length `keyforms[0].offsets.length`. Throws on empty.\n */\nexport function interpolateGridOffsets(\n keyforms: { value: number; offsets: number[] }[],\n value: number,\n): number[] {\n if (keyforms.length === 0) {\n throw new Error(\"interpolateGridOffsets: keyforms must be non-empty\");\n }\n\n if (value <= keyforms[0].value) {\n return [...keyforms[0].offsets];\n }\n const last = keyforms[keyforms.length - 1];\n if (value >= last.value) {\n return [...last.offsets];\n }\n\n // Find the bracketing pair (keyforms are small in practice).\n let lo = keyforms[0];\n let hi = keyforms[1];\n for (let k = 1; k < keyforms.length - 1; k++) {\n if (keyforms[k].value <= value) {\n lo = keyforms[k];\n hi = keyforms[k + 1];\n }\n }\n const t = (value - lo.value) / (hi.value - lo.value);\n return lo.offsets.map((loOff, i) => loOff + (hi.offsets[i] - loOff) * t);\n}\n\n/**\n * Per-control-point delta of the dragged grid from the rest grid: for each\n * point `i`, `(draggedX_i - restX_i, draggedY_i - restY_i)`. The DOM layer\n * assembles the full `restFrameDraggedPoints` (including untouched points), so\n * this is a straight subtract — no prior-offset blending.\n *\n * Both arrays must have the SAME length and that length must be even (x,y\n * pairs). Returns a NEW array of length `restPoints.length`.\n */\nexport function computeGridOffsets(\n restPoints: number[],\n restFrameDraggedPoints: number[],\n): number[] {\n if (restPoints.length !== restFrameDraggedPoints.length) {\n throw new Error(\n `computeGridOffsets: restFrameDraggedPoints length ${restFrameDraggedPoints.length} must equal restPoints length ${restPoints.length}`,\n );\n }\n if (restPoints.length % 2 !== 0) {\n throw new Error(\n `computeGridOffsets: restPoints length ${restPoints.length} must be even (x,y pairs)`,\n );\n }\n return restPoints.map((rest, i) => restFrameDraggedPoints[i] - rest);\n}\n\n/**\n * Insert or replace the keyform at `value`, returning a NEW array. If a keyform\n * already exists with an exact-match `value`, REPLACE its offsets (with a copy);\n * otherwise INSERT `{ value, offsets: [...offsets] }` at the position that keeps\n * the array strictly ascending by value. The input array and its keyform objects\n * are never mutated, and `offsets` is copied so the result never aliases the\n * caller's array.\n *\n * Deliberately RANGE-FREE — a generic, reusable array op. Value-range\n * enforcement is the command's job, not this helper's.\n */\nexport function upsertGridKeyform(\n keyforms: IkiGridKeyform[],\n value: number,\n offsets: number[],\n): IkiGridKeyform[] {\n const result = keyforms.map((kf) => ({\n value: kf.value,\n offsets: [...kf.offsets],\n }));\n const existing = result.findIndex((kf) => kf.value === value);\n if (existing !== -1) {\n result[existing] = { value, offsets: [...offsets] };\n return result;\n }\n const insertAt = result.findIndex((kf) => kf.value > value);\n const entry: IkiGridKeyform = { value, offsets: [...offsets] };\n if (insertAt === -1) {\n result.push(entry);\n } else {\n result.splice(insertAt, 0, entry);\n }\n return result;\n}\n","import type { IkiDeformer, IkiPart, IkiPhysicsChain } from \"@ikijs/format\";\n\n/**\n * Pure, DOM-free validation helpers for deformer reparenting and part attachment.\n * Each function throws a path-qualified plain Error on rejection and returns void\n * on success. Neither function mutates the input arrays or objects.\n */\n\nfunction kindOf(d: IkiDeformer): \"warp\" | \"matrix\" {\n return d.kind === \"warp\" ? \"warp\" : \"matrix\";\n}\n\n/**\n * Validate that reparenting `deformerId` under `newParentId` keeps the deformer\n * hierarchy valid. Pass `newParentId === undefined` to move to root (always legal).\n * Checks: existence, self-reference, undeclared parent, kind constraint (warp\n * deformers cannot be parents), and cycle detection via the proposed edge.\n */\nexport function validateDeformerReparent(\n deformers: IkiDeformer[],\n deformerId: string,\n newParentId: string | undefined,\n): void {\n // (1) Target deformer must exist.\n const target = deformers.find((d) => d.id === deformerId);\n if (target === undefined) {\n throw new Error(`deformers: no deformer with id \"${deformerId}\"`);\n }\n\n // (2) Root is always legal.\n if (newParentId === undefined) return;\n\n // (3) Self-reference.\n if (newParentId === deformerId) {\n throw new Error(\n `deformers.\"${deformerId}\".parent \"${newParentId}\" is a self-reference`,\n );\n }\n\n // (4) Parent must be declared.\n const parent = deformers.find((d) => d.id === newParentId);\n if (parent === undefined) {\n throw new Error(\n `deformers.\"${deformerId}\".parent \"${newParentId}\" is not a declared deformer`,\n );\n }\n\n // (5) Parent must be a matrix deformer.\n if (kindOf(parent) === \"warp\") {\n throw new Error(\n `deformers.\"${deformerId}\".parent \"${newParentId}\" must be a matrix deformer (warp deformers cannot be parents)`,\n );\n }\n\n // (6) Cycle detection: build parentOf from current state, then override with\n // the proposed edge, and walk from deformerId following the chain.\n const parentOf = new Map<string, string>();\n for (const d of deformers) {\n if (d.parent !== undefined) parentOf.set(d.id, d.parent);\n }\n // Override with the proposed edge.\n parentOf.set(deformerId, newParentId);\n\n const visited = new Set<string>();\n let cur: string | undefined = deformerId;\n while (cur !== undefined) {\n if (visited.has(cur)) {\n throw new Error(\n `deformers: reparenting \"${deformerId}\" under \"${newParentId}\" would create a cycle`,\n );\n }\n visited.add(cur);\n cur = parentOf.get(cur);\n }\n}\n\n/**\n * Validate that deleting `deformerId` is safe. Throws when the deformer does\n * not exist, when another deformer is parented to it (must be reparented or\n * detached first), when a part is attached to it (must be detached first), or\n * when a physics chain anchors to it (must be re-anchored or deleted first).\n *\n * Every id that can reference a deformer must be covered here: the format\n * validator rejects a dangling reference at export, so a delete this function\n * lets through does not fail now — it strands the document in a state\n * `toIkiModel()` refuses.\n *\n * Note: there is no validatePartDelete here, but NOT because parts are\n * unreferenced — `clip.masks` names parts by id. That invariant is enforced\n * closer to the edits that could break it: `DeletePart` refuses to remove a\n * part still used as a mask, and `SetPartMesh` refuses to strip the mesh off\n * one (masks must be mesh parts). No command creates or edits `clip`.\n */\nexport function validateDeformerDelete(\n deformers: IkiDeformer[],\n parts: IkiPart[],\n physicsChains: IkiPhysicsChain[],\n deformerId: string,\n): void {\n // (1) Target deformer must exist.\n const target = deformers.find((d) => d.id === deformerId);\n if (target === undefined) {\n throw new Error(`deformers: no deformer with id \"${deformerId}\"`);\n }\n\n // (2) No other deformer may be parented to it.\n const childDeformer = deformers.find((d) => d.parent === deformerId);\n if (childDeformer !== undefined) {\n throw new Error(\n `deformers.\"${deformerId}\": cannot delete — deformer \"${childDeformer.id}\" is parented to it; reparent or detach it first`,\n );\n }\n\n // (3) No part may be attached to it.\n const attachedPart = parts.find((p) => p.deformer === deformerId);\n if (attachedPart !== undefined) {\n throw new Error(\n `deformers.\"${deformerId}\": cannot delete — part \"${attachedPart.id}\" is attached to it; detach it first`,\n );\n }\n\n // (4) No physics chain may anchor to it.\n const anchoredChain = physicsChains.find(\n (c) => c.anchorDeformer === deformerId,\n );\n if (anchoredChain !== undefined) {\n throw new Error(\n `deformers.\"${deformerId}\": cannot delete — physics chain \"${anchoredChain.id}\" anchors to it; re-anchor or delete the chain first`,\n );\n }\n}\n\n/**\n * Validate that attaching part `partId` to deformer `newDeformerId` is valid.\n * Pass `newDeformerId === undefined` to detach (always legal).\n * Checks: part existence, undeclared deformer, and mesh-required-for-warp.\n */\nexport function validatePartAttach(\n deformers: IkiDeformer[],\n partId: string,\n parts: IkiPart[],\n newDeformerId: string | undefined,\n): void {\n // (1) Part must exist.\n const part = parts.find((p) => p.id === partId);\n if (part === undefined) {\n throw new Error(`parts: no part with id \"${partId}\"`);\n }\n\n // (2) Detach is always legal.\n if (newDeformerId === undefined) return;\n\n // (3) Deformer must be declared.\n const deformer = deformers.find((d) => d.id === newDeformerId);\n if (deformer === undefined) {\n throw new Error(\n `parts.\"${partId}\".deformer \"${newDeformerId}\" is not a declared deformer`,\n );\n }\n\n // (4) Warp deformer requires a mesh on the part.\n if (kindOf(deformer) === \"warp\" && part.mesh === undefined) {\n throw new Error(\n `parts.\"${partId}\".deformer \"${newDeformerId}\" is a warp deformer and requires a mesh`,\n );\n }\n}\n","/**\n * Alpha bounding-box scan shared by every auto-rig ingestion path.\n *\n * The scan itself is environment-free — it only needs indexable RGBA bytes — so\n * a browser editor (canvas `ImageData`) and the Node MCP server (a `sharp`\n * raw buffer) run the SAME code instead of two copies that have to be kept\n * byte-identical by hand. Only decoding differs between them.\n */\n\n/** Alpha at or above this counts as coverage; below it is treated as empty. */\nexport const ALPHA_BBOX_THRESHOLD = 8;\n\n/** Top-left origin, +y down — image space, not model space. */\nexport interface AlphaBbox {\n x: number;\n y: number;\n w: number;\n h: number;\n}\n\n/**\n * Tight bounding box of every pixel with alpha >= {@link ALPHA_BBOX_THRESHOLD},\n * expanded 1px on each side (clamped to the image) for AA / extrude margin.\n *\n * Returns `null` when no pixel passes the threshold, leaving the \"empty layer\"\n * error to the caller: each ingestion path reports it with its own error type\n * and message.\n */\nexport function detectAlphaBbox(\n rgba: ArrayLike<number>,\n width: number,\n height: number,\n): AlphaBbox | null {\n let minX = width;\n let maxX = -1;\n let minY = height;\n let maxY = -1;\n\n for (let y = 0; y < height; y++) {\n for (let x = 0; x < width; x++) {\n const alpha = rgba[(y * width + x) * 4 + 3];\n if (alpha >= ALPHA_BBOX_THRESHOLD) {\n if (x < minX) minX = x;\n if (x > maxX) maxX = x;\n if (y < minY) minY = y;\n if (y > maxY) maxY = y;\n }\n }\n }\n\n if (maxX === -1) return null;\n\n // Expand 1px each side, clamped to image bounds; the x2/y2 clamps make w/h\n // implicitly in-bounds (no separate w/h clamp needed).\n const x = Math.max(0, minX - 1);\n const y = Math.max(0, minY - 1);\n const x2 = Math.min(width - 1, maxX + 1);\n const y2 = Math.min(height - 1, maxY + 1);\n\n return { x, y, w: x2 - x + 1, h: y2 - y + 1 };\n}\n","import type { IkiTransformChannel } from \"@ikijs/format\";\n\n/**\n * Pure, binding-value logic for computing an endpoint (rest-to-posed delta or\n * ratio) when capturing a transform channel binding. No DOM, no @ikijs/engine —\n * the single home of the additive-vs-multiplicative rule, reused by both the\n * part and deformer capture paths.\n *\n * Additive channels (translateX, translateY, rotate, scaleX, scaleY) return the\n * delta: `posedValue - restValue`. The binding will multiply that delta across\n * the driven range.\n *\n * Opacity is multiplicative: returns the ratio `posedValue / restValue`. The\n * binding will multiply by that ratio across the driven range. When `restValue`\n * is 0 (a degenerate case: base opacity cannot be represented multiplicatively\n * since 0 * x ≡ 0), this returns 0 as a documented fallback — it does NOT\n * recover `posedValue` and is NOT unit-tested as an identity. The store layer\n * additionally skips an opacity capture when rest opacity is 0, surfacing an\n * editError.\n *\n * Deformer channels never include opacity (they use `IkiMatrixChannel`), so the\n * opacity branch is reached only for parts.\n *\n * Finiteness of the captured value is NOT validated here; the store's\n * `captureEndpoint` finite-value guard is the appropriate layer for that check.\n */\nexport function captureBindingEndpoint(\n channel: IkiTransformChannel,\n restValue: number,\n posedValue: number,\n): number {\n if (channel === \"opacity\") {\n return restValue === 0 ? 0 : posedValue / restValue;\n }\n return posedValue - restValue;\n}\n","import type { IkiUvRect } from \"@ikijs/format\";\n\nexport const ATLAS_PADDING = 2;\nexport const UV_INSET_PX = 0.5;\n\n/** Intrinsic pixel size of one decoded image; id is an editor-only stable key. */\nexport interface AtlasSource {\n id: string;\n width: number;\n height: number;\n}\n\n/** Sub-image pixel rect within the page, top-left origin, EXCLUDING the gutter. */\nexport interface AtlasPlacement {\n id: string;\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\nexport interface AtlasLayout {\n pageWidth: number;\n pageHeight: number;\n placements: AtlasPlacement[];\n padding: number;\n}\n\n/**\n * Deterministic shelf/row packer. Sources are sorted by id for stability so\n * identical inputs always produce an identical layout.\n *\n * Padding is one-sided: each placement reserves `padding` px on its RIGHT and\n * BOTTOM only. Page left/top edges need no gutter.\n *\n * pageWidth/pageHeight = tight bound (max x+width+padding, max y+height+padding).\n * Empty sources → { pageWidth: 0, pageHeight: 0, placements: [], padding }.\n * Throws a plain Error naming the offending source id on a non-finite or <= 0 dimension.\n */\nexport function packAtlas(\n sources: AtlasSource[],\n padding = ATLAS_PADDING,\n): AtlasLayout {\n for (const src of sources) {\n if (!isFinite(src.width) || src.width <= 0) {\n throw new Error(\n `packAtlas: source \"${src.id}\" has invalid width ${src.width}`,\n );\n }\n if (!isFinite(src.height) || src.height <= 0) {\n throw new Error(\n `packAtlas: source \"${src.id}\" has invalid height ${src.height}`,\n );\n }\n }\n\n if (sources.length === 0) {\n return { pageWidth: 0, pageHeight: 0, placements: [], padding };\n }\n\n // Stable sort by id so identical inputs always produce the same layout.\n const sorted = sources\n .slice()\n .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));\n\n // Target page width: ceil(sqrt(total padded area)), raised to a sane minimum.\n const totalArea = sorted.reduce(\n (sum, s) => sum + (s.width + padding) * (s.height + padding),\n 0,\n );\n const targetWidth = Math.max(\n Math.ceil(Math.sqrt(totalArea)),\n // Ensure at least the widest single source fits.\n Math.max(...sorted.map((s) => s.width + padding)),\n );\n\n const placements: AtlasPlacement[] = [];\n let shelfX = 0;\n let shelfY = 0;\n let shelfHeight = 0; // tallest padded item in the current row\n\n for (const src of sorted) {\n const paddedW = src.width + padding;\n const paddedH = src.height + padding;\n\n // Wrap to next row when the item doesn't fit on the current shelf.\n if (shelfX > 0 && shelfX + paddedW > targetWidth) {\n shelfY += shelfHeight;\n shelfX = 0;\n shelfHeight = 0;\n }\n\n placements.push({\n id: src.id,\n x: shelfX,\n y: shelfY,\n width: src.width,\n height: src.height,\n });\n\n shelfX += paddedW;\n if (paddedH > shelfHeight) shelfHeight = paddedH;\n }\n\n // Tight page bounds: max right/bottom padded edge across all placements.\n let pageWidth = 0;\n let pageHeight = 0;\n for (const p of placements) {\n const right = p.x + p.width + padding;\n const bottom = p.y + p.height + padding;\n if (right > pageWidth) pageWidth = right;\n if (bottom > pageHeight) pageHeight = bottom;\n }\n\n return { pageWidth, pageHeight, placements, padding };\n}\n\n/**\n * Convert a pixel placement into a UV rect, inset by `insetPx` on all four\n * edges and clamped to [0, 1] so the validator's bounds check always passes.\n */\nexport function uvRectFor(\n placement: AtlasPlacement,\n page: { width: number; height: number },\n insetPx = UV_INSET_PX,\n): IkiUvRect {\n const px = placement.x + insetPx;\n const py = placement.y + insetPx;\n const pw = placement.width - insetPx * 2;\n const ph = placement.height - insetPx * 2;\n\n const x = Math.max(0, px / page.width);\n const y = Math.max(0, py / page.height);\n const width = Math.min(1 - x, Math.max(0, pw / page.width));\n const height = Math.min(1 - y, Math.max(0, ph / page.height));\n\n return { x, y, width, height };\n}\n","import type {\n IkiMatrixDeformer,\n IkiMesh,\n IkiModel,\n IkiPart,\n IkiWarpDeformer,\n} from \"@ikijs/format\";\n\n/**\n * Pure, DOM-free factory helpers for creating new model objects from scratch.\n * Kept separate from `commands.ts` (which is edit-only) so that the \"add item\"\n * use-case has a dedicated, testable home with no mutation concerns.\n *\n * All factories accept the live model so they can derive a collision-free id and\n * a sensible draw-order / grid span without hard-coding sample assumptions.\n */\n\n/**\n * Return the first collision-free id in the shared part+deformer namespace.\n * Parts and deformers share a flat id namespace (validate.ts:692-706), so we\n * scan both arrays. Returns `base` if unused; otherwise tries `${base}_2`,\n * `${base}_3`, … until a free slot is found.\n *\n * Parameters are a separate namespace and are NOT included in the scan.\n */\nfunction generateUniqueId(model: IkiModel, base: string): string {\n const used = new Set<string>();\n for (const p of model.parts) {\n used.add(p.id);\n }\n for (const d of model.deformers ?? []) {\n used.add(d.id);\n }\n\n if (!used.has(base)) return base;\n\n let n = 2;\n while (true) {\n const candidate = `${base}_${n}`;\n if (!used.has(candidate)) return candidate;\n n++;\n }\n}\n\n/**\n * Generate the flat `[x0,y0, x1,y1, …]` rest-grid control points for a\n * regular axis-aligned lattice with `(cols+1)*(rows+1)` points, row-major.\n *\n * Row 0 is the TOP (y = maxY); y strictly decreases with row index.\n * Column 0 is left (x = minX); x strictly increases with column index.\n * This ordering satisfies the `IkiWarpGrid` rest-grid invariant\n * (packages/format/src/types.ts:178-196) required by the validator and\n * the engine's grid-sampling code.\n */\nfunction generateRegularGridPoints(\n cols: number,\n rows: number,\n minX: number,\n maxX: number,\n minY: number,\n maxY: number,\n): number[] {\n const pts: number[] = [];\n for (let row = 0; row <= rows; row++) {\n const t = row / rows;\n const y = maxY - t * (maxY - minY); // maxY at row 0, minY at row `rows`\n for (let col = 0; col <= cols; col++) {\n const s = col / cols;\n const x = minX + s * (maxX - minX); // minX at col 0, maxX at col `cols`\n pts.push(x, y);\n }\n }\n return pts;\n}\n\n/**\n * Create a minimal valid part with a collision-free id and a paint order one\n * above the current top-most part so it is immediately visible in the viewport.\n * Uses a distinct non-white blue tint so it is distinguishable from the canvas\n * background without requiring a texture.\n *\n * No optional keys (`texture`, `mesh`, `deformer`, `bindings`, `warps`) are set\n * — the format treats all of them as absent by default.\n */\nexport function createDefaultPart(model: IkiModel): IkiPart {\n const order = model.parts.length\n ? Math.max(...model.parts.map((p) => p.order)) + 1\n : 0;\n\n return {\n id: generateUniqueId(model, \"part\"),\n color: [0.45, 0.6, 0.85, 1],\n width: 150,\n height: 150,\n transform: { x: 0, y: 0 },\n order,\n };\n}\n\n/**\n * Create a minimal valid matrix deformer rooted at the canvas origin.\n * `kind` is omitted because the format treats its absence as \"matrix\" (the\n * default), keeping the serialised model compact.\n */\nexport function createDefaultMatrixDeformer(\n model: IkiModel,\n): IkiMatrixDeformer {\n return {\n id: generateUniqueId(model, \"deformer\"),\n pivot: { x: 0, y: 0 },\n };\n}\n\n/**\n * Create a regular grid mesh in part LOCAL space (±0.5 unit frame).\n *\n * Vertices span x ∈ [-0.5, 0.5] and y ∈ [-0.5, 0.5] (+y up, engine convention).\n * Row 0 is the TOP of the grid (y = +0.5); row index increases downward.\n * UVs are unit-square base coordinates: u = col/cols (0..1 left→right),\n * v = row/rows (0..1 top→bottom). The top row maps to v=0 because v and y\n * run in opposite directions — keeps textures upright without a post-flip.\n * The UV-to-texture remap (atlas rect) is applied later in SetPartMesh, not here.\n *\n * Index winding per cell: [BL, BR, TL] then [TL, BR, TR], matching the engine's\n * implicit-quad convention (see examples/editor/src/mesh-generator.ts).\n *\n * Bounds are validated BEFORE any array allocation because this factory runs\n * before SetPartMesh's parseIkiModel — an unbounded count would freeze the\n * editor before the format-level 65536 limit is ever reached.\n */\nexport function createGridMesh(cols: number, rows: number): IkiMesh {\n // Guard: cols/rows must be integers ≥1 and the vertex count must fit within\n // the format limit of 65536 vertices (packages/format/src/validate.ts parseMesh).\n if (\n !Number.isInteger(cols) ||\n cols < 1 ||\n !Number.isInteger(rows) ||\n rows < 1 ||\n (cols + 1) * (rows + 1) > 65536\n ) {\n throw new Error(\n \"createGridMesh: cols and rows must be integers >= 1 with (cols+1)*(rows+1) <= 65536\",\n );\n }\n\n const colVerts = cols + 1;\n const rowVerts = rows + 1;\n\n const vertices: number[] = [];\n const uvs: number[] = [];\n\n // Row 0 is TOP (y = +0.5). Row `rows` is BOTTOM (y = -0.5).\n // Column 0 is left (x = -0.5). Column `cols` is right (x = +0.5).\n for (let row = 0; row < rowVerts; row++) {\n const t = row / rows;\n const y = 0.5 - t; // +0.5 at row 0, -0.5 at row `rows`\n const v = t; // 0 at top row, 1 at bottom row\n\n for (let col = 0; col < colVerts; col++) {\n const s = col / cols;\n const x = -0.5 + s; // -0.5 at col 0, +0.5 at col `cols`\n const u = s; // 0 at left col, 1 at right col\n\n vertices.push(x, y);\n uvs.push(u, v);\n }\n }\n\n // Two triangles per cell: [BL, BR, TL] then [TL, BR, TR].\n const indices: number[] = [];\n for (let row = 0; row < rows; row++) {\n for (let col = 0; col < cols; col++) {\n const tl = row * colVerts + col;\n const tr = row * colVerts + col + 1;\n const bl = (row + 1) * colVerts + col;\n const br = (row + 1) * colVerts + col + 1;\n\n indices.push(bl, br, tl);\n indices.push(tl, br, tr);\n }\n }\n\n return { vertices, uvs, indices };\n}\n\n/**\n * Create a 4×4-cell warp deformer whose rest grid spans a quarter of the\n * canvas in each direction (`±canvas.width/4` × `±canvas.height/4`). For the\n * 1000-unit sample canvas this gives x,y ∈ [−250, 250], which is large enough\n * to cover a typical face part without spilling to the edge.\n *\n * `warps` is omitted — the format treats its absence as \"rest grid only\", so\n * the deformer is immediately usable without authored keyforms.\n * `parent` is omitted — the deformer is placed at root; the caller may\n * reparent it via `SetDeformerParent` after creation.\n */\nexport function createDefaultWarpDeformer(model: IkiModel): IkiWarpDeformer {\n const hx = model.canvas.width / 4;\n const hy = model.canvas.height / 4;\n\n return {\n kind: \"warp\",\n id: generateUniqueId(model, \"warp\"),\n grid: {\n cols: 4,\n rows: 4,\n points: generateRegularGridPoints(4, 4, -hx, hx, -hy, hy),\n },\n };\n}\n","/**\n * Role table, role parsing, bbox→transform math, and model assembly for the\n * AI auto-rig generator. All pure functions — no DOM, no canvas, no\n * crypto.randomUUID.\n *\n * L/R = CHARACTER frame: *_L is the character's left = screen right.\n */\n\nimport {\n IKI_FORMAT_VERSION,\n StandardParameter,\n parseIkiModel,\n type IkiBinding,\n type IkiDeformer,\n type IkiGrid2DWarp,\n type IkiGridWarp,\n type IkiMesh,\n type IkiModel,\n type IkiParameter,\n type IkiPart,\n type IkiWarp,\n type IkiWarpGrid,\n} from \"@ikijs/format\";\n\n// ── Types ────────────────────────────────────────────────────────────────────\n\nexport interface RoleSpec {\n /**\n * Which deformer the part is attached to in the generated rig. `\"none\"`\n * attaches it to nothing, so it holds still in world space; no shipped\n * role uses it.\n */\n deformer: \"faceWarp\" | \"headDeformer\" | \"bodyDeformer\" | \"none\";\n /** Back-to-front draw order. Higher = in front. */\n order: number;\n /** Whether this part gets a warp mesh (true) or is a static quad (false). */\n mesh: boolean;\n /** Present only for eye-family roles. */\n eyeSide?: \"L\" | \"R\";\n}\n\n/**\n * Input contract from the host app to this package's auto-rig functions.\n * Passed in after the host has decoded PNGs, computed alpha bboxes, and\n * mapped filenames to canonical roles.\n */\nexport interface LayerInput {\n /** Canonical role, e.g. \"eye_L\". */\n role: string;\n /** Original file name — used in error messages and as a stable id. */\n fileName: string;\n /** Shared canvas width (all layers have the same canvas size). */\n canvasW: number;\n /** Shared canvas height. */\n canvasH: number;\n /** Alpha-tight bounding box, top-left origin, +y down (image coords). */\n bbox: { x: number; y: number; w: number; h: number };\n /** Cropped image width = bbox.w. */\n cropW: number;\n /** Cropped image height = bbox.h. */\n cropH: number;\n}\n\n// ── Role table ───────────────────────────────────────────────────────────────\n\n/**\n * Single source of truth for every role the auto-rig generator understands.\n * Order values define back-to-front compositing (higher = in front).\n *\n * Eye-family roles use eyeSide to pair blink/gaze bindings correctly:\n * eye_L = character's left eye = screen right side.\n * eye_R = character's right eye = screen left side.\n */\nexport const ROLE_TABLE: Record<string, RoleSpec> = {\n // The silhouette behind the face. It rides the rigid head (not faceWarp,\n // whose grid it would stretch across the shoulders) as a MESH, so its ends\n // can swing on the hair-sway warps. It holds the head's outline STILL\n // through the turn — the head it hangs off no longer travels and it has no\n // turn warp of its own — while the face slides inside it.\n hair_back: { deformer: \"headDeformer\", order: 0, mesh: true },\n // The torso. It rides its own rigid deformer, not the head's: the head turns\n // about the neck pivot while the shoulders follow at BODY_TURN_FOLLOW and\n // follow the head's breath bob, which is what keeps the character from\n // reading as a floating head. Drawn over the back hair so long hair falls\n // behind the shoulders.\n body: { deformer: \"bodyDeformer\", order: 5, mesh: false },\n face: { deformer: \"faceWarp\", order: 10, mesh: true },\n nose: { deformer: \"faceWarp\", order: 15, mesh: true },\n blush_L: { deformer: \"faceWarp\", order: 20, mesh: true },\n blush_R: { deformer: \"faceWarp\", order: 20, mesh: true },\n mouth: { deformer: \"faceWarp\", order: 25, mesh: true },\n // An OPTIONAL second mouth drawing, open. When present the two cross-fade on\n // MouthOpen instead of the closed one being stretched, which is the\n // difference between a portrait rig and one that can lip-sync.\n mouth_open: { deformer: \"faceWarp\", order: 26, mesh: true },\n eye_L: { deformer: \"faceWarp\", order: 30, mesh: true, eyeSide: \"L\" },\n eye_R: { deformer: \"faceWarp\", order: 30, mesh: true, eyeSide: \"R\" },\n iris_L: { deformer: \"faceWarp\", order: 31, mesh: true, eyeSide: \"L\" },\n iris_R: { deformer: \"faceWarp\", order: 31, mesh: true, eyeSide: \"R\" },\n pupil_L: { deformer: \"faceWarp\", order: 32, mesh: true, eyeSide: \"L\" },\n pupil_R: { deformer: \"faceWarp\", order: 32, mesh: true, eyeSide: \"R\" },\n highlight_L: { deformer: \"faceWarp\", order: 33, mesh: true, eyeSide: \"L\" },\n highlight_R: { deformer: \"faceWarp\", order: 33, mesh: true, eyeSide: \"R\" },\n // Upper lashes: an OPTIONAL separate layer ABOVE the iris that folds down to\n // the closed-eye seam (the same crease the white folds to), covering the cut\n // eyeball cleanly. When absent, the white's own fold is the only closed line.\n lash_L: { deformer: \"faceWarp\", order: 34, mesh: true, eyeSide: \"L\" },\n lash_R: { deformer: \"faceWarp\", order: 34, mesh: true, eyeSide: \"R\" },\n brow_L: { deformer: \"faceWarp\", order: 40, mesh: true },\n brow_R: { deformer: \"faceWarp\", order: 40, mesh: true },\n // Front hair rides faceWarp (mesh) so it follows the head-turn curvature with\n // the face instead of detaching as a rigid blob; its bbox joins the faceWarp\n // grid union so the grid covers it.\n hair_front: { deformer: \"faceWarp\", order: 50, mesh: true },\n};\n\n/**\n * Roles the generator requires. BOTH eyes are mandatory because the rig pairs\n * left/right blink parameters — a one-eyed rig would produce mismatched bindings.\n */\nexport const REQUIRED_ROLES = [\"face\", \"eye_L\", \"eye_R\", \"mouth\"] as const;\n\n// ── Alias map ────────────────────────────────────────────────────────────────\n\n/**\n * Minimal spelling-variant aliases → canonical role.\n * Only covers real-world variants; grow this only when you have evidence.\n */\nconst ALIAS_MAP: Record<string, string> = {\n eyebrow_L: \"brow_L\",\n eyebrow_R: \"brow_R\",\n eye_white_L: \"eye_L\",\n eye_white_R: \"eye_R\",\n};\n\n// ── normalizeRole ─────────────────────────────────────────────────────────────\n\n/**\n * Convert a raw filename to a canonical role key:\n * 1. Strip file extension.\n * 2. Lowercase.\n * 3. Collapse hyphens and spaces to underscores.\n * 4. Uppercase a trailing `_l` or `_r` side suffix → `_L` / `_R`.\n * 5. Apply alias map for known spelling variants.\n *\n * Examples:\n * \"Eye-L.png\" → \"eye_L\"\n * \"Brow_R.png\" → \"brow_R\"\n * \"eyebrow_L.png\" → \"brow_L\"\n */\nexport function normalizeRole(raw: string): string {\n // Strip extension\n const noExt = raw.replace(/\\.[^.]+$/, \"\");\n // Lowercase, then collapse hyphens/spaces → underscores\n const collapsed = noExt.toLowerCase().replace(/[-\\s]+/g, \"_\");\n // Uppercase trailing _l / _r side suffix\n const sided = collapsed.replace(\n /_([lr])$/,\n (_, s: string) => `_${s.toUpperCase()}`,\n );\n // Alias map\n return ALIAS_MAP[sided] ?? sided;\n}\n\n// ── assertRoleSet ─────────────────────────────────────────────────────────────\n\n/**\n * Single home for the unknown/duplicate/required role contract. Takes CANONICAL\n * role names (already normalized). Throws on:\n * - Unknown role (not in ROLE_TABLE)\n * - Duplicate role\n * - Missing required role\n *\n * The \"unknown role\" message from this function is fileName-free. That is\n * intentional: callers that lack a fileName (e.g. a future\n * `validateLayerInputs` that receives pre-normalized roles) get a useful error\n * without needing to pre-check. Callers that DO have the original fileName\n * (e.g. `parseLayerRoles`) pre-check unknown roles themselves so they can embed\n * the fileName in the message — but that pre-check is an enrichment, not a\n * requirement for correctness.\n */\nexport function assertRoleSet(roles: string[]): void {\n const seen = new Set<string>();\n for (const role of roles) {\n if (!(role in ROLE_TABLE)) {\n throw new Error(`auto-rig: unknown role \"${role}\"`);\n }\n if (seen.has(role)) {\n throw new Error(`auto-rig: duplicate role \"${role}\"`);\n }\n seen.add(role);\n }\n for (const required of REQUIRED_ROLES) {\n if (!seen.has(required)) {\n throw new Error(`auto-rig: missing required role \"${required}\"`);\n }\n }\n}\n\n// ── parseLayerRoles ───────────────────────────────────────────────────────────\n\n/**\n * Map an array of raw filenames to canonical `{ role, fileName }` pairs.\n *\n * Steps:\n * 1. Normalize each filename → role (normalizeRole).\n * 2. Eagerly check each role against ROLE_TABLE — unknown roles throw early\n * with the offending fileName included in the message.\n * 3. Call assertRoleSet to check duplicates + required roles.\n *\n * Throws a path-qualified Error on any contract violation.\n */\nexport function parseLayerRoles(\n fileNames: string[],\n): { role: string; fileName: string }[] {\n const pairs = fileNames.map((fileName) => {\n const role = normalizeRole(fileName);\n if (!(role in ROLE_TABLE)) {\n throw new Error(\n `auto-rig: unknown role \"${role}\" from file \"${fileName}\"`,\n );\n }\n return { role, fileName };\n });\n\n assertRoleSet(pairs.map((p) => p.role));\n return pairs;\n}\n\n// ── bboxToTransform ───────────────────────────────────────────────────────────\n\n/**\n * Convert an alpha bounding box (image coordinates, +y down, top-left origin)\n * to a model-space translation (model coordinates, +y up, canvas-center origin).\n *\n * x = bbox.x + bbox.w/2 - canvasW/2 (center of bbox relative to canvas center)\n * y = canvasH/2 - (bbox.y + bbox.h/2) (flip axis: image +y down → model +y up)\n *\n * Result is NOT rounded — fractional .5 values must be preserved to avoid\n * sub-pixel jitter in blink/gaze animations when the eye center falls between\n * two canvas pixels.\n *\n * @param partLabel Optional label for error messages (role or part id).\n */\nexport function bboxToTransform(\n bbox: { x: number; y: number; w: number; h: number },\n canvasW: number,\n canvasH: number,\n partLabel?: string,\n): { x: number; y: number } {\n if (bbox.w <= 0 || bbox.h <= 0) {\n throw new Error(`auto-rig: empty bbox for ${partLabel ?? \"layer\"}`);\n }\n const x = bbox.x + bbox.w / 2 - canvasW / 2;\n const y = canvasH / 2 - (bbox.y + bbox.h / 2); // flip: image +y-down → model +y-up\n return { x, y };\n}\n\n// ── validateLayerInputs ───────────────────────────────────────────────────────\n\n/**\n * Validate a LayerInput array before assembly. Called first inside\n * `generateIkiFromLayerSet` — the public API validates before deriving anything.\n *\n * Checks (in order):\n * 1. Non-empty layer list.\n * 2. Unknown/duplicate/required role contract via `assertRoleSet` (single home).\n * 3. Non-positive bbox.w, bbox.h, cropW, cropH per layer.\n * 4. Per-layer canvas size vs. the supplied `canvas` argument.\n * Matching every layer to the `canvas` arg inherently guarantees all layers\n * agree with each other — no separate peer-comparison loop is needed.\n *\n * Validates `layer.role` DIRECTLY (not via fileName). A caller could supply\n * `fileName:\"face.png\"` with `role:\"bad_role\"` — a filename check would miss it.\n *\n * Throws a plain `Error` with a path-qualified message on the first violation.\n */\nexport function validateLayerInputs(\n layers: LayerInput[],\n canvas: { width: number; height: number },\n): void {\n if (layers.length === 0) {\n throw new Error(\"auto-rig: validateLayerInputs: layers must not be empty\");\n }\n\n // Unknown / duplicate / required — single home for this contract\n assertRoleSet(layers.map((l) => l.role));\n\n for (const layer of layers) {\n const { role, bbox, cropW, cropH, canvasW, canvasH } = layer;\n if (bbox.w <= 0) {\n throw new Error(\n `auto-rig: validateLayerInputs: role \"${role}\" has non-positive bbox.w (${bbox.w})`,\n );\n }\n if (bbox.h <= 0) {\n throw new Error(\n `auto-rig: validateLayerInputs: role \"${role}\" has non-positive bbox.h (${bbox.h})`,\n );\n }\n if (cropW <= 0) {\n throw new Error(\n `auto-rig: validateLayerInputs: role \"${role}\" has non-positive cropW (${cropW})`,\n );\n }\n if (cropH <= 0) {\n throw new Error(\n `auto-rig: validateLayerInputs: role \"${role}\" has non-positive cropH (${cropH})`,\n );\n }\n if (canvasW !== canvas.width || canvasH !== canvas.height) {\n throw new Error(\n `auto-rig: validateLayerInputs: role \"${role}\" canvas size (${canvasW}×${canvasH}) does not match canvas arg (${canvas.width}×${canvas.height})`,\n );\n }\n }\n}\n\n// ── generateGridPoints ────────────────────────────────────────────────────────\n\n/**\n * Generate the flat `[x0,y0, x1,y1, …]` rest-grid control points for a\n * regular axis-aligned lattice with `(cols+1)*(rows+1)` points, row-major.\n *\n * Row 0 is the TOP (y = maxY); y strictly decreases with row index.\n * Column 0 is left (x = minX); x strictly increases with column index.\n * This ordering satisfies `checkGridRegularity` in the format validator.\n *\n * Local copy — do NOT import the private `generateRegularGridPoints` from\n * factories.ts; that helper is private to this package's factory layer.\n */\nexport function generateGridPoints(\n cols: number,\n rows: number,\n minX: number,\n maxX: number,\n minY: number,\n maxY: number,\n): number[] {\n const pts: number[] = [];\n for (let row = 0; row <= rows; row++) {\n const t = row / rows;\n const y = maxY - t * (maxY - minY); // maxY at row 0, minY at row `rows`\n for (let col = 0; col <= cols; col++) {\n const s = col / cols;\n const x = minX + s * (maxX - minX); // minX at col 0, maxX at col `cols`\n pts.push(x, y);\n }\n }\n return pts;\n}\n\n// ── createPixelGridMesh ───────────────────────────────────────────────────────\n\n/**\n * Create a regular grid mesh in PIXEL space, with the local origin at the\n * crop center (matching the `feature(...)` convention in sample-model.ts).\n *\n * Callers set `part.width=1, part.height=1` so the engine's scale pipeline is\n * bypassed — the pixel coordinates ARE the final geometry, positioned only by\n * `part.transform`. scaleX/scaleY bindings then scale about each part's own\n * center without an additional unit-to-pixel conversion step.\n *\n * Vertices span x ∈ [-w/2, w/2] and y ∈ [-h/2, h/2] (+y up, engine convention).\n * Row 0 is the TOP of the grid (y = +h/2); row index increases downward.\n *\n * UVs are base unit-square coordinates: u = col/cols (0..1 left→right),\n * v = row/rows (0..1 top→bottom). Top row maps to v=0 (v and y run in\n * opposite directions — keeps textures upright). Atlas remapping is the\n * caller's responsibility (e.g. applyAtlas), not done here.\n *\n * Index winding per cell: [BL, BR, TL] then [TL, BR, TR] — same as\n * `createGridMesh` in factories.ts so the engine's implicit-quad convention\n * is preserved.\n */\nexport function createPixelGridMesh(\n cols: number,\n rows: number,\n w: number,\n h: number,\n): IkiMesh {\n const colVerts = cols + 1;\n const rowVerts = rows + 1;\n\n const vertices: number[] = [];\n const uvs: number[] = [];\n\n // Row 0 = TOP (y = +h/2). Row `rows` = BOTTOM (y = -h/2).\n // Col 0 = left (x = -w/2). Col `cols` = right (x = +w/2).\n for (let row = 0; row < rowVerts; row++) {\n const t = row / rows;\n const y = h / 2 - t * h; // +h/2 at row 0, -h/2 at row `rows`\n const v = t; // 0 at top, 1 at bottom\n\n for (let col = 0; col < colVerts; col++) {\n const s = col / cols;\n const x = -w / 2 + s * w; // -w/2 at col 0, +w/2 at col `cols`\n const u = s; // 0 at left, 1 at right\n\n vertices.push(x, y);\n uvs.push(u, v);\n }\n }\n\n // Two triangles per cell: [BL, BR, TL] then [TL, BR, TR]\n const indices: number[] = [];\n for (let row = 0; row < rows; row++) {\n for (let col = 0; col < cols; col++) {\n const tl = row * colVerts + col;\n const tr = row * colVerts + col + 1;\n const bl = (row + 1) * colVerts + col;\n const br = (row + 1) * colVerts + col + 1;\n\n indices.push(bl, br, tl);\n indices.push(tl, br, tr);\n }\n }\n\n return { vertices, uvs, indices };\n}\n\n// ── Mesh density ─────────────────────────────────────────────────────────────\n\n/** Target size of one mesh cell. A part's vertices sample the face-warp grid\n * (and its own per-vertex warps) at this spacing; the GPU is linear between\n * them, so the spacing bounds how smooth a bend can render. 64 px puts two\n * vertices per FACE_GRID_CELLS cell on the hero and eight rows on the back\n * hair for the sway curl. */\nconst MESH_CELL_PX = 64;\n/** Cell-count floor / ceiling per axis: small parts stay at 4×4 (their\n * bakes fold/collapse them, they never bend), a canvas-spanning layer is\n * capped at 81 vertices so per-vertex warps stay cheap. */\nconst MESH_CELLS_MIN = 4;\nconst MESH_CELLS_MAX = 8;\n\n/** Cell counts for a pixel-grid mesh of a `w`×`h` crop: `round(size / MESH_CELL_PX)`\n * per axis, clamped to [MESH_CELLS_MIN, MESH_CELLS_MAX]. Exported for tests. */\nexport function meshCellsFor(\n w: number,\n h: number,\n): { cols: number; rows: number } {\n const cells = (px: number) =>\n Math.max(\n MESH_CELLS_MIN,\n Math.min(MESH_CELLS_MAX, Math.round(px / MESH_CELL_PX)),\n );\n return { cols: cells(w), rows: cells(h) };\n}\n\n/** Cells per axis of the face-warp grid. Bilinear cells render the cylinder\n * bend as a chord: on the hero (821 px across) 4 cells = 205 px chords with\n * a 12 px worst-case sag at full turn on the bangs, 6 cells halve it for\n * ~10 KB of keyforms. The fold guard is the cylinder's own bound and does\n * not depend on this. */\nconst FACE_GRID_CELLS = 6;\n\n// ── Head-turn cylinder constants ─────────────────────────────────────────────\n\n/** Margin between a cylinder's radius and the reach it has to cover: at\n * |local|/radius <= 1/1.2 the asin stays clear of +/-1 and asin(1/1.2) + 30°\n * is still under 90°, so the bend never folds. The nod radius is derived from\n * the grid's vertical reach this way; the turn radius is an input, and\n * radius/factor is the bound `boundedCylinderBend` clamps `local` to. */\nconst HEAD_CYLINDER_RADIUS_FACTOR = 0.6 / 0.5;\n/** Outer keyform stop of the head-turn bake, matching ParamAngleX's range. */\nconst HEAD_TURN_MAX_DEG = 30;\n\n/** Keyform stops of the head-turn / nod bakes, degrees. The engine blends\n * parameter-LINEARLY between stops but the cylinder bend is sin-based, so the\n * stops must be dense enough that the chord stays near the arc: at 15° spacing\n * the worst mid-stop error on the hero's grid is ≈4 px at the outer column\n * (3.7 px at 7.5°, 4.0 px at 22.5°) against 15.5 px with stops at ±30 only.\n * The bake is analytic, so extra stops cost only model bytes. Ascending,\n * symmetric, includes 0 (the rest cell). */\nconst HEAD_TURN_STOPS = [-30, -15, 0, 15, 30] as const;\n\n/**\n * Sideways slide, at full head turn, of a surface sitting one full cylinder\n * radius in front of the rotation axis — the unit of depth parallax.\n *\n * This is exactly the bulk `axisShift` the cylinder bake subtracts back out of\n * the face warp. Pinning it there was right: applied to the face IN FULL it\n * shoved the head off the shoulders. But it is also the whole depth cue, so\n * layers that do NOT sit on the cylinder's axis have to get their own share of\n * it back, scaled by how far in front of (or behind) the axis they sit — the\n * bangs (HAIR_FRONT_DEPTH) and the features on the face (the solved\n * TurnDepths). The head's own travel is not one of those shares: it rides the\n * same bake as a separate uniform slide over every column (`turnSlide`), sized\n * by what the held shell has room for rather than by a depth.\n * `headNodParallaxUnit` is the same quantity on the nod axis.\n *\n * Takes the cylinder radius the face warp was baked with, so the two cannot\n * drift apart: the unit IS that bake's pinned-out `axisShift`.\n */\nexport function headTurnParallaxUnit(radius: number): number {\n return radius * Math.sin(HEAD_TURN_MAX_DEG * (Math.PI / 180));\n}\n\n// ── bakeHeadTurnGridWarpCentered ──────────────────────────────────────────────\n\n/**\n * Bake a cylinder head-turn grid warp for ParamAngleX, center-relative.\n *\n * WHY a cylinder: rotating a flat face mesh looks right head-on but the\n * silhouette doesn't narrow at the sides; projecting each point onto a\n * cylinder and rotating makes the face foreshorten naturally as it turns.\n *\n * HOW (center-relative): the cylinder axis sits at `centerX` (the face center\n * in model space). Each grid point at absolute x has local x = x - centerX,\n * which maps onto the cylinder. After rotating by theta, the new absolute x is:\n * xPrime = centerX + radiusX * sin(asin(localX/radiusX) + theta)\n * dx = xPrime - x, dy = 0\n *\n * At theta=0 the center keyform is all-zero (xPrime === x by identity).\n *\n * The axis column is pinned: the bulk sideways slide a cylinder rotation\n * produces is subtracted out, leaving only the foreshortening. See the comment\n * at the subtraction for why.\n *\n * `radiusX` is the caller's: how round the head reads is a modelling choice,\n * not a property of how far the grid happens to reach. Columns beyond what the\n * radius can carry ride along rigidly — see `boundedCylinderBend`.\n *\n * No production caller since faceWarp moved to the 2D bake; kept as the PURE\n * BEND reference the 2D bake's tests compare their AngleY = 0 row against,\n * keyed on the same `HEAD_TURN_STOPS`. That row equals this bake only at\n * `travel = 0`: the bend is all the shipped row has once its uniform sideways\n * slide is taken back out.\n */\nexport function bakeHeadTurnGridWarpCentered(\n grid: IkiWarpGrid,\n parameter: string,\n centerX: number,\n radiusX: number,\n): IkiGridWarp {\n // Keyform stops (degrees), matching ParamAngleX's −30..30 range.\n const ANGLES = HEAD_TURN_STOPS;\n\n const pointCount = grid.points.length / 2;\n const DEG_TO_RAD = Math.PI / 180;\n\n const keyforms = ANGLES.map((angleDeg) => {\n const theta = angleDeg * DEG_TO_RAD;\n const offsets: number[] = [];\n for (let i = 0; i < pointCount; i++) {\n const dx = boundedCylinderBend(\n grid.points[i * 2] - centerX,\n radiusX,\n theta,\n );\n // dy is zero — cylinder bend only deforms horizontal position.\n offsets.push(dx, 0);\n }\n return { value: angleDeg, offsets };\n });\n\n // keyforms are sorted ascending by construction (HEAD_TURN_STOPS).\n return { parameter, keyforms };\n}\n\n/** Farthest grid point from `center` along one axis (0 = x, 1 = y): the nod\n * radius scales from this so no point's |local|/radius exceeds 1/1.2, the\n * no-fold bound. The turn's radius is its caller's. */\nfunction gridReach(grid: IkiWarpGrid, axis: 0 | 1, center: number): number {\n let reach = 0;\n for (let i = axis; i < grid.points.length; i += 2) {\n reach = Math.max(reach, Math.abs(grid.points[i] - center));\n }\n return reach;\n}\n\n/**\n * Displacement of a point at signed distance `local` from the cylinder axis\n * after the cylinder rotates by `theta`, with the axis column pinned.\n *\n * Rotating a cylinder slides its whole visible surface sideways by\n * RADIUS*sin(theta) — 170px on a 430px-wide face at 30 degrees — on top of the\n * foreshortening. That bulk slide is what shoves the head off the shoulders and\n * detaches the back hair; the foreshortening alone is what reads as a turn.\n * Subtracting it leaves the differential and keeps the offsets monotonic, so no\n * cell folds. Deliberate bulk head motion is added back on its own, where it can\n * be tuned independently — the turn's as the 2D bake's uniform `travel` slide,\n * the nod's as a headDeformer translate — and the bangs get their\n * depth-scaled share of it back through `headTurnParallaxUnit`.\n */\nfunction pinnedCylinderBend(\n local: number,\n radius: number,\n theta: number,\n): number {\n // Clamp local/radius to [-1,1] to keep asin defined at boundary points.\n const alpha = Math.asin(Math.max(-1, Math.min(1, local / radius)));\n return radius * Math.sin(alpha + theta) - local - radius * Math.sin(theta);\n}\n\n/**\n * `pinnedCylinderBend` with the surface bounded to the part of the cylinder it\n * can actually carry: |local| is clamped to radius/HEAD_CYLINDER_RADIUS_FACTOR\n * and the overshoot rides along unchanged.\n *\n * Beyond that bound the surface has wrapped past the cylinder's side and the\n * bend turns back on itself: once alpha + theta passes 90° a point further out\n * lands NEARER the axis than the one inside it, and once the asin saturates\n * every remaining point piles onto the SAME x (the `- local` term cancels).\n * Either way the grid folds. Holding the bound's displacement and adding the\n * overshoot back gives those columns slope 1: they follow the silhouette\n * rigidly, keeping their order and their spacing. Inside the bound nothing\n * changes — a radius scaled from the grid's own reach puts the bound exactly\n * on the outer columns, so the generated rig never leaves the surface.\n */\nfunction boundedCylinderBend(\n local: number,\n radius: number,\n theta: number,\n): number {\n const bound = radius / HEAD_CYLINDER_RADIUS_FACTOR;\n const onSurface = Math.max(-bound, Math.min(bound, local));\n // The return is a DISPLACEMENT, so `local + dx` already carries the\n // overshoot: holding the bound's dx is what makes the outside rigid.\n return pinnedCylinderBend(onSurface, radius, theta);\n}\n\n/** The head's uniform sideways slide at one turn stop: the whole `travel` at\n * ±HEAD_TURN_MAX_DEG, linear in between, none at rest. The bake and\n * `turnColumnMap` share it so the map cannot describe a slide the grid does\n * not carry. */\nfunction turnSlide(travel: number, angleX: number): number {\n return (travel * angleX) / HEAD_TURN_MAX_DEG;\n}\n\n/**\n * Bake the head turn AND nod as one 2D grid warp over AngleX × AngleY.\n *\n * The same pinned cylinder bend as `bakeHeadTurnGridWarpCentered`, applied per\n * axis: dx from the horizontal bend at valuesX[i], dy from a vertical bend at\n * valuesY[j] about `centerY`. The axes are independent (dx depends only on x\n * and the yaw, dy only on y and the pitch), which is the same separable\n * convention the playground's 2D bake ships; the row at AngleY=0 is exactly the\n * 1D bake.\n *\n * The turn takes the caller's `radiusX`; the nod takes its radius from the\n * grid's vertical reach about `centerY`, with the margin factor, so its\n * |local|/radius stays ≤ 1/1.2 and asin(1/1.2) + 30° < 90° whether or not the\n * grid is symmetric about the axis. The turn holds the same no-fold guarantee\n * at any radius through `boundedCylinderBend`. The pitch itself is scaled by\n * NOD_BEND — see that constant for why a full nod is not a full 30° bend.\n *\n * `travel` is the head's own sideways travel at full turn (px at\n * ±HEAD_TURN_MAX_DEG), added to every point's dx as `turnSlide`'s uniform\n * per-stop offset. It belongs in the grid rather than on a headDeformer\n * translate because a rigid head translate carries the hair shell along with\n * it: in the grid only what rides the grid slides, so the face plate and its\n * features travel inside a silhouette the bangs hold still\n * (`bakeHairFrontSilhouetteWarp`), which is what a turned head actually does.\n * Being uniform it bends nothing — the AngleX = 0 cells stay all-zero and dy\n * never sees it — and being linear in AngleX it is the one part of this bake\n * the engine's parameter-linear blend reproduces EXACTLY between the stops:\n * the chord error the HEAD_TURN_STOPS spacing bounds is all the bend's.\n *\n * Layout is the format's row-major `k(i, j) = j * valuesX.length + i`.\n */\nexport function bakeHeadTurnGridWarp2DCentered(\n grid: IkiWarpGrid,\n parameterX: string,\n parameterY: string,\n centerX: number,\n centerY: number,\n radiusX: number,\n travel: number,\n): IkiGrid2DWarp {\n const STOPS = [...HEAD_TURN_STOPS];\n const RADIUS_Y = gridReach(grid, 1, centerY) * HEAD_CYLINDER_RADIUS_FACTOR;\n const pointCount = grid.points.length / 2;\n\n const DEG_TO_RAD = Math.PI / 180;\n const keyforms2d: { offsets: number[] }[] = [];\n for (const angleY of STOPS) {\n const thetaY = angleY * NOD_BEND * DEG_TO_RAD;\n for (const angleX of STOPS) {\n const thetaX = angleX * DEG_TO_RAD;\n const slide = turnSlide(travel, angleX);\n const offsets: number[] = [];\n for (let i = 0; i < pointCount; i++) {\n offsets.push(\n boundedCylinderBend(grid.points[i * 2] - centerX, radiusX, thetaX) +\n slide,\n pinnedCylinderBend(\n grid.points[i * 2 + 1] - centerY,\n RADIUS_Y,\n thetaY,\n ),\n );\n }\n keyforms2d.push({ offsets });\n }\n }\n\n return {\n parameter: parameterX,\n parameterY,\n valuesX: STOPS,\n valuesY: STOPS,\n keyforms2d,\n };\n}\n\n// ── turnColumnMap ─────────────────────────────────────────────────────────────\n\n/** Where the head turn sends each column of a face-warp grid, and back. */\nexport interface TurnColumnMap {\n /** Rest x of every grid column, ascending — row 0 of `grid.points`. */\n restX: number[];\n /** Where the turn puts each of those columns: strictly increasing whenever\n * `restX` is, since the bound and the capped angle rule out a fold. */\n warpedX: number[];\n /** Rest x → turned x, for anything riding the grid. */\n mapX(x: number): number;\n /** The inverse: which rest x lands on `X`. */\n invertX(X: number): number;\n}\n\n/**\n * The head turn as a 1D map on x: where the face-warp grid puts a point at\n * `angleX` — the bend AND the same uniform `turnSlide(travel, angleX)` the\n * bake ships, so a caller passes the travel the rig was baked with or it\n * measures a head the rig never renders. headDeformer adds no turn translate\n * on top of it.\n *\n * It equals the shipped bake AT the `HEAD_TURN_STOPS`, which is where callers\n * should key: between stops the engine blends the keyforms parameter-linearly\n * (the chord the HEAD_TURN_STOPS comment measures), while this is analytic, so\n * off-stop the two differ by that chord error.\n *\n * The bend depends only on a point's x, so a single row of columns describes\n * the whole grid, and the engine's sampling of it is piecewise-linear:\n * `bindPointToRestGrid` puts a point in the cell its rest x falls in and\n * `sampleWarpGrid` lerps between that cell's deformed corners — so between two\n * columns the map is a straight line, and outside the outer columns the\n * clamped (s, t) pin it to the edge column's warped x. `mapX` reproduces that\n * clamp rather than extrapolating, because a caller measuring a silhouette\n * needs where the geometry lands, not where an extended cylinder would put it.\n *\n * `invertX` answers the other direction — \"which rest x has to be here for the\n * turn to land it there\" — which is how a target silhouette becomes grid\n * geometry. It clamps `X` to the warped column range, the only place the\n * inverse is defined.\n *\n * Both directions need the warped columns to stay ordered, which holds while\n * asin(1/HEAD_CYLINDER_RADIUS_FACTOR) + |theta| < 90°, i.e. |angleX| ≲ 33.6°.\n * Past that the outer columns fold and `invertX`'s cell scan would silently\n * pick the wrong cell, so the range is capped at the parameter's own\n * HEAD_TURN_MAX_DEG rather than left to produce a quiet wrong answer.\n */\nexport function turnColumnMap(\n grid: IkiWarpGrid,\n faceCenterX: number,\n radiusX: number,\n angleX: number,\n travel: number,\n): TurnColumnMap {\n if (Math.abs(angleX) > HEAD_TURN_MAX_DEG) {\n throw new Error(\n `auto-rig: turnColumnMap: angleX ${angleX}° is outside the turn's ±${HEAD_TURN_MAX_DEG}° range`,\n );\n }\n const theta = angleX * (Math.PI / 180);\n const slide = turnSlide(travel, angleX);\n const restX: number[] = [];\n const warpedX: number[] = [];\n for (let col = 0; col <= grid.cols; col++) {\n const x = grid.points[col * 2];\n restX.push(x);\n warpedX.push(\n x + boundedCylinderBend(x - faceCenterX, radiusX, theta) + slide,\n );\n }\n\n // Same scan as bindPointToRestGrid: the first cell whose right edge is past\n // x, else the last one, with the within-cell fraction clamped to [0,1].\n const cellFor = (v: number, edges: number[]) => {\n for (let c = 0; c < grid.cols; c++) {\n if (v < edges[c + 1]) return c;\n }\n return grid.cols - 1;\n };\n\n return {\n restX,\n warpedX,\n mapX(x: number): number {\n const c = cellFor(x, restX);\n const s = Math.max(\n 0,\n Math.min(1, (x - restX[c]) / (restX[c + 1] - restX[c])),\n );\n return warpedX[c] + (warpedX[c + 1] - warpedX[c]) * s;\n },\n invertX(X: number): number {\n const c = cellFor(X, warpedX);\n const s = Math.max(\n 0,\n Math.min(1, (X - warpedX[c]) / (warpedX[c + 1] - warpedX[c])),\n );\n return restX[c] + (restX[c + 1] - restX[c]) * s;\n },\n };\n}\n\n// ── Turn targets ─────────────────────────────────────────────────────────────\n\n/**\n * What a head does at full turn, as the cues `measure_turn_reference` reads off\n * a front/turned reference pair. The turn is fitted to these — the cylinder's\n * radius and the features' depths are solved from them, not tuned.\n *\n * Every field is optional: `DEFAULT_TURN_TARGETS` supplies the three a\n * reference measures, and the nose and the mouth derive from the eyes when they\n * are not given.\n *\n * What happens when the layer set cannot reach a target depends on where the\n * target came from. A number the CALLER passed is a measurement, so an\n * unreachable one throws, naming the field and what was on offer. A number this\n * module filled in — a default, or a nose/mouth derived from the eyes — is a\n * style prior, not a promise about this character: it is CLAMPED to what the\n * layer set can do and the rig is built. The alternative is a generator that\n * refuses its own defaults, which would leave a perfectly good layer set with\n * no model at all. `solveTurnModel` reports which fields it clamped.\n */\nexport interface TurnTargets {\n /** How far the eye pair's centre slides toward the far side at full turn, as\n * a fraction of the head's half-width. The sign is ignored: which way is\n * the turn's own business. */\n eyeShift?: number;\n /** The far eye's width over the near eye's at full turn, divided by the same\n * ratio at rest. 1 is no foreshortening; 0 is an edge-on far eye. */\n farEyeRatio?: number;\n /** The head's half-width at full turn over its half-width at rest, as a\n * RENDER measures it — the outermost opaque edge on each side, whichever\n * part owns it. 1 holds the silhouette; below 1 narrows it. The hold that\n * produces it is fitted to it, so a measured one is met or refused, never\n * quietly rendered as something else. */\n silhouetteRatio?: number;\n /** `eyeShift` for the nose. Derived from `eyeShift` when absent. */\n noseShift?: number;\n /** `eyeShift` for the mouth. Derived from `eyeShift` when absent. */\n mouthShift?: number;\n /** The head's own half-width at the eye row, in canvas px: what the shift\n * fractions are fractions OF, and the rest distance the silhouette hold\n * pivots on. Without it both fall back to the face plate's half-width, which\n * is narrower than the head the hair draws, so the shifts land short — an\n * approximation for callers that have no pixels, not a default to prefer. */\n headHalfWidth?: number;\n}\n\n/**\n * A `turnTargets` field this generator will not build a rig from: not a number,\n * outside its range, unreachable on this layer set, or a measured head narrower\n * than the face plate it is drawn around. Every one of them carries a number\n * the CALLER passed, so a host can tell them apart from the invariant breaks the\n * rest of the generator throws and report them as bad input instead of failing.\n */\nexport class TurnTargetError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"TurnTargetError\";\n }\n}\n\n/**\n * The reference the turn is fitted to when the caller measured none of its own:\n * a 3/4 portrait, which `measure_turn_reference` reads as 0.671 / −0.224 /\n * 1.012 (its raw turned far/near iris ratio is 0.71 — 0.67 is that ratio\n * normalised by the same ratio at rest, which is what the field means).\n * The silhouette is held exactly: a 1 % change is inside that tool's own noise.\n */\nexport const DEFAULT_TURN_TARGETS: Readonly<\n Required<Pick<TurnTargets, \"eyeShift\" | \"farEyeRatio\" | \"silhouetteRatio\">>\n> = {\n eyeShift: 0.22,\n farEyeRatio: 0.67,\n silhouetteRatio: 1,\n} as const;\n\n/** Share of the eye pair's slide the nose and the mouth take when a reference\n * measured only the eyes. Both stand proud of the surface the eyes sit on —\n * the nose furthest — so both travel further, in the same proportion the 3/4\n * reference above shows. */\nconst NOSE_SHIFT_SHARE = 1.36;\nconst MOUTH_SHIFT_SHARE = 1.18;\n\n/** Bounds on the ratio targets. Outside them the number is a typo, not a head:\n * a far eye wider than the near one by half already means the pair was\n * measured the wrong way round. */\nconst TURN_RATIO_MAX = 1.5;\nconst SILHOUETTE_RATIO_MIN = 0.5;\n\n/** `TurnTargets` with every target filled in — what the solver fits. */\nexport interface ResolvedTurnTargets {\n eyeShift: number;\n farEyeRatio: number;\n silhouetteRatio: number;\n noseShift: number;\n mouthShift: number;\n /** The caller's measured head half-width, checked. Absent when it has none. */\n headHalfWidth?: number;\n /** The fields the caller did NOT supply: filled from DEFAULT_TURN_TARGETS, or\n * derived from the eyes. Those are the ones the solver may clamp; see\n * TurnTargets. */\n defaulted: ReadonlySet<keyof TurnTargets>;\n}\n\n/** The turn depth of each feature family: the share of the pinned bulk slide\n * (`headTurnParallaxUnit`) it gets back at full turn. Solved, never tuned. */\nexport interface TurnDepths {\n eye: number;\n nose: number;\n mouth: number;\n}\n\n/**\n * Fill in the targets the caller left out and check the ones it gave.\n *\n * The defaults go through the same checks as a caller's numbers — they are\n * targets like any other, and a bad one has to fail where it is written, not\n * in the geometry three functions down. The GEOMETRIC bounds are not checked\n * here: whether a target is reachable is a property of the layer set, so it\n * belongs to `solveTurnModel`.\n */\nexport function resolveTurnTargets(\n targets: TurnTargets = {},\n): ResolvedTurnTargets {\n const eyeShift = targets.eyeShift ?? DEFAULT_TURN_TARGETS.eyeShift;\n const check = (\n field: keyof TurnTargets,\n value: number | undefined,\n ok: (v: number) => boolean,\n expected: string,\n ) => {\n if (value === undefined) return;\n if (!Number.isFinite(value) || !ok(value)) {\n // A DERIVED target says so: the number in the message is not one the\n // caller wrote, and the field to fix is the one it came from. Only the\n // two shares are derived; the rest fall back to a default, which is a\n // number in its own right.\n const derived =\n (field === \"noseShift\" || field === \"mouthShift\") &&\n targets[field] === undefined;\n const from = derived\n ? `, derived from turnTargets.eyeShift (${eyeShift}),`\n : \"\";\n throw new TurnTargetError(\n `auto-rig: turnTargets.${field} (${value})${from} must be ${expected}`,\n );\n }\n };\n\n const farEyeRatio = targets.farEyeRatio ?? DEFAULT_TURN_TARGETS.farEyeRatio;\n const silhouetteRatio =\n targets.silhouetteRatio ?? DEFAULT_TURN_TARGETS.silhouetteRatio;\n const noseShift = targets.noseShift ?? NOSE_SHIFT_SHARE * eyeShift;\n const mouthShift = targets.mouthShift ?? MOUTH_SHIFT_SHARE * eyeShift;\n\n const isShift = (v: number) => Math.abs(v) <= 1;\n const shiftRange = \"a fraction of the head half-width, |value| <= 1\";\n check(\"eyeShift\", eyeShift, isShift, shiftRange);\n check(\n \"farEyeRatio\",\n farEyeRatio,\n (v) => v > 0 && v <= TURN_RATIO_MAX,\n `in (0, ${TURN_RATIO_MAX}]`,\n );\n check(\n \"silhouetteRatio\",\n silhouetteRatio,\n (v) => v >= SILHOUETTE_RATIO_MIN && v <= TURN_RATIO_MAX,\n `in [${SILHOUETTE_RATIO_MIN}, ${TURN_RATIO_MAX}]`,\n );\n check(\"noseShift\", noseShift, isShift, shiftRange);\n check(\"mouthShift\", mouthShift, isShift, shiftRange);\n // headHalfWidth is checked against the layer set's own plate in solveTurnModel,\n // which is where the two meet.\n check(\"headHalfWidth\", targets.headHalfWidth, (v) => v > 0, \"positive\");\n\n const defaulted = new Set<keyof TurnTargets>(\n (\n [\n \"eyeShift\",\n \"farEyeRatio\",\n \"silhouetteRatio\",\n \"noseShift\",\n \"mouthShift\",\n ] as const\n ).filter((field) => targets[field] === undefined),\n );\n return {\n eyeShift,\n farEyeRatio,\n silhouetteRatio,\n noseShift,\n mouthShift,\n headHalfWidth: targets.headHalfWidth,\n defaulted,\n };\n}\n\n// ── Turn landmarks ───────────────────────────────────────────────────────────\n\n/** A feature the turn slides across the face: its rest centre and its width in\n * model px, which is all the cues measure. `y` is optional — only the eye\n * pair's is used, to place the eye row against hair_front's own crop for the\n * bangs' turn-lead correction (see `solveTurnModel`'s `hairFront` param). */\nexport interface TurnLandmark {\n x: number;\n y?: number;\n w: number;\n}\n\n/** The landmarks each solved depth is fitted to. The eye family is a PAIR — the\n * cues are about the two eyes against each other — the other two a single\n * part, kept in arrays so one solver serves all three. */\nexport interface TurnLandmarkSet {\n eye: TurnLandmark[];\n nose: TurnLandmark[];\n mouth: TurnLandmark[];\n}\n\n/**\n * Pick the parts the turn cues are measured on out of a layer set, in absolute\n * model x.\n *\n * The eye pair is the WHITES, even when the layer set has irises. The white is\n * the eye's own extent — the thing that has to stay on the face plate as the\n * pair slides, and the widest part of the stack — while the iris is clipped to\n * it and slides with it, so the two foreshorten together: on the reference\n * character the far/near ratio read off the white and off the iris differ by\n * 0.01. Bounding the slide by the narrower iris instead would put the white's\n * outer edge over the side hair, where the render loses it.\n */\nexport function turnLandmarks(layers: LayerInput[]): TurnLandmarkSet {\n const markOf = (role: string): TurnLandmark | undefined => {\n const layer = layers.find((l) => l.role === role);\n if (!layer) return undefined;\n const t = bboxToTransform(layer.bbox, layer.canvasW, layer.canvasH, role);\n return { x: t.x, y: t.y, w: layer.cropW };\n };\n const pairOf = (left: string, right: string) => {\n const l = markOf(left);\n const r = markOf(right);\n return l && r ? [l, r] : undefined;\n };\n const one = (role: string) => {\n const m = markOf(role);\n return m ? [m] : [];\n };\n return {\n eye: pairOf(\"eye_L\", \"eye_R\") ?? [],\n nose: one(\"nose\"),\n mouth: one(\"mouth\"),\n };\n}\n\n// ── solveTurnDepth ───────────────────────────────────────────────────────────\n\n/** The depth that comes closest to a shift target at one radius. */\nexport interface TurnDepthSolution {\n /** Whether `depth` HITS the target, or the bounds cut it short. */\n reached: boolean;\n /** The depth to use: the target's own solution, or the nearest bound. */\n depth: number;\n /** The shift `depth` actually produces, px (negative = toward the far side). */\n achieved: number;\n /** Every shift this family could have had, px, ascending. */\n attainable: [number, number];\n}\n\n/** Bisection steps for both solvers. The intervals start finite and halve, so\n * 40 is exact in double precision for any radius or depth a head has. */\nconst TURN_BISECT_STEPS = 40;\n\n/** Px slack on `solveTurnDepthSigned`'s own attainable bound, so a target\n * that round-trips through a reported cue and back — e.g. a caller\n * re-submitting the exact `attainable` boundary this function itself\n * reported, cue-scaled by `hh` and back — is not refused by a float ulp of\n * rounding. Far below any physically meaningful px difference. */\nconst TURN_DEPTH_EPS = 1e-6;\n\n/**\n * The depth that slides a landmark family by `targetPx` at full turn —\n * `targetPx` a MAGNITUDE, always toward the far side (see `TurnTargets`'\n * \"the sign is ignored\" contract). The nose/mouth shares and every existing\n * caller of this export want exactly that; `evaluateTurnCandidate`'s eye\n * solve does not (see `solveTurnDepthSigned`), so it calls that directly.\n */\nexport function solveTurnDepth(\n targetPx: number,\n landmarks: TurnLandmark[],\n unit: number,\n map: TurnColumnMap,\n holdEdgeX: number,\n plateEdgeX: number,\n): TurnDepthSolution {\n // The cues are signed toward the far side; the target's own sign is ignored.\n return solveTurnDepthSigned(\n -Math.abs(targetPx),\n landmarks,\n unit,\n map,\n holdEdgeX,\n plateEdgeX,\n );\n}\n\n/**\n * The depth that lands a landmark family's slide exactly on the already-SIGNED\n * `target` (negative toward the far side, positive toward the near one),\n * or the nearer bound when `target` sits outside what any non-negative depth\n * can reach — depth cannot go negative (a feature does not stand BEHIND the\n * surface), so a positive `target` past `atRest` is exactly as unreached as a\n * too-negative one past `atCap`.\n *\n * The part is translated `depth * unit` toward the far side BEFORE its vertices\n * bind to the face grid (that is the order `applyWarpToChild` works in), so\n * what a reference measures is the grid's map OF the translated position, not\n * the translation: `achieved(d) = mean(mapX(x - d*unit) - x)`. The map\n * compresses the far side and stretches the near one, so the two are not the\n * same number, and the far side's compression makes `achieved` shrink faster\n * than `d` grows. It is monotone decreasing in `d` because the map is monotone\n * increasing, which is what makes a bisection valid.\n *\n * The travel has two upper bounds, and the tighter one wins:\n * - the FACE. Every landmark's far edge has to stay on the face plate, whose\n * contour the turn only foreshortens. A feature past it is drawn over the\n * side hair, which bends with the plate and swallows it, or hangs over the\n * cheek's edge with nothing behind it — on the reference character the far\n * eye went 35 px past the contour and the render came back with an eye 17\n * px wide where the solver had promised 53. The reference keeps the far eye\n * whole. Since `mapX` is monotone, staying inside the plate's MAPPED edge\n * is the same as staying inside its rest edge, so the bound is a plain rest\n * distance.\n * - the SILHOUETTE: the far edge's mapped position may not pass the held\n * silhouette edge, and its rest position may not leave the grid, where the\n * engine would clamp it onto the edge column.\n *\n * Both ends of the interval matter, not just the far one: at `d = 0` a landmark\n * already drifts, because the map carries the face's own sideways slide as well\n * as the bend, and the slide is much the larger of the two — on the assembly\n * fixture at −30° the two together drift the eye pair ≈64 px toward the far\n * side, whereas the bend alone would have moved it ≈11 px toward the near one.\n * That drift therefore runs toward the FAR side, and IS the floor under every\n * shift cue: a target asking for LESS far-side shift than that drift does has\n * no non-negative depth either. Neither end is an error here — the solution is\n * reported with `reached: false` and the bound it stopped at, and whether that\n * is a clamp or a failure is the caller's call (see solveTurnModel).\n *\n * `evaluateTurnCandidate`'s eye solve calls this directly instead of going\n * through `solveTurnDepth`'s magnitude wrapper: once the silhouette centre's\n * own drift is folded into the target, that target can legitimately land on\n * either side of zero, and `-Math.abs` would silently answer a different\n * question than the one asked (an eyeShift and its negation producing\n * different rigs). Every other caller (the nose/mouth shares) keeps the\n * magnitude-only contract through the wrapper.\n */\nfunction solveTurnDepthSigned(\n target: number,\n landmarks: TurnLandmark[],\n unit: number,\n map: TurnColumnMap,\n holdEdgeX: number,\n plateEdgeX: number,\n): TurnDepthSolution {\n if (landmarks.length === 0) {\n throw new Error(\"auto-rig: solveTurnDepth: no landmark to slide\");\n }\n const farEdge = Math.min(...landmarks.map((l) => l.x - l.w / 2));\n const floor = Math.max(map.invertX(holdEdgeX), map.restX[0], plateEdgeX);\n // Never negative: a landmark that starts outside a bound cannot be slid back\n // in by a depth, and a depth away from the turn is not a depth.\n const cap = Math.max(0, (farEdge - floor) / unit);\n const achieved = (d: number) =>\n landmarks.reduce((sum, l) => sum + (map.mapX(l.x - d * unit) - l.x), 0) /\n landmarks.length;\n\n const atCap = achieved(cap);\n const atRest = achieved(0);\n const attainable: [number, number] = [atCap, atRest];\n if (target > atRest + TURN_DEPTH_EPS)\n return { reached: false, depth: 0, achieved: atRest, attainable };\n if (target < atCap - TURN_DEPTH_EPS)\n return { reached: false, depth: cap, achieved: atCap, attainable };\n // Within EPS of a bound but past it either way (the ulp this guards\n // against) — clamp before bisecting, rather than searching for a target\n // outside the interval `achieved` can actually produce.\n const clampedTarget = Math.min(Math.max(target, atCap), atRest);\n\n let lo = 0;\n let hi = cap;\n for (let i = 0; i < TURN_BISECT_STEPS; i++) {\n const mid = (lo + hi) / 2;\n if (achieved(mid) > clampedTarget) lo = mid;\n else hi = mid;\n }\n const depth = (lo + hi) / 2;\n return { reached: true, depth, achieved: achieved(depth), attainable };\n}\n\n// ── solveTurnModel ───────────────────────────────────────────────────────────\n\n/** What the turn solve settled on, for a caller that wants to see it — the\n * cues it reached and the targets it had to cut down to reach them. */\nexport interface TurnSolveReport {\n /** Cylinder radius the face-warp bake and the parallax unit were built on. */\n radius: number;\n /** Rest distance from the face centre the silhouette hold pivots on. */\n holdBase: number;\n depths: TurnDepths;\n /** What the rig actually reaches, in the targets' own units. */\n achieved: { eyeShift: number; farEyeRatio: number; silhouetteRatio: number };\n /** Defaulted fields the layer set could not reach, cut down to what it can.\n * Empty when every target was met. */\n clamped: (keyof TurnTargets)[];\n}\n\n/** The turn the targets ask for, or the first CALLER target this layer set\n * cannot reach and what it could have had instead (in that target's own\n * units). Defaults never land in the second branch — they clamp. */\nexport type TurnModelSolution =\n | (TurnSolveReport & {\n unreachable: false;\n /** Where the hold's boundary is sent at each stop. */\n holdEdgeAt: (deg: number) => number;\n /** How far the face slides sideways inside that held boundary at full\n * turn, px — what the face bake and the hold's own column map have to\n * be built with, rather than the plate's own uncapped ask. */\n travel: number;\n })\n | {\n unreachable: true;\n field: keyof TurnTargets;\n /** The caller's own number — never a clamped stand-in. */\n value: number;\n /** What the caller could have asked for instead, inside the field's own\n * accepted domain. Absent only for `silhouetteRatio`, and only when\n * nothing that domain accepts renders on this layer set at all. */\n attainable?: [number, number];\n };\n\n/** Radius sweep, as multiples of the face plate's half-width. The floor is the\n * no-fold margin itself: at it the bend's bound sits exactly on the plate's\n * edge, and below it the plate — features included — leaves the analytic part\n * of the cylinder and rides along rigidly instead of foreshortening. The\n * ceiling is all but a plate: at 8 half-widths the face covers ±7° of the\n * cylinder and the turn is nearly a slide — the far/near ratio it can still\n * produce, 0.93 on the reference character, is the top of the attainable range\n * the solver reports. A head that wants less foreshortening wants none. */\nconst TURN_SWEEP_MAX_FACTOR = 8;\n/** Sample count of that sweep. The ratio curve is smooth in log-radius, so this\n * only has to be dense enough to bracket the target, not to resolve it. */\nconst TURN_SWEEP_SAMPLES = 32;\n/** How far past the plate's own reach a hold edge has to sit, px. The ramp onto\n * the strands runs outward, so a hold edge ON the reach is a zero-width ramp;\n * one pixel of clearance is what makes it a ramp. */\nconst HOLD_CLEARANCE = 1;\n\n/** Sampling of the hold's own usable range, per radius (see `holdRange`). What\n * a render shows moves smoothly with the hold but not monotonically, so the\n * range is sampled densely enough to bracket the turn-back where the travel's\n * own feedback reverses it, not to resolve the ask — the bisection inside the\n * bracketing interval does that. */\nconst TURN_HOLD_SAMPLES = 16;\n/** How many times `holdRange`'s ceiling may chase its own reach. Each pass\n * starts from the previous pass's grid reach and the travel that reach moves\n * with is bounded by the plate's own ask, so it settles in two or three. */\nconst TURN_HOLD_SATURATION_PASSES = 4;\n\n/** A radius that can carry the turn, with everything the fit reads off it. */\ninterface TurnCandidate {\n radius: number;\n /** The far/near eye width ratio it produces, at the eye depth below. */\n ratio: number;\n unit: number;\n /** The sideways travel it carries at full turn, px: `ctx.travel` capped to\n * what this radius' own held shell can swallow (`shellTravelCap`). The face\n * bake and the bangs' hold have to be built with THIS number — `map`\n * already is. */\n travel: number;\n /** Its column map at the −30° stop, where every cue is measured. */\n map: TurnColumnMap;\n holdBase: number;\n holdEdgeAt: (deg: number) => number;\n /** Model x of the held silhouette's far edge at full turn. */\n holdEdgeX: number;\n eye: TurnDepthSolution;\n /** Whether the ratio this radius ships had to be cut to the nearest one a\n * render of it can show. Only a DEFAULTED target ever gets here cut down —\n * a caller-measured one the range cannot render is refused instead. */\n silhouetteClamped: boolean;\n /** How far the silhouette's own centre drifts from the face centre at full\n * turn, against rest — each side's real landing (whichever part owns that\n * edge actually carries it there; see `landingAt`) minus how far its rest\n * x already sat off the face centre. `measure_turn_reference` reads the\n * eye pair against THIS moved centre, not the face centre, so it is what\n * `eye`'s target and `achieved.eyeShift` are corrected by. */\n silhouetteCenterShift: number;\n /** The silhouette ratio a render actually shows: each side's real LANDING\n * span over its rest span (`landingAt`/`restAt`, the same pair\n * `silhouetteCenterShift` reads) — as `measure_turn_reference` would\n * measure it off the two images, and the measure the hold above was FITTED\n * against. It is the target itself whenever one was reachable, so it is\n * what `TurnSolveReport.achieved.silhouetteRatio` reports. */\n renderedSilhouetteRatio: number;\n}\n\n/** Why a radius yielded no candidate, in the blocked target's own terms. */\ntype TurnCandidateMiss =\n | {\n blocked: \"silhouetteRatio\";\n /** The ratios a render of this radius can actually SHOW, ascending —\n * `renderedSilhouetteRatio`'s own measure, which is the measure a\n * caller's own number came from: the narrowest and the widest over the\n * holds this radius can ship (`holdRange`'s samples), clipped to the\n * ratios `resolveTurnTargets` accepts. The rendered ratio is continuous\n * in the hold, so every ratio between them is one the fit lands on the\n * shipped mesh's own terms (`hairFrontLandingAt`, which a raster of it\n * agrees with to a fraction of a pixel), and both ends are values a\n * caller can resubmit and have rigged. Infinity and -Infinity when\n * nothing inside that domain renders here: the neutral pair\n * `sweepTurnRadii`'s own min/max ignore. */\n lower: number;\n upper: number;\n }\n | {\n blocked: \"eyeShift\";\n /** The px interval it offered the eyes instead, ascending. */\n offeredShift: [number, number];\n };\n\n/** Everything a candidate is evaluated against that does not vary with the\n * radius. One pass of the solve holds one of these. */\ninterface TurnSolveContext {\n landmarks: TurnLandmarkSet;\n faceGrid: IkiWarpGrid;\n faceCenterX: number;\n faceHalfWidth: number;\n /** Rest x of the plate's far edge — the slide's hard stop. */\n plateEdgeX: number;\n /** The sideways travel this layer set ASKS the turn for, px at full turn:\n * `headTurnTravel` on its own plate. What a candidate actually carries is\n * this capped to what its own held shell can swallow — see\n * `TurnCandidate.travel`. */\n travel: number;\n /** The caller's measured head half-width, when it has one. */\n headHalfWidth?: number;\n /** What the shift fractions are fractions of, px. */\n hh: number;\n /** The silhouette ratio this pass is solving for. */\n silhouetteRatio: number;\n /** The MAGNITUDE of the eye shift this pass is solving for — `TurnTargets`'\n * own \"the sign is ignored\" contract taken at the door, so a request and\n * its negation reach identically here. */\n eyeShift: number;\n /** Whether an eye shift the bounds cut short is a clamp or a rejection. */\n clampEyeShift: boolean;\n /** Whether the ratio is this generator's own default — carried through as\n * the hold's own ratio and clamped to what the grid can reach — or a\n * caller's render measurement, which the hold is fitted to and which is\n * rejected when no hold renders it. Same split as `clampEyeShift`, for the\n * same reason. */\n clampSilhouetteRatio: boolean;\n /** The eye row's `u` in `bakeHairSwayWarp`'s own `tipShift · u^CURL` shape —\n * 0 at hair_front's root, 1 at its tips, BEFORE the curl exponent, which is\n * applied where this is used. See `solveTurnModel`'s `hairFront` param.\n * Undefined when there is no hair_front layer, or its eye row cannot be\n * placed against it: the silhouette centre then holds to the hold\n * destinations alone. */\n hairFrontLeadFraction?: number;\n /** hair_front's own rest x and crop width/height, for `hairFrontLandingAt`\n * — its own column geometry (`meshCellsFor`), and the silhouette point used\n * when `headHalfWidth` is not measured (see `evaluateTurnCandidate`'s\n * silhouette-centre correction). Undefined when there is no hair_front\n * layer, in which case that correction falls back to the ideal hold\n * destinations, symmetric about the face centre. */\n hairFrontSilhouette?: { x: number; cropW: number; cropH: number };\n /** Every role with an opaque pixel in the eye-row band, per side, each\n * with its OWN rest x there — as the mcp layer measures it (`rowSpansByRole`),\n * a companion to a MEASURED `headHalfWidth`, not a caller-facing target.\n * `evaluateTurnCandidate` takes the OUTERMOST *landing* across a side's own\n * list, not the outermost REST x: which part ends up furthest out after\n * the turn can differ from which one drew furthest out at rest (the bangs'\n * own lead carrying them past a held back-hair edge, say). Absent on the\n * fallback path (no measured head to report edges for), which keeps\n * hair_front's own crop edge as the silhouette point on both sides, as if\n * it always owned them. */\n headEdges?: {\n left: { role: string; x: number }[];\n right: { role: string; x: number }[];\n };\n}\n\n/**\n * The rest x a hair_front vertex is sent to at one turn stop — the three-zone\n * rule `bakeHairFrontSilhouetteWarp` bakes into a per-vertex offset and\n * `evaluateTurnCandidate` reads the composed landing of, factored into one\n * place so the two cannot drift apart:\n *\n * - within `faceHalfWidth` of the face centre, on the plate: exactly where\n * the grid puts it (`map.mapX(x)` — a no-op for anything actually inside\n * the grid, since the map does that on its own; it only bites a plate\n * vertex that sits outside it);\n * - out to `holdBase`: a straight ramp from the plate edge's own mapped\n * destination onto `holdEdge`'s, already capped to the deformed grid's\n * own reach at this stop;\n * - beyond `holdBase`: `holdEdge`'s own displacement, slope 1 in REST x, so\n * the outer strands carry whatever silhouette change was asked for.\n */\nfunction hairFrontHoldTarget(\n x: number,\n faceCenterX: number,\n faceHalfWidth: number,\n holdBase: number,\n holdEdge: [number, number],\n map: TurnColumnMap,\n): number {\n const local = x - faceCenterX;\n const dist = Math.abs(local);\n const side = Math.sign(local);\n const sideIdx = side < 0 ? 0 : 1;\n if (dist <= faceHalfWidth) return map.mapX(x);\n if (dist <= holdBase) {\n const inner = map.mapX(faceCenterX + side * faceHalfWidth);\n const u = (dist - faceHalfWidth) / (holdBase - faceHalfWidth);\n return inner + (faceCenterX + side * holdEdge[sideIdx] - inner) * u;\n }\n return x + side * (holdEdge[sideIdx] - holdBase);\n}\n\n/**\n * hair_front's own silhouette landing at an ARBITRARY rest point — not the\n * analytic hold∘lead∘grid at that point, but what the renderer actually draws\n * there, on the mesh the bake ships. That mesh has only `meshCellsFor`'s own\n * cell counts (8 columns on the hero, ~82 px apart), every vertex carries the\n * hold's own inverse (`hairFrontHoldTarget` through `invertX`, which clamps\n * onto the deformed grid's edge) plus the row's share of the bangs' turn lead,\n * and the GPU interpolates the FINAL, already-mapped positions of the vertices\n * around a point — so this maps each of the four surrounding vertices through\n * the grid first and interpolates those, column fraction and row fraction\n * alike. Interpolating anything earlier — the pre-bind xs, or an analytic lead\n * at a row the mesh has no vertex on — is a different number wherever a\n * column-map segment break falls between two columns, or the lead's own\n * `u^HAIR_SWAY_CURL` curve bends between two rows.\n *\n * `leadTipShift` is the bangs' full tip lead in px (positive; the −30° keyform\n * carries `0 − tipShift·u^CURL`, see `bakeHairSwayWarp`) and `leadRow` the\n * measured row's own fraction of the crop's height, 0 at the pinned root and 1\n * at the tips — the two the shipped lead warp is built from, rather than one\n * pre-sampled offset, because the rows the GPU interpolates between are the\n * mesh's, not the measured row itself.\n *\n * The point need not be inside hair_front's own crop at all — past its outer\n * column this clamps to that column's own landing (`t` is clamped below), the\n * same \"beyond the mesh's own edge\" answer `invertX` gives a real vertex there.\n */\nfunction hairFrontLandingAt(\n x: number,\n hairFront: { x: number; cropW: number; cropH: number },\n faceCenterX: number,\n faceHalfWidth: number,\n holdBase: number,\n cappedHoldEdge: [number, number],\n map: TurnColumnMap,\n leadTipShift: number,\n leadRow: number,\n): number {\n const { cols, rows } = meshCellsFor(hairFront.cropW, hairFront.cropH);\n const preBindAt = (col: number): number => {\n const colX =\n hairFront.x - hairFront.cropW / 2 + (col / cols) * hairFront.cropW;\n const target = hairFrontHoldTarget(\n colX,\n faceCenterX,\n faceHalfWidth,\n holdBase,\n cappedHoldEdge,\n map,\n );\n return map.invertX(target);\n };\n // Row 0 is the pinned root (createPixelGridMesh's own top row), row `rows`\n // the tips, and the lead each carries is the bake's own `u^CURL` at that\n // row's `u = row / rows`.\n const leadAt = (row: number): number =>\n -leadTipShift * Math.pow(row / rows, HAIR_SWAY_CURL);\n const bracket = (v: number, cells: number) => {\n const lo = Math.max(0, Math.min(cells, Math.floor(v)));\n return {\n lo,\n hi: Math.max(0, Math.min(cells, lo + 1)),\n t: Math.max(0, Math.min(1, v - lo)),\n };\n };\n const col = bracket(\n ((x - hairFront.x + hairFront.cropW / 2) / hairFront.cropW) * cols,\n cols,\n );\n const row = bracket(leadRow * rows, rows);\n const landingAt = (c: number, r: number) =>\n map.mapX(preBindAt(c) + leadAt(r));\n // `col.t` runs left→right and `row.t` top→bottom, the axes the mesh's own\n // cell is built on, so its TL→BR diagonal is `col.t === row.t` and the\n // lower-left triangle is the one with `col.t <= row.t`.\n const tl = landingAt(col.lo, row.lo);\n const br = landingAt(col.hi, row.hi);\n if (col.t <= row.t) {\n const bl = landingAt(col.lo, row.hi);\n return tl + row.t * (bl - tl) + col.t * (br - bl);\n }\n const tr = landingAt(col.hi, row.lo);\n return tl + col.t * (tr - tl) + row.t * (br - tr);\n}\n\n/**\n * The most sideways travel a MEASURED shell can swallow at this radius: how far\n * the face may slide before the plate's own edge reaches the hold's boundary.\n *\n * Read off the BEND ALONE (`bendOnlyMapAt` — the map at a stop with no travel\n * in it), the slide being what is solved for here: at each turned stop the\n * plate's two edges land either side of the face centre — `plateReachAt`'s own\n * two numbers, kept SIGNED — and the slide then pushes the edge on the side it\n * moves toward further out while pulling the other one in. So only the TOWARD\n * edge binds. A max-absolute reach would bind on the near edge instead, which\n * a tight bend throws wide precisely BEFORE the slide pulls it back inside the\n * shell, and would refuse the radius the slide was about to rescue.\n *\n * Each stop carries |deg|/30 of the travel, so the room one stop has left is\n * scaled back up by 30/|deg| to say what it allows at FULL turn, and the\n * tightest stop wins. Floored at 0: a plate already reaching past the shell\n * cannot be slid any further out.\n *\n * `holdEdgeAt` is asked for the boundary on the side the slide moves TOWARD,\n * because that is the only side the plate can breach: a measured shell need\n * not sit centred on the face (`TurnSolveContext.headEdges` records each\n * side's own extreme), and a shell 300 px out on one side and 500 on the\n * other has 300 px of room for a −30° slide however much it has for a +30°\n * one. Taking one symmetric number for both directions would let the plate\n * slide straight out of the narrow side and become the silhouette there.\n *\n * `askedTravel` is only read for the slide's SIGN at each stop, which is the\n * turn's own; the cap it returns is the whole ceiling, for the caller to take\n * a minimum with.\n */\nfunction shellTravelCap(\n faceCenterX: number,\n faceHalfWidth: number,\n askedTravel: number,\n bendOnlyMapAt: (deg: number) => TurnColumnMap,\n holdEdgeAt: (deg: number, side: -1 | 1) => number,\n): number {\n let cap = Infinity;\n for (const deg of HEAD_TURN_STOPS) {\n if (deg === 0) continue; // nothing slides at rest, so nothing to cap\n const map = bendOnlyMapAt(deg);\n const slideSign: -1 | 1 = turnSlide(askedTravel, deg) < 0 ? -1 : 1;\n // The further-out of the two edge landings ALONG the slide's own direction:\n // the toward edge's own distance from the centre, the away edge's negated.\n // The bend is monotone and pinned on the face centre, so exactly one edge\n // ever lands on the slide's own side and this max always resolves to it —\n // it is two-sided to mirror `plateReachAt`'s own shape, not to guard a\n // case the geometry allows.\n const towardReach = Math.max(\n ...[-1, 1].map(\n (side) =>\n slideSign *\n (map.mapX(faceCenterX + side * faceHalfWidth) - faceCenterX),\n ),\n );\n cap = Math.min(\n cap,\n (Math.max(0, holdEdgeAt(deg, slideSign) - HOLD_CLEARANCE - towardReach) *\n HEAD_TURN_MAX_DEG) /\n Math.abs(deg),\n );\n }\n return cap;\n}\n\n/** Everything ONE hold ratio settles inside `evaluateTurnCandidate`: the\n * travel that ratio leaves the sliding plate, the map that carries it, and\n * where each side's silhouette lands through that map. The ratio the rig\n * ships is picked among these — fitted until `renderedSilhouetteRatio` is the\n * target, whether that target was measured or defaulted. */\ninterface TurnHoldEval {\n /** The hold's own ratio: where its boundary is sent at full turn, as a\n * fraction of the rest distance it keeps. */\n ratio: number;\n /** The sideways travel it leaves the face at full turn, px. */\n travel: number;\n /** Its column map at the −30° stop, where every cue is measured. */\n map: TurnColumnMap;\n holdBase: number;\n holdEdgeAt: (deg: number) => number;\n /** Whether any stop sends the hold edge inside the plate's own reach, which\n * runs the ramp between them backwards — never acceptable, see\n * `bakeHairFrontSilhouetteWarp`. */\n folds: boolean;\n /** The ratio at which the tightest stop's hold edge would exactly clear the\n * plate ON THIS HOLD's own map — a seed for the range's floor, not the\n * floor itself, see `holdRange`. */\n foldLowerBound: number;\n /** How far the DEFORMED grid reaches either side of the face centre at full\n * turn (`TurnColumnMap.warpedX`'s edges — the bound `invertX` clamps to),\n * which is where the hold's own destinations are capped. */\n gridReach: [number, number];\n silhouetteCenterShift: number;\n renderedSilhouetteRatio: number;\n /** The silhouette's own rest span, px: what the rendered ratio is a\n * fraction of, and so the scale a px tolerance on it converts through. */\n restSpan: number;\n}\n\n/**\n * One radius, evaluated against the cues: the candidate it yields, or which\n * target blocked it and what it could have done instead.\n *\n * Everything hangs off ONE number here, the hold's own ratio: it sizes the\n * face's sideways travel (`shellTravelCap` measures the shell's room against\n * where the hold sends its boundary), and that travel builds the map every\n * landing is read through — so a slide that pulls an over-bent near edge back\n * inside the shell rescues a radius the bend alone would have folded, and a\n * bend that folds on its own still loses one. `evaluateHold` is that whole\n * chain for one ratio, and nothing outside it may assume a hold.\n *\n * The silhouette target is a measurement of a RENDER — the outermost opaque\n * edge on each side, whichever part owns it — so the hold is FITTED to it\n * rather than set from it: `holdRange` samples the holds this radius can ship\n * and the fit bisects the sampled interval that straddles the ask, which lands\n * on it exactly. What a render shows is NOT monotone in the hold (see\n * `holdRange`), so no sample's orientation is assumed. Where the target came\n * from decides only what happens when the range cannot render it: a\n * CALLER-measured one is refused, naming that range, and a DEFAULTED one takes\n * the nearest end of it and is reported in `TurnSolveReport.clamped`. Folding\n * is never acceptable either way.\n */\nfunction evaluateTurnCandidate(\n ctx: TurnSolveContext,\n radius: number,\n): TurnCandidate | TurnCandidateMiss {\n // The travel is settled BEFORE the map that carries it, the map being built\n // from it: the face slides INSIDE a shell the bangs hold, so what it may\n // slide is what that shell has room for — measured on the bend alone.\n const bendOnlyMapAt = (deg: number) =>\n turnColumnMap(ctx.faceGrid, ctx.faceCenterX, radius, deg, 0);\n // The measured silhouette's own rest extreme on one side, when `headEdges`\n // recorded one there. Read both by the travel cap below — which side of the\n // shell the sliding plate has to stay inside — and by `restAt`, so the two\n // cannot disagree about where the shell's boundary actually is.\n const measuredEdgeAt = (side: -1 | 1): number | undefined => {\n const candidates = side < 0 ? ctx.headEdges?.left : ctx.headEdges?.right;\n if (candidates === undefined || candidates.length === 0) return undefined;\n const xs = candidates.map((c) => c.x);\n return side < 0 ? Math.min(...xs) : Math.max(...xs);\n };\n // That side's own half-width: a measured shell can sit off centre, and the\n // narrow side is the one the plate can breach. Without an edge list there\n // the measured half-width stands in on both sides, as it always did.\n const shellHalfWidthAt = (side: -1 | 1): number => {\n const edge = measuredEdgeAt(side);\n return edge === undefined\n ? ctx.headHalfWidth!\n : side * (edge - ctx.faceCenterX);\n };\n const unit = headTurnParallaxUnit(radius);\n // The bangs' own root-pinned turn lead (bakeHairSwayWarp, shared with the\n // sway): the shipped warp's own full TIP shift, which each row takes its\n // `u^HAIR_SWAY_CURL` share of. It moves every row toward the far side by\n // that row's amount regardless of which side of the axis a column sits on —\n // a raw offset in REST x, added before the hold's own inverse is re-mapped\n // inside `hairFrontLandingAt`, exactly like `bakeHairSwayWarp` and\n // `bakeHairFrontSilhouetteWarp` sum their offsets on the shipped mesh; the\n // rows the renderer interpolates between are the MESH's, so the row fraction\n // goes down with it rather than a lead pre-sampled at the measured row.\n // Only hair_front leads — a role `landingOfRole` sends down any other path\n // does not use this at all. The hold does not move it, so it is settled\n // once, outside the fit.\n const leadTipShift =\n ctx.hairFrontLeadFraction === undefined ? 0 : HAIR_FRONT_DEPTH * unit;\n const leadRow = ctx.hairFrontLeadFraction ?? 0;\n\n const evaluateHold = (ratio: number): TurnHoldEval => {\n // The boundary stays put and its DESTINATION moves: the full ratio at the\n // outer stops, none of it at rest, linear in between.\n const holdEdgeFrom = (base: number) => (deg: number) =>\n base * (1 + ((ratio - 1) * Math.abs(deg)) / HEAD_TURN_MAX_DEG);\n // A measured head IS the hold's boundary, so the plate has to stay inside\n // it once slid — `shellTravelCap` is what keeps it there. A hold base\n // derived from the plate instead (below) is measured on the SLID maps and\n // clears the slid plate by construction, so there the whole ask stands.\n const travel =\n ctx.headHalfWidth === undefined\n ? ctx.travel\n : Math.min(\n ctx.travel,\n shellTravelCap(\n ctx.faceCenterX,\n ctx.faceHalfWidth,\n ctx.travel,\n bendOnlyMapAt,\n (deg, side) => {\n const rest = shellHalfWidthAt(side);\n // Never past the shell's own REST edge, whatever the hold\n // does: a hold wider than it (the fit sends one there to keep\n // a rendered ratio of 1 against the bangs' own lead) moves the\n // bangs, and the parts that draw the outline beside them — a\n // static hair_back above all — stay where they are drawn.\n return Math.min(rest, holdEdgeFrom(rest)(deg));\n },\n ),\n );\n const columnMapAt = (deg: number) =>\n turnColumnMap(ctx.faceGrid, ctx.faceCenterX, radius, deg, travel);\n // Without a measured head the boundary is the outermost the slid plate\n // ever reaches, clear of it by HOLD_CLEARANCE.\n const holdBase =\n ctx.headHalfWidth ??\n plateReach(ctx.faceCenterX, ctx.faceHalfWidth, columnMapAt) +\n HOLD_CLEARANCE;\n const holdEdgeAt = holdEdgeFrom(holdBase);\n\n // The bracket this hold could have been fitted in: the ratio the\n // plate-fold geometry allows, and the one the deformed grid can still\n // reach. Both are computed EVERY time, whichever one actually binds (or\n // neither), so a refusal never advertises a range the other would have\n // narrowed.\n //\n // The lower one is SAFE but not TIGHT: `reach` is read off `columnMapAt`,\n // the slid map, and with a measured head the ratio sizes that slide as\n // well (`shellTravelCap` reads `holdEdgeAt`). At the binding stop the cap\n // moves with the ratio at `holdBase` per unit, i.e. the reach moves at\n // `holdBase·|deg|/30` — exactly the rate the hold edge itself moves — so a\n // ratio narrower than this bound arrives with a smaller slide and can\n // still clear the plate. Everything at or above the bound holds; some\n // below it do too.\n let foldLowerBound = -Infinity;\n let folds = false;\n for (const deg of HEAD_TURN_STOPS) {\n if (deg === 0) continue; // proven never to bind — holdBase always clears the rest reach\n const reach = plateReachAt(\n columnMapAt(deg),\n ctx.faceCenterX,\n ctx.faceHalfWidth,\n );\n // What the ratio would have to be for the hold edge to clear the plate\n // at this stop, given the stop gets |deg|/30 of the ratio's travel.\n foldLowerBound = Math.max(\n foldLowerBound,\n 1 +\n ((reach + HOLD_CLEARANCE) / holdBase - 1) *\n (HEAD_TURN_MAX_DEG / Math.abs(deg)),\n );\n if (holdEdgeAt(deg) <= reach) folds = true;\n }\n\n const map = columnMapAt(-HEAD_TURN_MAX_DEG);\n // The hold can only place a vertex as far out as the deformed grid itself\n // reaches on that side; past it `invertX` clamps onto the edge column (see\n // bakeHairFrontSilhouetteWarp). So the ratio this radius can actually\n // carry at full turn is each side's requested destination capped to that\n // reach, not the requested ratio itself.\n const requestedHoldEdge = holdEdgeAt(HEAD_TURN_MAX_DEG);\n const gridReach: [number, number] = [\n Math.abs(map.warpedX[0] - ctx.faceCenterX),\n Math.abs(map.warpedX[map.warpedX.length - 1] - ctx.faceCenterX),\n ];\n const cappedHoldEdge: [number, number] = [\n Math.min(requestedHoldEdge, gridReach[0] - HOLD_CLEARANCE),\n Math.min(requestedHoldEdge, gridReach[1] - HOLD_CLEARANCE),\n ];\n // The silhouette point each side's centre is read from:\n // `measure_turn_reference` reads the eye pair against the OPAQUE UNION's\n // own centre, not an assumed faceCenterX-symmetric one, so with\n // `headEdges` this is that same union's own extreme — the min (−x side) /\n // max (+x side) of every candidate's own rest x, not `holdBase`'s\n // idealised, centred stand-in for it (`holdBase` still sizes the HOLD\n // itself; this is only where the DRIFT is measured from). Without an edge\n // list for this side — no measured head at all, or one measured but with\n // nothing recorded there — the measured head's own edge (`holdBase` IS\n // that edge then, on the nose by construction), or hair_front's own crop\n // edge otherwise: the best available stand-in for where a render's\n // outermost opaque pixel sits when nothing measured it.\n const restAt = (side: -1 | 1): number => {\n const edge = measuredEdgeAt(side);\n if (edge !== undefined) return edge;\n return ctx.headHalfWidth !== undefined ||\n ctx.hairFrontSilhouette === undefined\n ? ctx.faceCenterX + side * holdBase\n : ctx.hairFrontSilhouette.x +\n (side * ctx.hairFrontSilhouette.cropW) / 2;\n };\n const hairFrontAt = (x: number): number =>\n ctx.hairFrontSilhouette === undefined\n ? x\n : hairFrontLandingAt(\n x,\n ctx.hairFrontSilhouette,\n ctx.faceCenterX,\n ctx.faceHalfWidth,\n holdBase,\n cappedHoldEdge,\n map,\n leadTipShift,\n leadRow,\n );\n // Where a named role's OWN rest x lands after the turn — the mcp measures\n // the union of every layer's opaque pixels, so an edge can belong to any\n // role, not just the bangs. `face` and the FEATURE_NOD_DEPTH family (the\n // eye stack, lashes, brows, blush, the nose, both mouths) ride `faceWarp`,\n // so their own path is `map.mapX` — WITHOUT that family's own depth\n // parallax, which is not solved yet at this point in the sweep (the eye's\n // own signed solve below needs `silhouetteCenterShift`, computed from\n // this, and nose/mouth's are not solved until `solveFeatureDepths`, after\n // a radius is even chosen). The omission is harmless not because the\n // magnitude is small but because none of this family can realistically OWN\n // the eye-row silhouette edge in the first place: they sit near the face's\n // own centre, well inside whatever hair, face-plate, or body edge is\n // actually outermost there. `hair_back` and `body` ride their own rigid\n // deformers, not faceWarp, so neither uses `map` at all. Anything not in\n // `ROLE_TABLE` is a bug, not a role to render as unmoved.\n const landingOfRole = (role: string, x: number): number => {\n if (role === \"hair_front\") return hairFrontAt(x);\n if (role === \"hair_back\") {\n // It holds the head's outline: it rides a head that no longer travels\n // and has no turn binding or warp of its own, so it lands where it\n // sits.\n return x;\n }\n if (role === \"body\") {\n // The head no longer translates on the turn — its travel is in the\n // face grid, where only what rides the grid carries it — so the head's\n // frame is the rest frame and the body lands at its own AngleX travel\n // alone: the \"from\" value of its own translateX binding at this stop.\n return x - BODY_TURN_FOLLOW * travel;\n }\n if (\n role === \"face\" ||\n FEATURE_NOD_DEPTH[roleFamily(role)] !== undefined\n ) {\n return map.mapX(x);\n }\n throw new Error(\n `auto-rig: evaluateTurnCandidate: unrecognised role \"${role}\"`,\n );\n };\n const landingAt = (side: -1 | 1): number => {\n const candidates = side < 0 ? ctx.headEdges?.left : ctx.headEdges?.right;\n if (candidates === undefined || candidates.length === 0) {\n return hairFrontAt(restAt(side));\n }\n const landings = candidates.map((c) => landingOfRole(c.role, c.x));\n return side < 0 ? Math.min(...landings) : Math.max(...landings);\n };\n return {\n ratio,\n travel,\n map,\n holdBase,\n holdEdgeAt,\n folds,\n foldLowerBound,\n gridReach,\n // How far the silhouette's own centre drifts off the face centre at full\n // turn, against how far it already sat off centre at rest (zero unless\n // hair_front's crop is itself off-centre) — the held shell keeps its own\n // place while the face slides inside it, so this is the drift the hold\n // and the bangs' lead leave behind, and the slide itself shows up in the\n // eye cue instead of cancelling out of it.\n // `measure_turn_reference` reads the eye pair against THIS moved centre,\n // not the face centre, so it is what the eye cue is corrected by.\n silhouetteCenterShift:\n (landingAt(1) + landingAt(-1)) / 2 - (restAt(1) + restAt(-1)) / 2,\n // What a render actually shows: the real landing span over the real rest\n // span, the same pair silhouetteCenterShift reads — the measure the\n // hold is FITTED against, see this function's own doc.\n renderedSilhouetteRatio:\n (landingAt(1) - landingAt(-1)) / (restAt(1) - restAt(-1)),\n restSpan: restAt(1) - restAt(-1),\n };\n };\n\n // The widest hold worth trying at this radius: once BOTH sides' requested\n // destinations sit outside the deformed grid's own reach, the bake caps them\n // there (`bakeHairFrontSilhouetteWarp`) and a wider hold renders the\n // identical silhouette. That reach moves with the travel a wider hold buys,\n // so this walks up to it — each pass starting from the previous one's own\n // reach — and settles, the travel being bounded by the plate's own ask.\n const saturatedHold = (): TurnHoldEval => {\n let hold = evaluateHold(1);\n for (let i = 0; i < TURN_HOLD_SATURATION_PASSES; i++) {\n const needed =\n (Math.max(hold.gridReach[0], hold.gridReach[1]) - HOLD_CLEARANCE) /\n hold.holdBase;\n if (needed <= hold.ratio) break;\n hold = evaluateHold(needed);\n }\n return hold;\n };\n\n // Every hold this radius can actually ship, sampled across its own usable\n // range: from the narrowest that does not fold the ramp onto the strands up\n // to that saturation point. What a render SHOWS is not monotone in the hold\n // — a wider hold buys more travel (`shellTravelCap`), and more travel pulls\n // the deformed grid's near-side reach IN, which caps the near destination\n // the hold was widening — so the range is sampled rather than read off its\n // two ends, and the fit below works in whichever sampled interval actually\n // straddles the ask. Every sample is fold-free, so anything the fit returns\n // from one can be shipped.\n //\n // `foldLowerBound` is a SEED for the floor, not the floor: it reads the\n // plate's reach off one hold's own map, and that reach does not move with\n // the hold the way it assumes. A narrower hold buys a smaller slide, which\n // pulls the plate's toward edge in but pushes its AWAY edge out, and\n // `plateReachAt` takes the larger of the two — so the seed can itself fold,\n // and the real floor is bisected between it and the saturation point, where\n // the slide is largest and the hold furthest out. Undefined when even that\n // point folds: the radius can hold nothing, so it has nothing to offer.\n const holdRange = (): TurnHoldEval[] | undefined => {\n const ceiling = saturatedHold();\n if (ceiling.folds) return undefined;\n let floor = evaluateHold(Math.min(ceiling.foldLowerBound, ceiling.ratio));\n if (floor.folds) {\n let lo = floor;\n let hi = ceiling;\n for (let i = 0; i < TURN_BISECT_STEPS; i++) {\n const mid = evaluateHold((lo.ratio + hi.ratio) / 2);\n if (mid.folds) lo = mid;\n else hi = mid;\n }\n floor = hi;\n }\n const samples = [floor];\n for (let i = 1; i < TURN_HOLD_SAMPLES; i++) {\n const sample = evaluateHold(\n floor.ratio + ((ceiling.ratio - floor.ratio) * i) / TURN_HOLD_SAMPLES,\n );\n if (!sample.folds) samples.push(sample);\n }\n samples.push(ceiling);\n return samples;\n };\n\n /** What this radius offers a refused caller: the ratios a render of it can\n * show, clipped to the domain `resolveTurnTargets` accepts — offering one\n * outside it would be offering a number the validator throws on. Clipped\n * PER RADIUS, before `sweepTurnRadii` unions them, so each end of what the\n * sweep advertises is an end some radius can really be fitted to. The\n * neutral pair when nothing survives that clip (or there was nothing to\n * clip): `sweepTurnRadii`'s own min/max ignore it. */\n const silhouetteMiss = (span?: [number, number]): TurnCandidateMiss => {\n const lower = Math.max(span?.[0] ?? Infinity, SILHOUETTE_RATIO_MIN);\n const upper = Math.min(span?.[1] ?? -Infinity, TURN_RATIO_MAX);\n return lower > upper\n ? { blocked: \"silhouetteRatio\", lower: Infinity, upper: -Infinity }\n : { blocked: \"silhouetteRatio\", lower, upper };\n };\n\n const asked = ctx.silhouetteRatio;\n // The hold the target names outright, tried first. Where it already renders\n // the target it IS the answer: a static shell renders its own rest span\n // whatever the hold does, so every hold there is equally exact, and the one\n // that leaves the bangs where the target says is the one to ship rather than\n // an arbitrarily narrower one a search would settle on first.\n const natural = evaluateHold(asked);\n // solveTurnDepthSigned's own px slack, on the span the rendered ratio is a\n // fraction of: the fit stops there, and a caller resubmitting the exact\n // interval a refusal reported is not refused again by a float ulp of the\n // division that reported it.\n const eps = TURN_DEPTH_EPS / natural.restSpan;\n let hold: TurnHoldEval;\n let silhouetteClamped = false;\n if (\n !natural.folds &&\n Math.abs(natural.renderedSilhouetteRatio - asked) <= eps\n ) {\n hold = natural;\n } else {\n const samples = holdRange();\n if (samples === undefined) return silhouetteMiss();\n // Every ratio between the extreme samples is on offer, the rendered ratio\n // being continuous in the hold: the sampled values are all attainable and\n // so is everything they straddle.\n const rendered = samples.map((h) => h.renderedSilhouetteRatio);\n const span: [number, number] = [\n Math.min(...rendered),\n Math.max(...rendered),\n ];\n // Of two holds, the one whose render is nearer the target — and between\n // two that both hit it, the one whose own ratio is nearer what was asked,\n // so a flat stretch of the range does not move the bangs for nothing.\n const nearer = (a: TurnHoldEval, b: TurnHoldEval) => {\n const ea = Math.abs(a.renderedSilhouetteRatio - asked);\n const eb = Math.abs(b.renderedSilhouetteRatio - asked);\n if (ea <= eps && eb <= eps) {\n return Math.abs(a.ratio - asked) <= Math.abs(b.ratio - asked) ? a : b;\n }\n return ea <= eb ? a : b;\n };\n if (asked < span[0] - eps || asked > span[1] + eps) {\n // A CALLER-measured ratio no hold renders is refused in the rendered\n // range's own terms; a DEFAULTED one takes the nearest end of that range\n // and says so — see solveTurnModel's report.\n if (!ctx.clampSilhouetteRatio) return silhouetteMiss(span);\n const wanted = asked < span[0] ? span[0] : span[1];\n hold = samples.reduce((a, b) =>\n Math.abs(b.renderedSilhouetteRatio - wanted) <\n Math.abs(a.renderedSilhouetteRatio - wanted)\n ? b\n : a,\n );\n silhouetteClamped = true;\n } else {\n // The sampled interval the target actually sits in, bisected on the\n // HOLD: the rendered ratio is continuous in it, so an interval whose\n // ends straddle the target contains a hold that renders it — whichever\n // way round those ends sit, which is why the direction is read off the\n // interval rather than assumed.\n let best = samples.reduce(nearer);\n let lo: TurnHoldEval | undefined;\n let hi: TurnHoldEval | undefined;\n for (let i = 0; i + 1 < samples.length; i++) {\n const a = samples[i].renderedSilhouetteRatio - asked;\n const b = samples[i + 1].renderedSilhouetteRatio - asked;\n if (a * b <= 0) {\n lo = samples[i];\n hi = samples[i + 1];\n break;\n }\n }\n for (let i = 0; lo !== undefined && hi !== undefined; i++) {\n if (\n i >= TURN_BISECT_STEPS ||\n Math.abs(best.renderedSilhouetteRatio - asked) <= eps\n ) {\n break;\n }\n const mid = evaluateHold((lo.ratio + hi.ratio) / 2);\n if (!mid.folds) best = nearer(best, mid);\n // Keep the half the target is still inside, in the orientation these\n // two ends establish.\n if (\n (mid.renderedSilhouetteRatio - asked) *\n (lo.renderedSilhouetteRatio - asked) >\n 0\n ) {\n lo = mid;\n } else {\n hi = mid;\n }\n }\n hold = best;\n }\n }\n // Folding is never acceptable, fitted or clamped: `natural` is only kept\n // when it does not fold, every `holdRange()` sample is fold-free, and the\n // fit only ever replaces `best` with a non-folding evaluation — so a folding\n // hold here is a broken invariant, not a case to handle.\n if (hold.folds) {\n throw new Error(\n \"auto-rig: evaluateTurnCandidate: the hold it settled on folds\",\n );\n }\n const {\n travel,\n map,\n holdBase,\n holdEdgeAt,\n silhouetteCenterShift,\n renderedSilhouetteRatio,\n } = hold;\n const holdEdgeX = ctx.faceCenterX - holdEdgeAt(HEAD_TURN_MAX_DEG);\n // measure_turn_reference reads the eye pair against each pose's OWN\n // silhouette centre, not the face centre — at rest that centre IS the face\n // centre, but at full turn it has moved by `silhouetteCenterShift`. The raw\n // landmark slide `solveTurnDepthSigned` solves for is measured against the\n // face centre, so its target has to be `silhouetteCenterShift` MINUS the\n // requested magnitude's own px: adding the two back together at measurement\n // time (`achieved.eyeShift` below) lands back on the requested cue.\n //\n // That target can legitimately be positive (the centre's own drift already\n // meets or exceeds the request, so the eyes need to move toward the NEAR\n // side to land on it) as well as negative, which is exactly why this calls\n // the SIGNED solver directly: `solveTurnDepth`'s magnitude-only wrapper\n // would silently flip a negative request back to positive, quietly hitting\n // a different cue than the one asked for (a request and its negation must\n // produce the identical rig — `TurnTargets.eyeShift`'s own \"the sign is\n // ignored\" contract, taken here, not by folding an already-negative result\n // back to positive later).\n const eye = solveTurnDepthSigned(\n silhouetteCenterShift - ctx.eyeShift * ctx.hh,\n ctx.landmarks.eye,\n unit,\n map,\n holdEdgeX,\n ctx.plateEdgeX,\n );\n // A measured shift this radius cannot produce disqualifies the radius; a\n // defaulted one takes what the radius offers.\n if (!eye.reached && !ctx.clampEyeShift) {\n return {\n blocked: \"eyeShift\",\n // eyeShift is a MAGNITUDE (the sign is ignored — see TurnTargets), so a\n // negative lower bound here would advertise a value the field's own\n // contract already rules out; 0 is the true floor.\n offeredShift: [\n Math.max(0, (-eye.attainable[1] + silhouetteCenterShift) / ctx.hh),\n (-eye.attainable[0] + silhouetteCenterShift) / ctx.hh,\n ],\n };\n }\n\n // Each eye's rest width against its turned width, where the slide put it.\n const scaleOf = (l: TurnLandmark) =>\n (map.mapX(l.x + l.w / 2 - eye.depth * unit) -\n map.mapX(l.x - l.w / 2 - eye.depth * unit)) /\n l.w;\n // A −30° turn foreshortens the −x side: that eye is the far one. Dividing the\n // two rest-normalised scales IS the cue — the reference's own far/near ratio\n // is already divided by its rest one.\n const far = ctx.landmarks.eye.reduce((a, b) => (b.x < a.x ? b : a));\n const near = ctx.landmarks.eye.reduce((a, b) => (b.x > a.x ? b : a));\n return {\n radius,\n ratio: scaleOf(far) / scaleOf(near),\n unit,\n travel,\n map,\n holdBase,\n holdEdgeAt,\n holdEdgeX,\n eye,\n silhouetteClamped,\n silhouetteCenterShift,\n renderedSilhouetteRatio,\n };\n}\n\n/** A swept radius: the candidate it yielded and WHERE in the sweep it sat.\n * The feasible radii are not one unbroken run any more — a caller-measured\n * silhouette refuses a radius whose own rendered range cannot be fitted to\n * it, and that can bite in the middle of the sweep — so which candidates are\n * NEIGHBOURS has to be read off the sweep rather than off the array. */\ntype SweptCandidate = TurnCandidate & { sweepIndex: number };\n\n/** The radii that can carry the turn, and what blocked the ones that cannot. */\ninterface TurnSweep {\n candidates: SweptCandidate[];\n /** The narrowest silhouette a render of the radii blocked for the silhouette\n * could have shown, inside the ratios the targets accept (see\n * `TurnCandidateMiss.lower`); Infinity when no radius was blocked for the\n * silhouette at all, or when none of the blocked ones renders anything a\n * caller is allowed to ask for. */\n heldRatioLower: number;\n /** The widest silhouette a render of any radius blocked for the silhouette\n * could have shown — an UPPER bound, same convention; -Infinity in the same\n * two cases. */\n heldRatioUpper: number;\n /** The widest eye shift the blocked radii offered, in shift units; absent\n * when the slide blocked none. */\n offeredShift?: [number, number];\n}\n\n/** Log-spaced sweep of the radius: the shape of the head is not known to any\n * finer resolution than the cues themselves, so the search starts by looking\n * at the whole plausible range. */\nfunction sweepTurnRadii(ctx: TurnSolveContext): TurnSweep {\n const minRadius = ctx.faceHalfWidth * HEAD_CYLINDER_RADIUS_FACTOR;\n const maxRadius = ctx.faceHalfWidth * TURN_SWEEP_MAX_FACTOR;\n const candidates: SweptCandidate[] = [];\n let heldRatioLower = Infinity;\n let heldRatioUpper = -Infinity;\n let offeredShift: [number, number] | undefined;\n for (let i = 0; i < TURN_SWEEP_SAMPLES; i++) {\n const radius =\n minRadius * Math.pow(maxRadius / minRadius, i / (TURN_SWEEP_SAMPLES - 1));\n const result = evaluateTurnCandidate(ctx, radius);\n if (!(\"blocked\" in result)) {\n candidates.push({ ...result, sweepIndex: i });\n } else if (result.blocked === \"silhouetteRatio\") {\n heldRatioLower = Math.min(heldRatioLower, result.lower);\n heldRatioUpper = Math.max(heldRatioUpper, result.upper);\n } else {\n offeredShift = offeredShift\n ? [\n Math.min(offeredShift[0], result.offeredShift[0]),\n Math.max(offeredShift[1], result.offeredShift[1]),\n ]\n : result.offeredShift;\n }\n }\n return { candidates, heldRatioLower, heldRatioUpper, offeredShift };\n}\n\n/** One unbroken run of the sweep's feasible radii, and the far/near ratios it\n * covers: the ratio is continuous in the radius, so a run spans every ratio\n * between its own extremes, and a target BETWEEN two runs is one no radius in\n * the sweep produces. */\ninterface TurnRatioCluster {\n candidates: SweptCandidate[];\n /** Ascending. */\n ratios: [number, number];\n}\n\n/** The sweep's feasible radii, split where it skipped one. A caller-measured\n * silhouette can refuse a radius in the middle of the range — the rendered\n * ratios a radius can be fitted to move with it — and a far/near ratio that\n * falls in the gap between two runs is attainable on neither, so the two must\n * not be bracketed across. */\nfunction ratioClusters(candidates: SweptCandidate[]): TurnRatioCluster[] {\n const clusters: TurnRatioCluster[] = [];\n let run: SweptCandidate[] = [];\n const close = () => {\n if (run.length === 0) return;\n const ratios = run.map((c) => c.ratio);\n clusters.push({\n candidates: run,\n ratios: [Math.min(...ratios), Math.max(...ratios)],\n });\n run = [];\n };\n for (const candidate of candidates) {\n const previous = run[run.length - 1];\n if (\n previous !== undefined &&\n candidate.sweepIndex !== previous.sweepIndex + 1\n ) {\n close();\n }\n run.push(candidate);\n }\n close();\n return clusters;\n}\n\n/**\n * The radius whose far/near ratio comes nearest `target`, inside ONE cluster\n * of sweep-adjacent radii.\n *\n * The ratio is not monotone in the radius — a flatter cylinder foreshortens\n * less but needs a deeper slide to move the eyes as far, and the grid's cells\n * are straight lines the landmarks cross at different radii — so the cluster's\n * samples are scanned from the LARGEST radius down for the first adjacent pair\n * that brackets the target: where two radii both fit, the flatter head is the\n * one that keeps more of the face on the analytic part of the cylinder. The\n * bisection inside that pair is geometric, matching the log-spaced sweep.\n *\n * A pair that is adjacent in the array is adjacent in the SWEEP here, the\n * cluster being one unbroken run of it (see `ratioClusters`) — that is what\n * keeps the bisection out of a gap the sweep already found. Finer gaps inside\n * a pair it cannot see: the bisection stops when it lands on a refused radius,\n * and `solveTurnModel` checks what this actually reached against the target\n * rather than trusting it.\n */\nfunction fitTurnRadius(\n ctx: TurnSolveContext,\n candidates: SweptCandidate[],\n target: number,\n): TurnCandidate {\n // The cluster's whole span, narrowed to the bracketing pair — there is one\n // whenever the target sits inside its own ratio range.\n let lo: TurnCandidate = candidates[0];\n let hi: TurnCandidate = candidates[candidates.length - 1];\n for (let i = candidates.length - 2; i >= 0; i--) {\n if (\n (candidates[i].ratio - target) * (candidates[i + 1].ratio - target) <=\n 0\n ) {\n lo = candidates[i];\n hi = candidates[i + 1];\n break;\n }\n }\n for (let i = 0; i < TURN_BISECT_STEPS; i++) {\n const mid = evaluateTurnCandidate(ctx, Math.sqrt(lo.radius * hi.radius));\n // A gap in the feasible set inside the bracket: keep the pair we have.\n if (\"blocked\" in mid) break;\n if ((mid.ratio - target) * (lo.ratio - target) <= 0) hi = mid;\n else lo = mid;\n }\n return Math.abs(lo.ratio - target) <= Math.abs(hi.ratio - target) ? lo : hi;\n}\n\n/**\n * The nose's and the mouth's depth at the solved radius, on the same terms as\n * the eyes': a measured target must be hit, a derived one takes what it can get.\n *\n * A DERIVED share follows the eyes' actual travel, not the travel they were\n * asked for: the shares are proportions BETWEEN the three features, so against a\n * clamped eye pair the un-scaled ones would send the nose across the far eye.\n */\nfunction solveFeatureDepths(\n ctx: TurnSolveContext,\n targets: ResolvedTurnTargets,\n best: TurnCandidate,\n eyeShift: number,\n):\n | { nose: number; mouth: number; clamped: (keyof TurnTargets)[] }\n | { blocked: \"noseShift\" | \"mouthShift\"; attainable: [number, number] } {\n const clamped: (keyof TurnTargets)[] = [];\n const solved: Partial<Record<\"noseShift\" | \"mouthShift\", number>> = {};\n const families = [\n [\"noseShift\", ctx.landmarks.nose, NOSE_SHIFT_SHARE],\n [\"mouthShift\", ctx.landmarks.mouth, MOUTH_SHIFT_SHARE],\n ] as const;\n for (const [field, marks, share] of families) {\n // Both share eyeShift's own head-relative contract (a fraction of `hh`,\n // sign ignored), so the target gets the SAME silhouette-centre correction\n // the eye's own solve uses (see evaluateTurnCandidate) — measured against\n // the pose's own silhouette centre, not the raw landmark slide against the\n // fixed face centre.\n const m = Math.abs(\n targets.defaulted.has(field) ? share * eyeShift : targets[field],\n );\n const solution = solveTurnDepthSigned(\n best.silhouetteCenterShift - m * ctx.hh,\n marks,\n best.unit,\n best.map,\n best.holdEdgeX,\n ctx.plateEdgeX,\n );\n if (!solution.reached) {\n if (!targets.defaulted.has(field)) {\n return {\n blocked: field,\n // noseShift/mouthShift are MAGNITUDES too (see the correction\n // above), so a negative lower bound here would advertise a value\n // the field's own contract already rules out; 0 is the true floor.\n attainable: [\n Math.max(\n 0,\n (-solution.attainable[1] + best.silhouetteCenterShift) / ctx.hh,\n ),\n (-solution.attainable[0] + best.silhouetteCenterShift) / ctx.hh,\n ],\n };\n }\n clamped.push(field);\n }\n solved[field] = solution.depth;\n }\n return {\n nose: solved.noseShift!,\n mouth: solved.mouthShift!,\n clamped,\n };\n}\n\n/**\n * Solve the head turn from its measured cues: one radius and three depths.\n *\n * The radius and the eye depth are solved TOGETHER, because the two cues do not\n * separate: how far the eye pair slides depends on the radius (through the unit\n * and the map), and how much the far eye foreshortens depends on where the\n * slide put it. For a candidate radius the eye depth is whatever hits\n * `eyeShift` on that radius' own map — or, for a DEFAULTED eyeShift the bounds\n * cut short, as much of it as that radius allows — and the far/near width ratio\n * that falls out of it is the residual `fitTurnRadius` drives to `farEyeRatio`.\n *\n * Everything is measured on the SAME map the rig renders with — the piecewise\n * linear one `turnColumnMap` builds from the grid's own columns, at the −30°\n * stop — so a solved target is a promise about the shipped keyforms, not about\n * an idealised cylinder the engine never evaluates.\n *\n * A target that does not fit is clamped when it was a default and refused when\n * the caller measured it; see TurnTargets. Clamping the EYE SHIFT keeps the\n * radius search intact on purpose: the shift is bounded by the art (the far eye\n * has to stay on the plate), so trading the foreshortening cue away to buy the\n * last few pixels of slide would flatten the cylinder — on the reference\n * character, to the sweep's ceiling — and a head that does not foreshorten does\n * not read as turning at all.\n */\nexport function solveTurnModel(\n targets: ResolvedTurnTargets,\n landmarks: TurnLandmarkSet,\n faceGrid: IkiWarpGrid,\n faceCenterX: number,\n faceHalfWidth: number,\n /** hair_front's own transform x/y and crop width/height, when the layer set\n * has one — see `TurnSolveContext.hairFrontLeadFraction` and\n * `.hairFrontSilhouette`. Absent skips the turn-lead correction and falls\n * the silhouette-centre correction back to the ideal hold destinations,\n * which is also correct for a layer set with no hair_front: there is then\n * no bangs edge for either to read. */\n hairFront?: { x: number; centerY: number; cropW: number; cropH: number },\n /** Every role with an opaque pixel in the eye-row band, per side, each with\n * its own rest x there — see `TurnSolveContext.headEdges`. Absent (the\n * usual case for a caller with no measured head, or one built by hand)\n * falls the silhouette-centre correction back to hair_front on both sides. */\n headEdges?: {\n left: { role: string; x: number }[];\n right: { role: string; x: number }[];\n },\n): TurnModelSolution {\n // A measured head narrower than the face it is drawn around is not a head the\n // rest of this can make sense of: the hold's whole zone lives outside the\n // plate's edge, so the plate would be sticking out of the silhouette at rest.\n if (\n targets.headHalfWidth !== undefined &&\n targets.headHalfWidth <= faceHalfWidth\n ) {\n throw new TurnTargetError(\n `auto-rig: turnTargets.headHalfWidth (${targets.headHalfWidth}) must be wider than the face plate's own half-width (${faceHalfWidth})`,\n );\n }\n\n // The eye row's fraction of the way from hair_front's own root (pinned, the\n // TOP of its crop) to its tips (the bottom) — 0 at the root, 1 at the tips,\n // clamped for an eye row painted outside the crop's own span (unusual\n // art). Absent without a hair_front layer, or without both eyes' own y (a\n // caller that built landmarks by hand, e.g. a lower-level test).\n const eyeRowY =\n landmarks.eye.length === 2 &&\n landmarks.eye[0].y !== undefined &&\n landmarks.eye[1].y !== undefined\n ? (landmarks.eye[0].y + landmarks.eye[1].y) / 2\n : undefined;\n const hairFrontLeadFraction =\n hairFront === undefined || eyeRowY === undefined\n ? undefined\n : Math.max(\n 0,\n Math.min(\n 1,\n (hairFront.centerY + hairFront.cropH / 2 - eyeRowY) /\n hairFront.cropH,\n ),\n );\n\n const ctx: TurnSolveContext = {\n landmarks,\n faceGrid,\n faceCenterX,\n faceHalfWidth,\n plateEdgeX: faceCenterX - faceHalfWidth,\n // What this plate asks the turn for; each candidate caps it to its own\n // shell — see TurnSolveContext.travel.\n travel: headTurnTravel(faceHalfWidth),\n headHalfWidth: targets.headHalfWidth,\n // The pixels the shift fractions are fractions of. Without a measured head\n // the face plate stands in for it — see TurnTargets.headHalfWidth.\n hh: targets.headHalfWidth ?? faceHalfWidth,\n silhouetteRatio: targets.silhouetteRatio,\n // The sign is ignored (see TurnTargets.eyeShift) — taken here, once, so\n // every downstream use already has the magnitude.\n eyeShift: Math.abs(targets.eyeShift),\n clampEyeShift: targets.defaulted.has(\"eyeShift\"),\n clampSilhouetteRatio: targets.defaulted.has(\"silhouetteRatio\"),\n hairFrontLeadFraction,\n hairFrontSilhouette:\n hairFront === undefined\n ? undefined\n : { x: hairFront.x, cropW: hairFront.cropW, cropH: hairFront.cropH },\n // A companion to a MEASURED headHalfWidth only — see its own doc.\n headEdges: targets.headHalfWidth === undefined ? undefined : headEdges,\n };\n const clamped: (keyof TurnTargets)[] = [];\n const clampInto = (value: number, [lo, hi]: [number, number]) =>\n Math.min(Math.max(value, lo), hi);\n\n const pass = sweepTurnRadii(ctx);\n if (pass.candidates.length === 0) {\n // Either gate can empty the sweep, and only a MEASURED target can: the\n // shift, because a defaulted one is clamped per radius rather than gated,\n // and the silhouette, because a defaulted ratio of 1 leaves the hold edge\n // at its own rest distance — without a measured head that distance is the\n // plate's own scanned reach plus a pixel (`plateReach`), which clears every\n // stop by construction, and with one it is the head the slide is capped to\n // stay inside (`shellTravelCap`). So both values below are the caller's\n // own, never a clamped stand-in.\n return pass.offeredShift\n ? {\n unreachable: true,\n field: \"eyeShift\",\n value: targets.eyeShift,\n attainable: pass.offeredShift,\n }\n : {\n unreachable: true,\n field: \"silhouetteRatio\",\n value: targets.silhouetteRatio,\n // Two different bounds, not one number twice, and computed the\n // same way regardless of which ratio was actually rejected: both\n // are ratios a RENDER could have shown (the measure the caller's\n // own number came from), the narrowest and the widest any radius\n // could be fitted to, with every ratio between them one it can be\n // fitted to exactly; see evaluateTurnCandidate, which clips each\n // radius' own range to the domain `resolveTurnTargets` accepts\n // before it gets here. Absent when that left every radius with\n // nothing: no ratio a caller is allowed to ask for renders at all.\n attainable:\n pass.heldRatioLower === Infinity ||\n pass.heldRatioUpper === -Infinity\n ? undefined\n : [pass.heldRatioLower, pass.heldRatioUpper],\n };\n }\n\n // The radii that survived, in unbroken runs of the sweep: a far/near ratio\n // between two runs is one no radius produces, so the fit is confined to the\n // run that covers the target and a target no run covers is out of reach\n // rather than bisected across the gap. A DEFAULTED silhouette leaves every\n // radius standing and so leaves exactly one run — the gaps are what a\n // CALLER-measured one opens (see evaluateTurnCandidate).\n const clusters = ratioClusters(pass.candidates);\n const distanceTo = (c: TurnRatioCluster) =>\n Math.max(\n c.ratios[0] - targets.farEyeRatio,\n targets.farEyeRatio - c.ratios[1],\n 0,\n );\n let farEyeRatio = targets.farEyeRatio;\n let cluster = clusters.find(\n (c) =>\n farEyeRatio >= c.ratios[0] - TURN_DEPTH_EPS &&\n farEyeRatio <= c.ratios[1] + TURN_DEPTH_EPS,\n );\n if (cluster === undefined) {\n // What a caller could have asked for instead is one run's own range, not\n // the span across the gaps: every ratio inside it is one this sweep\n // reaches. The nearest run is the one to name.\n cluster = clusters.reduce((a, b) =>\n distanceTo(b) < distanceTo(a) ? b : a,\n );\n if (!targets.defaulted.has(\"farEyeRatio\")) {\n return {\n unreachable: true,\n field: \"farEyeRatio\",\n value: targets.farEyeRatio,\n attainable: cluster.ratios,\n };\n }\n farEyeRatio = clampInto(farEyeRatio, cluster.ratios);\n clamped.push(\"farEyeRatio\");\n }\n\n const best = fitTurnRadius(ctx, cluster.candidates, farEyeRatio);\n // What the fit actually reached, against what it was asked for: inside a run\n // the ratio is continuous, but the bisection can still stop on a radius the\n // silhouette gate refuses — a gap finer than the sweep resolves — and land\n // on a bracket end instead. A miss is refused or reported, never passed off\n // as met. The tolerance is solveTurnDepthSigned's own slack read in this\n // cue's own units, which are already dimensionless: the far/near ratio is a\n // width over a width, so there is no px scale to divide it by.\n if (Math.abs(best.ratio - farEyeRatio) > TURN_DEPTH_EPS) {\n if (!targets.defaulted.has(\"farEyeRatio\")) {\n return {\n unreachable: true,\n field: \"farEyeRatio\",\n value: targets.farEyeRatio,\n attainable: cluster.ratios,\n };\n }\n if (!clamped.includes(\"farEyeRatio\")) clamped.push(\"farEyeRatio\");\n }\n if (!best.eye.reached) clamped.push(\"eyeShift\");\n // Only a DEFAULTED silhouette can be here cut down: the fit lands a\n // caller-measured one or refuses the radius outright (evaluateTurnCandidate),\n // so a ratio that reached this point was either rendered as asked — nothing\n // to report — or is this generator's own default, cut to the nearest ratio a\n // render of this layer set can show.\n if (best.silhouetteClamped) clamped.push(\"silhouetteRatio\");\n // Undo the same silhouette-centre correction the depth was solved with (see\n // evaluateTurnCandidate), so this reports the cue measure_turn_reference\n // would read off a render — eye position against that pose's OWN silhouette\n // centre — not the raw landmark slide against the fixed face centre.\n const eyeShift = (-best.eye.achieved + best.silhouetteCenterShift) / ctx.hh;\n\n const features = solveFeatureDepths(ctx, targets, best, eyeShift);\n if (\"blocked\" in features) {\n return {\n unreachable: true,\n field: features.blocked,\n value: targets[features.blocked],\n attainable: features.attainable,\n };\n }\n\n return {\n unreachable: false,\n radius: best.radius,\n holdBase: best.holdBase,\n holdEdgeAt: best.holdEdgeAt,\n travel: best.travel,\n depths: { eye: best.eye.depth, nose: features.nose, mouth: features.mouth },\n achieved: {\n eyeShift,\n farEyeRatio: best.ratio,\n silhouetteRatio: best.renderedSilhouetteRatio,\n },\n clamped: [...clamped, ...features.clamped],\n };\n}\n\n// ── bindingsForRole ───────────────────────────────────────────────────────────\n\n// Role prefixes that belong to the eye stack (blink + optional gaze bindings).\n// Hoisted to module scope so it is not reallocated on every bindingsForRole call.\nconst EYE_STACK_PREFIXES = [\"eye_\", \"iris_\", \"pupil_\", \"highlight_\"] as const;\n\n/** Signed depth of the bangs from the head cylinder's axis, as a fraction of\n * the cylinder radius; positive is toward the viewer. Tuned by eye against\n * the rendered turn, not derived. The back hair takes no share of it: it\n * holds the head's outline while the face slides inside it.\n *\n * The bangs lead that sliding face by a little — this is the FRINGE TIPS'\n * lead specifically, not the whole sheet's: the crown is root-pinned by the\n * warp that applies it, not a binding (see bindingsForRole's doc for why).\n * 0.16 was right while the shift was rigid and the whole head travelled\n * with it, the lead the only depth cue; with the head no longer travelling,\n * the lead is the bangs' only motion against a held shell, and that much of\n * it reads as them running ahead, so it came down. At 0.06 the layering all\n * but vanishes. 0.1 is settled with the lead root-pinned: the fringe tips get\n * it, the crown gets none. */\nconst HAIR_FRONT_DEPTH = 0.1;\n\n/** Rigid vertical travel of the head at full nod (px at AngleY = ±30). */\nconst NOD_TRAVEL = 30;\n/** Geometric pitch per degree of ParamAngleY: a full ±30 nod bends the face\n * cylinder by ±15°. The face-warp grid reaches up to the hair crown, which\n * sits near 45° on the vertical cylinder; a full 30° pitch there drops the\n * bangs' crown ~140px while the rigid back hair stays put, and the back\n * hair's crown pops out above it as a second silhouette. At half pitch the\n * drop stays under the back hair and the nod still foreshortens visibly. */\nconst NOD_BEND = 0.5;\n/** Vertical share of the nod for hair_back, as a fraction of\n * `headNodParallaxUnit`. The back hair gets no vertical bend of its own, so\n * it only needs to follow the bent crown down to stay tucked beneath it.\n * Tuned against renders as −0.07 of a turn-sized unit (sin 30°), before the\n * nod had its own; the same pixels in the nod unit (sin 15°). */\nconst HAIR_BACK_NOD_DEPTH = -0.135;\n\n/**\n * The nod's counterpart of `headTurnParallaxUnit`: the bulk vertical slide the\n * 2D bake pins out of the face warp at full nod. The nod bends at NOD_BEND of\n * the angle, so this is RADIUS_Y · sin(30° · NOD_BEND), not RADIUS_Y · sin(30°).\n */\nexport function headNodParallaxUnit(gridHalfHeight: number): number {\n return (\n gridHalfHeight *\n HEAD_CYLINDER_RADIUS_FACTOR *\n Math.sin(HEAD_TURN_MAX_DEG * NOD_BEND * (Math.PI / 180))\n );\n}\n\n/** Depth of the features in front of the head cylinder's NOD axis, as a\n * fraction of its radius — the share of the bulk vertical slide the 2D bake\n * pins out of the face warp (`headNodParallaxUnit`) that each part gets back.\n *\n * The face contour is a cylinder rotating about its own axis: its silhouette\n * stays put and only foreshortens, which is what the pinned bake gives it.\n * The features painted on that surface do not stay put — they slide across it\n * toward the far side, and a nose that stands off the surface slides further.\n * Without their share they sat still while the outline squeezed around them,\n * and the head read as a flat sheet bending rather than pitching.\n *\n * The nose is the anchor of the whole read, so the parallax exists only when\n * it is a layer of its own (the composer cuts it out of the face). With the\n * nose painted on the face plate, any lead on the eyes and mouth inverts the\n * 3/4 view — the nose lands nearer the NEAR eye and the mouth hangs off its\n * far side — and a blind review ranked that below no parallax at all.\n *\n * Keyed by role family (`eye_L` → `eye`). The whole eye stack shares one\n * depth because iris/pupil/highlight clip to the white, and the lashes fold\n * onto it. The magnitudes are small on purpose, and judged by eye across a\n * ladder of rigs: the nose at twice the surface's share and the mouth on the\n * surface with the eyes is where the nod stopped looking like a plate without\n * looking like a slide. The TURN's depths are not tuned here at all — they are\n * solved per layer set from a measured reference (`solveTurnModel`); no nodded\n * reference has been measured, so this axis is still by eye. */\nconst FEATURE_NOD_DEPTH: Readonly<Record<string, number>> = {\n eye: 0.04,\n iris: 0.04,\n pupil: 0.04,\n highlight: 0.04,\n lash: 0.04,\n brow: 0.04,\n blush: 0.04,\n mouth: 0.04,\n mouth_open: 0.04,\n nose: 0.08,\n};\n\n/** The bangs' share of the nod: the brows'. They hang from the hairline, which\n * sits on the same surface as the brows, so they slide with them — held still\n * while the brows slid, the fringe closed onto the brows looking up and left\n * a bare forehead looking down. (Before the brows slid, a lead here lifted\n * the bangs clear off them, which is why they had none.) The turn lead stays\n * a root-pinned warp: on the turn the crown sits on the axis and must not\n * slide. */\nconst HAIR_FRONT_NOD_DEPTH = FEATURE_NOD_DEPTH.brow;\n\n/** `eye_L` → `eye`; an unsided role is its own family. */\nfunction roleFamily(role: string): string {\n return role.replace(/_[LR]$/, \"\");\n}\n\n/** Which solved turn depth a feature family slides on: the nose has its own,\n * the two mouth drawings share one, and everything else painted on the face —\n * the eye stack, the lashes, the brows, the blush — rides with the eyes. */\nfunction turnFamily(role: string): keyof TurnDepths {\n const family = roleFamily(role);\n if (family === \"nose\") return \"nose\";\n if (family === \"mouth\" || family === \"mouth_open\") return \"mouth\";\n return \"eye\";\n}\n\n/**\n * The depth-parallax translate bindings of a facial feature, or [] for any\n * other role, whenever the units are absent (a unit-less call is a rig without\n * a face warp to slide across), and whenever the rig has no nose layer to lead\n * (see FEATURE_NOD_DEPTH). Symmetric about zero, so the rest pose — the one\n * every proportion is judged on — is untouched.\n *\n * The two axes take their depth from different places: the turn's is SOLVED per\n * layer set from the measured cues (`turnDepths`, absent on a rig that solved\n * none), the nod's is the tuned table.\n */\nfunction featureParallaxBindings(\n role: string,\n options: {\n parallaxUnit?: number;\n parallaxUnitY?: number;\n hasNose?: boolean;\n turnDepths?: TurnDepths;\n },\n): IkiBinding[] {\n if (!options.hasNose) return [];\n // The nod's table is also the role gate: a family with no depth in it is not\n // a feature painted on the face, and slides on neither axis.\n const nodDepth = FEATURE_NOD_DEPTH[roleFamily(role)];\n if (nodDepth === undefined) return [];\n const bindings: IkiBinding[] = [];\n const turnDepth = options.turnDepths?.[turnFamily(role)] ?? 0;\n const shiftX = turnDepth * (options.parallaxUnit ?? 0);\n if (shiftX !== 0) {\n bindings.push({\n parameter: StandardParameter.AngleX,\n channel: \"translateX\",\n from: -shiftX,\n to: shiftX,\n });\n }\n const shiftY = nodDepth * (options.parallaxUnitY ?? 0);\n if (shiftY !== 0) {\n bindings.push({\n parameter: StandardParameter.AngleY,\n channel: \"translateY\",\n from: -shiftY,\n to: shiftY,\n });\n }\n return bindings;\n}\n\n/**\n * Derive the IkiBinding[] for a part from its role spec and crop dimensions.\n *\n * - face → no bindings: the contour is the cylinder itself, pinned by the bake\n * - every feature on that contour — eye stack, lashes, brows, both mouths,\n * nose, blush — carries the AngleX translateX / AngleY translateY depth\n * parallax: the turn's from the solved `turnDepths`, the nod's from\n * FEATURE_NOD_DEPTH (both need `parallaxUnit` / `parallaxUnitY` and\n * `hasNose`), on top of whatever its role adds below\n * - hair_front: an AngleY translateY nod follow at the brows' depth\n * (HAIR_FRONT_NOD_DEPTH), with the same `hasNose` gate as the brows it\n * follows, and nothing on the turn. Both its sway AND its\n * AngleX turn lead are root-pinned warps attached in\n * generateIkiFromLayerSet — a rigid translate/rotate would carry the whole\n * sheet (crown included) with the fringe tips, instead of leading from them.\n * - hair_back: nothing on the turn — it rides a head that no longer travels,\n * so it holds the head's outline while the face slides inside it — and an\n * AngleY translateY that tucks its crown under the bent front hair (needs\n * `parallaxUnitY`)\n * - brow_L/R: BrowLeftY/RightY translateY (raise/lower) + BrowLeftAngle/RightAngle rotate\n * (each brow rotates its own, CCW-positive)\n * - eye-stack:\n * iris_/pupil_/highlight_ → gaze translateX + translateY (no blink binding)\n * eye_ (white) → none here; its blink is a fold warp attached in assembly,\n * and iris/pupil/highlight clip to it so the closing white CUTS them away\n * - mouth: MouthForm scaleX (-0.2 to 0.4), plus MouthOpen as scaleY (0 to 3)\n * without a mouth_open layer, or as an opacity fade-out with one\n * - mouth_open: MouthOpen opacity fade-in + the same MouthForm scaleX\n *\n * Returns [] when the role has no bindings (callers skip the bindings key when\n * the array is empty).\n */\nexport function bindingsForRole(\n spec: RoleSpec,\n role: string,\n cropW: number,\n cropH: number,\n options: {\n hasMouthOpen?: boolean;\n parallaxUnit?: number;\n parallaxUnitY?: number;\n /** Whether the layer set has a `nose` role — the feature parallax's gate. */\n hasNose?: boolean;\n /** The turn depths solved for this layer set; without them nothing slides\n * on the turn. */\n turnDepths?: TurnDepths;\n } = {},\n): IkiBinding[] {\n return [\n ...roleOwnBindings(spec, role, cropW, cropH, options),\n ...featureParallaxBindings(role, options),\n ];\n}\n\n/** The bindings a role has for its own expression or motion — everything in\n * `bindingsForRole`'s list except the shared feature parallax. */\nfunction roleOwnBindings(\n spec: RoleSpec,\n role: string,\n cropW: number,\n cropH: number,\n options: {\n hasMouthOpen?: boolean;\n parallaxUnitY?: number;\n hasNose?: boolean;\n },\n): IkiBinding[] {\n const isEyeStack = EYE_STACK_PREFIXES.some((p) => role.startsWith(p));\n\n if (isEyeStack && spec.eyeSide !== undefined) {\n // Only iris/pupil/highlight move with gaze; the white (eye_) gets nothing.\n const isGazeRole =\n role.startsWith(\"iris_\") ||\n role.startsWith(\"pupil_\") ||\n role.startsWith(\"highlight_\");\n if (!isGazeRole) return [];\n\n // Gaze range: proportional to crop size, capped to avoid over-travel.\n const gx = Math.min(cropW * 0.18, 22);\n const gy = Math.min(cropH * 0.18, 16);\n return [\n {\n parameter: StandardParameter.EyeballX,\n channel: \"translateX\",\n from: -gx,\n to: gx,\n },\n {\n parameter: StandardParameter.EyeballY,\n channel: \"translateY\",\n from: -gy,\n to: gy,\n },\n ];\n }\n\n // Mouth form applies to whichever mouth drawing is showing.\n const mouthForm = {\n // Mouth form: scaleX from -0.2 (pursed, param=-1) to 0.4 (wide, param=1).\n parameter: StandardParameter.MouthForm,\n channel: \"scaleX\" as const,\n from: -0.2,\n to: 0.4,\n };\n\n if (role === \"mouth\") {\n // With an open-mouth drawing present, the closed one fades out rather than\n // being stretched. Stretching a closed mouth is what produced a smear: the\n // art is a ~15px-tall line and scaleY 3 blows it up to a blurred band.\n if (options.hasMouthOpen) {\n return [\n {\n parameter: StandardParameter.MouthOpen,\n channel: \"opacity\",\n from: 1,\n to: 0,\n },\n mouthForm,\n ];\n }\n return [\n // Mouth open: scaleY from 0 (closed, param=0) to 3 (wide open, param=1).\n // Only a fallback — it distorts, but it is better than a mouth that\n // cannot open at all when the layer set has no open drawing.\n {\n parameter: StandardParameter.MouthOpen,\n channel: \"scaleY\",\n from: 0,\n to: 3,\n },\n mouthForm,\n ];\n }\n\n if (role === \"mouth_open\") {\n return [\n {\n parameter: StandardParameter.MouthOpen,\n channel: \"opacity\",\n from: 0,\n to: 1,\n },\n mouthForm,\n ];\n }\n\n if (role === \"brow_L\" || role === \"brow_R\") {\n // Raise/lower capped to avoid over-travel; tilt is a fixed ±12° range.\n const ty = Math.min(cropH * 0.8, 18);\n const deg = 12;\n if (role === \"brow_L\") {\n return [\n {\n parameter: StandardParameter.BrowLeftY,\n channel: \"translateY\",\n from: -ty,\n to: ty,\n },\n {\n parameter: StandardParameter.BrowLeftAngle,\n channel: \"rotate\",\n from: -deg,\n to: deg,\n },\n ];\n } else {\n return [\n {\n parameter: StandardParameter.BrowRightY,\n channel: \"translateY\",\n from: -ty,\n to: ty,\n },\n {\n parameter: StandardParameter.BrowRightAngle,\n channel: \"rotate\",\n from: -deg,\n to: deg,\n },\n ];\n }\n }\n\n if (role === \"hair_front\" || role === \"hair_back\") {\n const isFront = role === \"hair_front\";\n // Neither hair part takes a binding on the TURN. hair_back holds the\n // head's outline still while the face slides inside it, and hair_front\n // leads that slide through a root-pinned warp attached in\n // generateIkiFromLayerSet, not a binding here — see the doc above for why.\n const parallax: IkiBinding[] = [];\n // On the nod the bangs slide with the brows they hang over — only when the\n // brows slide, i.e. with a nose to lead them — and the back hair follows\n // the bent crown down; see the two NOD_DEPTH constants.\n const frontDepth = options.hasNose ? HAIR_FRONT_NOD_DEPTH : 0;\n const shiftY =\n (isFront ? frontDepth : HAIR_BACK_NOD_DEPTH) *\n (options.parallaxUnitY ?? 0);\n if (shiftY !== 0) {\n parallax.push({\n parameter: StandardParameter.AngleY,\n channel: \"translateY\",\n from: -shiftY,\n to: shiftY,\n });\n }\n return parallax;\n }\n\n // face, blush_*, nose → nothing of their own\n return [];\n}\n\n// ── bakeEyelidFoldWarp ─────────────────────────────────────────────────────\n\n/** Crease sits this fraction of the eye height BELOW the white's center. */\nconst EYELID_FOLD_CREASE = 0.15;\n/** Without separate lashes, retain a thin band of the eye's own line art. */\nconst EYELID_FOLD_K = 0.04;\n/** The lash keeps a thicker band than the white when closed, so it reads as a\n * visible dark closed-eye line and covers the cut eyeball/seam. */\nconst LASH_FOLD_K = 0.2;\n\n/**\n * Live2D-style eyelid FOLD blink for the eye-white. Two EyeOpen keyforms collapse\n * the white toward a crease line `creaseOffsetY` below its center while scaling\n * its height by `k`: as EyeOpen → 0 the white folds shut. Because iris/pupil/\n * highlight CLIP to the white, the closing clip region CUTS the (static, round)\n * iris away instead of squashing it — unlike the old scaleY-collapse blink.\n * EyeOpen=1 → rest (zero offsets = the authored open art); =0 → folded.\n *\n * Offsets are authored in the mesh's own pixel frame (+y up, centered), matching\n * `createPixelGridMesh`, so the SAME mesh must be passed that the part renders.\n */\nexport function bakeEyelidFoldWarp(\n mesh: IkiMesh,\n parameter: string,\n creaseOffsetY: number,\n k: number,\n): IkiWarp {\n const closed: number[] = [];\n const zeros: number[] = [];\n for (let i = 0; i < mesh.vertices.length; i += 2) {\n const vy = mesh.vertices[i + 1];\n // closed y = creaseOffsetY + vy*k → dy added to the rest vertex vy.\n closed.push(0, creaseOffsetY - (1 - k) * vy);\n zeros.push(0, 0);\n }\n return {\n parameter,\n keyforms: [\n { value: 0, offsets: closed },\n { value: 1, offsets: zeros },\n ],\n };\n}\n\n// ── bakeHairFrontSilhouetteWarp ──────────────────────────────────────────────\n\n/**\n * How far the face plate's edge EVER travels from the face centre over the\n * turn, either side — the floor under any hold edge.\n *\n * It scans both sides at EVERY stop instead of taking the rest half-width or\n * the full-turn one, because the plate slides as well as bends, and the two\n * pull against each other on the edge the slide pushes out while the bend\n * foreshortens it in — the FAR one, whichever physical side that is at this\n * turn's sign, which is why the scan takes both. The slide's push is linear in\n * the angle and the same at any radius; the bend's pull is the cylinder's own,\n * so it shrinks as the radius flattens and gains on the slide as the angle\n * grows. Which stop reaches furthest therefore moves with the radius: on the\n * assembly fixture, whose plate is 300 px half-wide, the bend leads from the\n * first stop at a radius of 1.2 half-widths (REST wins, 300 px), the slide\n * leads until the bend catches it at 4 (a MID stop, 317 px), and is never\n * caught at 8 (FULL turn, 325 px). A hold edge inside that reach would make\n * the ramp between the two run backwards.\n */\nexport function plateReach(\n faceCenterX: number,\n faceHalfWidth: number,\n columnMapAt: (deg: number) => TurnColumnMap,\n): number {\n let reach = 0;\n for (const deg of HEAD_TURN_STOPS) {\n reach = Math.max(\n reach,\n plateReachAt(columnMapAt(deg), faceCenterX, faceHalfWidth),\n );\n }\n return reach;\n}\n\n/**\n * The same thing at ONE stop: how far the plate's edge lands from the face\n * centre, whichever side lands further out.\n *\n * Every gate on the hold edge is this number — the bake's own guard, and the\n * solver's check that a radius can hold the silhouette it was asked for. They\n * have to agree: a radius the solver accepts and the bake then refuses is a\n * generator that throws from inside its own answer.\n */\nexport function plateReachAt(\n map: TurnColumnMap,\n faceCenterX: number,\n faceHalfWidth: number,\n): number {\n let reach = 0;\n for (const side of [-1, 1]) {\n const dest = map.mapX(faceCenterX + side * faceHalfWidth);\n reach = Math.max(reach, Math.abs(dest - faceCenterX));\n }\n return reach;\n}\n\n/**\n * The head's OUTLINE, held through the turn by the part that draws it — the\n * bangs — as a per-vertex AngleX warp on hair_front, keyed on HEAD_TURN_STOPS.\n *\n * The face warp bends AND slides everything riding its grid, so the side\n * strands, which are where a viewer reads the head's width, squeeze in with the\n * plate and travel off with it: the head narrows and shifts instead of turning.\n * Ramping that out at the GRID level cannot separate them: at FACE_GRID_CELLS\n * columns the cell that carries a strand also carries the outer eye, so\n * un-bending one un-bends the other.\n *\n * hair_front therefore cancels the map — bend and slide both — on itself. Part\n * warps displace the mesh vertices BEFORE those vertices bind to the rest grid,\n * so this is an INVERSE: for the position `target` a vertex should end up at,\n * it displaces the vertex to `invertX(target)`, which the grid's own map then\n * sends back to `target`. In ABSOLUTE model x (`partX + vx`) throughout,\n * because that map is the grid's, not the part's.\n *\n * `target` is a MONOTONE three-zone function of the vertex's REST distance from\n * the face centre, so the warp can never fold a hair cell:\n * - within `faceHalfWidth`, on the plate: exactly where the grid puts it;\n * - out to `holdBase`: a straight ramp from the plate edge's destination onto\n * the hold edge's, `holdEdgeAt(deg)`;\n * - beyond `holdBase`: the hold edge's own displacement, slope 1, so the\n * outer strands carry whatever silhouette change the caller asked for.\n * The zone boundaries are REST distances and do not move with the stop — only\n * the destinations do, which is what keeps the ramp and the outer zone\n * continuous at every stop. A destination that MOVES is the point of\n * `holdEdgeAt`: a caller that has measured the head's width and wants a\n * specific silhouette ratio at full turn narrows it per stop, while the\n * boundary it pivots on stays put. `holdEdgeAt(0)` must therefore be\n * `holdBase` — enforced, since anything else displaces the bangs in the rest\n * pose, where every proportion was judged. A vertex exactly on the axis never\n * leaves the first zone, so its zero `side` is never read.\n *\n * The hold only reaches as far as the DEFORMED grid does: each side's\n * destination is capped to that side's own reach (`holdEdge` below) before\n * `invertX` runs, rather than leaving invertX's own clamp — pinning a target\n * past the edge column onto it — to decide silently. `evaluateTurnCandidate`\n * runs the identical cap inside the hold it ships, so what it fits and reports\n * is what lands here: a CALLER target the cap keeps any hold from rendering is\n * refused there instead, and a DEFAULTED one is reported through\n * `TurnSolveReport.achieved.silhouetteRatio` rather than landing short\n * unannounced. A vertex whose own REST x already sits beyond the grid is a\n * separate limit invertX still absorbs quietly, regardless of the cap: at full\n * turn the near side's outermost mesh columns are swallowed that way — see the\n * headroom cap in generateIkiFromLayerSet.\n */\nexport function bakeHairFrontSilhouetteWarp(\n mesh: IkiMesh,\n partX: number,\n faceCenterX: number,\n faceHalfWidth: number,\n holdBase: number,\n holdEdgeAt: (deg: number) => number,\n columnMapAt: (deg: number) => TurnColumnMap,\n): IkiWarp {\n if (holdEdgeAt(0) !== holdBase) {\n throw new Error(\n `auto-rig: bakeHairFrontSilhouetteWarp: holdEdgeAt(0) is ${holdEdgeAt(0)}, not the hold edge's own rest distance ${holdBase}, so the bangs would move in the rest pose`,\n );\n }\n const SIDES = [-1, 1] as const;\n const keyforms = HEAD_TURN_STOPS.map((deg) => {\n const map = columnMapAt(deg);\n const requestedHoldEdge = holdEdgeAt(deg);\n // Where the plate's own edges land, one per side: the ramp's inner end.\n const plateDest = SIDES.map((side) =>\n map.mapX(faceCenterX + side * faceHalfWidth),\n );\n // A hold edge inside the plate's mapped edge would run the ramp between\n // them backwards and fold the strands onto the cheek. Which stop catches it\n // moves with the radius — see plateReach.\n const reached = plateReachAt(map, faceCenterX, faceHalfWidth);\n if (requestedHoldEdge <= reached) {\n const side =\n Math.abs(plateDest[1] - faceCenterX) >\n Math.abs(plateDest[0] - faceCenterX)\n ? 1\n : -1;\n throw new Error(\n `auto-rig: bakeHairFrontSilhouetteWarp: at ${deg}° on the ${side < 0 ? \"-x\" : \"+x\"} side the hold edge sits ${requestedHoldEdge} from the face centre but the plate's edge maps to ${reached}, so the ramp between them would fold`,\n );\n }\n // The hold can only place a vertex as far out as the DEFORMED grid itself\n // reaches on that side — invertX below clamps anything past the edge\n // column onto it — so each side's destination is capped to that reach\n // (the same HOLD_CLEARANCE margin the plate check above uses) before the\n // ramp and the outer zone are built from it; solveTurnModel's\n // evaluateTurnCandidate runs this same cap first, so a caller-measured\n // target that would need it here never reaches the bake. This is a\n // per-STOP limit, never a rest-space one: at 0° `map` is the identity (no\n // bend to clamp against) and `requestedHoldEdge` already equals `holdBase`\n // exactly (enforced above), so capping it there would shrink the REST\n // silhouette itself — the rest keyform has to stay all-zero.\n const holdEdge: [number, number] =\n deg === 0\n ? [requestedHoldEdge, requestedHoldEdge]\n : (SIDES.map((_side, idx) =>\n Math.min(\n requestedHoldEdge,\n Math.abs(\n map.warpedX[idx === 0 ? 0 : map.warpedX.length - 1] -\n faceCenterX,\n ) - HOLD_CLEARANCE,\n ),\n ) as [number, number]);\n const offsets: number[] = [];\n for (let i = 0; i < mesh.vertices.length; i += 2) {\n const x = partX + mesh.vertices[i];\n // Shared with evaluateTurnCandidate's own silhouette-centre reading, so\n // the two cannot drift apart on what a vertex's target actually is.\n const target = hairFrontHoldTarget(\n x,\n faceCenterX,\n faceHalfWidth,\n holdBase,\n holdEdge,\n map,\n );\n // dy is zero — the hold is horizontal, like the map (bend and slide) it\n // cancels.\n offsets.push(map.invertX(target) - x, 0);\n }\n return { value: deg, offsets };\n });\n return { parameter: StandardParameter.AngleX, keyforms };\n}\n\n// ── bakeHairSwayWarp ───────────────────────────────────────────────────────────\n\n/** Tip travel of a swaying hair part at full sway, as a fraction of its own\n * height — \"the ends swing 9% of the hair's length\". Long back hair therefore\n * swings further than the bangs in pixels, as it should. */\nconst HAIR_SWAY_TIP_FRACTION = 0.09;\n/** Exponent on distance-from-root. 1 would be a hinge (straight shear); hair\n * bends, so the swing grows faster toward the ends. */\nconst HAIR_SWAY_CURL = 1.5;\n/** Range of the HairSwayX / HairSwayZ output parameters. */\nconst HAIR_SWAY_RANGE = 20;\n/** How far into that range one spring actually swings, as a fraction: the\n * hair rigs (mass 1, stiffness 80, damping 10 → ζ ≈ 0.56) overshoot a step\n * by ~12% on their ±10 steady state, peaking near 11.2. Used to size the\n * bangs' swing against the face-warp grid. */\nconst HAIR_SWAY_PEAK_FRACTION = 0.56;\n\n/**\n * Root-pinned sideways sway for a hair part, as a per-vertex warp.\n *\n * Parts have no pivot — a `rotate` binding turns a part about its crop centre.\n * Used for sway that swung the bangs' ROOTS off the hairline as much as their\n * ends, which is exactly how hair does not move. This warp instead leaves the\n * top row of the mesh where it is and displaces each row sideways by\n * `tipShift · u^HAIR_SWAY_CURL`, with `u` the row's distance from the top as a\n * fraction of the height: 0 at the roots, 1 at the ends.\n *\n * Keyforms sit at ±range with the rest pose in between, so a zero parameter is\n * zero offsets. Offsets are in the mesh's own pixel frame (+y up, centered),\n * matching `createPixelGridMesh` — pass the SAME mesh the part renders.\n *\n * Also reused for the bangs' AngleX turn lead: same root-pinned shape, driven\n * by the turn instead of a sway spring.\n */\nexport function bakeHairSwayWarp(\n mesh: IkiMesh,\n parameter: string,\n tipShift: number,\n range: number,\n): IkiWarp {\n let top = -Infinity;\n let bottom = Infinity;\n for (let i = 1; i < mesh.vertices.length; i += 2) {\n top = Math.max(top, mesh.vertices[i]);\n bottom = Math.min(bottom, mesh.vertices[i]);\n }\n const height = top - bottom;\n const swing: number[] = [];\n for (let i = 0; i < mesh.vertices.length; i += 2) {\n const u = height > 0 ? (top - mesh.vertices[i + 1]) / height : 0;\n swing.push(tipShift * Math.pow(u, HAIR_SWAY_CURL), 0);\n }\n return {\n parameter,\n keyforms: [\n // `0 - v`, not `-v`: the pinned root row must stay a plain +0.\n { value: -range, offsets: swing.map((v) => 0 - v) },\n { value: range, offsets: swing },\n ],\n };\n}\n\n// ── Head turn travel + body follow ──────────────────────────────────────────\n\n/**\n * The head's sideways travel at full turn (AngleX = ±30), as a fraction of the\n * face plate's own half-width.\n *\n * A fraction, not px, because the travel no longer cancels out of what the turn\n * is fitted to. It used to ride headDeformer, moving the eyes and the silhouette\n * together, so the measured `eyeShift` cue never saw it; baked into the face\n * grid (`bakeHeadTurnGridWarp2DCentered`'s `travel`) it moves the face inside a\n * held silhouette and IS the floor under that cue — an absolute px value would\n * be a quarter of one plate's half-width and the whole of a smaller one's.\n * 0.25 was judged in the playground on a 400 px-wide plate, where it is the\n * 50 px that shipped.\n */\nconst HEAD_TURN_TRAVEL_RATIO = 0.25;\n\n/** That travel in px, for a plate of this half-width: what a layer set ASKS\n * the turn for. A solve with a measured head cuts it down to what the held\n * silhouette has room for — see `shellTravelCap`. */\nfunction headTurnTravel(faceHalfWidth: number): number {\n return HEAD_TURN_TRAVEL_RATIO * faceHalfWidth;\n}\n\n/** The torso's share of that travel. Below ~0.2 the shoulders still read as\n * bolted down; at 1 the neck stops articulating. 0.3 keeps 70 % of the turn\n * in the neck while the shoulders visibly come along. Judged in the\n * playground, not derived. */\nconst BODY_TURN_FOLLOW = 0.3;\n/** Vertical travel of the head at full breath (px, +y up; negative = down). */\nconst HEAD_BREATH_BOB = -12;\n/** The torso's share of the head's breath bob, SAME direction. In phase with\n * the head (not the sample model's counter-phase +6/−12, which reads as a\n * shrug: chest up while the head settles) so the neck compresses by 6 px, not\n * 18. Moving DOWN also keeps the torso's flat canvas-bottom cut off-canvas —\n * a rise would lift that hard edge into view every breath. */\nconst BODY_BREATH_FOLLOW = 0.5;\n\n// ── generateIkiFromLayerSet ───────────────────────────────────────────────────\n\n/**\n * Auto-rig: given decoded layer inputs and the shared canvas size, produce a\n * valid IkiModel ready for parseIkiModel.\n *\n * - Validate all inputs before deriving anything.\n * - Place parts at source-derived positions (bboxToTransform, unshifted).\n * - Emit the standard parameters (same ids/ranges as sample-model.ts), plus a\n * conditional HairSwayX descriptor + hair-sway physics rig when a hair_front\n * layer is present.\n * - Build headDeformer (matrix, neck pivot, nod/tilt/breath bindings — the\n * turn translates nothing rigidly; its travel is in the face warp),\n * bodyDeformer when a body layer is present (matrix, torso-base pivot,\n * AngleX follow + Breath follow), and faceWarp (warp, FACE_GRID_CELLS²,\n * baked cylinder warp center-relative on faceCenterX).\n * - Mesh parts (spec.mesh===true) → width:1, height:1, pixel grid mesh sized by\n * meshCellsFor + role bindings.\n * - Static parts (spec.mesh===false) → width:cropW, height:cropH, no mesh.\n * - Part ids equal the role string (deterministic, no crypto.randomUUID).\n * - Return parseIkiModel(structuredClone(model)) — every caller gets a\n * validated model; bad assembly fails loudly.\n *\n * `options.turnTargets` is what the head turn is fitted to: the cues a 30°\n * reference measures, defaulting to DEFAULT_TURN_TARGETS. A target the CALLER\n * passed that this layer set cannot reach throws; one that came from the\n * defaults is clamped to what it can do — see TurnTargets for why the two\n * differ, and pass `options.onTurnSolved` to see which ones were clamped and\n * what the turn ended up reaching.\n */\nexport function generateIkiFromLayerSet(\n layers: LayerInput[],\n canvas: { width: number; height: number },\n options: {\n turnTargets?: TurnTargets;\n /** Called once, after a turn is solved, with what it settled on. Not called\n * for a layer set without a nose, which solves no turn at all. */\n onTurnSolved?: (report: TurnSolveReport) => void;\n /** Every role with an opaque pixel in the eye-row band, per side, when\n * `turnTargets.headHalfWidth` was measured off that same union — a\n * companion to it, not a turn target itself (the caller-facing contract\n * stays the three cues). See `TurnSolveContext.headEdges`. */\n headEdges?: {\n left: { role: string; x: number }[];\n right: { role: string; x: number }[];\n };\n } = {},\n): IkiModel {\n // Validate first — never derive anything from unchecked input.\n validateLayerInputs(layers, canvas);\n\n // Hair-sway secondary motion is gated on a front-hair layer being present.\n const hasHair = layers.some((l) => l.role === \"hair_front\");\n const hasMouthOpen = layers.some((l) => l.role === \"mouth_open\");\n // The feature parallax's gate — see FEATURE_NOD_DEPTH.\n const hasNose = layers.some((l) => l.role === \"nose\");\n const bodyLayer = layers.find((l) => l.role === \"body\");\n\n // ── Standard parameters — verbatim from sample-model.ts ──────────────────\n const parameters: IkiParameter[] = [\n {\n id: StandardParameter.MouthOpen,\n name: \"Mouth Open\",\n min: 0,\n max: 1,\n default: 0,\n },\n {\n id: StandardParameter.MouthForm,\n name: \"Mouth Form\",\n min: -1,\n max: 1,\n default: 0,\n },\n {\n id: StandardParameter.EyeOpenLeft,\n name: \"Eye L\",\n min: 0,\n max: 1,\n default: 1,\n },\n {\n id: StandardParameter.EyeOpenRight,\n name: \"Eye R\",\n min: 0,\n max: 1,\n default: 1,\n },\n {\n id: StandardParameter.EyeballX,\n name: \"Gaze X\",\n min: -1,\n max: 1,\n default: 0,\n },\n {\n id: StandardParameter.EyeballY,\n name: \"Gaze Y\",\n min: -1,\n max: 1,\n default: 0,\n },\n {\n id: StandardParameter.AngleX,\n name: \"Head Angle\",\n min: -30,\n max: 30,\n default: 0,\n },\n {\n id: StandardParameter.AngleY,\n name: \"Head Angle Y\",\n min: -30,\n max: 30,\n default: 0,\n },\n {\n id: StandardParameter.AngleZ,\n name: \"Head Angle Z\",\n min: -30,\n max: 30,\n default: 0,\n },\n {\n id: StandardParameter.Breath,\n name: \"Breath\",\n min: 0,\n max: 1,\n default: 0,\n },\n {\n id: StandardParameter.BrowLeftY,\n name: \"Brow L Y\",\n min: -1,\n max: 1,\n default: 0,\n },\n {\n id: StandardParameter.BrowRightY,\n name: \"Brow R Y\",\n min: -1,\n max: 1,\n default: 0,\n },\n {\n id: StandardParameter.BrowLeftAngle,\n name: \"Brow L Angle\",\n min: -1,\n max: 1,\n default: 0,\n },\n {\n id: StandardParameter.BrowRightAngle,\n name: \"Brow R Angle\",\n min: -1,\n max: 1,\n default: 0,\n },\n ];\n\n // Hair-sway output params (physics-driven), declared only when there is front\n // hair to drive — keeps no-hair models free of unused parameters.\n if (hasHair) {\n parameters.push(\n {\n id: StandardParameter.HairSwayX,\n name: \"Hair Sway X\",\n min: -HAIR_SWAY_RANGE,\n max: HAIR_SWAY_RANGE,\n default: 0,\n },\n {\n id: StandardParameter.HairSwayZ,\n name: \"Hair Sway Z\",\n min: -HAIR_SWAY_RANGE,\n max: HAIR_SWAY_RANGE,\n default: 0,\n },\n );\n }\n\n // ── Face layer: derive center and crop for pivot + grid ───────────────────\n const faceLayers = layers.filter((l) => l.role === \"face\");\n // validateLayerInputs guarantees \"face\" is present — safe to assert here.\n const faceLayer = faceLayers[0]!;\n const faceTransform = bboxToTransform(\n faceLayer.bbox,\n faceLayer.canvasW,\n faceLayer.canvasH,\n \"face\",\n );\n // faceCenterX: source-placed face center in model space (unshifted).\n const faceCenterX = faceTransform.x;\n const faceCropH = faceLayer.cropH;\n\n // ── Union bbox of all faceWarp-child layers (model space) ─────────────────\n // All faceWarp-assigned roles have spec.mesh===true (validated by ROLE_TABLE).\n // Each child's model-space extent: transform.{x,y} ± cropW/2, cropH/2\n // (centered pixel mesh convention — part.transform is the crop center).\n const faceWarpLayers = layers.filter(\n (l) => ROLE_TABLE[l.role].deformer === \"faceWarp\",\n );\n\n // Fall back to a full-canvas box only when no faceWarp layers exist (shouldn't\n // happen given required roles, but guards against future role-table changes).\n let unionMinX = -canvas.width / 2;\n let unionMaxX = canvas.width / 2;\n let unionMinY = -canvas.height / 2;\n let unionMaxY = canvas.height / 2;\n\n if (faceWarpLayers.length > 0) {\n const transforms = faceWarpLayers.map((l) =>\n bboxToTransform(l.bbox, l.canvasW, l.canvasH, l.role),\n );\n\n unionMinX = Math.min(\n ...transforms.map((t, i) => t.x - faceWarpLayers[i].cropW / 2),\n );\n unionMaxX = Math.max(\n ...transforms.map((t, i) => t.x + faceWarpLayers[i].cropW / 2),\n );\n unionMinY = Math.min(\n ...transforms.map((t, i) => t.y - faceWarpLayers[i].cropH / 2),\n );\n unionMaxY = Math.max(\n ...transforms.map((t, i) => t.y + faceWarpLayers[i].cropH / 2),\n );\n\n // Expand by 12% margin on each side so no child vertex lands on the grid\n // boundary and gets clamped by bindPointToRestGrid.\n const spanX = unionMaxX - unionMinX;\n const spanY = unionMaxY - unionMinY;\n const MARGIN = 0.12;\n unionMinX -= spanX * MARGIN;\n unionMaxX += spanX * MARGIN;\n unionMinY -= spanY * MARGIN;\n unionMaxY += spanY * MARGIN;\n }\n\n // ── faceWarp grid: symmetric about faceCenterX, spanning the margined union ─\n // Symmetric x so the cylinder axis aligns exactly with the face center.\n // halfW is the larger of the two distances from faceCenterX to the union edges,\n // ensuring the symmetric range [faceCenterX-halfW, faceCenterX+halfW] encloses\n // every child. y-range uses the margined union directly (not symmetric).\n const halfW = Math.max(faceCenterX - unionMinX, unionMaxX - faceCenterX);\n const faceGridMinX = faceCenterX - halfW;\n const faceGridMaxX = faceCenterX + halfW;\n\n const faceGrid = {\n cols: FACE_GRID_CELLS,\n rows: FACE_GRID_CELLS,\n points: generateGridPoints(\n FACE_GRID_CELLS,\n FACE_GRID_CELLS,\n faceGridMinX,\n faceGridMaxX,\n unionMinY,\n unionMaxY,\n ),\n };\n\n // The head cylinder's turn radius, and how far in front of its axis each\n // feature sits: SOLVED from the turn cues, on the very column map the face\n // warp will be baked with, so what the targets promise is what the keyforms\n // do. Without a nose there is no feature slide to fit (see FEATURE_NOD_DEPTH)\n // and nothing to solve the radius against, so it stays the grid's own reach\n // with the no-fold margin — halfW IS that reach, the grid being symmetric\n // about faceCenterX, and the bound then lands exactly on the outer columns.\n // Depth-parallax units for the hair and feature layers come off the same\n // cylinders the bake bends: this radius for the turn, the larger vertical\n // reach about the face center for the nod.\n const faceCenterY = faceTransform.y;\n const halfH = Math.max(faceCenterY - unionMinY, unionMaxY - faceCenterY);\n const faceHalfWidth = faceLayer.cropW / 2;\n const hairFrontLayer = layers.find((l) => l.role === \"hair_front\");\n const turn = hasNose\n ? solveTurnModel(\n resolveTurnTargets(options.turnTargets),\n turnLandmarks(layers),\n faceGrid,\n faceCenterX,\n faceHalfWidth,\n hairFrontLayer &&\n (() => {\n const t = bboxToTransform(\n hairFrontLayer.bbox,\n hairFrontLayer.canvasW,\n hairFrontLayer.canvasH,\n \"hair_front\",\n );\n return {\n x: t.x,\n centerY: t.y,\n cropW: hairFrontLayer.cropW,\n cropH: hairFrontLayer.cropH,\n };\n })(),\n options.headEdges,\n )\n : undefined;\n if (turn?.unreachable) {\n throw new TurnTargetError(\n turn.attainable === undefined\n ? // Only a silhouette can land here: nothing the field's own accepted\n // range allows renders on this layer set, so there is no interval to\n // offer instead — see `silhouetteMiss` in evaluateTurnCandidate.\n `auto-rig: turnTargets.${turn.field} ${turn.value} is unreachable for this layer set, and so is every ${turn.field} in [${SILHOUETTE_RATIO_MIN}, ${TURN_RATIO_MAX}]`\n : `auto-rig: turnTargets.${turn.field} ${turn.value} is unreachable for this layer set (attainable ${turn.attainable[0]}…${turn.attainable[1]})`,\n );\n }\n if (turn) {\n options.onTurnSolved?.({\n radius: turn.radius,\n holdBase: turn.holdBase,\n depths: turn.depths,\n achieved: turn.achieved,\n clamped: turn.clamped,\n });\n }\n const faceRadius = turn?.radius ?? halfW * HEAD_CYLINDER_RADIUS_FACTOR;\n // The turn's sideways travel for THIS plate, in px: what the solve settled\n // on — this plate's own ask, cut down to what the held silhouette has room\n // for — or that ask uncut when there was no solve to size it against. The\n // face bake, the bangs' hold and the body's follow all key off this one\n // number: the hold inverts the very map the face renders, so a second value\n // here would have it hold against a turn nothing ships.\n const headTravel = turn?.travel ?? headTurnTravel(faceHalfWidth);\n const parallaxUnit = headTurnParallaxUnit(faceRadius);\n const parallaxUnitY = headNodParallaxUnit(halfH);\n\n // ── headDeformer pivot (neck): slightly below the face bottom ─────────────\n // faceBottom is the model-space y of the bottom edge of the face crop.\n // The neck pivot sits 15% of the face crop height below the face bottom.\n const faceBottom = faceTransform.y - faceCropH / 2;\n const neckPivot = {\n x: faceCenterX,\n y: faceBottom - faceCropH * 0.15, // 15% below face bottom = neck\n };\n\n // ── Bake the center-relative turn × nod cylinder warp ─────────────────────\n const faceWarp2d = bakeHeadTurnGridWarp2DCentered(\n faceGrid,\n StandardParameter.AngleX,\n StandardParameter.AngleY,\n faceCenterX,\n faceCenterY,\n faceRadius,\n headTravel,\n );\n\n // ── Deformers ─────────────────────────────────────────────────────────────\n const deformers: IkiDeformer[] = [\n // headDeformer: rigid matrix rotating/translating the whole head about the\n // neck pivot. Same bindings as sample-model.ts apart from the turn, which\n // moves the face inside the head rather than the head itself (see below).\n {\n id: \"headDeformer\",\n pivot: neckPivot,\n bindings: [\n // No AngleX binding: the turn moves NOTHING rigidly. Its sideways\n // travel is baked into faceWarp's own grid (see\n // bakeHeadTurnGridWarp2DCentered's `travel`) so the face slides inside\n // a silhouette the bangs hold; a translate here would take the hair\n // shell with it. Nor is the turn a roll — the sample model's ±6° \"lean\n // into the turn\" was tried here and dropped, since rotating about the\n // neck pivot swings the crown (~500px above it) far more than the chin,\n // so the top of the head appeared to lunge ahead of the face on every\n // turn. Roll is AngleZ's job, below.\n\n // Nod: a vertical translate only. No rotate — a pitch expressed as a\n // rigid rotation would sum with the AngleZ roll below at diagonal\n // poses, collapsing pitch into roll.\n {\n parameter: StandardParameter.AngleY,\n channel: \"translateY\" as const,\n from: -NOD_TRAVEL,\n to: NOD_TRAVEL,\n },\n // Tilt: the whole head rolls about the neck pivot, one degree per\n // degree. Positive AngleZ is clockwise on screen (engine rotate is\n // CCW-positive, hence the flipped from/to) — Live2D's convention: in\n // its own sample motions AngleZ carries the sign of AngleX 214 times\n // out of 222, i.e. a head that leans into its turn tilts clockwise on\n // a turn to the viewer's right. The only rotate binding on the head.\n {\n parameter: StandardParameter.AngleZ,\n channel: \"rotate\" as const,\n from: 30,\n to: -30,\n },\n {\n parameter: StandardParameter.Breath,\n channel: \"translateY\" as const,\n from: 0,\n to: HEAD_BREATH_BOB,\n },\n ],\n },\n // faceWarp: cylinder-bend warp parented to headDeformer; grid is symmetric\n // about faceCenterX so the bake's cylinder axis aligns with the face center.\n // One 2D warp carries both the turn and the nod (a deformer holds either\n // `warps` or `warp2d`, never both).\n {\n kind: \"warp\" as const,\n id: \"faceWarp\",\n parent: \"headDeformer\",\n grid: faceGrid,\n warp2d: faceWarp2d,\n },\n ];\n\n // bodyDeformer: the torso's own rigid deformer. A sibling of headDeformer, not\n // its parent, so the head's own bindings stay untouched; reparent the head\n // under it once the torso gains a rotation. Translate-only, so the pivot is\n // inert today; it sits at the torso base, where a future lean would rock from.\n if (bodyLayer) {\n const bt = bboxToTransform(\n bodyLayer.bbox,\n bodyLayer.canvasW,\n bodyLayer.canvasH,\n \"body\",\n );\n deformers.push({\n id: \"bodyDeformer\",\n pivot: { x: bt.x, y: bt.y - bodyLayer.cropH / 2 },\n bindings: [\n // Low-weight follow of the turn, same direction as the face's own\n // slide — the only rigid part of the travel left.\n {\n parameter: StandardParameter.AngleX,\n channel: \"translateX\" as const,\n from: -BODY_TURN_FOLLOW * headTravel,\n to: BODY_TURN_FOLLOW * headTravel,\n },\n // Breath: follow the head's bob, same direction, half amplitude.\n {\n parameter: StandardParameter.Breath,\n channel: \"translateY\" as const,\n from: 0,\n to: BODY_BREATH_FOLLOW * HEAD_BREATH_BOB,\n },\n ],\n });\n }\n\n // ── Shared closed-eye crease per side ─────────────────────────────────────\n // The white AND the lash fold to the SAME seam (derived from the eye-white\n // center), so the lash lands on top of the cut eyeball and covers it.\n const eyeCreaseBySide: Partial<Record<\"L\" | \"R\", number>> = {};\n for (const layer of layers) {\n const side = ROLE_TABLE[layer.role].eyeSide;\n if ((layer.role === \"eye_L\" || layer.role === \"eye_R\") && side) {\n const ey = bboxToTransform(\n layer.bbox,\n layer.canvasW,\n layer.canvasH,\n layer.role,\n ).y;\n eyeCreaseBySide[side] = ey - EYELID_FOLD_CREASE * layer.cropH;\n }\n }\n\n // ── Parts ─────────────────────────────────────────────────────────────────\n const parts: IkiPart[] = layers.map((layer) => {\n const { role, bbox, cropW, cropH, canvasW, canvasH } = layer;\n const spec = ROLE_TABLE[role];\n const t = bboxToTransform(bbox, canvasW, canvasH, role);\n const roleBindings = bindingsForRole(spec, role, cropW, cropH, {\n hasMouthOpen,\n parallaxUnit,\n parallaxUnitY,\n hasNose,\n turnDepths: turn?.depths,\n });\n // `IkiPart.deformer` is optional, so a \"none\" role states its detachment by\n // leaving the field off rather than naming a deformer that must exist.\n const deformerId = spec.deformer === \"none\" ? undefined : spec.deformer;\n\n if (spec.mesh) {\n // Warp-deformer child: width:1, height:1 with a pixel grid mesh centered\n // at the crop center. The engine applies the part transform to position it.\n const { cols, rows } = meshCellsFor(cropW, cropH);\n const mesh = createPixelGridMesh(cols, rows, cropW, cropH);\n const part: IkiPart = {\n id: role,\n color: [1, 1, 1, 1] as [number, number, number, number],\n width: 1,\n height: 1,\n order: spec.order,\n transform: t,\n deformer: deformerId,\n mesh,\n };\n if (roleBindings.length > 0) {\n part.bindings = roleBindings;\n }\n // Hair sway and turn lead: the PhysicsMotion springs lag AngleX / AngleZ\n // onto HairSwayX / HairSwayZ (rigs and params exist only with front\n // hair), and both hair parts swing their ends on them with the roots\n // pinned. The back hair is longer, so the same fraction of its height is\n // a bigger swing.\n if ((role === \"hair_front\" || role === \"hair_back\") && hasHair) {\n let tipShift = HAIR_SWAY_TIP_FRACTION * cropH;\n let leadWarp: IkiWarp | undefined;\n let silhouetteWarp: IkiWarp | undefined;\n if (role === \"hair_front\") {\n // hair_front is a faceWarp child: its vertices are swayed, shifted\n // and silhouette-held BEFORE they bind to the rest grid, and past the\n // grid's edge they clamp to the edge column — the tips flatten into\n // a vertical line. The cap sizes the swing against the margin the\n // sway has to itself: the rest pose and small turns. As the turn\n // grows the silhouette hold spends that whole margin on the outermost\n // columns — holding their rest x means parking them on the grid's own\n // edge — so at the extremes their swing is swallowed whatever the cap\n // says, and reserving the hold's share here would zero the swing\n // everywhere instead, rest pose included. A wider grid would buy them\n // both room, but this generator still derives the turn radius from the\n // grid's own half-width, so widening it without decoupling the two\n // flattens the face's bend.\n const shift = HAIR_FRONT_DEPTH * parallaxUnit;\n const headroom =\n Math.min(\n faceGridMaxX - (t.x + cropW / 2),\n t.x - cropW / 2 - faceGridMinX,\n ) - shift;\n // The lead alone can never exhaust the headroom (tips get\n // 0.06·halfW against a ≥0.12·span margin); Math.max(0, headroom)\n // only guards the sway, which can still turn it negative.\n tipShift = Math.min(\n tipShift,\n Math.max(0, headroom) / HAIR_SWAY_PEAK_FRACTION,\n );\n // The bangs' turn lead: root-pinned like the sway, keyed to\n // HEAD_TURN_MAX_DEG instead of a sway spring's range — see\n // bindingsForRole's doc for why this is a warp, not a binding.\n // Warp order in the array doesn't matter; they sum.\n leadWarp = bakeHairSwayWarp(\n mesh,\n StandardParameter.AngleX,\n shift,\n HEAD_TURN_MAX_DEG,\n );\n // The bangs draw the head's outline, so they hold it through the\n // turn instead of squeezing in with the plate beneath them.\n const columnMapAt = (deg: number) =>\n turnColumnMap(faceGrid, faceCenterX, faceRadius, deg, headTravel);\n // The solve already picked this zone for the radius it picked: the\n // head's measured half-width when it had one, and a destination that\n // carries the silhouette ratio through the turn. Without a solve\n // nothing here has measured where the outline actually is, so the\n // hold edge is the outermost the plate ever reaches, clear of it by a\n // pixel, and it holds its rest position: the silhouette stops\n // narrowing without being asked to move. That reach already covers\n // the rest pose (its 0° stop is the identity map), and anything\n // inside it would fold the ramp.\n const holdBase =\n turn?.holdBase ??\n plateReach(faceCenterX, faceHalfWidth, columnMapAt) +\n HOLD_CLEARANCE;\n silhouetteWarp = bakeHairFrontSilhouetteWarp(\n mesh,\n t.x,\n faceCenterX,\n faceHalfWidth,\n holdBase,\n turn?.holdEdgeAt ?? (() => holdBase),\n columnMapAt,\n );\n }\n part.warps = [\n bakeHairSwayWarp(\n mesh,\n StandardParameter.HairSwayX,\n tipShift,\n HAIR_SWAY_RANGE,\n ),\n bakeHairSwayWarp(\n mesh,\n StandardParameter.HairSwayZ,\n tipShift,\n HAIR_SWAY_RANGE,\n ),\n ...(leadWarp ? [leadWarp] : []),\n ...(silhouetteWarp ? [silhouetteWarp] : []),\n ];\n }\n // Eye blink = fold: the white (eye_) and the lash (lash_) fold shut via a\n // warp toward the shared crease; iris/pupil/highlight clip to the white, so\n // the closing white CUTS them away (round, not squashed) and the lash lands\n // on top to cover the seam. The white is a required role (clip mask exists).\n if (spec.eyeSide !== undefined) {\n const isLash = role.startsWith(\"lash_\");\n if (role.startsWith(\"eye_\") || isLash) {\n const openParam =\n spec.eyeSide === \"L\"\n ? StandardParameter.EyeOpenLeft\n : StandardParameter.EyeOpenRight;\n const creaseWorldY =\n eyeCreaseBySide[spec.eyeSide] ?? t.y - EYELID_FOLD_CREASE * cropH;\n // Separate lashes supply the closed-eye line. Collapse their sclera\n // clip completely so a strip of iris cannot show underneath.\n const hasLash = layers.some((l) => l.role === `lash_${spec.eyeSide}`);\n part.warps = [\n bakeEyelidFoldWarp(\n mesh,\n openParam,\n creaseWorldY - t.y,\n isLash ? LASH_FOLD_K : hasLash ? 0 : EYELID_FOLD_K,\n ),\n ];\n } else {\n part.clip = { masks: [`eye_${spec.eyeSide}`] };\n }\n }\n return part;\n } else {\n // Static quad: no mesh, sized to the crop. It still takes bindings: no\n // static role carries any today, but this branch once dropped them\n // silently, and the next static role with a binding must not hit that.\n const part: IkiPart = {\n id: role,\n color: [1, 1, 1, 1] as [number, number, number, number],\n width: cropW,\n height: cropH,\n order: spec.order,\n transform: t,\n deformer: deformerId,\n };\n if (roleBindings.length > 0) {\n part.bindings = roleBindings;\n }\n return part;\n }\n });\n\n const model = {\n version: IKI_FORMAT_VERSION,\n name: \"Auto-Rigged Model\",\n canvas: { width: canvas.width, height: canvas.height },\n textures: [],\n parameters,\n deformers,\n parts,\n // Secondary motion: one spring lags AngleX onto HairSwayX so front hair\n // sways behind the head turn (same constants as the hand-authored sample),\n // and a second lags AngleZ onto HairSwayZ so it swings behind a tilt. Two\n // rigs because a rig has one input and one output (validator-enforced).\n // Both hair parts read the outputs through root-pinned sway warps.\n // Omitted when there is no front hair to drive.\n physics: hasHair\n ? [\n {\n id: \"hairSway\",\n input: { parameter: StandardParameter.AngleX, weight: 1 },\n output: { parameter: StandardParameter.HairSwayX, scale: -10 },\n mass: 1,\n stiffness: 80,\n damping: 10,\n },\n {\n id: \"hairTilt\",\n input: { parameter: StandardParameter.AngleZ, weight: 1 },\n output: { parameter: StandardParameter.HairSwayZ, scale: -10 },\n mass: 1,\n stiffness: 80,\n damping: 10,\n },\n ]\n : undefined,\n };\n\n // Gate: run through parseIkiModel so bad assembly fails loudly at the source.\n // structuredClone prevents the validator's normalizing output from aliasing\n // the local object, and ensures the returned model is fully independent.\n return parseIkiModel(structuredClone(model));\n}\n"],"mappings":"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","names":["parseIkiModel","parseIkiModel","y","x","IKI_FORMAT_VERSION","parseIkiModel","col","row","travel","holdBase","holdEdgeAt","map","gridReach","hold","IKI_FORMAT_VERSION","parseIkiModel"]}