@energy8platform/golem 0.5.0 → 0.7.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.
- package/README.md +18 -0
- package/dist/editor.css +3 -4
- package/dist/editor.js +177 -75
- package/dist/lib/cli.js +302 -126
- package/dist/lib/cli.js.map +4 -4
- package/dist/lib/e8/agent.d.ts +35 -0
- package/dist/lib/e8/host.d.ts +22 -0
- package/dist/lib/e8/node.d.ts +66 -0
- package/dist/lib/e8/runtime.d.ts +10 -0
- package/dist/lib/e8/schema.d.ts +3 -0
- package/dist/lib/e8/types.d.ts +80 -0
- package/dist/lib/e8-agent.js +6781 -0
- package/dist/lib/e8-agent.js.map +7 -0
- package/dist/lib/e8-client.js +116 -0
- package/dist/lib/e8-host.js +6929 -0
- package/dist/lib/e8-host.js.map +7 -0
- package/dist/lib/e8-runtime.js +2024 -0
- package/dist/lib/e8-runtime.js.map +7 -0
- package/dist/lib/e8-schema.js +7 -0
- package/dist/lib/e8-schema.js.map +7 -0
- package/dist/lib/editor/api.d.ts +1 -0
- package/dist/lib/editor/embed.d.ts +9 -0
- package/dist/lib/editor/server.d.ts +23 -1
- package/dist/lib/editor/store.d.ts +107 -0
- package/dist/lib/editor/timeline.d.ts +15 -0
- package/dist/lib/editor-entry.d.ts +1 -1
- package/dist/lib/editor-entry.js +303 -126
- package/dist/lib/editor-entry.js.map +4 -4
- package/dist/lib/harness.js +69 -14
- package/dist/lib/interactive-editor.js.map +1 -1
- package/dist/lib/rig-anim.d.ts +2 -0
- package/dist/lib/rig-control-qa.d.ts +27 -0
- package/dist/lib/rig-format.d.ts +989 -0
- package/dist/lib/rig-import-layers.d.ts +2 -0
- package/dist/lib/rig-version.d.ts +12 -0
- package/dist/lib/runtime.js +69 -14
- package/dist/lib/runtime.js.map +3 -3
- package/dist/lib/spine-import.d.ts +4 -0
- package/dist/lib/tool-schema.d.ts +3 -0
- package/dist/lib/tools.d.ts +2 -0
- package/dist/lib/tools.js +257 -73
- package/dist/lib/tools.js.map +4 -4
- package/editor.html +2 -2
- package/package.json +38 -3
- package/skills/e8-golem/SKILL.md +71 -0
- package/skills/golem-symbol-animation/SKILL.md +87 -0
- package/skills/golem-symbol-animation/agents/openai.yaml +4 -0
- package/skills/golem-symbol-animation/references/facial-controls.md +48 -0
- package/skills/golem-symbol-animation/references/game-engine-integration.md +133 -0
- package/skills/golem-symbol-animation/references/golem-authoring.md +138 -0
- package/skills/golem-symbol-animation/references/interactive-editor.md +17 -0
- package/skills/golem-symbol-animation/references/motion-craft.md +113 -0
- package/skills/golem-symbol-animation/references/packed-delivery.md +88 -0
- package/skills/golem-symbol-animation/references/quantitative-qa.md +106 -0
- package/skills/golem-symbol-animation/references/spine-rive-study.md +87 -0
- package/skills/golem-symbol-animation/scripts/atlas_parts.py +87 -0
- package/skills/golem-symbol-animation/scripts/check_package.py +71 -0
- package/skills/golem-symbol-animation/scripts/image_gates.py +143 -0
- package/skills/golem-symbol-cutting/SKILL.md +70 -0
- package/skills/golem-symbol-cutting/agents/openai.yaml +4 -0
- package/skills/golem-symbol-cutting/assets/h1/h1.atlas.png +0 -0
- package/skills/golem-symbol-cutting/assets/h1/h1.png +0 -0
- package/skills/golem-symbol-cutting/references/cutting-workflow.md +92 -0
- package/skills/golem-symbol-cutting/references/facial-layers.md +44 -0
- package/skills/golem-symbol-cutting/references/generation-workflow.md +109 -0
- package/skills/golem-symbol-cutting/references/golem-handoff.md +62 -0
- package/skills/golem-symbol-cutting/references/h1-example.md +46 -0
- package/skills/golem-symbol-cutting/references/hybrid-workflow.md +119 -0
- package/skills/golem-symbol-cutting/references/registration-and-motion.md +89 -0
- package/skills/golem-symbol-cutting/references/spine-rive-construction.md +73 -0
- package/skills/golem-symbol-cutting/scripts/atlas_parts.py +87 -0
- package/skills/golem-symbol-cutting/scripts/hybrid_parts.py +279 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/interactive-editor.ts", "../../src/rig-anim.ts", "../../src/interactive-bones.ts"],
|
|
4
|
-
"sourcesContent": ["import type { RigPlayer } from \"./rig-runtime\";\nimport { Matrix, type Mesh, type Sprite } from \"pixi.js\";\nimport type { Slot } from \"./rig-format\";\nimport type { Affine } from \"./rig-anim\";\n\nexport interface EditorPoint {\n x: number;\n y: number;\n}\nexport interface PartTransform {\n x: number;\n y: number;\n rotation: number;\n scaleX: number;\n scaleY: number;\n}\nexport interface InteractiveEditPatch {\n format: \"golem.interactive-edits\";\n version: 1;\n documentId: string;\n transforms: Record<string, PartTransform>;\n}\nexport interface EditorGroup {\n id: string;\n members: readonly string[];\n pivotBone?: string;\n}\nexport type InteractivePlayer = Pick<\n RigPlayer,\n | \"doc\"\n | \"view\"\n | \"world\"\n | \"renderItems\"\n | \"clipPolygons\"\n | \"onAfterPose\"\n | \"refreshPose\"\n>;\nexport interface InteractiveEditorOptions {\n player: InteractivePlayer;\n /** Transparent overlay aligned with the player's rendered viewport. It is cleared by the editor. */\n overlay: HTMLCanvasElement;\n /** Host-defined stable rig fingerprint; mismatched patches are rejected. */\n documentId: string;\n groups?: readonly EditorGroup[];\n /** Override when overlay backing pixels differ from the renderer's logical coordinates. */\n screenToDocument?: (client: EditorPoint) => EditorPoint;\n documentToOverlay?: (point: EditorPoint) => EditorPoint;\n alphaFallback?: \"skip\" | \"geometry\";\n onChange?: (patch: InteractiveEditPatch) => void;\n onSelection?: (id: string | null) => void;\n onHover?: (id: string | null) => void;\n onError?: (error: Error) => void;\n onGestureStart?: () => void;\n /** Render the host scene after a manual refresh; never called from the pose hook. */\n onRender?: () => void;\n historyLimit?: number;\n}\nconst neutral = (): PartTransform => ({\n x: 0,\n y: 0,\n rotation: 0,\n scaleX: 1,\n scaleY: 1,\n});\nconst copy = <T>(v: T): T => {\n const out = JSON.parse(JSON.stringify(v));\n if (out && typeof out === \"object\" && !Array.isArray(out))\n Object.setPrototypeOf(out, null);\n return out;\n};\nconst mat = (w: Affine) => new Matrix(w.a, w.b, w.c, w.d, w.tx, w.ty);\nconst rotate = (p: EditorPoint, r: number): EditorPoint => ({\n x: p.x * Math.cos(r) - p.y * Math.sin(r),\n y: p.x * Math.sin(r) + p.y * Math.cos(r),\n});\nconst pointIn = (p: EditorPoint, v: readonly number[]) => {\n let inside = false;\n for (let i = 0, j = v.length - 2; i < v.length; j = i, i += 2) {\n const x = v[i],\n y = v[i + 1],\n xx = v[j],\n yy = v[j + 1];\n if (y > p.y !== yy > p.y && p.x < ((xx - x) * (p.y - y)) / (yy - y) + x)\n inside = !inside;\n }\n return inside;\n};\ntype Shape = {\n id: string;\n points: EditorPoint[];\n uvs: number[];\n indices: number[];\n node: Sprite | Mesh;\n slot: Slot;\n z: number;\n order: number;\n};\nexport interface EditorFrame {\n id: string;\n center: EditorPoint;\n corners: EditorPoint[];\n handles: { name: string; p: EditorPoint }[];\n}\n\n/** Embedded part editor. Source documents are never mutated; edits replay after every evaluated pose. */\nexport function createInteractiveEditor(options: InteractiveEditorOptions) {\n const { player, overlay } = options,\n ctx = overlay.getContext(\"2d\");\n if (!ctx) throw Error(\"A 2D overlay context is required.\");\n if (!options.documentId) throw Error(\"documentId is required.\");\n if (\n options.historyLimit !== undefined &&\n (!Number.isInteger(options.historyLimit) ||\n options.historyLimit < 1 ||\n options.historyLimit > 10000)\n )\n throw Error(\"historyLimit must be an integer from 1 to 10000\");\n const members = new Map<string, string>(),\n groups = new Map<string, EditorGroup>(),\n attachments = new Set(\n player.doc.attachments.filter((a) => a.type !== \"path\").map((a) => a.id),\n );\n for (const g of options.groups ?? []) {\n if (!g.id || groups.has(g.id) || !g.members.length)\n throw Error(\"Invalid or duplicate editor group\");\n for (const id of g.members) {\n if (!attachments.has(id) || members.has(id))\n throw Error(\"Unknown or multiply grouped attachment: \" + id);\n members.set(id, g.id);\n }\n if (g.pivotBone && !player.doc.bones.some((b) => b.id === g.pivotBone))\n throw Error(\"Unknown group pivot bone\");\n groups.set(g.id, { ...g, members: [...g.members] });\n }\n for (const id of attachments)\n if (!members.has(id)) {\n if (groups.has(id))\n throw Error(\"Group id collides with an ungrouped attachment\");\n groups.set(id, { id, members: [id] });\n members.set(id, id);\n }\n let transforms: Record<string, PartTransform> = Object.create(null),\n past: (typeof transforms)[] = [],\n future: (typeof transforms)[] = [],\n selected: string | null = null,\n hovered: string | null = null,\n enabled = true,\n visible = true,\n disposed = false;\n let drag: {\n id: string;\n mode: string;\n f: NonNullable<ReturnType<typeof frame>>;\n start: EditorPoint;\n client: EditorPoint;\n initial: PartTransform;\n recorded: boolean;\n pointerId: number;\n } | null = null;\n const abort = new AbortController(),\n prior = {\n pointerEvents: overlay.style.pointerEvents,\n touchAction: overlay.style.touchAction,\n cursor: overlay.style.cursor,\n };\n overlay.style.pointerEvents = \"auto\";\n overlay.style.touchAction = \"none\";\n const alphaCache = new WeakMap<\n object,\n { width: number; height: number; data: Uint8ClampedArray } | null\n >();\n const project = (p: EditorPoint) =>\n options.documentToOverlay?.(p) ?? player.view.worldTransform.apply(p);\n const unproject = (p: EditorPoint) => {\n if (options.screenToDocument) return options.screenToDocument(p);\n const r = overlay.getBoundingClientRect();\n return player.view.worldTransform\n .clone()\n .invert()\n .apply({\n x: ((p.x - r.left) * overlay.width) / r.width,\n y: ((p.y - r.top) * overlay.height) / r.height,\n });\n };\n const screen = (p: EditorPoint) => {\n const v = project(p),\n r = overlay.getBoundingClientRect();\n return {\n x: r.left + (v.x * r.width) / overlay.width,\n y: r.top + (v.y * r.height) / overlay.height,\n };\n };\n const transformOf = (id: string) => ({ ...neutral(), ...transforms[id] });\n function ensure() {\n if (disposed) throw Error(\"Interactive editor is disposed\");\n }\n function known(id: string) {\n if (!groups.has(id)) throw Error(\"Unknown editor part: \" + id);\n }\n function shapes(): Shape[] {\n return player.renderItems\n .filter(\n (i) => i.node.visible && i.node.alpha > 0 && i.att.type !== \"path\",\n )\n .map((it, order) => {\n const n = it.node;\n n.updateLocalTransform();\n let p: number[], uv: number[], ind: number[];\n if (\"geometry\" in n) {\n p = Array.from(n.geometry.getBuffer(\"aPosition\").data);\n uv = Array.from(n.geometry.getBuffer(\"aUV\").data);\n ind = Array.from(n.geometry.indexBuffer.data);\n } else {\n const t = n.texture,\n u = t.uvs,\n a = n.anchor,\n trim = t.trim,\n x = (trim?.x ?? 0) - a.x * t.orig.width,\n y = (trim?.y ?? 0) - a.y * t.orig.height,\n w = trim?.width ?? t.orig.width,\n h = trim?.height ?? t.orig.height;\n p = [x, y, x + w, y, x + w, y + h, x, y + h];\n uv = [u.x0, u.y0, u.x1, u.y1, u.x2, u.y2, u.x3, u.y3];\n ind = [0, 1, 2, 0, 2, 3];\n }\n const points: EditorPoint[] = [];\n for (let i = 0; i < p.length; i += 2)\n points.push(n.localTransform.apply({ x: p[i], y: p[i + 1] }));\n return {\n id: members.get(it.att.id)!,\n points,\n uvs: uv,\n indices: ind,\n node: it.node,\n slot: it.slot,\n z: n.zIndex,\n order,\n };\n })\n .sort((a, b) => a.z - b.z || a.order - b.order);\n }\n function alpha(n: Sprite | Mesh, u: number, v: number) {\n const source = n.texture.source;\n if (!alphaCache.has(source)) {\n try {\n const resource = source.resource;\n if (!resource) throw Error(\"Texture has no readable image resource\");\n const c = document.createElement(\"canvas\");\n c.width = source.pixelWidth ?? source.width;\n c.height = source.pixelHeight ?? source.height;\n const cx = c.getContext(\"2d\")!;\n cx.drawImage(resource as CanvasImageSource, 0, 0, c.width, c.height);\n alphaCache.set(source, {\n width: c.width,\n height: c.height,\n data: cx.getImageData(0, 0, c.width, c.height).data,\n });\n } catch (e) {\n alphaCache.set(source, null);\n options.onError?.(\n new Error(\n \"Texture alpha cannot be read (CORS or unsupported source): \" +\n String(e),\n ),\n );\n }\n }\n const a = alphaCache.get(source);\n if (!a) return options.alphaFallback === \"geometry\";\n if (u < 0 || v < 0 || u > 1 || v > 1) return false;\n const x = u * a.width - 0.5,\n y = v * a.height - 0.5,\n x0 = Math.floor(x),\n y0 = Math.floor(y),\n fx = x - x0,\n fy = y - y0;\n let value = 0;\n for (let dy = 0; dy < 2; dy++)\n for (let dx = 0; dx < 2; dx++) {\n const xx = Math.max(0, Math.min(a.width - 1, x0 + dx)),\n yy = Math.max(0, Math.min(a.height - 1, y0 + dy));\n value +=\n a.data[(yy * a.width + xx) * 4 + 3] *\n (dx ? fx : 1 - fx) *\n (dy ? fy : 1 - fy);\n }\n return value >= 24;\n }\n function hitTest(p: EditorPoint) {\n ensure();\n const all = shapes();\n for (let s = all.length - 1; s >= 0; s--) {\n const a = all[s],\n clip = a.slot.clip && player.clipPolygons.get(a.slot.clip);\n if (clip && !pointIn(p, clip)) continue;\n for (let i = 0; i < a.indices.length; i += 3) {\n const ia = a.indices[i],\n ib = a.indices[i + 1],\n ic = a.indices[i + 2],\n A = a.points[ia],\n B = a.points[ib],\n C = a.points[ic],\n den = (B.y - C.y) * (A.x - C.x) + (C.x - B.x) * (A.y - C.y);\n if (Math.abs(den) < 1e-10) continue;\n const wa =\n ((B.y - C.y) * (p.x - C.x) + (C.x - B.x) * (p.y - C.y)) / den,\n wb = ((C.y - A.y) * (p.x - C.x) + (A.x - C.x) * (p.y - C.y)) / den,\n wc = 1 - wa - wb;\n if (Math.min(wa, wb, wc) < -1e-7) continue;\n if (\n alpha(\n a.node,\n wa * a.uvs[ia * 2] + wb * a.uvs[ib * 2] + wc * a.uvs[ic * 2],\n wa * a.uvs[ia * 2 + 1] +\n wb * a.uvs[ib * 2 + 1] +\n wc * a.uvs[ic * 2 + 1],\n )\n )\n return a.id;\n }\n }\n return null;\n }\n function pivot(id: string) {\n const g = groups.get(id)!;\n const a = player.doc.attachments.find((a) => a.id === g.members[0])!,\n slot = player.doc.slots.find((s) => s.id === a.slot)!;\n const w = player.world.get(g.pivotBone ?? slot.bone);\n if (\n !w ||\n !Number.isFinite(w.a * w.d - w.b * w.c) ||\n Math.abs(w.a * w.d - w.b * w.c) < 1e-10\n )\n return null;\n return mat(w);\n }\n function frame(id: string) {\n const viewport = overlay.getBoundingClientRect();\n if (!viewport.width || !viewport.height) return null;\n const points = shapes()\n .filter((s) => s.id === id)\n .flatMap((s) => s.points);\n if (!points.length) return null;\n const W = pivot(id);\n if (!W) return null;\n const inv = W.clone().invert(),\n o = transformOf(id),\n r = (o.rotation * Math.PI) / 180,\n q = points.map((p) => {\n const p0 = inv.apply(p);\n return rotate({ x: p0.x - o.x, y: p0.y - o.y }, -r);\n }),\n x0 = Math.min(...q.map((p) => p.x)),\n x1 = Math.max(...q.map((p) => p.x)),\n y0 = Math.min(...q.map((p) => p.y)),\n y1 = Math.max(...q.map((p) => p.y)),\n cx = (x0 + x1) / 2,\n cy = (y0 + y1) / 2,\n toWorld = (p: EditorPoint) => {\n const v = rotate(p, r);\n return W.apply({ x: v.x + o.x, y: v.y + o.y });\n };\n const handles: { name: string; p: EditorPoint }[] = [\n [\"nw\", x0, y0],\n [\"n\", cx, y0],\n [\"ne\", x1, y0],\n [\"e\", x1, cy],\n [\"se\", x1, y1],\n [\"s\", cx, y1],\n [\"sw\", x0, y1],\n [\"w\", x0, cy],\n ].map(([name, x, y]) => ({\n name: String(name),\n p: toWorld({ x: Number(x), y: Number(y) }),\n })),\n center = toWorld({ x: cx, y: cy }),\n top = handles[1].p,\n st = screen(top),\n sc = screen(center),\n d = Math.hypot(st.x - sc.x, st.y - sc.y) || 1;\n handles.push({\n name: \"rotate\",\n p: unproject({\n x: st.x + ((st.x - sc.x) / d) * 28,\n y: st.y + ((st.y - sc.y) / d) * 28,\n }),\n });\n return {\n id,\n W,\n inv,\n o,\n r,\n x0,\n x1,\n y0,\n y1,\n cx,\n cy,\n center,\n top,\n handles,\n corners: [handles[0].p, handles[2].p, handles[4].p, handles[6].p],\n };\n }\n function draw() {\n ctx!.clearRect(0, 0, overlay.width, overlay.height);\n if (!enabled || !visible) return;\n const box = overlay.getBoundingClientRect(),\n sx = overlay.width / (box.width || overlay.width),\n sy = overlay.height / (box.height || overlay.height);\n const outline = (id: string, color: string, controls: boolean) => {\n const f = frame(id);\n if (!f) return;\n ctx!.strokeStyle = color;\n ctx!.lineWidth = 1.5 * Math.min(sx, sy);\n ctx!.beginPath();\n f.corners\n .map(project)\n .forEach((p, i) => (i ? ctx!.lineTo(p.x, p.y) : ctx!.moveTo(p.x, p.y)));\n ctx!.closePath();\n ctx!.stroke();\n if (controls) {\n const top = project(f.top),\n rot = project(f.handles[8].p);\n ctx!.beginPath();\n ctx!.moveTo(top.x, top.y);\n ctx!.lineTo(rot.x, rot.y);\n ctx!.stroke();\n for (const h of f.handles) {\n const p = project(h.p);\n ctx!.beginPath();\n h.name === \"rotate\"\n ? ctx!.ellipse(p.x, p.y, 5 * sx, 5 * sy, 0, 0, Math.PI * 2)\n : ctx!.rect(p.x - 4 * sx, p.y - 4 * sy, 8 * sx, 8 * sy);\n ctx!.fillStyle = \"#17212c\";\n ctx!.fill();\n ctx!.stroke();\n }\n }\n };\n if (hovered && hovered !== selected) outline(hovered, \"#68d5fa\", false);\n if (selected) outline(selected, \"#ffda81\", true);\n }\n function apply() {\n if (!visible) {\n draw();\n return;\n }\n for (const it of player.renderItems) {\n if (!it.node.visible) continue;\n const id = members.get(it.att.id)!,\n o = transforms[id];\n if (!o) continue;\n const W = pivot(id);\n if (!W) continue;\n const r = (o.rotation * Math.PI) / 180,\n D = W.clone()\n .append(\n new Matrix(\n Math.cos(r) * o.scaleX,\n Math.sin(r) * o.scaleX,\n -Math.sin(r) * o.scaleY,\n Math.cos(r) * o.scaleY,\n o.x,\n o.y,\n ),\n )\n .append(W.clone().invert());\n it.node.updateLocalTransform();\n it.node.setFromMatrix(D.append(it.node.localTransform.clone()));\n }\n if (enabled && hovered)\n for (const it of player.renderItems)\n if (members.get(it.att.id) === hovered && it.node.visible)\n it.node.tint = 0xb9edff;\n draw();\n }\n const unsubscribe = player.onAfterPose(apply);\n const exportPatch = (): InteractiveEditPatch => ({\n format: \"golem.interactive-edits\",\n version: 1,\n documentId: options.documentId,\n transforms: copy(transforms),\n });\n function refresh() {\n player.refreshPose();\n draw();\n options.onRender?.();\n }\n function notify() {\n refresh();\n options.onChange?.(exportPatch());\n }\n function normalize() {\n for (const [id, t] of Object.entries(transforms))\n if (\n Object.entries(t).every(\n ([k, v]) => v === neutral()[k as keyof PartTransform],\n )\n )\n delete transforms[id];\n }\n function remember() {\n past.push(copy(transforms));\n if (past.length > (options.historyLimit ?? 100)) past.shift();\n future = [];\n }\n function validateTransform(value: unknown): PartTransform {\n if (!value || typeof value !== \"object\" || Array.isArray(value))\n throw Error(\"Invalid transform\");\n const out = neutral();\n for (const [k, v] of Object.entries(value)) {\n if (\n !Object.hasOwn(out, k) ||\n typeof v !== \"number\" ||\n !Number.isFinite(v) ||\n Math.abs(v) > 10000 ||\n (k.startsWith(\"scale\") && (v < 0.1 || v > 4))\n )\n throw Error(\"Invalid transform parameter: \" + k);\n out[k as keyof PartTransform] = v;\n }\n return out;\n }\n function select(id: string | null) {\n ensure();\n if (id !== null) known(id);\n selected = id;\n options.onSelection?.(id);\n draw();\n }\n function handleAt(p: EditorPoint, f: ReturnType<typeof frame>) {\n return f?.handles.find((h) => {\n const s = screen(h.p);\n return Math.hypot(p.x - s.x, p.y - s.y) < 9;\n });\n }\n const down = (e: PointerEvent) => {\n if (!enabled || !visible || e.button !== 0 || drag) return;\n const client = { x: e.clientX, y: e.clientY },\n q = unproject(client);\n let f = selected ? frame(selected) : null,\n h = handleAt(client, f);\n if (!h) {\n select(hitTest(q));\n if (!selected) return;\n f = frame(selected);\n }\n if (!f) return;\n options.onGestureStart?.();\n drag = {\n id: selected!,\n mode: h?.name ?? \"move\",\n f,\n start: f.inv.apply(q),\n client,\n initial: transformOf(selected!),\n recorded: false,\n pointerId: e.pointerId,\n };\n overlay.setPointerCapture(e.pointerId);\n e.preventDefault();\n };\n const move = (e: PointerEvent) => {\n if (!enabled || !visible || (drag && drag.pointerId !== e.pointerId))\n return;\n const client = { x: e.clientX, y: e.clientY },\n q = unproject(client);\n if (!drag) {\n const f = selected ? frame(selected) : null,\n h = handleAt(client, f),\n next = h ? null : hitTest(q);\n if (hovered !== next) {\n hovered = next;\n options.onHover?.(next);\n }\n if (h && f) {\n const a = screen(h.p),\n c = screen(f.center),\n angle =\n ((Math.atan2(a.y - c.y, a.x - c.x) * 180) / Math.PI + 180) % 180;\n overlay.style.cursor =\n h.name === \"rotate\"\n ? \"crosshair\"\n : [\"ew-resize\", \"nwse-resize\", \"ns-resize\", \"nesw-resize\"][\n Math.round(angle / 45) % 4\n ];\n } else overlay.style.cursor = hovered ? \"move\" : \"default\";\n refresh();\n return;\n }\n const { f, initial: o } = drag;\n if (!drag.recorded) {\n if (\n Math.hypot(e.clientX - drag.client.x, e.clientY - drag.client.y) < 0.35\n )\n return;\n remember();\n drag.recorded = true;\n }\n const p = f.inv.apply(q);\n let n = { ...o };\n if (drag.mode === \"move\") {\n n.x += p.x - drag.start.x;\n n.y += p.y - drag.start.y;\n } else if (drag.mode === \"rotate\") {\n const c = f.inv.apply(f.center),\n a0 = Math.atan2(drag.start.y - c.y, drag.start.x - c.x),\n a1 = Math.atan2(p.y - c.y, p.x - c.x);\n n.rotation =\n o.rotation +\n (Math.atan2(Math.sin(a1 - a0), Math.cos(a1 - a0)) * 180) / Math.PI;\n const v = rotate({ x: f.cx, y: f.cy }, (n.rotation * Math.PI) / 180);\n n.x = c.x - v.x;\n n.y = c.y - v.y;\n } else {\n const mode = drag.mode,\n p0 = rotate({ x: p.x - o.x, y: p.y - o.y }, -f.r),\n s0 = rotate({ x: drag.start.x - o.x, y: drag.start.y - o.y }, -f.r),\n ax = mode.includes(\"e\") ? f.x0 : mode.includes(\"w\") ? f.x1 : f.cx,\n ay = mode.includes(\"s\") ? f.y0 : mode.includes(\"n\") ? f.y1 : f.cy;\n let fx = /[ew]/.test(mode) ? (p0.x - ax) / (s0.x - ax || 1) : 1,\n fy = /[ns]/.test(mode) ? (p0.y - ay) / (s0.y - ay || 1) : 1;\n n.scaleX = Math.max(0.1, Math.min(4, o.scaleX * fx));\n n.scaleY = Math.max(0.1, Math.min(4, o.scaleY * fy));\n fx = n.scaleX / o.scaleX;\n fy = n.scaleY / o.scaleY;\n const a = rotate({ x: ax, y: ay }, f.r),\n b = rotate({ x: ax * fx, y: ay * fy }, f.r);\n n.x = o.x + a.x - b.x;\n n.y = o.y + a.y - b.y;\n }\n for (const k of [\"x\", \"y\", \"rotation\"] as const)\n n[k] = Number.isFinite(n[k])\n ? Math.max(-10000, Math.min(10000, n[k]))\n : o[k];\n transforms[drag.id] = validateTransform(n);\n normalize();\n notify();\n };\n const end = (event?: PointerEvent) => {\n if (!drag || (event && event.pointerId !== drag.pointerId)) return;\n const id = drag.pointerId;\n drag = null;\n if (overlay.hasPointerCapture(id)) overlay.releasePointerCapture(id);\n };\n const listen = (name: string, fn: EventListener) =>\n overlay.addEventListener(name, fn, { signal: abort.signal });\n listen(\"pointerdown\", down as EventListener);\n listen(\"pointermove\", move as EventListener);\n listen(\"pointerup\", end as EventListener);\n listen(\"pointercancel\", end as EventListener);\n listen(\"lostpointercapture\", end as EventListener);\n listen(\"pointerleave\", () => {\n if (!drag) {\n hovered = null;\n options.onHover?.(null);\n refresh();\n }\n });\n refresh();\n return {\n select,\n hitTest,\n getFrame: (id: string): EditorFrame | null => {\n ensure();\n known(id);\n return frame(id);\n },\n getTransform: (id: string) => {\n ensure();\n known(id);\n return transformOf(id);\n },\n get selection() {\n return selected;\n },\n get canUndo() {\n return past.length > 0;\n },\n get canRedo() {\n return future.length > 0;\n },\n exportPatch,\n setEnabled(value: boolean) {\n ensure();\n end();\n enabled = value;\n hovered = null;\n overlay.style.pointerEvents = value ? \"auto\" : \"none\";\n refresh();\n },\n setVisible(value: boolean) {\n ensure();\n end();\n visible = value;\n refresh();\n },\n setTransform(id: string, value: Partial<PartTransform>) {\n ensure();\n known(id);\n const t = validateTransform({ ...transformOf(id), ...value });\n if (JSON.stringify(t) === JSON.stringify(transformOf(id))) return;\n remember();\n transforms[id] = t;\n normalize();\n notify();\n },\n reset(id?: string) {\n ensure();\n if (id !== undefined) known(id);\n if (id !== undefined ? !transforms[id] : !Object.keys(transforms).length)\n return;\n remember();\n if (id !== undefined) delete transforms[id];\n else transforms = Object.create(null);\n notify();\n },\n undo() {\n ensure();\n if (!past.length) return;\n future.push(copy(transforms));\n transforms = past.pop()!;\n notify();\n },\n redo() {\n ensure();\n if (!future.length) return;\n past.push(copy(transforms));\n transforms = future.pop()!;\n notify();\n },\n importPatch(value: unknown) {\n ensure();\n if (!value || typeof value !== \"object\") throw Error(\"Invalid patch\");\n const v = value as InteractiveEditPatch;\n if (\n v.format !== \"golem.interactive-edits\" ||\n v.version !== 1 ||\n v.documentId !== options.documentId\n )\n throw Error(\"Patch format/version/document mismatch\");\n if (\n !v.transforms ||\n typeof v.transforms !== \"object\" ||\n Array.isArray(v.transforms)\n )\n throw Error(\"Invalid transforms\");\n const next: Record<string, PartTransform> = Object.create(null);\n for (const [id, t] of Object.entries(v.transforms)) {\n known(id);\n next[id] = validateTransform(t);\n }\n remember();\n transforms = next;\n normalize();\n notify();\n },\n dispose() {\n if (disposed) return;\n end();\n disposed = true;\n abort.abort();\n unsubscribe();\n player.refreshPose();\n ctx.clearRect(0, 0, overlay.width, overlay.height);\n Object.assign(overlay.style, prior);\n options.onRender?.();\n },\n };\n}\nexport type InteractiveEditor = ReturnType<typeof createInteractiveEditor>;\n\nexport { createInteractiveBoneEditor, type BoneEditPatch, type InteractiveBoneEditorOptions } from \"./interactive-bones\";\n", "/**\n * rig-anim \u2014 pure animation math shared by the runtime, the checker, the importer and tests. No PixiJS.\n *\n * - easing + track sampling, Pose: Map<\"target:id:prop\", value>\n * - 2D affine matrices with PixiJS layout (a b c d tx ty; x' = a\u00B7x + c\u00B7y + tx, y' = b\u00B7x + d\u00B7y + ty)\n * - bone world transforms with shear and inherit modes, constraints (ik / transform / path) applied in `order`\n * - attachment geometry in document space: region corners, skinned mesh vertices, lattice deformation\n */\nimport type { RigDocument, Animation, Track, Bone, Attachment, RegionAttachment, MeshAttachment, PathAttachment, IkConstraint, TransformConstraint, PathConstraint } from \"./rig-format\";\nimport { controlOffsets, evalPatch, type Pivot } from \"./rig-deform\";\nimport { resolveControls } from \"./rig-controls\";\n\nconst DEG = Math.PI / 180;\n\n// ---------- easing ----------\n\nexport type EaseSpec = Track[\"keys\"][number][\"ease\"];\n\nconst named: Record<string, (t: number) => number> = {\n linear: t => t,\n step: () => 0,\n ease_in: t => t * t,\n ease_out: t => 1 - (1 - t) * (1 - t),\n ease_in_out: t => (t < 0.5 ? 2 * t * t : 1 - (-2 * t + 2) ** 2 / 2),\n minimum_jerk: t => t * t * t * (10 + t * (-15 + 6 * t)),\n back_out: t => 1 + 2.70158 * (t - 1) ** 3 + 1.70158 * (t - 1) ** 2,\n elastic_out: t =>\n t === 0 ? 0 : t === 1 ? 1 : 2 ** (-10 * t) * Math.sin((t * 10 - 0.75) * ((2 * Math.PI) / 3)) + 1,\n};\n\n/**\n * Cubic bezier easing sampled into 10 linear segments walked in order of the curve parameter (the same\n * approximation Spine uses: 9 interior samples per curve), so control points with non-monotonic times\n * behave identically to Spine.\n */\nconst BEZIER_SEGMENTS = 10;\nfunction bezier(x1: number, y1: number, x2: number, y2: number) {\n const px: number[] = [0], py: number[] = [0];\n for (let i = 1; i <= BEZIER_SEGMENTS; i++) {\n const t = i / BEZIER_SEGMENTS, u = 1 - t;\n px.push(3 * u * u * t * x1 + 3 * u * t * t * x2 + t * t * t);\n py.push(3 * u * u * t * y1 + 3 * u * t * t * y2 + t * t * t);\n }\n return (x: number) => {\n if (x <= 0) return 0;\n for (let i = 1; i <= BEZIER_SEGMENTS; i++) {\n if (px[i] >= x) {\n const span = px[i] - px[i - 1];\n return py[i - 1] + (span > 0 ? ((x - px[i - 1]) / span) * (py[i] - py[i - 1]) : 0);\n }\n }\n return 1;\n };\n}\n\nconst easeCache = new Map<string, (t: number) => number>();\nexport function ease(spec: EaseSpec): (t: number) => number {\n if (typeof spec === \"string\") return named[spec];\n const key = spec.bezier.join(\",\");\n let f = easeCache.get(key);\n if (!f) easeCache.set(key, (f = bezier(...spec.bezier)));\n return f;\n}\n\n// ---------- sampling ----------\n\nexport type Val = number | string | null | number[] | string[];\n/** key: \"bone:id:prop\" | \"slot:id:prop\" | \"deform:id:prop\" | \"ik:id:prop\" | \"transform:id:prop\" | \"path:id:prop\" | \"drawOrder:order:order\" */\nexport type Pose = Map<string, Val>;\nexport const pk = (target: string, id: string, prop: string) => `${target}:${id}:${prop}`;\n\n/** Shape-preserving cubic, evaluated analytically rather than through the legacy Bezier lookup table. */\nfunction smoothValue(track: Track, i: number, t: number, component: number | undefined, loop: boolean): number {\n const k = track.keys, n = k.length;\n const v = (j: number) => component === undefined ? k[j].v as number : (k[j].v as number[])[component];\n const h = (j: number) => k[j + 1].t - k[j].t;\n const d = (j: number) => (v(j + 1) - v(j)) / h(j);\n const harmonic = (a: number, b: number, h0: number, h1: number) => a * b <= 0 ? 0 : 3 * (h0 + h1) / ((2 * h1 + h0) / a + (h1 + 2 * h0) / b);\n const tangent = (j: number): number => {\n if (j === 0 || j === n - 1) {\n if (loop && n > 2 && v(0) === v(n - 1) && k[0].ease !== \"step\" && k[n - 2].ease !== \"step\")\n return harmonic(d(n - 2), d(0), h(n - 2), h(0));\n return 0;\n }\n if (k[j - 1].ease === \"step\" || k[j].ease === \"step\") return 0;\n return harmonic(d(j - 1), d(j), h(j - 1), h(j));\n };\n const u = (t - k[i].t) / h(i), u2 = u * u, u3 = u2 * u;\n return (2 * u3 - 3 * u2 + 1) * v(i) + (u3 - 2 * u2 + u) * h(i) * tangent(i)\n + (-2 * u3 + 3 * u2) * v(i + 1) + (u3 - u2) * h(i) * tangent(i + 1);\n}\n\nexport function sample(track: Track, t: number, loop = false): Val {\n const keys = track.keys;\n if (t <= keys[0].t) return keys[0].v;\n const last = keys[keys.length - 1];\n if (t >= last.t) return last.v;\n let i = 0;\n while (keys[i + 1].t <= t) i++;\n const a = keys[i], b = keys[i + 1];\n if (a.ease === \"step\") return a.v;\n if (typeof a.v === \"number\" && typeof b.v === \"number\") {\n if (track.interpolation === \"pchip\") return smoothValue(track, i, t, undefined, loop);\n const u = ease(a.ease)((t - a.t) / (b.t - a.t));\n return a.v + (b.v - a.v) * u;\n }\n if (Array.isArray(a.v) && Array.isArray(b.v) && typeof a.v[0] === \"number\") {\n if (track.interpolation === \"pchip\") return (a.v as number[]).map((_, j) => smoothValue(track, i, t, j, loop));\n const u = ease(a.ease)((t - a.t) / (b.t - a.t)), av = a.v as number[], bv = b.v as number[];\n return av.map((v, k) => v + ((bv[k] ?? v) - v) * u);\n }\n return a.v; // discrete\n}\n\nexport function poseOf(anim: Animation, t: number, out: Pose = new Map()): Pose {\n for (const tr of anim.tracks) out.set(pk(tr.target, tr.id, tr.prop), sample(tr, t, anim.loop && tr.keys[0].t === 0 && tr.keys.at(-1)!.t === anim.duration));\n return out;\n}\n\n/** Crossfade: numbers (and number arrays) interpolate toward `to` (or rest when absent there); discrete values hold until w = 1. */\nexport function blendPoses(from: Pose, to: Pose, w: number, restOf: (key: string) => Val): Pose {\n if (w >= 1) return new Map(to);\n const out: Pose = new Map();\n for (const k of new Set([...from.keys(), ...to.keys()])) {\n const f = from.has(k) ? from.get(k)! : restOf(k);\n const cur = to.has(k) ? to.get(k)! : restOf(k);\n if (typeof f === \"number\" && !Array.isArray(cur)) {\n const target = typeof cur === \"number\" ? cur : (restOf(k) as number);\n out.set(k, f + (target - f) * w);\n } else if (Array.isArray(f) && typeof f[0] === \"number\") {\n const fv = f as number[], tv = (Array.isArray(cur) && typeof cur[0] === \"number\" ? cur : fv.map(() => 0)) as number[];\n out.set(k, fv.map((v, i) => v + ((tv[i] ?? 0) - v) * w));\n } else if (Array.isArray(cur) && typeof cur[0] === \"number\" && typeof f !== \"string\") {\n out.set(k, (cur as number[]).map(v => v * w));\n } else out.set(k, f ?? null);\n }\n return out;\n}\n\n/** Setup value of a pose key. */\nexport function restValue(doc: RigDocument, key: string): Val {\n const [target, id, prop] = key.split(\":\");\n if (target === \"control\") return doc.controls?.find(c => c.id === id)?.default ?? 0;\n if (target === \"bone\") return (doc.bones.find(b => b.id === id)!.transform as Record<string, number>)[prop];\n if (target === \"slot\") {\n const s = doc.slots.find(s => s.id === id)!;\n if (prop === \"color\") return colorToArray(s.color);\n return (s as unknown as Record<string, Val>)[prop];\n }\n if (target === \"ik\" || target === \"transform\" || target === \"path\") {\n const c = (doc[target] as { id: string }[]).find(c => c.id === id) as Record<string, unknown> | undefined;\n const v = c?.[prop];\n return typeof v === \"boolean\" ? (v ? 1 : 0) : ((v as number) ?? 0);\n }\n if (target === \"drawOrder\") return doc.slots.map(s => s.id);\n return 0;\n}\n\n/** \"#rrggbbaa\" \u2192 [r, g, b, a] in 0..1 */\nexport const colorToArray = (c: string): number[] => [0, 2, 4, 6].map(k => parseInt(c.slice(1 + k, 3 + k), 16) / 255);\n\n/** Numeric lattice deform params of one attachment present in the pose, or undefined when it has none. */\nexport function deformParams(pose: Pose, attachmentId: string): Record<string, number> | undefined {\n let out: Record<string, number> | undefined;\n const prefix = `deform:${attachmentId}:`;\n for (const [k, v] of pose)\n if (k.startsWith(prefix) && typeof v === \"number\") (out ??= {})[k.slice(prefix.length)] = v;\n return out;\n}\n\n// ---------- affine ----------\n\nexport interface Affine { a: number; b: number; c: number; d: number; tx: number; ty: number }\n\n/** translate \u00B7 rotate(deg, clockwise in Y-down) \u00B7 shear \u00B7 scale \u2014 PixiJS setTransform semantics with shear as axis angles */\nexport function affineOf(x: number, y: number, rotationDeg: number, sx: number, sy: number, shearX = 0, shearY = 0): Affine {\n const rx = (rotationDeg + shearX) * DEG, ry = (rotationDeg + 90 + shearY) * DEG;\n return { a: Math.cos(rx) * sx, b: Math.sin(rx) * sx, c: Math.cos(ry) * sy, d: Math.sin(ry) * sy, tx: x, ty: y };\n}\n\n/** parent \u2218 local (apply local first, then parent) \u2014 PixiJS Matrix.append */\nexport function mul(p: Affine, l: Affine): Affine {\n return {\n a: p.a * l.a + p.c * l.b, b: p.b * l.a + p.d * l.b,\n c: p.a * l.c + p.c * l.d, d: p.b * l.c + p.d * l.d,\n tx: p.a * l.tx + p.c * l.ty + p.tx, ty: p.b * l.tx + p.d * l.ty + p.ty,\n };\n}\n\nexport const apply = (m: Affine, x: number, y: number): [number, number] => [m.a * x + m.c * y + m.tx, m.b * x + m.d * y + m.ty];\n\nexport function invert(m: Affine): Affine {\n const det = m.a * m.d - m.b * m.c || 1e-12;\n const a = m.d / det, b = -m.b / det, c = -m.c / det, d = m.a / det;\n return { a, b, c, d, tx: -(a * m.tx + c * m.ty), ty: -(b * m.tx + d * m.ty) };\n}\n\n/** rotation of the x axis of an affine, degrees */\nexport const rotationOf = (m: Affine) => Math.atan2(m.b, m.a) / DEG;\n\n/**\n * Where a world matrix puts a bone, which way its x axis points, and how long its axes are \u2014 the inverse of\n * `affineOf` for everything except shear, which cannot be recovered from the axes alone (a sheared y axis is\n * reported by its length, and `angle` follows the x axis only). Reading a posed bone goes through here so the\n * numbers an agent gets are the numbers the runtime drew.\n */\nexport function decompose(m: Affine): { x: number; y: number; angle: number; scaleX: number; scaleY: number } {\n return { x: m.tx, y: m.ty, angle: rotationOf(m), scaleX: Math.hypot(m.a, m.b), scaleY: Math.hypot(m.c, m.d) };\n}\n\nconst num = (pose: Pose, key: string, rest: number) => { const v = pose.get(key); return typeof v === \"number\" ? v : rest; };\n\n// ---------- bones ----------\n\nexport interface BoneState {\n /** effective local values (setup + pose + constraint adjustments) */\n x: number; y: number; rotation: number; scaleX: number; scaleY: number; shearX: number; shearY: number;\n world: Affine;\n}\n\nexport interface Skeleton {\n doc: RigDocument;\n pose: Pose;\n bones: Map<string, BoneState>;\n /** attachment shown per slot (pose or setup), null = hidden */\n attachment: Map<string, Attachment | null>;\n}\n\nconst boneIndex = new WeakMap<RigDocument, Map<string, Bone>>();\nfunction boneOf(doc: RigDocument, id: string): Bone {\n let idx = boneIndex.get(doc);\n if (!idx) boneIndex.set(doc, (idx = new Map(doc.bones.map(b => [b.id, b]))));\n const b = idx.get(id);\n if (!b) throw new Error(`unknown bone \"${id}\"`);\n return b;\n}\n\n/** world transform of one bone from its effective local values and its parent's world transform, honouring `inherit` */\nexport function boneWorldOf(bone: Bone, s: Omit<BoneState, \"world\">, parent: Affine | undefined): Affine {\n const local = affineOf(s.x, s.y, s.rotation, s.scaleX, s.scaleY, s.shearX, s.shearY);\n if (!parent) return local;\n if (bone.inherit === \"normal\") return mul(parent, local);\n const [tx, ty] = apply(parent, s.x, s.y);\n return { ...mul(inheritedParentLinear(bone, parent), { ...local, tx: 0, ty: 0 }), tx, ty };\n}\n\n/** Order bones parent-first (memoized per document). */\nconst orderCache = new WeakMap<RigDocument, Bone[]>();\nexport function boneOrder(doc: RigDocument): Bone[] {\n let order = orderCache.get(doc);\n if (order) return order;\n order = [];\n const emitted = new Set<string>();\n while (order.length < doc.bones.length) {\n let progressed = false;\n for (const b of doc.bones) if (!emitted.has(b.id) && (!b.parent || emitted.has(b.parent))) { emitted.add(b.id); order.push(b); progressed = true; }\n if (!progressed) throw new Error(\"bone hierarchy has a cycle\");\n }\n orderCache.set(doc, order);\n return order;\n}\n\nconst localOf = (doc: RigDocument, pose: Pose, b: Bone): Omit<BoneState, \"world\"> => {\n const r = b.transform, id = b.id;\n return {\n x: num(pose, pk(\"bone\", id, \"x\"), r.x), y: num(pose, pk(\"bone\", id, \"y\"), r.y),\n rotation: num(pose, pk(\"bone\", id, \"rotation\"), r.rotation),\n scaleX: num(pose, pk(\"bone\", id, \"scaleX\"), r.scaleX), scaleY: num(pose, pk(\"bone\", id, \"scaleY\"), r.scaleY),\n shearX: num(pose, pk(\"bone\", id, \"shearX\"), r.shearX), shearY: num(pose, pk(\"bone\", id, \"shearY\"), r.shearY),\n };\n};\n\n/** Recompute world transforms of `bone` and its descendants from their current local values. */\nfunction updateSubtree(skel: Skeleton, root: string) {\n const doc = skel.doc;\n const kids = new Map<string, Bone[]>();\n for (const b of doc.bones) if (b.parent) (kids.get(b.parent) ?? kids.set(b.parent, []).get(b.parent)!).push(b);\n const stack = [root];\n while (stack.length) {\n const id = stack.pop()!, b = boneOf(doc, id), st = skel.bones.get(id)!;\n st.world = boneWorldOf(b, st, b.parent ? skel.bones.get(b.parent)!.world : undefined);\n for (const k of kids.get(id) ?? []) stack.push(k.id);\n }\n}\n\n/**\n * Resolve controls, then apply optional pose modifiers before bone locals, constraints and skinning.\n * Each modifier receives its own map; its output is not passed through control bindings again.\n */\nexport function evaluate(doc: RigDocument, pose: Pose, modifiers?: Iterable<(pose: Pose) => Pose>): Skeleton {\n pose = resolveControls(doc, pose);\n if (modifiers) for (const modify of modifiers) pose = modify(new Map(pose));\n const skel: Skeleton = { doc, pose, bones: new Map(), attachment: new Map() };\n for (const b of boneOrder(doc)) {\n const s = localOf(doc, pose, b);\n const world = boneWorldOf(b, s, b.parent ? skel.bones.get(b.parent)!.world : undefined);\n skel.bones.set(b.id, { ...s, world });\n }\n const attachments = new Map(doc.attachments.map(a => [a.id, a]));\n for (const s of doc.slots) {\n const v = pose.get(pk(\"slot\", s.id, \"attachment\"));\n const id = v === undefined ? s.attachment : (v as string | null);\n skel.attachment.set(s.id, id ? attachments.get(id) ?? null : null);\n }\n // constraints, in document order (ties broken by array order)\n const all: { order: number; kind: \"ik\" | \"transform\" | \"path\"; c: IkConstraint | TransformConstraint | PathConstraint }[] = [\n ...doc.ik.map(c => ({ order: c.order, kind: \"ik\" as const, c })),\n ...doc.transform.map(c => ({ order: c.order, kind: \"transform\" as const, c })),\n ...doc.path.map(c => ({ order: c.order, kind: \"path\" as const, c })),\n ].sort((a, b) => a.order - b.order);\n for (const { kind, c } of all) {\n if (kind === \"ik\") applyIk(skel, c as IkConstraint);\n else if (kind === \"transform\") applyTransformConstraint(skel, c as TransformConstraint);\n else applyPathConstraint(skel, c as PathConstraint);\n }\n return skel;\n}\n\n/** Bone world transforms for a pose (constraints applied). */\nexport function boneWorld(doc: RigDocument, pose: Pose): Map<string, Affine> {\n const skel = evaluate(doc, pose);\n return new Map([...skel.bones].map(([id, s]) => [id, s.world]));\n}\n\n// ---------- constraints ----------\n\n/** a constraint's value at this frame: the keyed one, else the document's \u2014 what every early-out below reads\n * (`applyIk` at `mix <= 0`, the transform/path solvers when all of `MIX_PROPS` are 0). `rig-check`'s `poseNum`\n * answers the same question elsewhere and must keep agreeing with it.\n * Outside the solvers, ask `mixAt` in rig-constraints.ts \u2014 the exported one, so a fourth copy is never needed. */\nconst cnum = (skel: Skeleton, kind: string, id: string, prop: string, rest: number | boolean) =>\n num(skel.pose, pk(kind, id, prop), typeof rest === \"boolean\" ? (rest ? 1 : 0) : rest);\n\n/** point in bone-local space of `bone` for a document-space point */\nfunction toLocal(skel: Skeleton, bone: string, x: number, y: number): [number, number] {\n return apply(invert(skel.bones.get(bone)!.world), x, y);\n}\n/** document-space direction of a bone's local x axis, degrees */\nconst worldRotation = (skel: Skeleton, bone: string) => rotationOf(skel.bones.get(bone)!.world);\n\nconst IDENTITY: Affine = { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 };\nconst wrapRad = (r: number) => (r > Math.PI ? r - 2 * Math.PI : r < -Math.PI ? r + 2 * Math.PI : r);\n\n/**\n * Single-bone IK: rotate `bone` so its (sheared) x axis points at the world point, blended by `mix`. Solved in the\n * parent's local space, which aims correctly under any parent scale; a negative scaleX flips the axis by 180\u00B0.\n * compress/stretch scale the bone's length to the target distance (uniform: both axes).\n */\nfunction aimAt(skel: Skeleton, boneId: string, tx: number, ty: number, mix: number, compress = false, stretch = false, uniform = false) {\n const b = boneOf(skel.doc, boneId), st = skel.bones.get(boneId)!;\n const parent = b.parent ? skel.bones.get(b.parent)!.world : undefined;\n let dx: number, dy: number;\n if (parent && b.inherit === \"onlyTranslation\") { dx = tx - st.world.tx; dy = ty - st.world.ty; }\n else {\n const [px, py] = parent ? apply(invert(parent), tx, ty) : [tx, ty];\n dx = px - st.x; dy = py - st.y;\n }\n let r = Math.atan2(dy, dx) / DEG - st.shearX - st.rotation;\n if (st.scaleX < 0) r += 180;\n r = shortest(r);\n let sx = st.scaleX, sy = st.scaleY;\n if (compress || stretch) {\n const len = b.length * sx;\n if (len > 0.0001) {\n const dd = dx * dx + dy * dy;\n if ((compress && dd < len * len) || (stretch && dd > len * len)) { const f = (Math.sqrt(dd) / len - 1) * mix + 1; sx *= f; if (uniform) sy *= f; }\n }\n }\n st.rotation += r * mix; st.scaleX = sx; st.scaleY = sy;\n updateSubtree(skel, boneId);\n}\n\nconst shortest = (deg: number) => ((((deg + 180) % 360) + 360) % 360) - 180;\n\n/** CCD for longer chains. Solves a fresh pose each evaluation, then blends the local rotations once. */\nfunction applyLongIk(skel: Skeleton, c: IkConstraint, tx: number, ty: number, mix: number) {\n const initial = c.bones.map(id => ({ ...skel.bones.get(id)! }));\n const last = c.bones.at(-1)!, len = boneOf(skel.doc, last).length;\n const end = () => apply(skel.bones.get(last)!.world, len, 0);\n const tolerance = c.tolerance ?? 0.05;\n // A straight chain aimed along its axis has zero CCD gradient even when it must fold.\n const root = skel.bones.get(c.bones[0])!.world, tip = end();\n if (Math.abs((tip[0] - root.tx) * (ty - root.ty) - (tip[1] - root.ty) * (tx - root.tx)) < 1e-7 && Math.hypot(tx - root.tx, ty - root.ty) < Math.hypot(tip[0] - root.tx, tip[1] - root.ty) - tolerance) {\n skel.bones.get(last)!.rotation += cnum(skel, \"ik\", c.id, \"bendPositive\", c.bendPositive) > 0.5 ? 5 : -5;\n updateSubtree(skel, last);\n }\n for (let iter = 0; iter < (c.iterations ?? 100); iter++) {\n const e = end();\n if (Math.hypot(e[0] - tx, e[1] - ty) <= tolerance) break;\n for (let i = c.bones.length - 1; i >= 0; i--) {\n const id = c.bones[i], b = boneOf(skel.doc, id), st = skel.bones.get(id)!, w = st.world, e = end();\n const angle = wrapRad(Math.atan2(ty - w.ty, tx - w.tx) - Math.atan2(e[1] - w.ty, e[0] - w.tx));\n const cos = Math.cos(angle), sin = Math.sin(angle);\n setEffectiveWorld(skel, b, st, { ...w, a: cos * w.a - sin * w.b, b: sin * w.a + cos * w.b, c: cos * w.c - sin * w.d, d: sin * w.c + cos * w.d });\n }\n }\n if (mix < 1) {\n c.bones.forEach((id, i) => {\n const st = skel.bones.get(id)!, from = initial[i];\n for (const p of [\"rotation\", \"shearX\", \"shearY\"] as const) st[p] = from[p] + shortest(st[p] - from[p]) * mix;\n for (const p of [\"x\", \"y\", \"scaleX\", \"scaleY\"] as const) st[p] = from[p] + (st[p] - from[p]) * mix;\n });\n updateSubtree(skel, c.bones[0]);\n }\n}\n\n/**\n * IK constraint. One bone aims at the target. Two bones: the child is moved onto the parent's x axis and both are\n * rotated so the child's tip reaches the target, bending to the requested side. Solved in the parent's parent space:\n * the law of cosines when the parent's scale is uniform, otherwise the child's tip sweeps an ellipse (parent scale)\n * and the reachable point is solved analytically. Negative scales flip axes by 180\u00B0 and mirror the bend; the parent's\n * shear is dropped. Softness eases the last stretch before full extension; stretch lengthens the parent to reach.\n */\nfunction applyIk(skel: Skeleton, c: IkConstraint) {\n const mix = cnum(skel, \"ik\", c.id, \"mix\", c.mix);\n if (mix <= 0) return;\n const target = skel.bones.get(c.target)!.world;\n if (c.bones.length > 2) { applyLongIk(skel, c, target.tx, target.ty, mix); return; }\n const bendDir = cnum(skel, \"ik\", c.id, \"bendPositive\", c.bendPositive) > 0.5 ? 1 : -1;\n const compress = cnum(skel, \"ik\", c.id, \"compress\", c.compress) > 0.5, stretch = cnum(skel, \"ik\", c.id, \"stretch\", c.stretch) > 0.5;\n let softness = cnum(skel, \"ik\", c.id, \"softness\", c.softness);\n if (c.bones.length === 1) { aimAt(skel, c.bones[0], target.tx, target.ty, mix, compress, stretch, c.uniform); return; }\n const [pId, cId] = c.bones;\n const parentBone = boneOf(skel.doc, pId), childBone = boneOf(skel.doc, cId);\n const p = skel.bones.get(pId)!, ch = skel.bones.get(cId)!;\n const px = p.x, py = p.y;\n let psx = p.scaleX, psy = p.scaleY, csx = ch.scaleX, sx = psx, sy = psy;\n let os1 = 0, os2 = 0, s2 = 1;\n if (psx < 0) { psx = -psx; os1 = 180; s2 = -1; }\n if (psy < 0) { psy = -psy; s2 = -s2; }\n if (csx < 0) { csx = -csx; os2 = 180; }\n const uniform = Math.abs(psx - psy) <= 0.0001;\n const cx = ch.x, cy = !uniform || stretch ? 0 : ch.y;\n const [cwx, cwy] = apply(p.world, cx, cy);\n const ppInv = invert(parentBone.parent ? skel.bones.get(parentBone.parent)!.world : IDENTITY);\n const [clx, cly] = apply(ppInv, cwx, cwy);\n const dx = clx - px, dy = cly - py, l1 = Math.hypot(dx, dy);\n let l2 = childBone.length * csx;\n if (l1 < 0.0001) { // parent and child share an origin: aim the parent, the child keeps its offset\n aimAt(skel, pId, target.tx, target.ty, mix, false, stretch, false);\n ch.x = cx; ch.y = cy; ch.rotation = 0;\n updateSubtree(skel, pId);\n return;\n }\n const [tlx, tly] = apply(ppInv, target.tx, target.ty);\n let tx = tlx - px, ty = tly - py, dd = tx * tx + ty * ty;\n if (softness !== 0) {\n softness *= psx * (csx + 1) * 0.5;\n const td = Math.sqrt(dd), sd = td - l1 - l2 * psx + softness;\n if (sd > 0) {\n let q = Math.min(1, sd / (softness * 2)) - 1;\n q = (sd - softness * (1 - q * q)) / td;\n tx -= q * tx; ty -= q * ty; dd = tx * tx + ty * ty;\n }\n }\n let a1: number, a2: number;\n if (uniform) {\n l2 *= psx;\n let cos = (dd - l1 * l1 - l2 * l2) / (2 * l1 * l2);\n if (cos < -1) { cos = -1; a2 = Math.PI * bendDir; }\n else if (cos > 1) {\n cos = 1; a2 = 0;\n if (stretch) { const f = (Math.sqrt(dd) / (l1 + l2) - 1) * mix + 1; sx *= f; if (c.uniform) sy *= f; }\n } else a2 = Math.acos(cos) * bendDir;\n const a = l1 + l2 * cos, b = l2 * Math.sin(a2);\n a1 = Math.atan2(ty * a - tx * b, tx * a + ty * b);\n } else {\n // child tip on the ellipse (l1 + a\u00B7cos \u03B8, b\u00B7sin \u03B8): find the point at distance \u221Add, else the nearest extreme\n const a = psx * l2, b = psy * l2, aa = a * a, bb = b * b, ta = Math.atan2(ty, tx);\n const c0 = bb * l1 * l1 + aa * dd - aa * bb, c1 = -2 * bb * l1, c2 = bb - aa;\n const disc = c1 * c1 - 4 * c2 * c0;\n let solved = false;\n a1 = a2 = 0;\n if (disc >= 0) {\n let q = Math.sqrt(disc);\n if (c1 < 0) q = -q;\n q = -(c1 + q) * 0.5;\n const r0 = q / c2, r1 = c0 / q, r = Math.abs(r0) < Math.abs(r1) ? r0 : r1, yy = dd - r * r;\n if (yy >= 0) {\n const y = Math.sqrt(yy) * bendDir;\n a1 = ta - Math.atan2(y, r);\n a2 = Math.atan2(y / psy, (r - l1) / psx);\n solved = true;\n }\n }\n if (!solved) {\n let minAngle = Math.PI, minX = l1 - a, minDist = minX * minX, minY = 0, maxAngle = 0, maxX = l1 + a, maxDist = maxX * maxX, maxY = 0;\n const k = -a * l1 / (aa - bb);\n if (k >= -1 && k <= 1) {\n const ang = Math.acos(k), x = a * Math.cos(ang) + l1, y = b * Math.sin(ang), dist = x * x + y * y;\n if (dist < minDist) { minAngle = ang; minDist = dist; minX = x; minY = y; }\n if (dist > maxDist) { maxAngle = ang; maxDist = dist; maxX = x; maxY = y; }\n }\n if (dd <= (minDist + maxDist) * 0.5) { a1 = ta - Math.atan2(minY * bendDir, minX); a2 = minAngle * bendDir; }\n else { a1 = ta - Math.atan2(maxY * bendDir, maxX); a2 = maxAngle * bendDir; }\n }\n }\n const os = Math.atan2(cy, cx) * s2;\n const d1 = shortest((a1 - os) / DEG + os1 - p.rotation);\n const d2 = shortest(((a2 + os) / DEG - ch.shearX) * s2 + os2 - ch.rotation);\n p.rotation += d1 * mix; p.scaleX = sx; p.scaleY = sy; p.shearX = 0; p.shearY = 0;\n ch.x = cx; ch.y = cy; ch.rotation += d2 * mix;\n updateSubtree(skel, pId);\n}\n\n/**\n * Transform constraint: each bone follows the target's rotation / position / scale / shear by the mix amounts, with\n * the constraint's offsets. World mode edits the bone's world matrix (rotation turns the whole matrix, scales the\n * axis lengths, shear turns the y axis) and folds it back into effective locals; local mode mixes the effective\n * local values. `relative` adds the target's values instead of matching them.\n */\nfunction applyTransformConstraint(skel: Skeleton, c: TransformConstraint) {\n const mixRotate = cnum(skel, \"transform\", c.id, \"mixRotate\", c.mixRotate), mixX = cnum(skel, \"transform\", c.id, \"mixX\", c.mixX), mixY = cnum(skel, \"transform\", c.id, \"mixY\", c.mixY);\n const mixScaleX = cnum(skel, \"transform\", c.id, \"mixScaleX\", c.mixScaleX), mixScaleY = cnum(skel, \"transform\", c.id, \"mixScaleY\", c.mixScaleY), mixShearY = cnum(skel, \"transform\", c.id, \"mixShearY\", c.mixShearY);\n if (!mixRotate && !mixX && !mixY && !mixScaleX && !mixScaleY && !mixShearY) return;\n c = { ...c, ...Object.fromEntries([\"x\", \"y\", \"rotation\", \"scaleX\", \"scaleY\", \"shearY\"].map(p => [p, cnum(skel, \"transform\", c.id, p, c[p as \"x\"])])) };\n const target = skel.bones.get(c.target)!, tw = target.world;\n const offsetRotation = c.rotation * DEG, offsetShearY = c.shearY * DEG;\n for (const id of c.bones) {\n const b = boneOf(skel.doc, id), st = skel.bones.get(id)!;\n if (c.local) {\n if (c.relative) {\n st.rotation += (target.rotation + c.rotation) * mixRotate;\n st.x += (target.x + c.x) * mixX; st.y += (target.y + c.y) * mixY;\n st.scaleX *= (target.scaleX - 1 + c.scaleX) * mixScaleX + 1;\n st.scaleY *= (target.scaleY - 1 + c.scaleY) * mixScaleY + 1;\n st.shearY += (target.shearY + c.shearY) * mixShearY;\n } else {\n st.rotation += (target.rotation - st.rotation + c.rotation) * mixRotate;\n st.x += (target.x - st.x + c.x) * mixX; st.y += (target.y - st.y + c.y) * mixY;\n if (st.scaleX !== 0) st.scaleX += (target.scaleX - st.scaleX + c.scaleX) * mixScaleX;\n if (st.scaleY !== 0) st.scaleY += (target.scaleY - st.scaleY + c.scaleY) * mixScaleY;\n st.shearY += (target.shearY - st.shearY + c.shearY) * mixShearY;\n }\n updateSubtree(skel, id);\n continue;\n }\n let w: Affine = { ...st.world };\n const targetRot = Math.atan2(tw.b, tw.a), rotate = (m: Affine, r: number): Affine => {\n const cos = Math.cos(r), sin = Math.sin(r);\n return { a: cos * m.a - sin * m.b, b: sin * m.a + cos * m.b, c: cos * m.c - sin * m.d, d: sin * m.c + cos * m.d, tx: m.tx, ty: m.ty };\n };\n if (mixRotate !== 0) {\n const r = wrapRad(c.relative ? targetRot + offsetRotation : targetRot - Math.atan2(w.b, w.a) + offsetRotation);\n w = rotate(w, r * mixRotate);\n }\n if (mixX !== 0 || mixY !== 0) {\n const [ox, oy] = apply(tw, c.x, c.y); // the offset lives in the target's space\n if (c.relative) { w.tx += ox * mixX; w.ty += oy * mixY; }\n else { w.tx += (ox - w.tx) * mixX; w.ty += (oy - w.ty) * mixY; }\n }\n if (mixScaleX !== 0) {\n const ts = Math.hypot(tw.a, tw.b);\n let f: number;\n if (c.relative) f = (ts - 1 + c.scaleX) * mixScaleX + 1;\n else { const s0 = Math.hypot(w.a, w.b); f = s0 === 0 ? 1 : (s0 + (ts - s0 + c.scaleX) * mixScaleX) / s0; }\n w.a *= f; w.b *= f;\n }\n if (mixScaleY !== 0) {\n const ts = Math.hypot(tw.c, tw.d);\n let f: number;\n if (c.relative) f = (ts - 1 + c.scaleY) * mixScaleY + 1;\n else { const s0 = Math.hypot(w.c, w.d); f = s0 === 0 ? 1 : (s0 + (ts - s0 + c.scaleY) * mixScaleY) / s0; }\n w.c *= f; w.d *= f;\n }\n if (mixShearY > 0) {\n const by = Math.atan2(w.d, w.c), targetShear = Math.atan2(tw.d, tw.c) - targetRot;\n const r = c.relative\n ? by + (wrapRad(targetShear) - Math.PI / 2 + offsetShearY) * mixShearY\n : by + (wrapRad(targetShear - (by - Math.atan2(w.b, w.a))) + offsetShearY) * mixShearY;\n const len = Math.hypot(w.c, w.d);\n w.c = Math.cos(r) * len; w.d = Math.sin(r) * len;\n }\n setEffectiveWorld(skel, b, st, w);\n }\n}\n\n// ---------- paths ----------\n\n/** world-space control points of a path attachment (2 numbers per vertex) */\nexport function pathWorldVertices(skel: Skeleton, slotId: string, att: PathAttachment): number[] {\n return attachmentWorldVertices(skel, slotId, att);\n}\n\n/** path length: `lengths[i]` is the length up to the end of curve i; an open path stores one extra entry */\nexport function pathLength(vertexCount: number, closed: boolean, lengths: number[]): number {\n const curveCount = closed ? vertexCount / 3 : vertexCount / 3 - 1;\n return curveCount >= 1 && lengths.length >= curveCount ? lengths[curveCount - 1] : lengths[lengths.length - 1] ?? 1;\n}\n\n/**\n * Sample positions and tangents along a path made of cubic bezier curves through the attachment vertices\n * (vertex layout: [c_prev, p, c_next] triples; curve i runs p_i \u2192 c_next_i \u2192 c_prev_{i+1} \u2192 p_{i+1}).\n */\n/**\n * Points along a path attachment (document space). `positions` are absolute distances along the path (or fractions of\n * its length when `percent`). With `constantSpeed` the length of the posed curve is measured (4 chords per curve for\n * the total, 10 chords inside the curve that holds the position); otherwise the stored setup `lengths` and the raw\n * curve parameter are used. Closed paths wrap; positions beyond an open path continue along its end tangents.\n * `angle` is the tangent direction in degrees.\n */\nexport function samplePath(points: number[], closed: boolean, lengths: number[], constantSpeed: boolean, positions: number[], percent: boolean): { x: number; y: number; angle: number }[] {\n const n = points.length / 2;\n // anchors sit at indices 1, 4, 7, \u2026 (each vertex = [cPrev, p, cNext]); curve i runs from anchor i to anchor i + 1\n const curveCount = closed ? n / 3 : n / 3 - 1;\n if (curveCount < 1) return positions.map(() => ({ x: points[2] ?? 0, y: points[3] ?? 0, angle: 0 }));\n const curve = (i: number) => {\n const a = ((i * 3 + 1) % n) * 2, c1 = ((i * 3 + 2) % n) * 2, c2 = ((i * 3 + 3) % n) * 2, b = ((i * 3 + 4) % n) * 2;\n return [points[a], points[a + 1], points[c1], points[c1 + 1], points[c2], points[c2 + 1], points[b], points[b + 1]];\n };\n const pointAt = (i: number, t: number) => {\n const [x0, y0, x1, y1, x2, y2, x3, y3] = curve(i), u = 1 - t;\n return { x: u * u * u * x0 + 3 * u * u * t * x1 + 3 * u * t * t * x2 + t * t * t * x3, y: u * u * u * y0 + 3 * u * u * t * y1 + 3 * u * t * t * y2 + t * t * t * y3 };\n };\n const angleAt = (i: number, t: number) => {\n const [x0, y0, x1, y1, x2, y2, x3, y3] = curve(i), u = 1 - t;\n const dx = 3 * u * u * (x1 - x0) + 6 * u * t * (x2 - x1) + 3 * t * t * (x3 - x2);\n const dy = 3 * u * u * (y1 - y0) + 6 * u * t * (y2 - y1) + 3 * t * t * (y3 - y2);\n return Math.atan2(dy, dx) / DEG;\n };\n const chords = (i: number, count: number) => {\n const acc: number[] = []; let sum = 0, prev = pointAt(i, 0);\n for (let k = 1; k <= count; k++) { const q = pointAt(i, k / count); sum += Math.hypot(q.x - prev.x, q.y - prev.y); acc.push(sum); prev = q; }\n return acc;\n };\n const cum: number[] = [];\n if (constantSpeed) { let acc = 0; for (let i = 0; i < curveCount; i++) { acc += chords(i, 4)[3]; cum.push(acc); } }\n else for (let i = 0; i < curveCount; i++) cum.push(lengths[i] ?? (i ? cum[i - 1] : 0));\n const total = cum[curveCount - 1];\n const out: { x: number; y: number; angle: number }[] = [];\n for (let pos of positions) {\n if (percent) pos *= total;\n if (closed) {\n if (total > 0) { pos %= total; if (pos < 0) pos += total; } else pos = 0;\n } else if (pos < 0) { // before the start: continue the start tangent (anchor 0 towards its outgoing control)\n const r = Math.atan2(points[5] - points[3], points[4] - points[2]);\n out.push({ x: points[2] + pos * Math.cos(r), y: points[3] + pos * Math.sin(r), angle: r / DEG });\n continue;\n } else if (pos > total) { // after the end: continue the end tangent (incoming control towards the last anchor)\n const a = (curveCount * 3 + 1) * 2, c = curveCount * 3 * 2, e = pos - total;\n const r = Math.atan2(points[a + 1] - points[c + 1], points[a] - points[c]);\n out.push({ x: points[a] + e * Math.cos(r), y: points[a + 1] + e * Math.sin(r), angle: r / DEG });\n continue;\n }\n let i = 0;\n while (i < curveCount - 1 && pos > cum[i]) i++;\n const prev = i ? cum[i - 1] : 0, span = cum[i] - prev;\n const f = span > 0 ? (pos - prev) / span : 0;\n let t = f;\n if (constantSpeed) { // weight the parameter by chord lengths inside the curve\n const seg = chords(i, 10), target = f * seg[9];\n let k = 0;\n while (k < 9 && target > seg[k]) k++;\n const sPrev = k ? seg[k - 1] : 0, sSpan = seg[k] - sPrev;\n t = (k + (sSpan > 0 ? (target - sPrev) / sSpan : 0)) / 10;\n }\n out.push({ ...pointAt(i, t), angle: angleAt(i, t) });\n }\n return out;\n}\n\nconst PATH_EPSILON = 0.00001;\n\n/**\n * Path constraint: bones are laid along the target slot's path attachment (spacing modes: length = bone length +\n * spacing, fixed = spacing px, percent = fraction of the path per bone, proportional = the path shared by bone length),\n * translated with mixX/mixY and rotated with mixRotate (tangent = path direction, chain = towards the next bone,\n * chainScale = chain + scaled to reach it). Applied in world space, then folded back into the bones' effective locals.\n */\nfunction applyPathConstraint(skel: Skeleton, c: PathConstraint) {\n const mixRotate = cnum(skel, \"path\", c.id, \"mixRotate\", c.mixRotate), mixX = cnum(skel, \"path\", c.id, \"mixX\", c.mixX), mixY = cnum(skel, \"path\", c.id, \"mixY\", c.mixY);\n if (!mixRotate && !mixX && !mixY) return;\n const att = skel.attachment.get(c.target);\n if (!att || att.type !== \"path\") return;\n const points = pathWorldVertices(skel, c.target, att);\n const position = cnum(skel, \"path\", c.id, \"position\", c.position), spacing = cnum(skel, \"path\", c.id, \"spacing\", c.spacing);\n const tangents = c.rotateMode === \"tangent\", scale = c.rotateMode === \"chainScale\";\n const bones = c.bones.map(id => ({ b: boneOf(skel.doc, id), st: skel.bones.get(id)! }));\n const spacesCount = tangents ? bones.length : bones.length + 1;\n // spacing between consecutive bones; length-based modes scale with the bone's world x axis\n const spaces = new Array<number>(spacesCount).fill(0), worldLengths: number[] = [];\n let sum = 0;\n for (let i = 0; i < spacesCount - 1; i++) {\n const { b, st } = bones[i], setupLength = b.length;\n const len = setupLength < PATH_EPSILON ? 0 : setupLength * Math.hypot(st.world.a, st.world.b);\n worldLengths.push(len);\n if (c.spacingMode === \"percent\") spaces[i + 1] = spacing;\n else if (setupLength < PATH_EPSILON) spaces[i + 1] = spacing;\n else if (c.spacingMode === \"proportional\") { spaces[i + 1] = len; sum += len; }\n else spaces[i + 1] = (c.spacingMode === \"length\" ? setupLength + spacing : spacing) * len / setupLength;\n }\n if (c.spacingMode === \"proportional\" && sum > 0) { const f = spacesCount / sum * spacing; for (let i = 1; i < spacesCount; i++) spaces[i] *= f; }\n const total = measuredPathLength(points, att.closed, att.lengths, att.constantSpeed);\n const multiplier = c.spacingMode === \"percent\" ? total : c.spacingMode === \"proportional\" ? total / spacesCount : 1;\n let p = c.positionMode === \"percent\" ? position * total : position;\n const positions: number[] = [];\n for (let i = 0; i < spacesCount; i++) { p += spaces[i] * multiplier; positions.push(p); }\n const samples = samplePath(points, att.closed, att.lengths, att.constantSpeed, positions, false);\n // offset rotation follows the target bone's handedness; chain mode without an offset links bones tip to tail\n const tip = c.rotation === 0 && c.rotateMode === \"chain\";\n let offsetRotation = c.rotation * DEG;\n if (c.rotation !== 0) {\n const slot = skel.doc.slots.find(sl => sl.id === c.target)!, tw = skel.bones.get(slot.bone)!.world;\n if (tw.a * tw.d - tw.b * tw.c < 0) offsetRotation = -offsetRotation;\n }\n let boneX = samples[0].x, boneY = samples[0].y;\n for (let i = 0; i < bones.length; i++) {\n const { b, st } = bones[i];\n let w: Affine = { ...st.world };\n w.tx += (boneX - w.tx) * mixX;\n w.ty += (boneY - w.ty) * mixY;\n const next = samples[i + 1] ?? { x: 0, y: 0, angle: 0 };\n const dx = next.x - boneX, dy = next.y - boneY;\n if (scale) {\n const len = worldLengths[i];\n if (len !== 0) { const f = (Math.hypot(dx, dy) / len - 1) * mixRotate + 1; w.a *= f; w.b *= f; }\n }\n boneX = next.x; boneY = next.y;\n if (mixRotate > 0) {\n const { a, b: bb, c: cc, d } = w;\n let r = (tangents ? samples[i].angle : spaces[i + 1] === 0 ? next.angle : Math.atan2(dy, dx) / DEG) * DEG;\n r -= Math.atan2(bb, a);\n if (tip) {\n const cos = Math.cos(r), sin = Math.sin(r), length = b.length;\n boneX += (length * (cos * a - sin * bb) - dx) * mixRotate;\n boneY += (length * (sin * a + cos * bb) - dy) * mixRotate;\n } else r += offsetRotation;\n if (r > Math.PI) r -= 2 * Math.PI; else if (r < -Math.PI) r += 2 * Math.PI;\n r *= mixRotate;\n const cos = Math.cos(r), sin = Math.sin(r);\n w = { a: cos * a - sin * bb, b: sin * a + cos * bb, c: cos * cc - sin * d, d: sin * cc + cos * d, tx: w.tx, ty: w.ty };\n }\n setEffectiveWorld(skel, b, st, w);\n }\n}\n\n/** total length used for percent positions/spacing: measured on the posed curve (constant speed) or the stored setup lengths */\nfunction measuredPathLength(points: number[], closed: boolean, lengths: number[], constantSpeed: boolean): number {\n const n = points.length / 2, curveCount = closed ? n / 3 : n / 3 - 1;\n if (curveCount < 1) return 0;\n if (!constantSpeed) return pathLength(n, closed, lengths);\n let total = 0;\n for (let i = 0; i < curveCount; i++) {\n const a = ((i * 3 + 1) % n) * 2, c1 = ((i * 3 + 2) % n) * 2, c2 = ((i * 3 + 3) % n) * 2, b = ((i * 3 + 4) % n) * 2;\n let px = points[a], py = points[a + 1];\n for (let k = 1; k <= 4; k++) {\n const t = k / 4, u = 1 - t;\n const x = u * u * u * points[a] + 3 * u * u * t * points[c1] + 3 * u * t * t * points[c2] + t * t * t * points[b];\n const y = u * u * u * points[a + 1] + 3 * u * u * t * points[c1 + 1] + 3 * u * t * t * points[c2 + 1] + t * t * t * points[b + 1];\n total += Math.hypot(x - px, y - py); px = x; py = y;\n }\n }\n return total;\n}\n\n/** the parent's linear part a bone actually inherits under its `inherit` mode */\nfunction inheritedParentLinear(bone: Bone, parent: Affine): Affine {\n switch (bone.inherit) {\n case \"normal\": return { ...parent, tx: 0, ty: 0 };\n case \"onlyTranslation\": return { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 };\n case \"noRotationOrReflection\": {\n // scale along the parent's axes without its rotation; keeps the parent's area scale, drops reflection\n const lenX = Math.hypot(parent.a, parent.b) || 1;\n const det = parent.a * parent.d - parent.b * parent.c;\n return { a: lenX, b: 0, c: 0, d: Math.abs(det) / lenX, tx: 0, ty: 0 };\n }\n case \"noScale\": case \"noScaleOrReflection\": {\n const lenX = Math.hypot(parent.a, parent.b) || 1;\n const ua = parent.a / lenX, ub = parent.b / lenX;\n const det = parent.a * parent.d - parent.b * parent.c;\n const reflect = det < 0 && bone.inherit === \"noScale\" ? -1 : 1; // noScale keeps the parent's reflection\n return { a: ua, b: ub, c: -ub * reflect, d: ua * reflect, tx: 0, ty: 0 };\n }\n }\n}\n\n/** Give a bone the world transform `world`: its effective locals are decomposed relative to the parent (shearX folds into rotation), descendants follow. */\nfunction setEffectiveWorld(skel: Skeleton, bone: Bone, st: BoneState, world: Affine) {\n const parent = bone.parent ? skel.bones.get(bone.parent)!.world : undefined;\n let lin: Affine = world, tx = world.tx, ty = world.ty;\n if (parent) {\n [tx, ty] = apply(invert(parent), world.tx, world.ty);\n lin = mul(invert(inheritedParentLinear(bone, parent)), { ...world, tx: 0, ty: 0 });\n }\n st.x = tx; st.y = ty;\n st.rotation = Math.atan2(lin.b, lin.a) / DEG;\n st.scaleX = Math.hypot(lin.a, lin.b);\n st.scaleY = Math.hypot(lin.c, lin.d);\n st.shearX = 0;\n st.shearY = shortest(Math.atan2(lin.d, lin.c) / DEG - st.rotation - 90);\n updateSubtree(skel, bone.id);\n}\n\n// ---------- attachment geometry ----------\n\n/** skinned or bone-local vertices of a mesh/path attachment in document space, deform offsets applied */\nexport function attachmentWorldVertices(skel: Skeleton, slotId: string, att: MeshAttachment | PathAttachment): number[] {\n const deform = skel.pose.get(pk(\"deform\", att.id, \"vertices\"));\n const off = Array.isArray(deform) && typeof deform[0] === \"number\" ? (deform as number[]) : undefined;\n const out: number[] = [];\n if (att.weights) {\n let k = 0;\n for (const list of att.weights) {\n let x = 0, y = 0;\n for (const inf of list) {\n const w = skel.bones.get(inf.bone)!.world;\n const vx = inf.x + (off ? off[k] : 0), vy = inf.y + (off ? off[k + 1] : 0);\n k += 2;\n x += (w.a * vx + w.c * vy + w.tx) * inf.w; y += (w.b * vx + w.d * vy + w.ty) * inf.w;\n }\n out.push(x, y);\n }\n } else {\n const slot = skel.doc.slots.find(s => s.id === slotId)!, w = skel.bones.get(slot.bone)!.world, v = att.vertices!;\n for (let i = 0; i < v.length; i += 2) {\n const vx = v[i] + (off ? off[i] : 0), vy = v[i + 1] + (off ? off[i + 1] : 0);\n out.push(w.a * vx + w.c * vy + w.tx, w.b * vx + w.d * vy + w.ty);\n }\n }\n return out;\n}\n\n/** image-local (centre-relative) \u2192 document matrix for a region attachment */\nexport function regionMatrix(skel: Skeleton, slotId: string, att: RegionAttachment): Affine {\n const slot = skel.doc.slots.find(s => s.id === slotId)!;\n return mul(skel.bones.get(slot.bone)!.world, affineOf(att.x, att.y, att.rotation, att.scaleX, att.scaleY));\n}\n\nexport function regionSize(doc: RigDocument, att: RegionAttachment): [number, number] {\n const asset = doc.assets.find(a => a.id === att.asset)!;\n return [att.width ?? asset.width, att.height ?? asset.height];\n}\n\n/**\n * Outline of a slot's shown attachment in document space: region \u2192 `samplesPerEdge` points per edge clockwise\n * from the top-left (lattice deform applied), mesh \u2192 hull vertices (or all), path/hidden \u2192 undefined.\n */\nexport function slotOutline(doc: RigDocument, pose: Pose, slotId: string, samplesPerEdge = 1, skel = evaluate(doc, pose)): [number, number][] | undefined {\n if (!doc.slots.some(s => s.id === slotId)) throw new Error(`unknown slot \"${slotId}\"`);\n const att = skel.attachment.get(slotId);\n if (!att) return undefined;\n if (att.type === \"mesh\") {\n const v = attachmentWorldVertices(skel, slotId, att), n = att.hull > 0 ? att.hull : v.length / 2;\n const pts: [number, number][] = [];\n for (let i = 0; i < n; i++) pts.push([v[i * 2], v[i * 2 + 1]]);\n return pts;\n }\n if (att.type === \"path\") return undefined;\n const [w, h] = regionSize(doc, att), m = regionMatrix(skel, slotId, att), pivot: Pivot = { x: 0.5, y: 0.5 };\n const params = deformParams(pose, att.id);\n const offsets = params ? controlOffsets(params, w, h, pivot) : undefined;\n const pts: [number, number][] = [];\n const push = (u: number, v: number) => {\n const [ox, oy] = offsets ? evalPatch(offsets, u, v) : [0, 0];\n pts.push(apply(m, (u - 0.5) * w + ox, (v - 0.5) * h + oy));\n };\n const n = Math.max(1, samplesPerEdge);\n for (let i = 0; i < n; i++) push(i / n, 0);\n for (let i = 0; i < n; i++) push(1, i / n);\n for (let i = 0; i < n; i++) push(1 - i / n, 1);\n for (let i = 0; i < n; i++) push(0, 1 - i / n);\n return pts;\n}\n", "import type { RigPlayer } from \"./rig-runtime\";\nimport type { Pose } from \"./rig-anim\";\nimport { pk } from \"./rig-anim\";\nimport type { PartTransform } from \"./interactive-editor\";\n\nexport interface BoneEditPatch {\n format: \"golem.bone-edits\";\n version: 1;\n documentId: string;\n transforms: Record<string, PartTransform>;\n}\nexport interface InteractiveBoneEditorOptions {\n player: Pick<RigPlayer, \"doc\" | \"onBeforePose\" | \"refreshPose\">;\n documentId: string;\n onChange?: (patch: BoneEditPatch) => void;\n onRender?: () => void;\n historyLimit?: number;\n}\nconst neutral = (): PartTransform => ({\n x: 0,\n y: 0,\n rotation: 0,\n scaleX: 1,\n scaleY: 1,\n});\nconst clone = (\n v: Record<string, PartTransform>,\n): Record<string, PartTransform> =>\n Object.assign(Object.create(null), JSON.parse(JSON.stringify(v)));\n/** Add bone offsets after expression controls, before constraints and native skinning. */\nexport function createInteractiveBoneEditor(\n options: InteractiveBoneEditorOptions,\n) {\n if (!options.documentId) throw Error(\"documentId is required\");\n const limit = options.historyLimit ?? 100;\n if (!Number.isInteger(limit) || limit < 1 || limit > 10000)\n throw Error(\"historyLimit must be an integer from 1 to 10000\");\n const { player } = options,\n bones = new Map(player.doc.bones.map((b) => [b.id, b]));\n let transforms: Record<string, PartTransform> = Object.create(null),\n past: (typeof transforms)[] = [],\n future: (typeof transforms)[] = [],\n visible = true,\n disposed = false;\n const ensure = () => {\n if (disposed) throw Error(\"Bone editor is disposed\");\n };\n const known = (id: string) => {\n if (!bones.has(id)) throw Error(\"Unknown bone: \" + id);\n };\n const getTransform = (id: string) => {\n ensure();\n known(id);\n return { ...neutral(), ...transforms[id] };\n };\n const exportPatch = (): BoneEditPatch => ({\n format: \"golem.bone-edits\",\n version: 1,\n documentId: options.documentId,\n transforms: clone(transforms),\n });\n const refresh = () => {\n player.refreshPose();\n options.onRender?.();\n };\n const notify = () => {\n refresh();\n options.onChange?.(exportPatch());\n };\n const remember = () => {\n past.push(clone(transforms));\n if (past.length > limit) past.shift();\n future = [];\n };\n const normalize = () => {\n for (const [id, t] of Object.entries(transforms))\n if (\n Object.entries(t).every(\n ([k, v]) => v === neutral()[k as keyof PartTransform],\n )\n )\n delete transforms[id];\n };\n function validateTransform(value: unknown): PartTransform {\n if (!value || typeof value !== \"object\" || Array.isArray(value))\n throw Error(\"Invalid bone transform\");\n const out = neutral();\n for (const [k, v] of Object.entries(value)) {\n if (\n !Object.hasOwn(out, k) ||\n typeof v !== \"number\" ||\n !Number.isFinite(v) ||\n Math.abs(v) > 10000 ||\n (k.startsWith(\"scale\") && (v < 0.1 || v > 4))\n )\n throw Error(\"Invalid bone parameter: \" + k);\n out[k as keyof PartTransform] = v;\n }\n return out;\n }\n function validatePatch(value: unknown): BoneEditPatch {\n const v = value as BoneEditPatch;\n if (\n !v ||\n v.format !== \"golem.bone-edits\" ||\n v.version !== 1 ||\n v.documentId !== options.documentId\n )\n throw Error(\"Bone patch format/version/document mismatch\");\n if (\n !v.transforms ||\n typeof v.transforms !== \"object\" ||\n Array.isArray(v.transforms)\n )\n throw Error(\"Invalid bone transforms\");\n const next: Record<string, PartTransform> = Object.create(null);\n for (const [id, t] of Object.entries(v.transforms)) {\n known(id);\n next[id] = validateTransform(t);\n }\n return { ...v, transforms: next };\n }\n function editPose(input: Pose): Pose {\n if (!visible || !Object.keys(transforms).length) return input;\n const out = new Map(input);\n for (const [id, t] of Object.entries(transforms)) {\n const bone = bones.get(id)!;\n for (const [prop, offset] of Object.entries(t)) {\n const k = pk(\"bone\", id, prop),\n rest =\n bone.transform[prop as keyof typeof bone.transform] ??\n (prop.startsWith(\"scale\") ? 1 : 0),\n value = out.get(k) ?? rest;\n if (typeof value === \"number\")\n out.set(\n k,\n prop.startsWith(\"scale\") ? value * offset : value + offset,\n );\n }\n // A manual ancestor/chain pose must not be overwritten by its IK constraint.\n // Editing the target itself preserves the live constraint.\n for (const ik of player.doc.ik ?? []) {\n const affected = ik.bones.some((bid) => {\n let b = bones.get(bid);\n while (b) {\n if (b.id === id) return true;\n b = b.parent ? bones.get(b.parent) : undefined;\n }\n return false;\n });\n let target = bones.get(ik.target);\n let sharedAncestor = false;\n while (target) {\n if (target.id === id) { sharedAncestor = true; break; }\n target = target.parent ? bones.get(target.parent) : undefined;\n }\n if (affected && !sharedAncestor) out.set(pk(\"ik\", ik.id, \"mix\"), 0);\n }\n }\n return out;\n }\n const unsubscribe = player.onBeforePose(editPose);\n return {\n getTransform,\n exportPatch,\n validatePatch,\n get canUndo() {\n return past.length > 0;\n },\n get canRedo() {\n return future.length > 0;\n },\n setTransform(id: string, value: Partial<PartTransform>) {\n ensure();\n known(id);\n const next = validateTransform({ ...getTransform(id), ...value });\n if (JSON.stringify(next) === JSON.stringify(getTransform(id))) return;\n remember();\n transforms[id] = next;\n normalize();\n notify();\n },\n importPatch(value: unknown) {\n ensure();\n const next = validatePatch(value);\n remember();\n transforms = next.transforms;\n normalize();\n notify();\n },\n reset(id?: string) {\n ensure();\n if (id !== undefined) known(id);\n if (id !== undefined ? !transforms[id] : !Object.keys(transforms).length)\n return;\n remember();\n if (id !== undefined) delete transforms[id];\n else transforms = Object.create(null);\n notify();\n },\n setVisible(value: boolean) {\n ensure();\n visible = value;\n refresh();\n },\n undo() {\n ensure();\n if (!past.length) return;\n future.push(clone(transforms));\n transforms = past.pop()!;\n notify();\n },\n redo() {\n ensure();\n if (!future.length) return;\n past.push(clone(transforms));\n transforms = future.pop()!;\n notify();\n },\n dispose() {\n if (disposed) return;\n disposed = true;\n unsubscribe();\n refresh();\n },\n };\n}\nexport type InteractiveBoneEditor = ReturnType<\n typeof createInteractiveBoneEditor\n>;\n"],
|
|
4
|
+
"sourcesContent": ["import type { RigPlayer } from \"./rig-runtime\";\nimport { Matrix, type Mesh, type Sprite } from \"pixi.js\";\nimport type { Slot } from \"./rig-format\";\nimport type { Affine } from \"./rig-anim\";\n\nexport interface EditorPoint {\n x: number;\n y: number;\n}\nexport interface PartTransform {\n x: number;\n y: number;\n rotation: number;\n scaleX: number;\n scaleY: number;\n}\nexport interface InteractiveEditPatch {\n format: \"golem.interactive-edits\";\n version: 1;\n documentId: string;\n transforms: Record<string, PartTransform>;\n}\nexport interface EditorGroup {\n id: string;\n members: readonly string[];\n pivotBone?: string;\n}\nexport type InteractivePlayer = Pick<\n RigPlayer,\n | \"doc\"\n | \"view\"\n | \"world\"\n | \"renderItems\"\n | \"clipPolygons\"\n | \"onAfterPose\"\n | \"refreshPose\"\n>;\nexport interface InteractiveEditorOptions {\n player: InteractivePlayer;\n /** Transparent overlay aligned with the player's rendered viewport. It is cleared by the editor. */\n overlay: HTMLCanvasElement;\n /** Host-defined stable rig fingerprint; mismatched patches are rejected. */\n documentId: string;\n groups?: readonly EditorGroup[];\n /** Override when overlay backing pixels differ from the renderer's logical coordinates. */\n screenToDocument?: (client: EditorPoint) => EditorPoint;\n documentToOverlay?: (point: EditorPoint) => EditorPoint;\n alphaFallback?: \"skip\" | \"geometry\";\n onChange?: (patch: InteractiveEditPatch) => void;\n onSelection?: (id: string | null) => void;\n onHover?: (id: string | null) => void;\n onError?: (error: Error) => void;\n onGestureStart?: () => void;\n /** Render the host scene after a manual refresh; never called from the pose hook. */\n onRender?: () => void;\n historyLimit?: number;\n}\nconst neutral = (): PartTransform => ({\n x: 0,\n y: 0,\n rotation: 0,\n scaleX: 1,\n scaleY: 1,\n});\nconst copy = <T>(v: T): T => {\n const out = JSON.parse(JSON.stringify(v));\n if (out && typeof out === \"object\" && !Array.isArray(out))\n Object.setPrototypeOf(out, null);\n return out;\n};\nconst mat = (w: Affine) => new Matrix(w.a, w.b, w.c, w.d, w.tx, w.ty);\nconst rotate = (p: EditorPoint, r: number): EditorPoint => ({\n x: p.x * Math.cos(r) - p.y * Math.sin(r),\n y: p.x * Math.sin(r) + p.y * Math.cos(r),\n});\nconst pointIn = (p: EditorPoint, v: readonly number[]) => {\n let inside = false;\n for (let i = 0, j = v.length - 2; i < v.length; j = i, i += 2) {\n const x = v[i],\n y = v[i + 1],\n xx = v[j],\n yy = v[j + 1];\n if (y > p.y !== yy > p.y && p.x < ((xx - x) * (p.y - y)) / (yy - y) + x)\n inside = !inside;\n }\n return inside;\n};\ntype Shape = {\n id: string;\n points: EditorPoint[];\n uvs: number[];\n indices: number[];\n node: Sprite | Mesh;\n slot: Slot;\n z: number;\n order: number;\n};\nexport interface EditorFrame {\n id: string;\n center: EditorPoint;\n corners: EditorPoint[];\n handles: { name: string; p: EditorPoint }[];\n}\n\n/** Embedded part editor. Source documents are never mutated; edits replay after every evaluated pose. */\nexport function createInteractiveEditor(options: InteractiveEditorOptions) {\n const { player, overlay } = options,\n ctx = overlay.getContext(\"2d\");\n if (!ctx) throw Error(\"A 2D overlay context is required.\");\n if (!options.documentId) throw Error(\"documentId is required.\");\n if (\n options.historyLimit !== undefined &&\n (!Number.isInteger(options.historyLimit) ||\n options.historyLimit < 1 ||\n options.historyLimit > 10000)\n )\n throw Error(\"historyLimit must be an integer from 1 to 10000\");\n const members = new Map<string, string>(),\n groups = new Map<string, EditorGroup>(),\n attachments = new Set(\n player.doc.attachments.filter((a) => a.type !== \"path\").map((a) => a.id),\n );\n for (const g of options.groups ?? []) {\n if (!g.id || groups.has(g.id) || !g.members.length)\n throw Error(\"Invalid or duplicate editor group\");\n for (const id of g.members) {\n if (!attachments.has(id) || members.has(id))\n throw Error(\"Unknown or multiply grouped attachment: \" + id);\n members.set(id, g.id);\n }\n if (g.pivotBone && !player.doc.bones.some((b) => b.id === g.pivotBone))\n throw Error(\"Unknown group pivot bone\");\n groups.set(g.id, { ...g, members: [...g.members] });\n }\n for (const id of attachments)\n if (!members.has(id)) {\n if (groups.has(id))\n throw Error(\"Group id collides with an ungrouped attachment\");\n groups.set(id, { id, members: [id] });\n members.set(id, id);\n }\n let transforms: Record<string, PartTransform> = Object.create(null),\n past: (typeof transforms)[] = [],\n future: (typeof transforms)[] = [],\n selected: string | null = null,\n hovered: string | null = null,\n enabled = true,\n visible = true,\n disposed = false;\n let drag: {\n id: string;\n mode: string;\n f: NonNullable<ReturnType<typeof frame>>;\n start: EditorPoint;\n client: EditorPoint;\n initial: PartTransform;\n recorded: boolean;\n pointerId: number;\n } | null = null;\n const abort = new AbortController(),\n prior = {\n pointerEvents: overlay.style.pointerEvents,\n touchAction: overlay.style.touchAction,\n cursor: overlay.style.cursor,\n };\n overlay.style.pointerEvents = \"auto\";\n overlay.style.touchAction = \"none\";\n const alphaCache = new WeakMap<\n object,\n { width: number; height: number; data: Uint8ClampedArray } | null\n >();\n const project = (p: EditorPoint) =>\n options.documentToOverlay?.(p) ?? player.view.worldTransform.apply(p);\n const unproject = (p: EditorPoint) => {\n if (options.screenToDocument) return options.screenToDocument(p);\n const r = overlay.getBoundingClientRect();\n return player.view.worldTransform\n .clone()\n .invert()\n .apply({\n x: ((p.x - r.left) * overlay.width) / r.width,\n y: ((p.y - r.top) * overlay.height) / r.height,\n });\n };\n const screen = (p: EditorPoint) => {\n const v = project(p),\n r = overlay.getBoundingClientRect();\n return {\n x: r.left + (v.x * r.width) / overlay.width,\n y: r.top + (v.y * r.height) / overlay.height,\n };\n };\n const transformOf = (id: string) => ({ ...neutral(), ...transforms[id] });\n function ensure() {\n if (disposed) throw Error(\"Interactive editor is disposed\");\n }\n function known(id: string) {\n if (!groups.has(id)) throw Error(\"Unknown editor part: \" + id);\n }\n function shapes(): Shape[] {\n return player.renderItems\n .filter(\n (i) => i.node.visible && i.node.alpha > 0 && i.att.type !== \"path\",\n )\n .map((it, order) => {\n const n = it.node;\n n.updateLocalTransform();\n let p: number[], uv: number[], ind: number[];\n if (\"geometry\" in n) {\n p = Array.from(n.geometry.getBuffer(\"aPosition\").data);\n uv = Array.from(n.geometry.getBuffer(\"aUV\").data);\n ind = Array.from(n.geometry.indexBuffer.data);\n } else {\n const t = n.texture,\n u = t.uvs,\n a = n.anchor,\n trim = t.trim,\n x = (trim?.x ?? 0) - a.x * t.orig.width,\n y = (trim?.y ?? 0) - a.y * t.orig.height,\n w = trim?.width ?? t.orig.width,\n h = trim?.height ?? t.orig.height;\n p = [x, y, x + w, y, x + w, y + h, x, y + h];\n uv = [u.x0, u.y0, u.x1, u.y1, u.x2, u.y2, u.x3, u.y3];\n ind = [0, 1, 2, 0, 2, 3];\n }\n const points: EditorPoint[] = [];\n for (let i = 0; i < p.length; i += 2)\n points.push(n.localTransform.apply({ x: p[i], y: p[i + 1] }));\n return {\n id: members.get(it.att.id)!,\n points,\n uvs: uv,\n indices: ind,\n node: it.node,\n slot: it.slot,\n z: n.zIndex,\n order,\n };\n })\n .sort((a, b) => a.z - b.z || a.order - b.order);\n }\n function alpha(n: Sprite | Mesh, u: number, v: number) {\n const source = n.texture.source;\n if (!alphaCache.has(source)) {\n try {\n const resource = source.resource;\n if (!resource) throw Error(\"Texture has no readable image resource\");\n const c = document.createElement(\"canvas\");\n c.width = source.pixelWidth ?? source.width;\n c.height = source.pixelHeight ?? source.height;\n const cx = c.getContext(\"2d\")!;\n cx.drawImage(resource as CanvasImageSource, 0, 0, c.width, c.height);\n alphaCache.set(source, {\n width: c.width,\n height: c.height,\n data: cx.getImageData(0, 0, c.width, c.height).data,\n });\n } catch (e) {\n alphaCache.set(source, null);\n options.onError?.(\n new Error(\n \"Texture alpha cannot be read (CORS or unsupported source): \" +\n String(e),\n ),\n );\n }\n }\n const a = alphaCache.get(source);\n if (!a) return options.alphaFallback === \"geometry\";\n if (u < 0 || v < 0 || u > 1 || v > 1) return false;\n const x = u * a.width - 0.5,\n y = v * a.height - 0.5,\n x0 = Math.floor(x),\n y0 = Math.floor(y),\n fx = x - x0,\n fy = y - y0;\n let value = 0;\n for (let dy = 0; dy < 2; dy++)\n for (let dx = 0; dx < 2; dx++) {\n const xx = Math.max(0, Math.min(a.width - 1, x0 + dx)),\n yy = Math.max(0, Math.min(a.height - 1, y0 + dy));\n value +=\n a.data[(yy * a.width + xx) * 4 + 3] *\n (dx ? fx : 1 - fx) *\n (dy ? fy : 1 - fy);\n }\n return value >= 24;\n }\n function hitTest(p: EditorPoint) {\n ensure();\n const all = shapes();\n for (let s = all.length - 1; s >= 0; s--) {\n const a = all[s],\n clip = a.slot.clip && player.clipPolygons.get(a.slot.clip);\n if (clip && !pointIn(p, clip)) continue;\n for (let i = 0; i < a.indices.length; i += 3) {\n const ia = a.indices[i],\n ib = a.indices[i + 1],\n ic = a.indices[i + 2],\n A = a.points[ia],\n B = a.points[ib],\n C = a.points[ic],\n den = (B.y - C.y) * (A.x - C.x) + (C.x - B.x) * (A.y - C.y);\n if (Math.abs(den) < 1e-10) continue;\n const wa =\n ((B.y - C.y) * (p.x - C.x) + (C.x - B.x) * (p.y - C.y)) / den,\n wb = ((C.y - A.y) * (p.x - C.x) + (A.x - C.x) * (p.y - C.y)) / den,\n wc = 1 - wa - wb;\n if (Math.min(wa, wb, wc) < -1e-7) continue;\n if (\n alpha(\n a.node,\n wa * a.uvs[ia * 2] + wb * a.uvs[ib * 2] + wc * a.uvs[ic * 2],\n wa * a.uvs[ia * 2 + 1] +\n wb * a.uvs[ib * 2 + 1] +\n wc * a.uvs[ic * 2 + 1],\n )\n )\n return a.id;\n }\n }\n return null;\n }\n function pivot(id: string) {\n const g = groups.get(id)!;\n const a = player.doc.attachments.find((a) => a.id === g.members[0])!,\n slot = player.doc.slots.find((s) => s.id === a.slot)!;\n const w = player.world.get(g.pivotBone ?? slot.bone);\n if (\n !w ||\n !Number.isFinite(w.a * w.d - w.b * w.c) ||\n Math.abs(w.a * w.d - w.b * w.c) < 1e-10\n )\n return null;\n return mat(w);\n }\n function frame(id: string) {\n const viewport = overlay.getBoundingClientRect();\n if (!viewport.width || !viewport.height) return null;\n const points = shapes()\n .filter((s) => s.id === id)\n .flatMap((s) => s.points);\n if (!points.length) return null;\n const W = pivot(id);\n if (!W) return null;\n const inv = W.clone().invert(),\n o = transformOf(id),\n r = (o.rotation * Math.PI) / 180,\n q = points.map((p) => {\n const p0 = inv.apply(p);\n return rotate({ x: p0.x - o.x, y: p0.y - o.y }, -r);\n }),\n x0 = Math.min(...q.map((p) => p.x)),\n x1 = Math.max(...q.map((p) => p.x)),\n y0 = Math.min(...q.map((p) => p.y)),\n y1 = Math.max(...q.map((p) => p.y)),\n cx = (x0 + x1) / 2,\n cy = (y0 + y1) / 2,\n toWorld = (p: EditorPoint) => {\n const v = rotate(p, r);\n return W.apply({ x: v.x + o.x, y: v.y + o.y });\n };\n const handles: { name: string; p: EditorPoint }[] = [\n [\"nw\", x0, y0],\n [\"n\", cx, y0],\n [\"ne\", x1, y0],\n [\"e\", x1, cy],\n [\"se\", x1, y1],\n [\"s\", cx, y1],\n [\"sw\", x0, y1],\n [\"w\", x0, cy],\n ].map(([name, x, y]) => ({\n name: String(name),\n p: toWorld({ x: Number(x), y: Number(y) }),\n })),\n center = toWorld({ x: cx, y: cy }),\n top = handles[1].p,\n st = screen(top),\n sc = screen(center),\n d = Math.hypot(st.x - sc.x, st.y - sc.y) || 1;\n handles.push({\n name: \"rotate\",\n p: unproject({\n x: st.x + ((st.x - sc.x) / d) * 28,\n y: st.y + ((st.y - sc.y) / d) * 28,\n }),\n });\n return {\n id,\n W,\n inv,\n o,\n r,\n x0,\n x1,\n y0,\n y1,\n cx,\n cy,\n center,\n top,\n handles,\n corners: [handles[0].p, handles[2].p, handles[4].p, handles[6].p],\n };\n }\n function draw() {\n ctx!.clearRect(0, 0, overlay.width, overlay.height);\n if (!enabled || !visible) return;\n const box = overlay.getBoundingClientRect(),\n sx = overlay.width / (box.width || overlay.width),\n sy = overlay.height / (box.height || overlay.height);\n const outline = (id: string, color: string, controls: boolean) => {\n const f = frame(id);\n if (!f) return;\n ctx!.strokeStyle = color;\n ctx!.lineWidth = 1.5 * Math.min(sx, sy);\n ctx!.beginPath();\n f.corners\n .map(project)\n .forEach((p, i) => (i ? ctx!.lineTo(p.x, p.y) : ctx!.moveTo(p.x, p.y)));\n ctx!.closePath();\n ctx!.stroke();\n if (controls) {\n const top = project(f.top),\n rot = project(f.handles[8].p);\n ctx!.beginPath();\n ctx!.moveTo(top.x, top.y);\n ctx!.lineTo(rot.x, rot.y);\n ctx!.stroke();\n for (const h of f.handles) {\n const p = project(h.p);\n ctx!.beginPath();\n h.name === \"rotate\"\n ? ctx!.ellipse(p.x, p.y, 5 * sx, 5 * sy, 0, 0, Math.PI * 2)\n : ctx!.rect(p.x - 4 * sx, p.y - 4 * sy, 8 * sx, 8 * sy);\n ctx!.fillStyle = \"#17212c\";\n ctx!.fill();\n ctx!.stroke();\n }\n }\n };\n if (hovered && hovered !== selected) outline(hovered, \"#68d5fa\", false);\n if (selected) outline(selected, \"#ffda81\", true);\n }\n function apply() {\n if (!visible) {\n draw();\n return;\n }\n for (const it of player.renderItems) {\n if (!it.node.visible) continue;\n const id = members.get(it.att.id)!,\n o = transforms[id];\n if (!o) continue;\n const W = pivot(id);\n if (!W) continue;\n const r = (o.rotation * Math.PI) / 180,\n D = W.clone()\n .append(\n new Matrix(\n Math.cos(r) * o.scaleX,\n Math.sin(r) * o.scaleX,\n -Math.sin(r) * o.scaleY,\n Math.cos(r) * o.scaleY,\n o.x,\n o.y,\n ),\n )\n .append(W.clone().invert());\n it.node.updateLocalTransform();\n it.node.setFromMatrix(D.append(it.node.localTransform.clone()));\n }\n if (enabled && hovered)\n for (const it of player.renderItems)\n if (members.get(it.att.id) === hovered && it.node.visible)\n it.node.tint = 0xb9edff;\n draw();\n }\n const unsubscribe = player.onAfterPose(apply);\n const exportPatch = (): InteractiveEditPatch => ({\n format: \"golem.interactive-edits\",\n version: 1,\n documentId: options.documentId,\n transforms: copy(transforms),\n });\n function refresh() {\n player.refreshPose();\n draw();\n options.onRender?.();\n }\n function notify() {\n refresh();\n options.onChange?.(exportPatch());\n }\n function normalize() {\n for (const [id, t] of Object.entries(transforms))\n if (\n Object.entries(t).every(\n ([k, v]) => v === neutral()[k as keyof PartTransform],\n )\n )\n delete transforms[id];\n }\n function remember() {\n past.push(copy(transforms));\n if (past.length > (options.historyLimit ?? 100)) past.shift();\n future = [];\n }\n function validateTransform(value: unknown): PartTransform {\n if (!value || typeof value !== \"object\" || Array.isArray(value))\n throw Error(\"Invalid transform\");\n const out = neutral();\n for (const [k, v] of Object.entries(value)) {\n if (\n !Object.hasOwn(out, k) ||\n typeof v !== \"number\" ||\n !Number.isFinite(v) ||\n Math.abs(v) > 10000 ||\n (k.startsWith(\"scale\") && (v < 0.1 || v > 4))\n )\n throw Error(\"Invalid transform parameter: \" + k);\n out[k as keyof PartTransform] = v;\n }\n return out;\n }\n function select(id: string | null) {\n ensure();\n if (id !== null) known(id);\n selected = id;\n options.onSelection?.(id);\n draw();\n }\n function handleAt(p: EditorPoint, f: ReturnType<typeof frame>) {\n return f?.handles.find((h) => {\n const s = screen(h.p);\n return Math.hypot(p.x - s.x, p.y - s.y) < 9;\n });\n }\n const down = (e: PointerEvent) => {\n if (!enabled || !visible || e.button !== 0 || drag) return;\n const client = { x: e.clientX, y: e.clientY },\n q = unproject(client);\n let f = selected ? frame(selected) : null,\n h = handleAt(client, f);\n if (!h) {\n select(hitTest(q));\n if (!selected) return;\n f = frame(selected);\n }\n if (!f) return;\n options.onGestureStart?.();\n drag = {\n id: selected!,\n mode: h?.name ?? \"move\",\n f,\n start: f.inv.apply(q),\n client,\n initial: transformOf(selected!),\n recorded: false,\n pointerId: e.pointerId,\n };\n overlay.setPointerCapture(e.pointerId);\n e.preventDefault();\n };\n const move = (e: PointerEvent) => {\n if (!enabled || !visible || (drag && drag.pointerId !== e.pointerId))\n return;\n const client = { x: e.clientX, y: e.clientY },\n q = unproject(client);\n if (!drag) {\n const f = selected ? frame(selected) : null,\n h = handleAt(client, f),\n next = h ? null : hitTest(q);\n if (hovered !== next) {\n hovered = next;\n options.onHover?.(next);\n }\n if (h && f) {\n const a = screen(h.p),\n c = screen(f.center),\n angle =\n ((Math.atan2(a.y - c.y, a.x - c.x) * 180) / Math.PI + 180) % 180;\n overlay.style.cursor =\n h.name === \"rotate\"\n ? \"crosshair\"\n : [\"ew-resize\", \"nwse-resize\", \"ns-resize\", \"nesw-resize\"][\n Math.round(angle / 45) % 4\n ];\n } else overlay.style.cursor = hovered ? \"move\" : \"default\";\n refresh();\n return;\n }\n const { f, initial: o } = drag;\n if (!drag.recorded) {\n if (\n Math.hypot(e.clientX - drag.client.x, e.clientY - drag.client.y) < 0.35\n )\n return;\n remember();\n drag.recorded = true;\n }\n const p = f.inv.apply(q);\n let n = { ...o };\n if (drag.mode === \"move\") {\n n.x += p.x - drag.start.x;\n n.y += p.y - drag.start.y;\n } else if (drag.mode === \"rotate\") {\n const c = f.inv.apply(f.center),\n a0 = Math.atan2(drag.start.y - c.y, drag.start.x - c.x),\n a1 = Math.atan2(p.y - c.y, p.x - c.x);\n n.rotation =\n o.rotation +\n (Math.atan2(Math.sin(a1 - a0), Math.cos(a1 - a0)) * 180) / Math.PI;\n const v = rotate({ x: f.cx, y: f.cy }, (n.rotation * Math.PI) / 180);\n n.x = c.x - v.x;\n n.y = c.y - v.y;\n } else {\n const mode = drag.mode,\n p0 = rotate({ x: p.x - o.x, y: p.y - o.y }, -f.r),\n s0 = rotate({ x: drag.start.x - o.x, y: drag.start.y - o.y }, -f.r),\n ax = mode.includes(\"e\") ? f.x0 : mode.includes(\"w\") ? f.x1 : f.cx,\n ay = mode.includes(\"s\") ? f.y0 : mode.includes(\"n\") ? f.y1 : f.cy;\n let fx = /[ew]/.test(mode) ? (p0.x - ax) / (s0.x - ax || 1) : 1,\n fy = /[ns]/.test(mode) ? (p0.y - ay) / (s0.y - ay || 1) : 1;\n n.scaleX = Math.max(0.1, Math.min(4, o.scaleX * fx));\n n.scaleY = Math.max(0.1, Math.min(4, o.scaleY * fy));\n fx = n.scaleX / o.scaleX;\n fy = n.scaleY / o.scaleY;\n const a = rotate({ x: ax, y: ay }, f.r),\n b = rotate({ x: ax * fx, y: ay * fy }, f.r);\n n.x = o.x + a.x - b.x;\n n.y = o.y + a.y - b.y;\n }\n for (const k of [\"x\", \"y\", \"rotation\"] as const)\n n[k] = Number.isFinite(n[k])\n ? Math.max(-10000, Math.min(10000, n[k]))\n : o[k];\n transforms[drag.id] = validateTransform(n);\n normalize();\n notify();\n };\n const end = (event?: PointerEvent) => {\n if (!drag || (event && event.pointerId !== drag.pointerId)) return;\n const id = drag.pointerId;\n drag = null;\n if (overlay.hasPointerCapture(id)) overlay.releasePointerCapture(id);\n };\n const listen = (name: string, fn: EventListener) =>\n overlay.addEventListener(name, fn, { signal: abort.signal });\n listen(\"pointerdown\", down as EventListener);\n listen(\"pointermove\", move as EventListener);\n listen(\"pointerup\", end as EventListener);\n listen(\"pointercancel\", end as EventListener);\n listen(\"lostpointercapture\", end as EventListener);\n listen(\"pointerleave\", () => {\n if (!drag) {\n hovered = null;\n options.onHover?.(null);\n refresh();\n }\n });\n refresh();\n return {\n select,\n hitTest,\n getFrame: (id: string): EditorFrame | null => {\n ensure();\n known(id);\n return frame(id);\n },\n getTransform: (id: string) => {\n ensure();\n known(id);\n return transformOf(id);\n },\n get selection() {\n return selected;\n },\n get canUndo() {\n return past.length > 0;\n },\n get canRedo() {\n return future.length > 0;\n },\n exportPatch,\n setEnabled(value: boolean) {\n ensure();\n end();\n enabled = value;\n hovered = null;\n overlay.style.pointerEvents = value ? \"auto\" : \"none\";\n refresh();\n },\n setVisible(value: boolean) {\n ensure();\n end();\n visible = value;\n refresh();\n },\n setTransform(id: string, value: Partial<PartTransform>) {\n ensure();\n known(id);\n const t = validateTransform({ ...transformOf(id), ...value });\n if (JSON.stringify(t) === JSON.stringify(transformOf(id))) return;\n remember();\n transforms[id] = t;\n normalize();\n notify();\n },\n reset(id?: string) {\n ensure();\n if (id !== undefined) known(id);\n if (id !== undefined ? !transforms[id] : !Object.keys(transforms).length)\n return;\n remember();\n if (id !== undefined) delete transforms[id];\n else transforms = Object.create(null);\n notify();\n },\n undo() {\n ensure();\n if (!past.length) return;\n future.push(copy(transforms));\n transforms = past.pop()!;\n notify();\n },\n redo() {\n ensure();\n if (!future.length) return;\n past.push(copy(transforms));\n transforms = future.pop()!;\n notify();\n },\n importPatch(value: unknown) {\n ensure();\n if (!value || typeof value !== \"object\") throw Error(\"Invalid patch\");\n const v = value as InteractiveEditPatch;\n if (\n v.format !== \"golem.interactive-edits\" ||\n v.version !== 1 ||\n v.documentId !== options.documentId\n )\n throw Error(\"Patch format/version/document mismatch\");\n if (\n !v.transforms ||\n typeof v.transforms !== \"object\" ||\n Array.isArray(v.transforms)\n )\n throw Error(\"Invalid transforms\");\n const next: Record<string, PartTransform> = Object.create(null);\n for (const [id, t] of Object.entries(v.transforms)) {\n known(id);\n next[id] = validateTransform(t);\n }\n remember();\n transforms = next;\n normalize();\n notify();\n },\n dispose() {\n if (disposed) return;\n end();\n disposed = true;\n abort.abort();\n unsubscribe();\n player.refreshPose();\n ctx.clearRect(0, 0, overlay.width, overlay.height);\n Object.assign(overlay.style, prior);\n options.onRender?.();\n },\n };\n}\nexport type InteractiveEditor = ReturnType<typeof createInteractiveEditor>;\n\nexport { createInteractiveBoneEditor, type BoneEditPatch, type InteractiveBoneEditorOptions } from \"./interactive-bones\";\n", "/**\n * rig-anim \u2014 pure animation math shared by the runtime, the checker, the importer and tests. No PixiJS.\n *\n * - easing + track sampling, Pose: Map<\"target:id:prop\", value>\n * - 2D affine matrices with PixiJS layout (a b c d tx ty; x' = a\u00B7x + c\u00B7y + tx, y' = b\u00B7x + d\u00B7y + ty)\n * - bone world transforms with shear and inherit modes, constraints (ik / transform / path) applied in `order`\n * - attachment geometry in document space: region corners, skinned mesh vertices, lattice deformation\n */\nimport type { RigDocument, Animation, Track, Bone, Attachment, RegionAttachment, MeshAttachment, PathAttachment, IkConstraint, TransformConstraint, PathConstraint } from \"./rig-format\";\nimport { controlOffsets, evalPatch, type Pivot } from \"./rig-deform\";\nimport { resolveControls } from \"./rig-controls\";\n\nconst DEG = Math.PI / 180;\n\n// ---------- easing ----------\n\nexport type EaseSpec = Track[\"keys\"][number][\"ease\"];\n\nconst named: Record<string, (t: number) => number> = {\n linear: t => t,\n step: () => 0,\n ease_in: t => t * t,\n ease_out: t => 1 - (1 - t) * (1 - t),\n ease_in_out: t => (t < 0.5 ? 2 * t * t : 1 - (-2 * t + 2) ** 2 / 2),\n minimum_jerk: t => t * t * t * (10 + t * (-15 + 6 * t)),\n back_out: t => 1 + 2.70158 * (t - 1) ** 3 + 1.70158 * (t - 1) ** 2,\n elastic_out: t =>\n t === 0 ? 0 : t === 1 ? 1 : 2 ** (-10 * t) * Math.sin((t * 10 - 0.75) * ((2 * Math.PI) / 3)) + 1,\n};\n\n/**\n * Cubic bezier easing sampled into 10 linear segments walked in order of the curve parameter (the same\n * approximation Spine uses: 9 interior samples per curve), so control points with non-monotonic times\n * behave identically to Spine.\n */\nconst BEZIER_SEGMENTS = 10;\nfunction bezier(x1: number, y1: number, x2: number, y2: number) {\n const px: number[] = [0], py: number[] = [0];\n for (let i = 1; i <= BEZIER_SEGMENTS; i++) {\n const t = i / BEZIER_SEGMENTS, u = 1 - t;\n px.push(3 * u * u * t * x1 + 3 * u * t * t * x2 + t * t * t);\n py.push(3 * u * u * t * y1 + 3 * u * t * t * y2 + t * t * t);\n }\n return (x: number) => {\n if (x <= 0) return 0;\n for (let i = 1; i <= BEZIER_SEGMENTS; i++) {\n if (px[i] >= x) {\n const span = px[i] - px[i - 1];\n return py[i - 1] + (span > 0 ? ((x - px[i - 1]) / span) * (py[i] - py[i - 1]) : 0);\n }\n }\n return 1;\n };\n}\n\nconst easeCache = new Map<string, (t: number) => number>();\nexport function ease(spec: EaseSpec): (t: number) => number {\n if (typeof spec === \"string\") return named[spec];\n const key = spec.bezier.join(\",\");\n let f = easeCache.get(key);\n if (!f) easeCache.set(key, (f = bezier(...spec.bezier)));\n return f;\n}\n\n// ---------- sampling ----------\n\nexport type Val = number | string | null | number[] | string[];\n/** key: \"bone:id:prop\" | \"slot:id:prop\" | \"deform:id:prop\" | \"ik:id:prop\" | \"transform:id:prop\" | \"path:id:prop\" | \"drawOrder:order:order\" */\nexport type Pose = Map<string, Val>;\nexport const pk = (target: string, id: string, prop: string) => `${target}:${id}:${prop}`;\n\n/** Shape-preserving cubic, evaluated analytically rather than through the legacy Bezier lookup table. */\nfunction smoothValue(track: Track, i: number, t: number, component: number | undefined, loop: boolean): number {\n const k = track.keys, n = k.length;\n const v = (j: number) => component === undefined ? k[j].v as number : (k[j].v as number[])[component];\n const h = (j: number) => k[j + 1].t - k[j].t;\n const d = (j: number) => (v(j + 1) - v(j)) / h(j);\n const harmonic = (a: number, b: number, h0: number, h1: number) => a * b <= 0 ? 0 : 3 * (h0 + h1) / ((2 * h1 + h0) / a + (h1 + 2 * h0) / b);\n const tangent = (j: number): number => {\n if (j === 0 || j === n - 1) {\n if (loop && n > 2 && v(0) === v(n - 1) && k[0].ease !== \"step\" && k[n - 2].ease !== \"step\")\n return harmonic(d(n - 2), d(0), h(n - 2), h(0));\n return 0;\n }\n if (k[j - 1].ease === \"step\" || k[j].ease === \"step\") return 0;\n return harmonic(d(j - 1), d(j), h(j - 1), h(j));\n };\n const u = (t - k[i].t) / h(i), u2 = u * u, u3 = u2 * u;\n return (2 * u3 - 3 * u2 + 1) * v(i) + (u3 - 2 * u2 + u) * h(i) * tangent(i)\n + (-2 * u3 + 3 * u2) * v(i + 1) + (u3 - u2) * h(i) * tangent(i + 1);\n}\n\nexport function sample(track: Track, t: number, loop = false): Val {\n const keys = track.keys;\n if (t <= keys[0].t) return keys[0].v;\n const last = keys[keys.length - 1];\n if (t >= last.t) return last.v;\n let i = 0;\n while (keys[i + 1].t <= t) i++;\n const a = keys[i], b = keys[i + 1];\n if (a.ease === \"step\") return a.v;\n if (typeof a.v === \"number\" && typeof b.v === \"number\") {\n if (track.interpolation === \"pchip\") return smoothValue(track, i, t, undefined, loop);\n const u = ease(a.ease)((t - a.t) / (b.t - a.t));\n return a.v + (b.v - a.v) * u;\n }\n if (Array.isArray(a.v) && Array.isArray(b.v) && typeof a.v[0] === \"number\") {\n if (track.interpolation === \"pchip\") return (a.v as number[]).map((_, j) => smoothValue(track, i, t, j, loop));\n const phase = (t - a.t) / (b.t - a.t), av = a.v as number[], bv = b.v as number[];\n return av.map((v, k) => v + ((bv[k] ?? v) - v) * ease(a.componentEase?.[k] ?? a.ease)(phase));\n }\n return a.v; // discrete\n}\n\nexport function poseOf(anim: Animation, t: number, out: Pose = new Map()): Pose {\n for (const tr of anim.tracks) out.set(pk(tr.target, tr.id, tr.prop), sample(tr, t, anim.loop && tr.keys[0].t === 0 && tr.keys.at(-1)!.t === anim.duration));\n return out;\n}\n\n/** Crossfade: numbers (and number arrays) interpolate toward `to` (or rest when absent there); discrete values hold until w = 1. */\nexport function blendPoses(from: Pose, to: Pose, w: number, restOf: (key: string) => Val): Pose {\n if (w >= 1) return new Map(to);\n const out: Pose = new Map();\n for (const k of new Set([...from.keys(), ...to.keys()])) {\n const f = from.has(k) ? from.get(k)! : restOf(k);\n const cur = to.has(k) ? to.get(k)! : restOf(k);\n if (typeof f === \"number\" && !Array.isArray(cur)) {\n const target = typeof cur === \"number\" ? cur : (restOf(k) as number);\n out.set(k, f + (target - f) * w);\n } else if (Array.isArray(f) && typeof f[0] === \"number\") {\n const fv = f as number[], tv = (Array.isArray(cur) && typeof cur[0] === \"number\" ? cur : fv.map(() => 0)) as number[];\n out.set(k, fv.map((v, i) => v + ((tv[i] ?? 0) - v) * w));\n } else if (Array.isArray(cur) && typeof cur[0] === \"number\" && typeof f !== \"string\") {\n out.set(k, (cur as number[]).map(v => v * w));\n } else out.set(k, f ?? null);\n }\n return out;\n}\n\n/** Setup value of a pose key. */\nexport function restValue(doc: RigDocument, key: string): Val {\n const [target, id, prop] = key.split(\":\");\n if (target === \"control\") return doc.controls?.find(c => c.id === id)?.default ?? 0;\n if (target === \"bone\") return (doc.bones.find(b => b.id === id)!.transform as Record<string, number>)[prop];\n if (target === \"slot\") {\n const s = doc.slots.find(s => s.id === id)!;\n if (prop === \"color\") return colorToArray(s.color);\n return (s as unknown as Record<string, Val>)[prop];\n }\n if (target === \"ik\" || target === \"transform\" || target === \"path\") {\n const c = (doc[target] as { id: string }[]).find(c => c.id === id) as Record<string, unknown> | undefined;\n const v = c?.[prop];\n return typeof v === \"boolean\" ? (v ? 1 : 0) : ((v as number) ?? 0);\n }\n if (target === \"drawOrder\") return doc.slots.map(s => s.id);\n return 0;\n}\n\n/** \"#rrggbbaa\" \u2192 [r, g, b, a] in 0..1 */\nexport const colorToArray = (c: string): number[] => [0, 2, 4, 6].map(k => parseInt(c.slice(1 + k, 3 + k), 16) / 255);\n\n/** Numeric lattice deform params of one attachment present in the pose, or undefined when it has none. */\nexport function deformParams(pose: Pose, attachmentId: string): Record<string, number> | undefined {\n let out: Record<string, number> | undefined;\n const prefix = `deform:${attachmentId}:`;\n for (const [k, v] of pose)\n if (k.startsWith(prefix) && typeof v === \"number\") (out ??= {})[k.slice(prefix.length)] = v;\n return out;\n}\n\n// ---------- affine ----------\n\nexport interface Affine { a: number; b: number; c: number; d: number; tx: number; ty: number }\n\n/** translate \u00B7 rotate(deg, clockwise in Y-down) \u00B7 shear \u00B7 scale \u2014 PixiJS setTransform semantics with shear as axis angles */\nexport function affineOf(x: number, y: number, rotationDeg: number, sx: number, sy: number, shearX = 0, shearY = 0): Affine {\n const rx = (rotationDeg + shearX) * DEG, ry = (rotationDeg + 90 + shearY) * DEG;\n return { a: Math.cos(rx) * sx, b: Math.sin(rx) * sx, c: Math.cos(ry) * sy, d: Math.sin(ry) * sy, tx: x, ty: y };\n}\n\n/** parent \u2218 local (apply local first, then parent) \u2014 PixiJS Matrix.append */\nexport function mul(p: Affine, l: Affine): Affine {\n return {\n a: p.a * l.a + p.c * l.b, b: p.b * l.a + p.d * l.b,\n c: p.a * l.c + p.c * l.d, d: p.b * l.c + p.d * l.d,\n tx: p.a * l.tx + p.c * l.ty + p.tx, ty: p.b * l.tx + p.d * l.ty + p.ty,\n };\n}\n\nexport const apply = (m: Affine, x: number, y: number): [number, number] => [m.a * x + m.c * y + m.tx, m.b * x + m.d * y + m.ty];\n\nexport function invert(m: Affine): Affine {\n const det = m.a * m.d - m.b * m.c || 1e-12;\n const a = m.d / det, b = -m.b / det, c = -m.c / det, d = m.a / det;\n return { a, b, c, d, tx: -(a * m.tx + c * m.ty), ty: -(b * m.tx + d * m.ty) };\n}\n\n/** rotation of the x axis of an affine, degrees */\nexport const rotationOf = (m: Affine) => Math.atan2(m.b, m.a) / DEG;\n\n/**\n * Where a world matrix puts a bone, which way its x axis points, and how long its axes are \u2014 the inverse of\n * `affineOf` for everything except shear, which cannot be recovered from the axes alone (a sheared y axis is\n * reported by its length, and `angle` follows the x axis only). Reading a posed bone goes through here so the\n * numbers an agent gets are the numbers the runtime drew.\n */\nexport function decompose(m: Affine): { x: number; y: number; angle: number; scaleX: number; scaleY: number } {\n return { x: m.tx, y: m.ty, angle: rotationOf(m), scaleX: Math.hypot(m.a, m.b), scaleY: Math.hypot(m.c, m.d) };\n}\n\nconst num = (pose: Pose, key: string, rest: number) => { const v = pose.get(key); return typeof v === \"number\" ? v : rest; };\n\n// ---------- bones ----------\n\nexport interface BoneState {\n /** effective local values (setup + pose + constraint adjustments) */\n x: number; y: number; rotation: number; scaleX: number; scaleY: number; shearX: number; shearY: number;\n world: Affine;\n}\n\nexport interface Skeleton {\n doc: RigDocument;\n pose: Pose;\n bones: Map<string, BoneState>;\n /** attachment shown per slot (pose or setup), null = hidden */\n attachment: Map<string, Attachment | null>;\n}\n\nconst boneIndex = new WeakMap<RigDocument, Map<string, Bone>>();\nfunction boneOf(doc: RigDocument, id: string): Bone {\n let idx = boneIndex.get(doc);\n if (!idx) boneIndex.set(doc, (idx = new Map(doc.bones.map(b => [b.id, b]))));\n const b = idx.get(id);\n if (!b) throw new Error(`unknown bone \"${id}\"`);\n return b;\n}\n\n/** world transform of one bone from its effective local values and its parent's world transform, honouring `inherit` */\nexport function boneWorldOf(bone: Bone, s: Omit<BoneState, \"world\">, parent: Affine | undefined): Affine {\n const local = affineOf(s.x, s.y, s.rotation, s.scaleX, s.scaleY, s.shearX, s.shearY);\n if (!parent) return local;\n if (bone.inherit === \"normal\") return mul(parent, local);\n const [tx, ty] = apply(parent, s.x, s.y);\n if (bone.inherit === \"noScale\" || bone.inherit === \"noScaleOrReflection\") {\n // Preserve the direction of the child's rotated x axis, removing its length.\n const r = s.rotation * DEG, x = parent.a * Math.cos(r) + parent.c * Math.sin(r), y = parent.b * Math.cos(r) + parent.d * Math.sin(r);\n const length = Math.hypot(x, y), a = length > 1e-5 ? x / length : x, b = length > 1e-5 ? y / length : y;\n const sign = bone.inherit === \"noScale\" && parent.a * parent.d - parent.b * parent.c < 0 ? -1 : 1;\n const basis = { a, b, c: -b * sign, d: a * sign, tx: 0, ty: 0 };\n return { ...mul(basis, affineOf(0, 0, 0, s.scaleX, s.scaleY, s.shearX, s.shearY)), tx, ty };\n }\n return { ...mul(inheritedParentLinear(bone, parent), { ...local, tx: 0, ty: 0 }), tx, ty };\n}\n\n/** Order bones parent-first (memoized per document). */\nconst orderCache = new WeakMap<RigDocument, Bone[]>();\nexport function boneOrder(doc: RigDocument): Bone[] {\n let order = orderCache.get(doc);\n if (order) return order;\n order = [];\n const emitted = new Set<string>();\n while (order.length < doc.bones.length) {\n let progressed = false;\n for (const b of doc.bones) if (!emitted.has(b.id) && (!b.parent || emitted.has(b.parent))) { emitted.add(b.id); order.push(b); progressed = true; }\n if (!progressed) throw new Error(\"bone hierarchy has a cycle\");\n }\n orderCache.set(doc, order);\n return order;\n}\n\nconst localOf = (doc: RigDocument, pose: Pose, b: Bone): Omit<BoneState, \"world\"> => {\n const r = b.transform, id = b.id;\n return {\n x: num(pose, pk(\"bone\", id, \"x\"), r.x), y: num(pose, pk(\"bone\", id, \"y\"), r.y),\n rotation: num(pose, pk(\"bone\", id, \"rotation\"), r.rotation),\n scaleX: num(pose, pk(\"bone\", id, \"scaleX\"), r.scaleX), scaleY: num(pose, pk(\"bone\", id, \"scaleY\"), r.scaleY),\n shearX: num(pose, pk(\"bone\", id, \"shearX\"), r.shearX), shearY: num(pose, pk(\"bone\", id, \"shearY\"), r.shearY),\n };\n};\n\n/** Recompute world transforms of `bone` and its descendants from their current local values. */\nfunction updateSubtree(skel: Skeleton, root: string, includeRoot = true) {\n const doc = skel.doc;\n const kids = new Map<string, Bone[]>();\n for (const b of doc.bones) if (b.parent) (kids.get(b.parent) ?? kids.set(b.parent, []).get(b.parent)!).push(b);\n const stack = includeRoot ? [root] : (kids.get(root) ?? []).map(b => b.id);\n while (stack.length) {\n const id = stack.pop()!, b = boneOf(doc, id), st = skel.bones.get(id)!;\n st.world = boneWorldOf(b, st, b.parent ? skel.bones.get(b.parent)!.world : undefined);\n for (const k of kids.get(id) ?? []) stack.push(k.id);\n }\n}\n\n/**\n * Resolve controls, then apply optional pose modifiers before bone locals, constraints and skinning.\n * Each modifier receives its own map; its output is not passed through control bindings again.\n */\nexport function evaluate(doc: RigDocument, pose: Pose, modifiers?: Iterable<(pose: Pose) => Pose>): Skeleton {\n pose = resolveControls(doc, pose);\n if (modifiers) for (const modify of modifiers) pose = modify(new Map(pose));\n const skel: Skeleton = { doc, pose, bones: new Map(), attachment: new Map() };\n for (const b of boneOrder(doc)) {\n const s = localOf(doc, pose, b);\n const world = boneWorldOf(b, s, b.parent ? skel.bones.get(b.parent)!.world : undefined);\n skel.bones.set(b.id, { ...s, world });\n }\n const attachments = new Map(doc.attachments.map(a => [a.id, a]));\n for (const s of doc.slots) {\n const v = pose.get(pk(\"slot\", s.id, \"attachment\"));\n const id = v === undefined ? s.attachment : (v as string | null);\n skel.attachment.set(s.id, id ? attachments.get(id) ?? null : null);\n }\n // constraints, in document order (ties broken by array order)\n const all: { order: number; kind: \"ik\" | \"transform\" | \"path\"; c: IkConstraint | TransformConstraint | PathConstraint }[] = [\n ...doc.ik.map(c => ({ order: c.order, kind: \"ik\" as const, c })),\n ...doc.transform.map(c => ({ order: c.order, kind: \"transform\" as const, c })),\n ...doc.path.map(c => ({ order: c.order, kind: \"path\" as const, c })),\n ].sort((a, b) => a.order - b.order);\n for (const { kind, c } of all) {\n if (kind === \"ik\") applyIk(skel, c as IkConstraint);\n else if (kind === \"transform\") applyTransformConstraint(skel, c as TransformConstraint);\n else applyPathConstraint(skel, c as PathConstraint);\n }\n return skel;\n}\n\n/** Bone world transforms for a pose (constraints applied). */\nexport function boneWorld(doc: RigDocument, pose: Pose): Map<string, Affine> {\n const skel = evaluate(doc, pose);\n return new Map([...skel.bones].map(([id, s]) => [id, s.world]));\n}\n\n/** Explicit authoring masks plus active Spine clipping spans, resolved after attachment/draw-order keys. */\nexport function slotClips(skel: Skeleton): Map<string, string> {\n const keyed = skel.pose.get(pk(\"drawOrder\", \"order\", \"order\"));\n const order = Array.isArray(keyed) ? keyed as string[] : [...skel.doc.slots].sort((a, b) => a.z - b.z).map(s => s.id);\n const masks = new Map<string, string>();\n let active: PathAttachment | undefined;\n for (const id of order) {\n const att = skel.attachment.get(id);\n if (!active && att?.type === \"path\" && att.clipping) { active = att; continue; }\n if (active) masks.set(id, active.id);\n if (active?.clipping?.end === id) active = undefined;\n }\n for (const s of skel.doc.slots) if (s.clip) masks.set(s.id, s.clip);\n return masks;\n}\n\n// ---------- constraints ----------\n\n/** a constraint's value at this frame: the keyed one, else the document's \u2014 what every early-out below reads\n * (`applyIk` at `mix <= 0`, the transform/path solvers when all of `MIX_PROPS` are 0). `rig-check`'s `poseNum`\n * answers the same question elsewhere and must keep agreeing with it.\n * Outside the solvers, ask `mixAt` in rig-constraints.ts \u2014 the exported one, so a fourth copy is never needed. */\nconst cnum = (skel: Skeleton, kind: string, id: string, prop: string, rest: number | boolean) =>\n num(skel.pose, pk(kind, id, prop), typeof rest === \"boolean\" ? (rest ? 1 : 0) : rest);\n\n/** point in bone-local space of `bone` for a document-space point */\nfunction toLocal(skel: Skeleton, bone: string, x: number, y: number): [number, number] {\n return apply(invert(skel.bones.get(bone)!.world), x, y);\n}\n/** document-space direction of a bone's local x axis, degrees */\nconst worldRotation = (skel: Skeleton, bone: string) => rotationOf(skel.bones.get(bone)!.world);\n\nconst IDENTITY: Affine = { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 };\nconst wrapRad = (r: number) => (r > Math.PI ? r - 2 * Math.PI : r < -Math.PI ? r + 2 * Math.PI : r);\n\n/**\n * Single-bone IK: rotate `bone` so its (sheared) x axis points at the world point, blended by `mix`. Solved in the\n * parent's local space, which aims correctly under any parent scale; a negative scaleX flips the axis by 180\u00B0.\n * compress/stretch scale the bone's length to the target distance (uniform: both axes).\n */\nfunction aimAt(skel: Skeleton, boneId: string, tx: number, ty: number, mix: number, compress = false, stretch = false, uniform = false) {\n const b = boneOf(skel.doc, boneId), st = skel.bones.get(boneId)!;\n const parent = b.parent ? skel.bones.get(b.parent)!.world : undefined;\n let dx: number, dy: number;\n if (parent && b.inherit === \"onlyTranslation\") { dx = tx - st.world.tx; dy = ty - st.world.ty; }\n else {\n const [px, py] = parent ? apply(invert(parent), tx, ty) : [tx, ty];\n dx = px - st.x; dy = py - st.y;\n }\n let r = Math.atan2(dy, dx) / DEG - st.shearX - st.rotation;\n if (st.scaleX < 0) r += 180;\n r = shortest(r);\n let sx = st.scaleX, sy = st.scaleY;\n if (compress || stretch) {\n const len = b.length * sx;\n if (len > 0.0001) {\n const dd = dx * dx + dy * dy;\n if ((compress && dd < len * len) || (stretch && dd > len * len)) { const f = (Math.sqrt(dd) / len - 1) * mix + 1; sx *= f; if (uniform) sy *= f; }\n }\n }\n st.rotation += r * mix; st.scaleX = sx; st.scaleY = sy;\n updateSubtree(skel, boneId);\n}\n\nconst shortest = (deg: number) => ((((deg + 180) % 360) + 360) % 360) - 180;\n\n/** CCD for longer chains. Solves a fresh pose each evaluation, then blends the local rotations once. */\nfunction applyLongIk(skel: Skeleton, c: IkConstraint, tx: number, ty: number, mix: number) {\n const initial = c.bones.map(id => ({ ...skel.bones.get(id)! }));\n const last = c.bones.at(-1)!, len = boneOf(skel.doc, last).length;\n const end = () => apply(skel.bones.get(last)!.world, len, 0);\n const tolerance = c.tolerance ?? 0.05;\n // A straight chain aimed along its axis has zero CCD gradient even when it must fold.\n const root = skel.bones.get(c.bones[0])!.world, tip = end();\n if (Math.abs((tip[0] - root.tx) * (ty - root.ty) - (tip[1] - root.ty) * (tx - root.tx)) < 1e-7 && Math.hypot(tx - root.tx, ty - root.ty) < Math.hypot(tip[0] - root.tx, tip[1] - root.ty) - tolerance) {\n skel.bones.get(last)!.rotation += cnum(skel, \"ik\", c.id, \"bendPositive\", c.bendPositive) > 0.5 ? 5 : -5;\n updateSubtree(skel, last);\n }\n for (let iter = 0; iter < (c.iterations ?? 100); iter++) {\n const e = end();\n if (Math.hypot(e[0] - tx, e[1] - ty) <= tolerance) break;\n for (let i = c.bones.length - 1; i >= 0; i--) {\n const id = c.bones[i], b = boneOf(skel.doc, id), st = skel.bones.get(id)!, w = st.world, e = end();\n const angle = wrapRad(Math.atan2(ty - w.ty, tx - w.tx) - Math.atan2(e[1] - w.ty, e[0] - w.tx));\n const cos = Math.cos(angle), sin = Math.sin(angle);\n setEffectiveWorld(skel, b, st, { ...w, a: cos * w.a - sin * w.b, b: sin * w.a + cos * w.b, c: cos * w.c - sin * w.d, d: sin * w.c + cos * w.d });\n }\n }\n if (mix < 1) {\n c.bones.forEach((id, i) => {\n const st = skel.bones.get(id)!, from = initial[i];\n for (const p of [\"rotation\", \"shearX\", \"shearY\"] as const) st[p] = from[p] + shortest(st[p] - from[p]) * mix;\n for (const p of [\"x\", \"y\", \"scaleX\", \"scaleY\"] as const) st[p] = from[p] + (st[p] - from[p]) * mix;\n });\n updateSubtree(skel, c.bones[0]);\n }\n}\n\n/**\n * IK constraint. One bone aims at the target. Two bones: the child is moved onto the parent's x axis and both are\n * rotated so the child's tip reaches the target, bending to the requested side. Solved in the parent's parent space:\n * the law of cosines when the parent's scale is uniform, otherwise the child's tip sweeps an ellipse (parent scale)\n * and the reachable point is solved analytically. Negative scales flip axes by 180\u00B0 and mirror the bend; the parent's\n * shear is dropped. Softness eases the last stretch before full extension; stretch lengthens the parent to reach.\n */\nfunction applyIk(skel: Skeleton, c: IkConstraint) {\n const mix = cnum(skel, \"ik\", c.id, \"mix\", c.mix);\n if (mix <= 0) return;\n const target = skel.bones.get(c.target)!.world;\n if (c.bones.length > 2) { applyLongIk(skel, c, target.tx, target.ty, mix); return; }\n const bendDir = cnum(skel, \"ik\", c.id, \"bendPositive\", c.bendPositive) > 0.5 ? 1 : -1;\n const compress = cnum(skel, \"ik\", c.id, \"compress\", c.compress) > 0.5, stretch = cnum(skel, \"ik\", c.id, \"stretch\", c.stretch) > 0.5;\n let softness = cnum(skel, \"ik\", c.id, \"softness\", c.softness);\n if (c.bones.length === 1) { aimAt(skel, c.bones[0], target.tx, target.ty, mix, compress, stretch, c.uniform); return; }\n const [pId, cId] = c.bones;\n const parentBone = boneOf(skel.doc, pId), childBone = boneOf(skel.doc, cId);\n const p = skel.bones.get(pId)!, ch = skel.bones.get(cId)!;\n const px = p.x, py = p.y;\n let psx = p.scaleX, psy = p.scaleY, csx = ch.scaleX, sx = psx, sy = psy;\n let os1 = 0, os2 = 0, s2 = 1;\n if (psx < 0) { psx = -psx; os1 = 180; s2 = -1; }\n if (psy < 0) { psy = -psy; s2 = -s2; }\n if (csx < 0) { csx = -csx; os2 = 180; }\n const uniform = Math.abs(psx - psy) <= 0.0001;\n const cx = ch.x, cy = !uniform || stretch ? 0 : ch.y;\n const [cwx, cwy] = apply(p.world, cx, cy);\n const ppInv = invert(parentBone.parent ? skel.bones.get(parentBone.parent)!.world : IDENTITY);\n const [clx, cly] = apply(ppInv, cwx, cwy);\n const dx = clx - px, dy = cly - py, l1 = Math.hypot(dx, dy);\n let l2 = childBone.length * csx;\n if (l1 < 0.0001) { // parent and child share an origin: aim the parent, the child keeps its offset\n aimAt(skel, pId, target.tx, target.ty, mix, false, stretch, false);\n ch.x = cx; ch.y = cy; ch.rotation = 0;\n updateSubtree(skel, pId);\n return;\n }\n const [tlx, tly] = apply(ppInv, target.tx, target.ty);\n let tx = tlx - px, ty = tly - py, dd = tx * tx + ty * ty;\n if (softness !== 0) {\n softness *= psx * (csx + 1) * 0.5;\n const td = Math.sqrt(dd), sd = td - l1 - l2 * psx + softness;\n if (sd > 0) {\n let q = Math.min(1, sd / (softness * 2)) - 1;\n q = (sd - softness * (1 - q * q)) / td;\n tx -= q * tx; ty -= q * ty; dd = tx * tx + ty * ty;\n }\n }\n let a1: number, a2: number;\n if (uniform) {\n l2 *= psx;\n let cos = (dd - l1 * l1 - l2 * l2) / (2 * l1 * l2);\n if (cos < -1) { cos = -1; a2 = Math.PI * bendDir; }\n else if (cos > 1) {\n cos = 1; a2 = 0;\n if (stretch) { const f = (Math.sqrt(dd) / (l1 + l2) - 1) * mix + 1; sx *= f; if (c.uniform) sy *= f; }\n } else a2 = Math.acos(cos) * bendDir;\n const a = l1 + l2 * cos, b = l2 * Math.sin(a2);\n a1 = Math.atan2(ty * a - tx * b, tx * a + ty * b);\n } else {\n // child tip on the ellipse (l1 + a\u00B7cos \u03B8, b\u00B7sin \u03B8): find the point at distance \u221Add, else the nearest extreme\n const a = psx * l2, b = psy * l2, aa = a * a, bb = b * b, ta = Math.atan2(ty, tx);\n const c0 = bb * l1 * l1 + aa * dd - aa * bb, c1 = -2 * bb * l1, c2 = bb - aa;\n const disc = c1 * c1 - 4 * c2 * c0;\n let solved = false;\n a1 = a2 = 0;\n if (disc >= 0) {\n let q = Math.sqrt(disc);\n if (c1 < 0) q = -q;\n q = -(c1 + q) * 0.5;\n const r0 = q / c2, r1 = c0 / q, r = Math.abs(r0) < Math.abs(r1) ? r0 : r1, yy = dd - r * r;\n if (yy >= 0) {\n const y = Math.sqrt(yy) * bendDir;\n a1 = ta - Math.atan2(y, r);\n a2 = Math.atan2(y / psy, (r - l1) / psx);\n solved = true;\n }\n }\n if (!solved) {\n let minAngle = Math.PI, minX = l1 - a, minDist = minX * minX, minY = 0, maxAngle = 0, maxX = l1 + a, maxDist = maxX * maxX, maxY = 0;\n const k = -a * l1 / (aa - bb);\n if (k >= -1 && k <= 1) {\n const ang = Math.acos(k), x = a * Math.cos(ang) + l1, y = b * Math.sin(ang), dist = x * x + y * y;\n if (dist < minDist) { minAngle = ang; minDist = dist; minX = x; minY = y; }\n if (dist > maxDist) { maxAngle = ang; maxDist = dist; maxX = x; maxY = y; }\n }\n if (dd <= (minDist + maxDist) * 0.5) { a1 = ta - Math.atan2(minY * bendDir, minX); a2 = minAngle * bendDir; }\n else { a1 = ta - Math.atan2(maxY * bendDir, maxX); a2 = maxAngle * bendDir; }\n }\n }\n const os = Math.atan2(cy, cx) * s2;\n const d1 = shortest((a1 - os) / DEG + os1 - p.rotation);\n const d2 = shortest(((a2 + os) / DEG - ch.shearX) * s2 + os2 - ch.rotation);\n p.rotation += d1 * mix; p.scaleX = sx; p.scaleY = sy; p.shearX = 0; p.shearY = 0;\n ch.x = cx; ch.y = cy; ch.rotation += d2 * mix;\n updateSubtree(skel, pId);\n}\n\n/**\n * Transform constraint: each bone follows the target's rotation / position / scale / shear by the mix amounts, with\n * the constraint's offsets. World mode edits the bone's world matrix (rotation turns the whole matrix, scales the\n * axis lengths, shear turns the y axis) and folds it back into effective locals; local mode mixes the effective\n * local values. `relative` adds the target's values instead of matching them.\n */\nfunction applyTransformConstraint(skel: Skeleton, c: TransformConstraint) {\n const mixRotate = cnum(skel, \"transform\", c.id, \"mixRotate\", c.mixRotate), mixX = cnum(skel, \"transform\", c.id, \"mixX\", c.mixX), mixY = cnum(skel, \"transform\", c.id, \"mixY\", c.mixY);\n const mixScaleX = cnum(skel, \"transform\", c.id, \"mixScaleX\", c.mixScaleX), mixScaleY = cnum(skel, \"transform\", c.id, \"mixScaleY\", c.mixScaleY), mixShearY = cnum(skel, \"transform\", c.id, \"mixShearY\", c.mixShearY);\n if (!mixRotate && !mixX && !mixY && !mixScaleX && !mixScaleY && !mixShearY) return;\n c = { ...c, ...Object.fromEntries([\"x\", \"y\", \"rotation\", \"scaleX\", \"scaleY\", \"shearY\"].map(p => [p, cnum(skel, \"transform\", c.id, p, c[p as \"x\"])])) };\n const target = skel.bones.get(c.target)!, tw = target.world;\n // A reflected target reverses the orientation of its rotation/shear offsets.\n const handedness = tw.a * tw.d - tw.b * tw.c < 0 ? -1 : 1;\n const offsetRotation = c.rotation * DEG * (c.local ? 1 : handedness), offsetShearY = c.shearY * DEG * (c.local ? 1 : handedness);\n for (const id of c.bones) {\n const b = boneOf(skel.doc, id), st = skel.bones.get(id)!;\n if (c.local) {\n if (c.relative) {\n st.rotation += (target.rotation + c.rotation) * mixRotate;\n st.x += (target.x + c.x) * mixX; st.y += (target.y + c.y) * mixY;\n st.scaleX *= (target.scaleX - 1 + c.scaleX) * mixScaleX + 1;\n st.scaleY *= (target.scaleY - 1 + c.scaleY) * mixScaleY + 1;\n st.shearY += (target.shearY + c.shearY) * mixShearY;\n } else {\n st.rotation += (target.rotation - st.rotation + c.rotation) * mixRotate;\n st.x += (target.x - st.x + c.x) * mixX; st.y += (target.y - st.y + c.y) * mixY;\n if (st.scaleX !== 0) st.scaleX += (target.scaleX - st.scaleX + c.scaleX) * mixScaleX;\n if (st.scaleY !== 0) st.scaleY += (target.scaleY - st.scaleY + c.scaleY) * mixScaleY;\n st.shearY += (target.shearY - st.shearY + c.shearY) * mixShearY;\n }\n updateSubtree(skel, id);\n continue;\n }\n let w: Affine = { ...st.world };\n const targetRot = Math.atan2(tw.b, tw.a), rotate = (m: Affine, r: number): Affine => {\n const cos = Math.cos(r), sin = Math.sin(r);\n return { a: cos * m.a - sin * m.b, b: sin * m.a + cos * m.b, c: cos * m.c - sin * m.d, d: sin * m.c + cos * m.d, tx: m.tx, ty: m.ty };\n };\n if (mixRotate !== 0) {\n const r = wrapRad(c.relative ? targetRot + offsetRotation : targetRot - Math.atan2(w.b, w.a) + offsetRotation);\n w = rotate(w, r * mixRotate);\n }\n if (mixX !== 0 || mixY !== 0) {\n const [ox, oy] = apply(tw, c.x, c.y); // the offset lives in the target's space\n if (c.relative) { w.tx += ox * mixX; w.ty += oy * mixY; }\n else { w.tx += (ox - w.tx) * mixX; w.ty += (oy - w.ty) * mixY; }\n }\n if (mixScaleX !== 0) {\n const ts = Math.hypot(tw.a, tw.b);\n let f: number;\n if (c.relative) f = (ts - 1 + c.scaleX) * mixScaleX + 1;\n else { const s0 = Math.hypot(w.a, w.b); f = s0 === 0 ? 1 : (s0 + (ts - s0 + c.scaleX) * mixScaleX) / s0; }\n w.a *= f; w.b *= f;\n }\n if (mixScaleY !== 0) {\n const ts = Math.hypot(tw.c, tw.d);\n let f: number;\n if (c.relative) f = (ts - 1 + c.scaleY) * mixScaleY + 1;\n else { const s0 = Math.hypot(w.c, w.d); f = s0 === 0 ? 1 : (s0 + (ts - s0 + c.scaleY) * mixScaleY) / s0; }\n w.c *= f; w.d *= f;\n }\n if (mixShearY > 0) {\n const by = Math.atan2(w.d, w.c), targetShear = Math.atan2(tw.d, tw.c) - targetRot;\n const r = c.relative\n ? by + (wrapRad(targetShear) - Math.PI / 2 + offsetShearY) * mixShearY\n : by + (wrapRad(targetShear - (by - Math.atan2(w.b, w.a))) + offsetShearY) * mixShearY;\n const len = Math.hypot(w.c, w.d);\n w.c = Math.cos(r) * len; w.d = Math.sin(r) * len;\n }\n setEffectiveWorld(skel, b, st, w);\n }\n}\n\n// ---------- paths ----------\n\n/** world-space control points of a path attachment (2 numbers per vertex) */\nexport function pathWorldVertices(skel: Skeleton, slotId: string, att: PathAttachment): number[] {\n return attachmentWorldVertices(skel, slotId, att);\n}\n\n/** path length: `lengths[i]` is the length up to the end of curve i; an open path stores one extra entry */\nexport function pathLength(vertexCount: number, closed: boolean, lengths: number[]): number {\n const curveCount = closed ? vertexCount / 3 : vertexCount / 3 - 1;\n return curveCount >= 1 && lengths.length >= curveCount ? lengths[curveCount - 1] : lengths[lengths.length - 1] ?? 1;\n}\n\n/**\n * Sample positions and tangents along a path made of cubic bezier curves through the attachment vertices\n * (vertex layout: [c_prev, p, c_next] triples; curve i runs p_i \u2192 c_next_i \u2192 c_prev_{i+1} \u2192 p_{i+1}).\n */\n/**\n * Points along a path attachment (document space). `positions` are absolute distances along the path (or fractions of\n * its length when `percent`). With `constantSpeed` the length of the posed curve is measured (4 chords per curve for\n * the total, 10 chords inside the curve that holds the position); otherwise the stored setup `lengths` and the raw\n * curve parameter are used. Closed paths wrap; positions beyond an open path continue along its end tangents.\n * `angle` is the tangent direction in degrees.\n */\nexport function samplePath(points: number[], closed: boolean, lengths: number[], constantSpeed: boolean, positions: number[], percent: boolean): { x: number; y: number; angle: number }[] {\n const n = points.length / 2;\n // anchors sit at indices 1, 4, 7, \u2026 (each vertex = [cPrev, p, cNext]); curve i runs from anchor i to anchor i + 1\n const curveCount = closed ? n / 3 : n / 3 - 1;\n if (curveCount < 1) return positions.map(() => ({ x: points[2] ?? 0, y: points[3] ?? 0, angle: 0 }));\n const curve = (i: number) => {\n const a = ((i * 3 + 1) % n) * 2, c1 = ((i * 3 + 2) % n) * 2, c2 = ((i * 3 + 3) % n) * 2, b = ((i * 3 + 4) % n) * 2;\n return [points[a], points[a + 1], points[c1], points[c1 + 1], points[c2], points[c2 + 1], points[b], points[b + 1]];\n };\n const pointAt = (i: number, t: number) => {\n const [x0, y0, x1, y1, x2, y2, x3, y3] = curve(i), u = 1 - t;\n return { x: u * u * u * x0 + 3 * u * u * t * x1 + 3 * u * t * t * x2 + t * t * t * x3, y: u * u * u * y0 + 3 * u * u * t * y1 + 3 * u * t * t * y2 + t * t * t * y3 };\n };\n const angleAt = (i: number, t: number) => {\n const [x0, y0, x1, y1, x2, y2, x3, y3] = curve(i), u = 1 - t;\n const dx = 3 * u * u * (x1 - x0) + 6 * u * t * (x2 - x1) + 3 * t * t * (x3 - x2);\n const dy = 3 * u * u * (y1 - y0) + 6 * u * t * (y2 - y1) + 3 * t * t * (y3 - y2);\n return Math.atan2(dy, dx) / DEG;\n };\n const chords = (i: number, count: number) => {\n const acc: number[] = []; let sum = 0, prev = pointAt(i, 0);\n for (let k = 1; k <= count; k++) { const q = pointAt(i, k / count); sum += Math.hypot(q.x - prev.x, q.y - prev.y); acc.push(sum); prev = q; }\n return acc;\n };\n const cum: number[] = [];\n if (constantSpeed) { let acc = 0; for (let i = 0; i < curveCount; i++) { acc += chords(i, 4)[3]; cum.push(acc); } }\n else for (let i = 0; i < curveCount; i++) cum.push(lengths[i] ?? (i ? cum[i - 1] : 0));\n const total = cum[curveCount - 1];\n const out: { x: number; y: number; angle: number }[] = [];\n for (let pos of positions) {\n if (percent) pos *= total;\n if (closed) {\n if (total > 0) { pos %= total; if (pos < 0) pos += total; } else pos = 0;\n } else if (pos < 0) { // before the start: continue the start tangent (anchor 0 towards its outgoing control)\n const r = Math.atan2(points[5] - points[3], points[4] - points[2]);\n out.push({ x: points[2] + pos * Math.cos(r), y: points[3] + pos * Math.sin(r), angle: r / DEG });\n continue;\n } else if (pos > total) { // after the end: continue the end tangent (incoming control towards the last anchor)\n const a = (curveCount * 3 + 1) * 2, c = curveCount * 3 * 2, e = pos - total;\n const r = Math.atan2(points[a + 1] - points[c + 1], points[a] - points[c]);\n out.push({ x: points[a] + e * Math.cos(r), y: points[a + 1] + e * Math.sin(r), angle: r / DEG });\n continue;\n }\n let i = 0;\n while (i < curveCount - 1 && pos > cum[i]) i++;\n const prev = i ? cum[i - 1] : 0, span = cum[i] - prev;\n const f = span > 0 ? (pos - prev) / span : 0;\n let t = f;\n if (constantSpeed) { // weight the parameter by chord lengths inside the curve\n const seg = chords(i, 10), target = f * seg[9];\n let k = 0;\n while (k < 9 && target > seg[k]) k++;\n const sPrev = k ? seg[k - 1] : 0, sSpan = seg[k] - sPrev;\n t = (k + (sSpan > 0 ? (target - sPrev) / sSpan : 0)) / 10;\n }\n out.push({ ...pointAt(i, t), angle: angleAt(i, t) });\n }\n return out;\n}\n\nconst PATH_EPSILON = 0.00001;\n\n/**\n * Path constraint: bones are laid along the target slot's path attachment (spacing modes: length = bone length +\n * spacing, fixed = spacing px, percent = fraction of the path per bone, proportional = the path shared by bone length),\n * translated with mixX/mixY and rotated with mixRotate (tangent = path direction, chain = towards the next bone,\n * chainScale = chain + scaled to reach it). Applied in world space, then folded back into the bones' effective locals.\n */\nfunction applyPathConstraint(skel: Skeleton, c: PathConstraint) {\n const mixRotate = cnum(skel, \"path\", c.id, \"mixRotate\", c.mixRotate), mixX = cnum(skel, \"path\", c.id, \"mixX\", c.mixX), mixY = cnum(skel, \"path\", c.id, \"mixY\", c.mixY);\n if (!mixRotate && !mixX && !mixY) return;\n const att = skel.attachment.get(c.target);\n if (!att || att.type !== \"path\") return;\n const points = pathWorldVertices(skel, c.target, att);\n const position = cnum(skel, \"path\", c.id, \"position\", c.position), spacing = cnum(skel, \"path\", c.id, \"spacing\", c.spacing);\n const tangents = c.rotateMode === \"tangent\", scale = c.rotateMode === \"chainScale\";\n const bones = c.bones.map(id => ({ b: boneOf(skel.doc, id), st: skel.bones.get(id)! }));\n // The chain solver reads the pre-constraint axes of all controlled bones. Updating a parent's\n // descendants during the solve must not compound its scale/rotation into the next controlled bone.\n const inputWorlds = bones.map(({ st }) => ({ ...st.world }));\n const spacesCount = tangents ? bones.length : bones.length + 1;\n // spacing between consecutive bones; length-based modes scale with the bone's world x axis\n const spaces = new Array<number>(spacesCount).fill(0), worldLengths: number[] = [];\n let sum = 0;\n for (let i = 0; i < spacesCount - 1; i++) {\n const { b, st } = bones[i], setupLength = b.length;\n const len = setupLength < PATH_EPSILON ? 0 : setupLength * Math.hypot(st.world.a, st.world.b);\n worldLengths.push(len);\n if (c.spacingMode === \"percent\") spaces[i + 1] = spacing;\n else if (setupLength < PATH_EPSILON) spaces[i + 1] = spacing;\n else if (c.spacingMode === \"proportional\") { spaces[i + 1] = len; sum += len; }\n else spaces[i + 1] = (c.spacingMode === \"length\" ? setupLength + spacing : spacing) * len / setupLength;\n }\n if (c.spacingMode === \"proportional\" && sum > 0) { const f = spacesCount / sum * spacing; for (let i = 1; i < spacesCount; i++) spaces[i] *= f; }\n const total = measuredPathLength(points, att.closed, att.lengths, att.constantSpeed);\n const multiplier = c.spacingMode === \"percent\" ? total : c.spacingMode === \"proportional\" ? total / spacesCount : 1;\n let p = c.positionMode === \"percent\" ? position * total : position;\n const positions: number[] = [];\n for (let i = 0; i < spacesCount; i++) { p += spaces[i] * multiplier; positions.push(p); }\n const samples = samplePath(points, att.closed, att.lengths, att.constantSpeed, positions, false);\n // offset rotation follows the target bone's handedness; chain mode without an offset links bones tip to tail\n const tip = c.rotation === 0 && c.rotateMode === \"chain\";\n let offsetRotation = c.rotation * DEG;\n if (c.rotation !== 0) {\n const slot = skel.doc.slots.find(sl => sl.id === c.target)!, tw = skel.bones.get(slot.bone)!.world;\n if (tw.a * tw.d - tw.b * tw.c < 0) offsetRotation = -offsetRotation;\n }\n let boneX = samples[0].x, boneY = samples[0].y;\n const solvedWorlds = new Map<string, Affine>();\n for (let i = 0; i < bones.length; i++) {\n const { b, st } = bones[i];\n let w: Affine = { ...inputWorlds[i] };\n w.tx += (boneX - w.tx) * mixX;\n w.ty += (boneY - w.ty) * mixY;\n const next = samples[i + 1] ?? { x: 0, y: 0, angle: 0 };\n const dx = next.x - boneX, dy = next.y - boneY;\n if (scale) {\n const len = worldLengths[i];\n if (len !== 0) { const f = (Math.hypot(dx, dy) / len - 1) * mixRotate + 1; w.a *= f; w.b *= f; }\n }\n boneX = next.x; boneY = next.y;\n if (mixRotate > 0) {\n const { a, b: bb, c: cc, d } = w;\n let r = (tangents ? samples[i].angle : spaces[i + 1] === 0 ? next.angle : Math.atan2(dy, dx) / DEG) * DEG;\n r -= Math.atan2(bb, a);\n if (tip) {\n const cos = Math.cos(r), sin = Math.sin(r), length = b.length;\n boneX += (length * (cos * a - sin * bb) - dx) * mixRotate;\n boneY += (length * (sin * a + cos * bb) - dy) * mixRotate;\n } else r += offsetRotation;\n if (r > Math.PI) r -= 2 * Math.PI; else if (r < -Math.PI) r += 2 * Math.PI;\n r *= mixRotate;\n const cos = Math.cos(r), sin = Math.sin(r);\n w = { a: cos * a - sin * bb, b: sin * a + cos * bb, c: cos * cc - sin * d, d: sin * cc + cos * d, tx: w.tx, ty: w.ty };\n }\n solvedWorlds.set(b.id, w);\n }\n // A path's control order may run from child to parent. Commit the complete solution\n // parent-first so recomputing descendants cannot overwrite an already solved child.\n for (const b of boneOrder(skel.doc)) {\n const world = solvedWorlds.get(b.id);\n if (world) setEffectiveWorld(skel, b, skel.bones.get(b.id)!, world);\n }\n}\n\n/** total length used for percent positions/spacing: measured on the posed curve (constant speed) or the stored setup lengths */\nfunction measuredPathLength(points: number[], closed: boolean, lengths: number[], constantSpeed: boolean): number {\n const n = points.length / 2, curveCount = closed ? n / 3 : n / 3 - 1;\n if (curveCount < 1) return 0;\n if (!constantSpeed) return pathLength(n, closed, lengths);\n let total = 0;\n for (let i = 0; i < curveCount; i++) {\n const a = ((i * 3 + 1) % n) * 2, c1 = ((i * 3 + 2) % n) * 2, c2 = ((i * 3 + 3) % n) * 2, b = ((i * 3 + 4) % n) * 2;\n let px = points[a], py = points[a + 1];\n for (let k = 1; k <= 4; k++) {\n const t = k / 4, u = 1 - t;\n const x = u * u * u * points[a] + 3 * u * u * t * points[c1] + 3 * u * t * t * points[c2] + t * t * t * points[b];\n const y = u * u * u * points[a + 1] + 3 * u * u * t * points[c1 + 1] + 3 * u * t * t * points[c2 + 1] + t * t * t * points[b + 1];\n total += Math.hypot(x - px, y - py); px = x; py = y;\n }\n }\n return total;\n}\n\n/** the parent's linear part a bone actually inherits under its `inherit` mode */\nfunction inheritedParentLinear(bone: Bone, parent: Affine): Affine {\n switch (bone.inherit) {\n case \"normal\": return { ...parent, tx: 0, ty: 0 };\n case \"onlyTranslation\": return { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 };\n case \"noRotationOrReflection\": {\n // R(theta) diag(sx, sy) R(-theta) retains anisotropic scale along the parent's\n // world axes, while removing its rotation and reflection.\n const lenX = Math.hypot(parent.a, parent.b);\n const det = parent.a * parent.d - parent.b * parent.c;\n if (lenX * lenX <= 1e-4) {\n const lenY = Math.hypot(parent.c, parent.d), ux = lenY ? parent.c / lenY : 0, uy = lenY ? parent.d / lenY : 0;\n return { a: lenY * ux * ux, b: lenY * ux * uy, c: lenY * ux * uy, d: lenY * uy * uy, tx: 0, ty: 0 };\n }\n const ux = parent.a / lenX, uy = parent.b / lenX, sy = Math.abs(det) / lenX;\n return { a: lenX * ux * ux + sy * uy * uy, b: (lenX - sy) * ux * uy, c: (lenX - sy) * ux * uy, d: lenX * uy * uy + sy * ux * ux, tx: 0, ty: 0 };\n }\n case \"noScale\": case \"noScaleOrReflection\": {\n const lenX = Math.hypot(parent.a, parent.b) || 1;\n const ua = parent.a / lenX, ub = parent.b / lenX;\n const det = parent.a * parent.d - parent.b * parent.c;\n const reflect = det < 0 && bone.inherit === \"noScale\" ? -1 : 1; // noScale keeps the parent's reflection\n return { a: ua, b: ub, c: -ub * reflect, d: ua * reflect, tx: 0, ty: 0 };\n }\n }\n}\n\n/** Give a bone the world transform `world`: its effective locals are decomposed relative to the parent (shearX folds into rotation), descendants follow. */\nfunction setEffectiveWorld(skel: Skeleton, bone: Bone, st: BoneState, world: Affine) {\n const parent = bone.parent ? skel.bones.get(bone.parent)!.world : undefined;\n let lin: Affine = world, tx = world.tx, ty = world.ty;\n if (parent) {\n [tx, ty] = apply(invert(parent), world.tx, world.ty);\n lin = mul(invert(inheritedParentLinear(bone, parent)), { ...world, tx: 0, ty: 0 });\n }\n st.x = tx; st.y = ty;\n st.rotation = Math.atan2(lin.b, lin.a) / DEG;\n st.scaleX = Math.hypot(lin.a, lin.b);\n st.scaleY = Math.hypot(lin.c, lin.d);\n st.shearX = 0;\n st.shearY = shortest(Math.atan2(lin.d, lin.c) / DEG - st.rotation - 90);\n if (parent && (bone.inherit === \"noScale\" || bone.inherit === \"noScaleOrReflection\")) {\n const direction = mul(invert({ ...parent, tx: 0, ty: 0 }), { ...world, tx: 0, ty: 0 });\n st.rotation = Math.atan2(direction.b, direction.a) / DEG;\n st.scaleX = Math.hypot(world.a, world.b); st.scaleY = Math.hypot(world.c, world.d);\n const sign = bone.inherit === \"noScale\" && parent.a * parent.d - parent.b * parent.c < 0 ? -1 : 1;\n st.shearY = shortest((Math.atan2(world.d, world.c) - Math.atan2(world.b, world.a)) / DEG * sign - 90);\n }\n // Keep the solved matrix itself. A zero-scale parent has no inverse that can reconstruct\n // a independently constrained child's world transform; recomposing it would collapse the chain.\n st.world = world;\n updateSubtree(skel, bone.id, false);\n}\n\n// ---------- attachment geometry ----------\n\n/** skinned or bone-local vertices of a mesh/path attachment in document space, deform offsets applied */\nexport function attachmentWorldVertices(skel: Skeleton, slotId: string, att: MeshAttachment | PathAttachment): number[] {\n const deform = skel.pose.get(pk(\"deform\", att.id, \"vertices\"));\n const off = Array.isArray(deform) && typeof deform[0] === \"number\" ? (deform as number[]) : undefined;\n const out: number[] = [];\n if (att.weights) {\n let k = 0;\n for (const list of att.weights) {\n let x = 0, y = 0;\n for (const inf of list) {\n const w = skel.bones.get(inf.bone)!.world;\n const vx = inf.x + (off ? off[k] : 0), vy = inf.y + (off ? off[k + 1] : 0);\n k += 2;\n x += (w.a * vx + w.c * vy + w.tx) * inf.w; y += (w.b * vx + w.d * vy + w.ty) * inf.w;\n }\n out.push(x, y);\n }\n } else {\n const slot = skel.doc.slots.find(s => s.id === slotId)!, w = skel.bones.get(slot.bone)!.world, v = att.vertices!;\n for (let i = 0; i < v.length; i += 2) {\n const vx = v[i] + (off ? off[i] : 0), vy = v[i + 1] + (off ? off[i + 1] : 0);\n out.push(w.a * vx + w.c * vy + w.tx, w.b * vx + w.d * vy + w.ty);\n }\n }\n return out;\n}\n\n/** image-local (centre-relative) \u2192 document matrix for a region attachment */\nexport function regionMatrix(skel: Skeleton, slotId: string, att: RegionAttachment): Affine {\n const slot = skel.doc.slots.find(s => s.id === slotId)!;\n return mul(skel.bones.get(slot.bone)!.world, affineOf(att.x, att.y, att.rotation, att.scaleX, att.scaleY));\n}\n\nexport function regionSize(doc: RigDocument, att: RegionAttachment): [number, number] {\n const asset = doc.assets.find(a => a.id === att.asset)!;\n return [att.width ?? asset.width, att.height ?? asset.height];\n}\n\n/**\n * Outline of a slot's shown attachment in document space: region \u2192 `samplesPerEdge` points per edge clockwise\n * from the top-left (lattice deform applied), mesh \u2192 hull vertices (or all), path/hidden \u2192 undefined.\n */\nexport function slotOutline(doc: RigDocument, pose: Pose, slotId: string, samplesPerEdge = 1, skel = evaluate(doc, pose)): [number, number][] | undefined {\n if (!doc.slots.some(s => s.id === slotId)) throw new Error(`unknown slot \"${slotId}\"`);\n const att = skel.attachment.get(slotId);\n if (!att) return undefined;\n if (att.type === \"mesh\") {\n const v = attachmentWorldVertices(skel, slotId, att), n = att.hull > 0 ? att.hull : v.length / 2;\n const pts: [number, number][] = [];\n for (let i = 0; i < n; i++) pts.push([v[i * 2], v[i * 2 + 1]]);\n return pts;\n }\n if (att.type === \"path\") return undefined;\n const [w, h] = regionSize(doc, att), m = regionMatrix(skel, slotId, att), pivot: Pivot = { x: 0.5, y: 0.5 };\n const params = deformParams(pose, att.id);\n const offsets = params ? controlOffsets(params, w, h, pivot) : undefined;\n const pts: [number, number][] = [];\n const push = (u: number, v: number) => {\n const [ox, oy] = offsets ? evalPatch(offsets, u, v) : [0, 0];\n pts.push(apply(m, (u - 0.5) * w + ox, (v - 0.5) * h + oy));\n };\n const n = Math.max(1, samplesPerEdge);\n for (let i = 0; i < n; i++) push(i / n, 0);\n for (let i = 0; i < n; i++) push(1, i / n);\n for (let i = 0; i < n; i++) push(1 - i / n, 1);\n for (let i = 0; i < n; i++) push(0, 1 - i / n);\n return pts;\n}\n", "import type { RigPlayer } from \"./rig-runtime\";\nimport type { Pose } from \"./rig-anim\";\nimport { pk } from \"./rig-anim\";\nimport type { PartTransform } from \"./interactive-editor\";\n\nexport interface BoneEditPatch {\n format: \"golem.bone-edits\";\n version: 1;\n documentId: string;\n transforms: Record<string, PartTransform>;\n}\nexport interface InteractiveBoneEditorOptions {\n player: Pick<RigPlayer, \"doc\" | \"onBeforePose\" | \"refreshPose\">;\n documentId: string;\n onChange?: (patch: BoneEditPatch) => void;\n onRender?: () => void;\n historyLimit?: number;\n}\nconst neutral = (): PartTransform => ({\n x: 0,\n y: 0,\n rotation: 0,\n scaleX: 1,\n scaleY: 1,\n});\nconst clone = (\n v: Record<string, PartTransform>,\n): Record<string, PartTransform> =>\n Object.assign(Object.create(null), JSON.parse(JSON.stringify(v)));\n/** Add bone offsets after expression controls, before constraints and native skinning. */\nexport function createInteractiveBoneEditor(\n options: InteractiveBoneEditorOptions,\n) {\n if (!options.documentId) throw Error(\"documentId is required\");\n const limit = options.historyLimit ?? 100;\n if (!Number.isInteger(limit) || limit < 1 || limit > 10000)\n throw Error(\"historyLimit must be an integer from 1 to 10000\");\n const { player } = options,\n bones = new Map(player.doc.bones.map((b) => [b.id, b]));\n let transforms: Record<string, PartTransform> = Object.create(null),\n past: (typeof transforms)[] = [],\n future: (typeof transforms)[] = [],\n visible = true,\n disposed = false;\n const ensure = () => {\n if (disposed) throw Error(\"Bone editor is disposed\");\n };\n const known = (id: string) => {\n if (!bones.has(id)) throw Error(\"Unknown bone: \" + id);\n };\n const getTransform = (id: string) => {\n ensure();\n known(id);\n return { ...neutral(), ...transforms[id] };\n };\n const exportPatch = (): BoneEditPatch => ({\n format: \"golem.bone-edits\",\n version: 1,\n documentId: options.documentId,\n transforms: clone(transforms),\n });\n const refresh = () => {\n player.refreshPose();\n options.onRender?.();\n };\n const notify = () => {\n refresh();\n options.onChange?.(exportPatch());\n };\n const remember = () => {\n past.push(clone(transforms));\n if (past.length > limit) past.shift();\n future = [];\n };\n const normalize = () => {\n for (const [id, t] of Object.entries(transforms))\n if (\n Object.entries(t).every(\n ([k, v]) => v === neutral()[k as keyof PartTransform],\n )\n )\n delete transforms[id];\n };\n function validateTransform(value: unknown): PartTransform {\n if (!value || typeof value !== \"object\" || Array.isArray(value))\n throw Error(\"Invalid bone transform\");\n const out = neutral();\n for (const [k, v] of Object.entries(value)) {\n if (\n !Object.hasOwn(out, k) ||\n typeof v !== \"number\" ||\n !Number.isFinite(v) ||\n Math.abs(v) > 10000 ||\n (k.startsWith(\"scale\") && (v < 0.1 || v > 4))\n )\n throw Error(\"Invalid bone parameter: \" + k);\n out[k as keyof PartTransform] = v;\n }\n return out;\n }\n function validatePatch(value: unknown): BoneEditPatch {\n const v = value as BoneEditPatch;\n if (\n !v ||\n v.format !== \"golem.bone-edits\" ||\n v.version !== 1 ||\n v.documentId !== options.documentId\n )\n throw Error(\"Bone patch format/version/document mismatch\");\n if (\n !v.transforms ||\n typeof v.transforms !== \"object\" ||\n Array.isArray(v.transforms)\n )\n throw Error(\"Invalid bone transforms\");\n const next: Record<string, PartTransform> = Object.create(null);\n for (const [id, t] of Object.entries(v.transforms)) {\n known(id);\n next[id] = validateTransform(t);\n }\n return { ...v, transforms: next };\n }\n function editPose(input: Pose): Pose {\n if (!visible || !Object.keys(transforms).length) return input;\n const out = new Map(input);\n for (const [id, t] of Object.entries(transforms)) {\n const bone = bones.get(id)!;\n for (const [prop, offset] of Object.entries(t)) {\n const k = pk(\"bone\", id, prop),\n rest =\n bone.transform[prop as keyof typeof bone.transform] ??\n (prop.startsWith(\"scale\") ? 1 : 0),\n value = out.get(k) ?? rest;\n if (typeof value === \"number\")\n out.set(\n k,\n prop.startsWith(\"scale\") ? value * offset : value + offset,\n );\n }\n // A manual ancestor/chain pose must not be overwritten by its IK constraint.\n // Editing the target itself preserves the live constraint.\n for (const ik of player.doc.ik ?? []) {\n const affected = ik.bones.some((bid) => {\n let b = bones.get(bid);\n while (b) {\n if (b.id === id) return true;\n b = b.parent ? bones.get(b.parent) : undefined;\n }\n return false;\n });\n let target = bones.get(ik.target);\n let sharedAncestor = false;\n while (target) {\n if (target.id === id) { sharedAncestor = true; break; }\n target = target.parent ? bones.get(target.parent) : undefined;\n }\n if (affected && !sharedAncestor) out.set(pk(\"ik\", ik.id, \"mix\"), 0);\n }\n }\n return out;\n }\n const unsubscribe = player.onBeforePose(editPose);\n return {\n getTransform,\n exportPatch,\n validatePatch,\n get canUndo() {\n return past.length > 0;\n },\n get canRedo() {\n return future.length > 0;\n },\n setTransform(id: string, value: Partial<PartTransform>) {\n ensure();\n known(id);\n const next = validateTransform({ ...getTransform(id), ...value });\n if (JSON.stringify(next) === JSON.stringify(getTransform(id))) return;\n remember();\n transforms[id] = next;\n normalize();\n notify();\n },\n importPatch(value: unknown) {\n ensure();\n const next = validatePatch(value);\n remember();\n transforms = next.transforms;\n normalize();\n notify();\n },\n reset(id?: string) {\n ensure();\n if (id !== undefined) known(id);\n if (id !== undefined ? !transforms[id] : !Object.keys(transforms).length)\n return;\n remember();\n if (id !== undefined) delete transforms[id];\n else transforms = Object.create(null);\n notify();\n },\n setVisible(value: boolean) {\n ensure();\n visible = value;\n refresh();\n },\n undo() {\n ensure();\n if (!past.length) return;\n future.push(clone(transforms));\n transforms = past.pop()!;\n notify();\n },\n redo() {\n ensure();\n if (!future.length) return;\n past.push(clone(transforms));\n transforms = future.pop()!;\n notify();\n },\n dispose() {\n if (disposed) return;\n disposed = true;\n unsubscribe();\n refresh();\n },\n };\n}\nexport type InteractiveBoneEditor = ReturnType<\n typeof createInteractiveBoneEditor\n>;\n"],
|
|
5
5
|
"mappings": "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",
|
|
6
6
|
"names": ["neutral", "a", "f"]
|
|
7
7
|
}
|
package/dist/lib/rig-anim.d.ts
CHANGED
|
@@ -80,6 +80,8 @@ export declare function boneOrder(doc: RigDocument): Bone[];
|
|
|
80
80
|
export declare function evaluate(doc: RigDocument, pose: Pose, modifiers?: Iterable<(pose: Pose) => Pose>): Skeleton;
|
|
81
81
|
/** Bone world transforms for a pose (constraints applied). */
|
|
82
82
|
export declare function boneWorld(doc: RigDocument, pose: Pose): Map<string, Affine>;
|
|
83
|
+
/** Explicit authoring masks plus active Spine clipping spans, resolved after attachment/draw-order keys. */
|
|
84
|
+
export declare function slotClips(skel: Skeleton): Map<string, string>;
|
|
83
85
|
/** world-space control points of a path attachment (2 numbers per vertex) */
|
|
84
86
|
export declare function pathWorldVertices(skel: Skeleton, slotId: string, att: PathAttachment): number[];
|
|
85
87
|
/** path length: `lengths[i]` is the length up to the end of curve i; an open path stores one extra entry */
|
|
@@ -171,6 +171,9 @@ export declare function makeControlSweep(doc: RigDocument, probes: Record<string
|
|
|
171
171
|
bone: string;
|
|
172
172
|
w: number;
|
|
173
173
|
}[][] | undefined;
|
|
174
|
+
clipping?: {
|
|
175
|
+
end?: string | undefined;
|
|
176
|
+
} | undefined;
|
|
174
177
|
})[];
|
|
175
178
|
animations: {
|
|
176
179
|
id: string;
|
|
@@ -183,6 +186,9 @@ export declare function makeControlSweep(doc: RigDocument, probes: Record<string
|
|
|
183
186
|
ease: "linear" | "step" | "ease_in" | "ease_out" | "ease_in_out" | "minimum_jerk" | "back_out" | "elastic_out" | {
|
|
184
187
|
bezier: [number, number, number, number];
|
|
185
188
|
};
|
|
189
|
+
componentEase?: ("linear" | "step" | "ease_in" | "ease_out" | "ease_in_out" | "minimum_jerk" | "back_out" | "elastic_out" | {
|
|
190
|
+
bezier: [number, number, number, number];
|
|
191
|
+
})[] | undefined;
|
|
186
192
|
}[];
|
|
187
193
|
id: string;
|
|
188
194
|
target: "control";
|
|
@@ -195,6 +201,9 @@ export declare function makeControlSweep(doc: RigDocument, probes: Record<string
|
|
|
195
201
|
ease: "linear" | "step" | "ease_in" | "ease_out" | "ease_in_out" | "minimum_jerk" | "back_out" | "elastic_out" | {
|
|
196
202
|
bezier: [number, number, number, number];
|
|
197
203
|
};
|
|
204
|
+
componentEase?: ("linear" | "step" | "ease_in" | "ease_out" | "ease_in_out" | "minimum_jerk" | "back_out" | "elastic_out" | {
|
|
205
|
+
bezier: [number, number, number, number];
|
|
206
|
+
})[] | undefined;
|
|
198
207
|
}[];
|
|
199
208
|
id: string;
|
|
200
209
|
target: "bone";
|
|
@@ -207,6 +216,9 @@ export declare function makeControlSweep(doc: RigDocument, probes: Record<string
|
|
|
207
216
|
ease: "linear" | "step" | "ease_in" | "ease_out" | "ease_in_out" | "minimum_jerk" | "back_out" | "elastic_out" | {
|
|
208
217
|
bezier: [number, number, number, number];
|
|
209
218
|
};
|
|
219
|
+
componentEase?: ("linear" | "step" | "ease_in" | "ease_out" | "ease_in_out" | "minimum_jerk" | "back_out" | "elastic_out" | {
|
|
220
|
+
bezier: [number, number, number, number];
|
|
221
|
+
})[] | undefined;
|
|
210
222
|
}[];
|
|
211
223
|
id: string;
|
|
212
224
|
target: "slot";
|
|
@@ -219,6 +231,9 @@ export declare function makeControlSweep(doc: RigDocument, probes: Record<string
|
|
|
219
231
|
ease: "linear" | "step" | "ease_in" | "ease_out" | "ease_in_out" | "minimum_jerk" | "back_out" | "elastic_out" | {
|
|
220
232
|
bezier: [number, number, number, number];
|
|
221
233
|
};
|
|
234
|
+
componentEase?: ("linear" | "step" | "ease_in" | "ease_out" | "ease_in_out" | "minimum_jerk" | "back_out" | "elastic_out" | {
|
|
235
|
+
bezier: [number, number, number, number];
|
|
236
|
+
})[] | undefined;
|
|
222
237
|
}[];
|
|
223
238
|
id: string;
|
|
224
239
|
target: "deform";
|
|
@@ -231,6 +246,9 @@ export declare function makeControlSweep(doc: RigDocument, probes: Record<string
|
|
|
231
246
|
ease: "linear" | "step" | "ease_in" | "ease_out" | "ease_in_out" | "minimum_jerk" | "back_out" | "elastic_out" | {
|
|
232
247
|
bezier: [number, number, number, number];
|
|
233
248
|
};
|
|
249
|
+
componentEase?: ("linear" | "step" | "ease_in" | "ease_out" | "ease_in_out" | "minimum_jerk" | "back_out" | "elastic_out" | {
|
|
250
|
+
bezier: [number, number, number, number];
|
|
251
|
+
})[] | undefined;
|
|
234
252
|
}[];
|
|
235
253
|
id: string;
|
|
236
254
|
target: "ik";
|
|
@@ -243,6 +261,9 @@ export declare function makeControlSweep(doc: RigDocument, probes: Record<string
|
|
|
243
261
|
ease: "linear" | "step" | "ease_in" | "ease_out" | "ease_in_out" | "minimum_jerk" | "back_out" | "elastic_out" | {
|
|
244
262
|
bezier: [number, number, number, number];
|
|
245
263
|
};
|
|
264
|
+
componentEase?: ("linear" | "step" | "ease_in" | "ease_out" | "ease_in_out" | "minimum_jerk" | "back_out" | "elastic_out" | {
|
|
265
|
+
bezier: [number, number, number, number];
|
|
266
|
+
})[] | undefined;
|
|
246
267
|
}[];
|
|
247
268
|
id: string;
|
|
248
269
|
target: "transform";
|
|
@@ -255,6 +276,9 @@ export declare function makeControlSweep(doc: RigDocument, probes: Record<string
|
|
|
255
276
|
ease: "linear" | "step" | "ease_in" | "ease_out" | "ease_in_out" | "minimum_jerk" | "back_out" | "elastic_out" | {
|
|
256
277
|
bezier: [number, number, number, number];
|
|
257
278
|
};
|
|
279
|
+
componentEase?: ("linear" | "step" | "ease_in" | "ease_out" | "ease_in_out" | "minimum_jerk" | "back_out" | "elastic_out" | {
|
|
280
|
+
bezier: [number, number, number, number];
|
|
281
|
+
})[] | undefined;
|
|
258
282
|
}[];
|
|
259
283
|
id: string;
|
|
260
284
|
target: "path";
|
|
@@ -267,6 +291,9 @@ export declare function makeControlSweep(doc: RigDocument, probes: Record<string
|
|
|
267
291
|
ease: "linear" | "step" | "ease_in" | "ease_out" | "ease_in_out" | "minimum_jerk" | "back_out" | "elastic_out" | {
|
|
268
292
|
bezier: [number, number, number, number];
|
|
269
293
|
};
|
|
294
|
+
componentEase?: ("linear" | "step" | "ease_in" | "ease_out" | "ease_in_out" | "minimum_jerk" | "back_out" | "elastic_out" | {
|
|
295
|
+
bezier: [number, number, number, number];
|
|
296
|
+
})[] | undefined;
|
|
270
297
|
}[];
|
|
271
298
|
id: string;
|
|
272
299
|
target: "drawOrder";
|