fundus 0.25.0 → 0.27.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.
Files changed (186) hide show
  1. package/README.md +27 -0
  2. package/build/client/_app/immutable/chunks/B8-iUbxt.js +121 -0
  3. package/build/client/_app/immutable/chunks/B8-iUbxt.js.br +0 -0
  4. package/build/client/_app/immutable/chunks/B8-iUbxt.js.gz +0 -0
  5. package/build/client/_app/immutable/chunks/{Cml0-AYv.js → DItbpj5-.js} +2 -2
  6. package/build/client/_app/immutable/chunks/DItbpj5-.js.br +0 -0
  7. package/build/client/_app/immutable/chunks/DItbpj5-.js.gz +0 -0
  8. package/build/client/_app/immutable/chunks/{C4w39LtR.js → DTsuHg02.js} +1 -1
  9. package/build/client/_app/immutable/chunks/DTsuHg02.js.br +0 -0
  10. package/build/client/_app/immutable/chunks/DTsuHg02.js.gz +0 -0
  11. package/build/client/_app/immutable/chunks/DUBuA2Dm.js +1 -0
  12. package/build/client/_app/immutable/chunks/DUBuA2Dm.js.br +0 -0
  13. package/build/client/_app/immutable/chunks/DUBuA2Dm.js.gz +0 -0
  14. package/build/client/_app/immutable/chunks/{DGYcFVZ3.js → Pe5EQzUq.js} +1 -1
  15. package/build/client/_app/immutable/chunks/Pe5EQzUq.js.br +0 -0
  16. package/build/client/_app/immutable/chunks/Pe5EQzUq.js.gz +0 -0
  17. package/build/client/_app/immutable/entry/{app.GgnXYpzJ.js → app.CIuRQnTG.js} +2 -2
  18. package/build/client/_app/immutable/entry/app.CIuRQnTG.js.br +0 -0
  19. package/build/client/_app/immutable/entry/app.CIuRQnTG.js.gz +0 -0
  20. package/build/client/_app/immutable/entry/start.CaIV0LUo.js +1 -0
  21. package/build/client/_app/immutable/entry/start.CaIV0LUo.js.br +1 -0
  22. package/build/client/_app/immutable/entry/start.CaIV0LUo.js.gz +0 -0
  23. package/build/client/_app/immutable/nodes/0.D0OQowHt.js +1 -0
  24. package/build/client/_app/immutable/nodes/0.D0OQowHt.js.br +0 -0
  25. package/build/client/_app/immutable/nodes/0.D0OQowHt.js.gz +0 -0
  26. package/build/client/_app/immutable/nodes/{1.DzAnL2O6.js → 1.CJmLUeSL.js} +1 -1
  27. package/build/client/_app/immutable/nodes/1.CJmLUeSL.js.br +0 -0
  28. package/build/client/_app/immutable/nodes/1.CJmLUeSL.js.gz +0 -0
  29. package/build/client/_app/immutable/nodes/{2.BMN1YTJ4.js → 2.CxPn7qxz.js} +2 -2
  30. package/build/client/_app/immutable/nodes/2.CxPn7qxz.js.br +0 -0
  31. package/build/client/_app/immutable/nodes/2.CxPn7qxz.js.gz +0 -0
  32. package/build/client/_app/immutable/nodes/3.CK4enFv6.js +1 -0
  33. package/build/client/_app/immutable/nodes/3.CK4enFv6.js.br +0 -0
  34. package/build/client/_app/immutable/nodes/3.CK4enFv6.js.gz +0 -0
  35. package/build/client/_app/immutable/nodes/4.DxAj27VZ.js +2 -0
  36. package/build/client/_app/immutable/nodes/4.DxAj27VZ.js.br +0 -0
  37. package/build/client/_app/immutable/nodes/4.DxAj27VZ.js.gz +0 -0
  38. package/build/client/_app/version.json +1 -1
  39. package/build/client/_app/version.json.br +0 -0
  40. package/build/client/_app/version.json.gz +0 -0
  41. package/build/handler.js +7 -7
  42. package/build/index.js +7 -7
  43. package/build/server/chunks/chunks/{context.js-4TO_atTF.js → context.js-Bd8bqgno.js} +49 -16
  44. package/build/server/chunks/chunks/context.js-Bd8bqgno.js.map +1 -0
  45. package/build/server/chunks/chunks/{contract.js-B6KBK8D9.js → contract.js-CvgjfKhk.js} +7 -7
  46. package/build/server/chunks/chunks/{contract.js-B6KBK8D9.js.map → contract.js-CvgjfKhk.js.map} +1 -1
  47. package/build/server/chunks/chunks/{core.js-Bp-ReC60.js → core.js-z3Oyfjut.js} +10 -4
  48. package/build/server/chunks/chunks/core.js-z3Oyfjut.js.map +1 -0
  49. package/build/server/chunks/chunks/{exports.js-BAfR8rsb.js → exports.js-DAZwf-PG.js} +2 -2
  50. package/build/server/chunks/chunks/{exports.js-BAfR8rsb.js.map → exports.js-DAZwf-PG.js.map} +1 -1
  51. package/build/server/chunks/chunks/{http.js-BTq44nR5.js → http.js-BDlmMiVn.js} +2 -2
  52. package/build/server/chunks/chunks/{http.js-BTq44nR5.js.map → http.js-BDlmMiVn.js.map} +1 -1
  53. package/build/server/chunks/chunks/{internal.js-B3hOADoz.js → internal.js-Ct2VLuOf.js} +3 -3
  54. package/build/server/chunks/chunks/{internal.js-B3hOADoz.js.map → internal.js-Ct2VLuOf.js.map} +1 -1
  55. package/build/server/chunks/chunks/internal2.js-BlXt2rfE.js.map +1 -1
  56. package/build/server/chunks/chunks/{manifest-membership.js-Dj3WwZgz.js → manifest-membership.js-dUxJtQsv.js} +2 -2
  57. package/build/server/chunks/chunks/{manifest-membership.js-Dj3WwZgz.js.map → manifest-membership.js-dUxJtQsv.js.map} +1 -1
  58. package/build/server/chunks/chunks/{raw-reconciliation.js-mWOAOdgL.js → raw-reconciliation.js-CVpSamHq.js} +3 -3
  59. package/build/server/chunks/chunks/raw-reconciliation.js-CVpSamHq.js.map +1 -0
  60. package/build/server/chunks/chunks/{raw-reconciliation2.js-Bo1jJgvd.js → raw-reconciliation2.js-d0VZiZIt.js} +2 -2
  61. package/build/server/chunks/chunks/{raw-reconciliation2.js-Bo1jJgvd.js.map → raw-reconciliation2.js-d0VZiZIt.js.map} +1 -1
  62. package/build/server/chunks/chunks/{shared.js-DN477dAr.js → shared.js-7W9eRLAO.js} +2 -2
  63. package/build/server/chunks/chunks/{shared.js-DN477dAr.js.map → shared.js-7W9eRLAO.js.map} +1 -1
  64. package/build/server/chunks/chunks/{utils.js-zf3Q16Eh.js → utils.js-DOyEfB14.js} +3 -3
  65. package/build/server/chunks/chunks/{utils.js-zf3Q16Eh.js.map → utils.js-DOyEfB14.js.map} +1 -1
  66. package/build/server/chunks/entries/endpoints/api/_...path_/{_server.ts.js-B_aa_zh7.js → _server.ts.js-D1lzCgKx.js} +6 -6
  67. package/build/server/chunks/entries/endpoints/api/_...path_/{_server.ts.js-B_aa_zh7.js.map → _server.ts.js-D1lzCgKx.js.map} +1 -1
  68. package/build/server/chunks/entries/endpoints/api/assets/_id_/{_server.ts.js-Aa7zYzUB.js → _server.ts.js-B_exlIvG.js} +7 -7
  69. package/build/server/chunks/entries/endpoints/api/assets/_id_/{_server.ts.js-Aa7zYzUB.js.map → _server.ts.js-B_exlIvG.js.map} +1 -1
  70. package/build/server/chunks/entries/endpoints/api/assets/_id_/raw/{_server.ts.js-CM73kt1H.js → _server.ts.js-BVRoG0ks.js} +7 -7
  71. package/build/server/chunks/entries/endpoints/api/assets/_id_/raw/{_server.ts.js-CM73kt1H.js.map → _server.ts.js-BVRoG0ks.js.map} +1 -1
  72. package/build/server/chunks/entries/endpoints/api/assets/_id_/reimport/{_server.ts.js-CyiEsjc3.js → _server.ts.js-CGl_1qOp.js} +7 -7
  73. package/build/server/chunks/entries/endpoints/api/assets/_id_/reimport/{_server.ts.js-CyiEsjc3.js.map → _server.ts.js-CGl_1qOp.js.map} +1 -1
  74. package/build/server/chunks/entries/endpoints/api/assets/_id_/retag/{_server.ts.js-Cpt_vVY0.js → _server.ts.js-Dc5XtD59.js} +7 -7
  75. package/build/server/chunks/entries/endpoints/api/assets/_id_/retag/{_server.ts.js-Cpt_vVY0.js.map → _server.ts.js-Dc5XtD59.js.map} +1 -1
  76. package/build/server/chunks/entries/endpoints/api/assets/{_server.ts.js-BVdeCRy0.js → _server.ts.js-D18kmCae.js} +7 -7
  77. package/build/server/chunks/entries/endpoints/api/assets/{_server.ts.js-BVdeCRy0.js.map → _server.ts.js-D18kmCae.js.map} +1 -1
  78. package/build/server/chunks/entries/endpoints/api/assets/delete/{_server.ts.js-QEa0nYZE.js → _server.ts.js-Dma9az04.js} +7 -7
  79. package/build/server/chunks/entries/endpoints/api/assets/delete/{_server.ts.js-QEa0nYZE.js.map → _server.ts.js-Dma9az04.js.map} +1 -1
  80. package/build/server/chunks/entries/endpoints/api/assets/move/{_server.ts.js-Dk0sFdoM.js → _server.ts.js-Du28ZFF6.js} +7 -7
  81. package/build/server/chunks/entries/endpoints/api/assets/move/{_server.ts.js-Dk0sFdoM.js.map → _server.ts.js-Du28ZFF6.js.map} +1 -1
  82. package/build/server/chunks/entries/endpoints/api/events/{_server.ts.js-CO8RZm3e.js → _server.ts.js-CaQbcAKe.js} +7 -7
  83. package/build/server/chunks/entries/endpoints/api/events/{_server.ts.js-CO8RZm3e.js.map → _server.ts.js-CaQbcAKe.js.map} +1 -1
  84. package/build/server/chunks/entries/endpoints/api/folders/_path_/{_server.ts.js-8RzyUK6g.js → _server.ts.js-BIdR0hXk.js} +7 -7
  85. package/build/server/chunks/entries/endpoints/api/folders/_path_/{_server.ts.js-8RzyUK6g.js.map → _server.ts.js-BIdR0hXk.js.map} +1 -1
  86. package/build/server/chunks/entries/endpoints/api/folders/{_server.ts.js-YWliM590.js → _server.ts.js-DoYqBtnH.js} +7 -7
  87. package/build/server/chunks/entries/endpoints/api/folders/{_server.ts.js-YWliM590.js.map → _server.ts.js-DoYqBtnH.js.map} +1 -1
  88. package/build/server/chunks/entries/endpoints/api/imports/{_server.ts.js-CeXmOrwN.js → _server.ts.js-ydm9S1XX.js} +7 -7
  89. package/build/server/chunks/entries/endpoints/api/imports/{_server.ts.js-CeXmOrwN.js.map → _server.ts.js-ydm9S1XX.js.map} +1 -1
  90. package/build/server/chunks/entries/endpoints/api/manifests/_name_/{_server.ts.js-DQaLSdGl.js → _server.ts.js-BEAasQO4.js} +7 -7
  91. package/build/server/chunks/entries/endpoints/api/manifests/_name_/{_server.ts.js-DQaLSdGl.js.map → _server.ts.js-BEAasQO4.js.map} +1 -1
  92. package/build/server/chunks/entries/endpoints/api/manifests/{_server.ts.js-BE_nYFP_.js → _server.ts.js-CXINoU3b.js} +7 -7
  93. package/build/server/chunks/entries/endpoints/api/manifests/{_server.ts.js-BE_nYFP_.js.map → _server.ts.js-CXINoU3b.js.map} +1 -1
  94. package/build/server/chunks/entries/endpoints/api/raw/adopt/{_server.ts.js-BGxvZ4EV.js → _server.ts.js-CvuzMBDw.js} +7 -7
  95. package/build/server/chunks/entries/endpoints/api/raw/adopt/{_server.ts.js-BGxvZ4EV.js.map → _server.ts.js-CvuzMBDw.js.map} +1 -1
  96. package/build/server/chunks/entries/endpoints/api/raw/edited/accept/{_server.ts.js-Cvwr9a0l.js → _server.ts.js-kgHGsyn-.js} +7 -7
  97. package/build/server/chunks/entries/endpoints/api/raw/edited/accept/{_server.ts.js-Cvwr9a0l.js.map → _server.ts.js-kgHGsyn-.js.map} +1 -1
  98. package/build/server/chunks/entries/endpoints/api/raw/edited/dismiss/{_server.ts.js-DEMDXHsR.js → _server.ts.js-CzS37BdF.js} +7 -7
  99. package/build/server/chunks/entries/endpoints/api/raw/edited/dismiss/{_server.ts.js-DEMDXHsR.js.map → _server.ts.js-CzS37BdF.js.map} +1 -1
  100. package/build/server/chunks/entries/endpoints/api/reconcile/{_server.ts.js-DoeEIUoG.js → _server.ts.js-BXE_mCX3.js} +7 -7
  101. package/build/server/chunks/entries/endpoints/api/reconcile/{_server.ts.js-DoeEIUoG.js.map → _server.ts.js-BXE_mCX3.js.map} +1 -1
  102. package/build/server/chunks/entries/endpoints/api/reveal/{_server.ts.js-0hMtYoB1.js → _server.ts.js-DM8Nj2U6.js} +7 -7
  103. package/build/server/chunks/entries/endpoints/api/reveal/{_server.ts.js-0hMtYoB1.js.map → _server.ts.js-DM8Nj2U6.js.map} +1 -1
  104. package/build/server/chunks/entries/endpoints/api/state/{_server.ts.js-B4YWmlkC.js → _server.ts.js-C0a_L0JL.js} +7 -7
  105. package/build/server/chunks/entries/endpoints/api/state/{_server.ts.js-B4YWmlkC.js.map → _server.ts.js-C0a_L0JL.js.map} +1 -1
  106. package/build/server/chunks/entries/endpoints/api/usage/{_server.ts.js-dNLtLO2U.js → _server.ts.js-CavHY0Ie.js} +7 -7
  107. package/build/server/chunks/entries/endpoints/api/usage/{_server.ts.js-dNLtLO2U.js.map → _server.ts.js-CavHY0Ie.js.map} +1 -1
  108. package/build/server/chunks/entries/endpoints/files/_...path_/{_server.ts.js-DkYQlG9U.js → _server.ts.js-c03akA1c.js} +6 -6
  109. package/build/server/chunks/entries/endpoints/files/_...path_/{_server.ts.js-DkYQlG9U.js.map → _server.ts.js-c03akA1c.js.map} +1 -1
  110. package/build/server/chunks/entries/endpoints/files/proxy/_...path_/{_server.ts.js-BtCkf-hk.js → _server.ts.js-CKKJ-v3P.js} +7 -7
  111. package/build/server/chunks/entries/endpoints/files/proxy/_...path_/{_server.ts.js-BtCkf-hk.js.map → _server.ts.js-CKKJ-v3P.js.map} +1 -1
  112. package/build/server/chunks/entries/endpoints/files/raw/_...path_/{_server.ts.js-CozrPtRv.js → _server.ts.js-DiZ3D46Z.js} +7 -7
  113. package/build/server/chunks/entries/endpoints/files/raw/_...path_/{_server.ts.js-CozrPtRv.js.map → _server.ts.js-DiZ3D46Z.js.map} +1 -1
  114. package/build/server/chunks/entries/{hooks.server.js-Cnsi5KsE.js → hooks.server.js-DwnveOre.js} +6 -6
  115. package/build/server/chunks/entries/{hooks.server.js-Cnsi5KsE.js.map → hooks.server.js-DwnveOre.js.map} +1 -1
  116. package/build/server/chunks/{handler-BN27c-Da.js → handler-ZL0S9fLH.js} +5 -5
  117. package/build/server/chunks/{handler-BN27c-Da.js.map → handler-ZL0S9fLH.js.map} +1 -1
  118. package/build/server/chunks/{index.js-C1f-Qrf0.js → index.js-Ddbaaikg.js} +5 -5
  119. package/build/server/chunks/{index.js-C1f-Qrf0.js.map → index.js-Ddbaaikg.js.map} +1 -1
  120. package/build/server/chunks/{manifest.js-CFkuYTzu.js → manifest.js-D3J7G7mh.js} +31 -31
  121. package/build/server/chunks/{manifest.js-CFkuYTzu.js.map → manifest.js-D3J7G7mh.js.map} +1 -1
  122. package/build/server/chunks/nodes/{0.js-B1i6yC_e.js → 0.js-DjvM5GIw.js} +2 -2
  123. package/build/server/chunks/nodes/{0.js-B1i6yC_e.js.map → 0.js-DjvM5GIw.js.map} +1 -1
  124. package/build/server/chunks/nodes/1.js-D_KuDyBR.js +7 -0
  125. package/build/server/chunks/nodes/{1.js-Bv64OO0t.js.map → 1.js-D_KuDyBR.js.map} +1 -1
  126. package/build/server/chunks/nodes/{2.js-DivaYfS1.js → 2.js-CEThjyv1.js} +2 -2
  127. package/build/server/chunks/nodes/{2.js-DivaYfS1.js.map → 2.js-CEThjyv1.js.map} +1 -1
  128. package/build/server/chunks/nodes/3.js-7gq-At5G.js +7 -0
  129. package/build/server/chunks/nodes/{3.js-CUAbHOxb.js.map → 3.js-7gq-At5G.js.map} +1 -1
  130. package/build/server/chunks/nodes/4.js-DsoSGC53.js +7 -0
  131. package/build/server/chunks/nodes/{4.js-Ceg0ylPI.js.map → 4.js-DsoSGC53.js.map} +1 -1
  132. package/dist/cli.js +9 -0
  133. package/dist/core/chroma-key.d.ts +11 -5
  134. package/dist/core/chroma-key.js +11 -3
  135. package/dist/core/figma-tag-script.d.ts +141 -0
  136. package/dist/core/figma-tag-script.js +236 -0
  137. package/dist/core/index.d.ts +2 -0
  138. package/dist/core/index.js +1 -0
  139. package/dist/runtime/chroma-key-gl.d.ts +12 -4
  140. package/dist/runtime/chroma-key-gl.js +63 -6
  141. package/dist/runtime/components/ChromaKeyVideo.svelte +2 -1
  142. package/dist/server/cli-commands.d.ts +6 -0
  143. package/dist/server/cli-commands.js +1 -1
  144. package/dist/server/cli-figma-tags.d.ts +2 -0
  145. package/dist/server/cli-figma-tags.js +222 -0
  146. package/dist/server/figma-tags.d.ts +61 -0
  147. package/dist/server/figma-tags.js +153 -0
  148. package/dist/server/importers/figma.d.ts +17 -0
  149. package/dist/server/importers/figma.js +43 -18
  150. package/package.json +1 -1
  151. package/build/client/_app/immutable/chunks/C4w39LtR.js.br +0 -1
  152. package/build/client/_app/immutable/chunks/C4w39LtR.js.gz +0 -0
  153. package/build/client/_app/immutable/chunks/Cml0-AYv.js.br +0 -0
  154. package/build/client/_app/immutable/chunks/Cml0-AYv.js.gz +0 -0
  155. package/build/client/_app/immutable/chunks/DGYcFVZ3.js.br +0 -0
  156. package/build/client/_app/immutable/chunks/DGYcFVZ3.js.gz +0 -0
  157. package/build/client/_app/immutable/chunks/DGy-bMvO.js +0 -1
  158. package/build/client/_app/immutable/chunks/DGy-bMvO.js.br +0 -0
  159. package/build/client/_app/immutable/chunks/DGy-bMvO.js.gz +0 -0
  160. package/build/client/_app/immutable/chunks/WV4B0d_0.js +0 -108
  161. package/build/client/_app/immutable/chunks/WV4B0d_0.js.br +0 -0
  162. package/build/client/_app/immutable/chunks/WV4B0d_0.js.gz +0 -0
  163. package/build/client/_app/immutable/entry/app.GgnXYpzJ.js.br +0 -0
  164. package/build/client/_app/immutable/entry/app.GgnXYpzJ.js.gz +0 -0
  165. package/build/client/_app/immutable/entry/start.O9RwuDjf.js +0 -1
  166. package/build/client/_app/immutable/entry/start.O9RwuDjf.js.br +0 -0
  167. package/build/client/_app/immutable/entry/start.O9RwuDjf.js.gz +0 -0
  168. package/build/client/_app/immutable/nodes/0.D1g1plhI.js +0 -1
  169. package/build/client/_app/immutable/nodes/0.D1g1plhI.js.br +0 -0
  170. package/build/client/_app/immutable/nodes/0.D1g1plhI.js.gz +0 -0
  171. package/build/client/_app/immutable/nodes/1.DzAnL2O6.js.br +0 -2
  172. package/build/client/_app/immutable/nodes/1.DzAnL2O6.js.gz +0 -0
  173. package/build/client/_app/immutable/nodes/2.BMN1YTJ4.js.br +0 -0
  174. package/build/client/_app/immutable/nodes/2.BMN1YTJ4.js.gz +0 -0
  175. package/build/client/_app/immutable/nodes/3.DL_TiE5x.js +0 -1
  176. package/build/client/_app/immutable/nodes/3.DL_TiE5x.js.br +0 -0
  177. package/build/client/_app/immutable/nodes/3.DL_TiE5x.js.gz +0 -0
  178. package/build/client/_app/immutable/nodes/4.D7FHmbgq.js +0 -2
  179. package/build/client/_app/immutable/nodes/4.D7FHmbgq.js.br +0 -0
  180. package/build/client/_app/immutable/nodes/4.D7FHmbgq.js.gz +0 -0
  181. package/build/server/chunks/chunks/context.js-4TO_atTF.js.map +0 -1
  182. package/build/server/chunks/chunks/core.js-Bp-ReC60.js.map +0 -1
  183. package/build/server/chunks/chunks/raw-reconciliation.js-mWOAOdgL.js.map +0 -1
  184. package/build/server/chunks/nodes/1.js-Bv64OO0t.js +0 -7
  185. package/build/server/chunks/nodes/3.js-CUAbHOxb.js +0 -7
  186. package/build/server/chunks/nodes/4.js-Ceg0ylPI.js +0 -7
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Plugin-API code that reads and writes Fundus tags inside a Figma document.
3
+ * Two runtimes execute it: the Fundus Figma plugin imports these functions
4
+ * directly, and the CLI serializes them (via `Function.prototype.toString`)
5
+ * into a script an agent runs through Figma's MCP `use_figma` tool. The Figma
6
+ * REST API cannot write plugin data, so a script is the only headless write
7
+ * path.
8
+ *
9
+ * Serialization constrains every function listed in `SCRIPT_FUNCTIONS`: it
10
+ * may reference only its parameters, the other listed functions, and the
11
+ * constants in `SCRIPT_CONSTANTS` — never imports or other module scope. The
12
+ * `figma` global is passed in as a parameter so tests can drive the same code
13
+ * against a fake document.
14
+ */
15
+ /** Shared-plugin-data namespace; the REST API returns it under `sharedPluginData`. */
16
+ export declare const FIGMA_TAG_NAMESPACE = "fundus";
17
+ /** Key holding the node's Fundus asset id (the tag). */
18
+ export declare const FIGMA_TAG_KEY = "assetId";
19
+ /**
20
+ * Key holding the exact label of the annotation Fundus owns on the node; empty
21
+ * when it owns none. Tracking ownership out-of-band keeps the visible label
22
+ * exactly equal to the tag and leaves the user's own annotations untouched.
23
+ */
24
+ export declare const FIGMA_ANNOTATION_LABEL_KEY = "annotationLabel";
25
+ /**
26
+ * Visible prefix earlier plugin builds used as the ownership marker. Still
27
+ * recognized so those annotations are migrated or stripped instead of
28
+ * duplicated.
29
+ */
30
+ export declare const LEGACY_ANNOTATION_PREFIX = "Fundus asset: ";
31
+ /** The annotation fields Fundus reads and writes (a subset of Figma's `Annotation`). */
32
+ export interface FigmaAnnotation {
33
+ readonly label?: string;
34
+ readonly labelMarkdown?: string;
35
+ readonly properties?: ReadonlyArray<{
36
+ readonly type: string;
37
+ }>;
38
+ readonly categoryId?: string;
39
+ }
40
+ /** The node surface Fundus uses (a structural subset of Figma's `BaseNode`). */
41
+ export interface FigmaTagNode {
42
+ readonly id: string;
43
+ readonly name: string;
44
+ readonly type: string;
45
+ readonly parent: FigmaTagNode | null;
46
+ getSharedPluginData(namespace: string, key: string): string;
47
+ setSharedPluginData(namespace: string, key: string, value: string): void;
48
+ }
49
+ export interface FigmaAnnotatableNode extends FigmaTagNode {
50
+ annotations: ReadonlyArray<FigmaAnnotation>;
51
+ }
52
+ export interface FigmaTagPage extends FigmaTagNode {
53
+ loadAsync(): Promise<void>;
54
+ findAll(predicate: (node: FigmaTagNode) => boolean): FigmaTagNode[];
55
+ }
56
+ /** The `figma` global surface the tag scripts use. */
57
+ export interface FigmaTagApi {
58
+ readonly root: {
59
+ readonly children: ReadonlyArray<FigmaTagPage>;
60
+ };
61
+ getNodeByIdAsync(id: string): Promise<FigmaTagNode | null>;
62
+ }
63
+ export interface SetFigmaTagInput {
64
+ nodeId: string;
65
+ tag: string;
66
+ /** Replace a different tag on the node, or rewrite the same one. */
67
+ force: boolean;
68
+ }
69
+ export interface SetFigmaTagResult {
70
+ ok: true;
71
+ nodeId: string;
72
+ nodeName: string;
73
+ tag: string;
74
+ previousTag: string | null;
75
+ /** False for node types without annotation support (e.g. Group, Section). */
76
+ annotated: boolean;
77
+ }
78
+ export interface DeleteFigmaTagInput {
79
+ nodeId: string;
80
+ /** The tag the node is expected to carry; guards against a stale plan. */
81
+ tag: string;
82
+ }
83
+ export interface DeleteFigmaTagResult {
84
+ ok: true;
85
+ nodeId: string;
86
+ nodeName: string;
87
+ removedTag: string;
88
+ annotationRemoved: boolean;
89
+ }
90
+ /**
91
+ * Whether the id names a node inside an instance (`I<instance>;<source>`).
92
+ * Such nodes inherit the shared plugin data of the main component's node, so
93
+ * they report its tag without being a separate taggable node.
94
+ */
95
+ export declare function isInstanceSublayerId(id: string): boolean;
96
+ export declare function supportsAnnotations(node: FigmaTagNode): node is FigmaAnnotatableNode;
97
+ /**
98
+ * The annotation's shown text. Figma reports a plain-label annotation with
99
+ * `labelMarkdown: ""`, so an empty markdown field falls back to `label`.
100
+ */
101
+ export declare function annotationText(annotation: FigmaAnnotation): string;
102
+ /**
103
+ * Figma returns BOTH `label` and `labelMarkdown` when reading annotations, but
104
+ * the setter rejects an annotation that carries both. Re-emit a single-field
105
+ * copy (non-empty markdown first), preserving `properties`/`categoryId`, so
106
+ * preserved annotations can be written back without failing validation.
107
+ */
108
+ export declare function writableAnnotation(annotation: FigmaAnnotation): FigmaAnnotation;
109
+ /** Whether this annotation is one Fundus owns — by the stored label or the legacy prefix. */
110
+ export declare function isFundusAnnotation(annotation: FigmaAnnotation, ownedLabel: string): boolean;
111
+ /**
112
+ * Ensure the node carries exactly one Fundus annotation whose label is the tag,
113
+ * preserving all others. Writes only when not already in sync, so a steady
114
+ * state neither dirties the file nor adds undo steps.
115
+ */
116
+ export declare function writeFundusAnnotation(node: FigmaAnnotatableNode, tag: string): void;
117
+ /** Remove the Fundus annotation (incl. legacy), preserving all others. */
118
+ export declare function stripFundusAnnotation(node: FigmaAnnotatableNode): void;
119
+ export declare function pageOfNode(node: FigmaTagNode): FigmaTagPage | null;
120
+ /**
121
+ * Resolve a scene node and load its page. `loadAsync` loads without
122
+ * navigating, so running a script never moves anyone's canvas.
123
+ */
124
+ export declare function loadTaggableNode(figma: FigmaTagApi, nodeId: string): Promise<FigmaTagNode>;
125
+ /**
126
+ * The first other node carrying `tag`, across every page. Scripts start with
127
+ * only the first page loaded, and `findAll` on an unloaded page silently
128
+ * misses content, so each page is loaded first.
129
+ */
130
+ export declare function findTagHolder(figma: FigmaTagApi, tag: string, exceptNodeId: string): Promise<{
131
+ node: FigmaTagNode;
132
+ page: FigmaTagPage;
133
+ } | null>;
134
+ /**
135
+ * Tag a node, re-checking the live document right before writing: the CLI's
136
+ * REST snapshot can lag behind edits made since it ran.
137
+ */
138
+ export declare function setFundusTag(figma: FigmaTagApi, input: SetFigmaTagInput): Promise<SetFigmaTagResult>;
139
+ export declare function deleteFundusTag(figma: FigmaTagApi, input: DeleteFigmaTagInput): Promise<DeleteFigmaTagResult>;
140
+ export declare function buildSetTagScript(input: SetFigmaTagInput): string;
141
+ export declare function buildDeleteTagScript(input: DeleteFigmaTagInput): string;
@@ -0,0 +1,236 @@
1
+ /**
2
+ * Plugin-API code that reads and writes Fundus tags inside a Figma document.
3
+ * Two runtimes execute it: the Fundus Figma plugin imports these functions
4
+ * directly, and the CLI serializes them (via `Function.prototype.toString`)
5
+ * into a script an agent runs through Figma's MCP `use_figma` tool. The Figma
6
+ * REST API cannot write plugin data, so a script is the only headless write
7
+ * path.
8
+ *
9
+ * Serialization constrains every function listed in `SCRIPT_FUNCTIONS`: it
10
+ * may reference only its parameters, the other listed functions, and the
11
+ * constants in `SCRIPT_CONSTANTS` — never imports or other module scope. The
12
+ * `figma` global is passed in as a parameter so tests can drive the same code
13
+ * against a fake document.
14
+ */
15
+ /** Shared-plugin-data namespace; the REST API returns it under `sharedPluginData`. */
16
+ export const FIGMA_TAG_NAMESPACE = 'fundus';
17
+ /** Key holding the node's Fundus asset id (the tag). */
18
+ export const FIGMA_TAG_KEY = 'assetId';
19
+ /**
20
+ * Key holding the exact label of the annotation Fundus owns on the node; empty
21
+ * when it owns none. Tracking ownership out-of-band keeps the visible label
22
+ * exactly equal to the tag and leaves the user's own annotations untouched.
23
+ */
24
+ export const FIGMA_ANNOTATION_LABEL_KEY = 'annotationLabel';
25
+ /**
26
+ * Visible prefix earlier plugin builds used as the ownership marker. Still
27
+ * recognized so those annotations are migrated or stripped instead of
28
+ * duplicated.
29
+ */
30
+ export const LEGACY_ANNOTATION_PREFIX = 'Fundus asset: ';
31
+ /**
32
+ * Whether the id names a node inside an instance (`I<instance>;<source>`).
33
+ * Such nodes inherit the shared plugin data of the main component's node, so
34
+ * they report its tag without being a separate taggable node.
35
+ */
36
+ export function isInstanceSublayerId(id) {
37
+ return id.startsWith('I');
38
+ }
39
+ export function supportsAnnotations(node) {
40
+ return 'annotations' in node;
41
+ }
42
+ /**
43
+ * The annotation's shown text. Figma reports a plain-label annotation with
44
+ * `labelMarkdown: ""`, so an empty markdown field falls back to `label`.
45
+ */
46
+ export function annotationText(annotation) {
47
+ return annotation.labelMarkdown || annotation.label || '';
48
+ }
49
+ /**
50
+ * Figma returns BOTH `label` and `labelMarkdown` when reading annotations, but
51
+ * the setter rejects an annotation that carries both. Re-emit a single-field
52
+ * copy (non-empty markdown first), preserving `properties`/`categoryId`, so
53
+ * preserved annotations can be written back without failing validation.
54
+ */
55
+ export function writableAnnotation(annotation) {
56
+ const copy = {};
57
+ if (annotation.labelMarkdown)
58
+ copy.labelMarkdown = annotation.labelMarkdown;
59
+ else if (annotation.label !== undefined)
60
+ copy.label = annotation.label;
61
+ if (annotation.properties)
62
+ copy.properties = annotation.properties;
63
+ if (annotation.categoryId)
64
+ copy.categoryId = annotation.categoryId;
65
+ return copy;
66
+ }
67
+ /** Whether this annotation is one Fundus owns — by the stored label or the legacy prefix. */
68
+ export function isFundusAnnotation(annotation, ownedLabel) {
69
+ const text = annotationText(annotation);
70
+ return (ownedLabel !== '' && text === ownedLabel) || text.startsWith(LEGACY_ANNOTATION_PREFIX);
71
+ }
72
+ /**
73
+ * Ensure the node carries exactly one Fundus annotation whose label is the tag,
74
+ * preserving all others. Writes only when not already in sync, so a steady
75
+ * state neither dirties the file nor adds undo steps.
76
+ */
77
+ export function writeFundusAnnotation(node, tag) {
78
+ const owned = node.getSharedPluginData(FIGMA_TAG_NAMESPACE, FIGMA_ANNOTATION_LABEL_KEY);
79
+ const current = node.annotations;
80
+ const ours = current.filter((annotation) => isFundusAnnotation(annotation, owned));
81
+ const inSync = owned === tag && ours.length === 1 && annotationText(ours[0]) === tag;
82
+ if (inSync)
83
+ return;
84
+ const kept = current
85
+ .filter((annotation) => !isFundusAnnotation(annotation, owned) && annotationText(annotation) !== tag)
86
+ .map(writableAnnotation);
87
+ node.annotations = [...kept, { label: tag }];
88
+ if (owned !== tag) {
89
+ node.setSharedPluginData(FIGMA_TAG_NAMESPACE, FIGMA_ANNOTATION_LABEL_KEY, tag);
90
+ }
91
+ }
92
+ /** Remove the Fundus annotation (incl. legacy), preserving all others. */
93
+ export function stripFundusAnnotation(node) {
94
+ const owned = node.getSharedPluginData(FIGMA_TAG_NAMESPACE, FIGMA_ANNOTATION_LABEL_KEY);
95
+ const current = node.annotations;
96
+ const kept = current
97
+ .filter((annotation) => !isFundusAnnotation(annotation, owned))
98
+ .map(writableAnnotation);
99
+ if (kept.length !== current.length)
100
+ node.annotations = kept;
101
+ if (owned !== '')
102
+ node.setSharedPluginData(FIGMA_TAG_NAMESPACE, FIGMA_ANNOTATION_LABEL_KEY, '');
103
+ }
104
+ export function pageOfNode(node) {
105
+ let current = node;
106
+ while (current && current.type !== 'PAGE')
107
+ current = current.parent;
108
+ return current ? current : null;
109
+ }
110
+ /**
111
+ * Resolve a scene node and load its page. `loadAsync` loads without
112
+ * navigating, so running a script never moves anyone's canvas.
113
+ */
114
+ export async function loadTaggableNode(figma, nodeId) {
115
+ const node = await figma.getNodeByIdAsync(nodeId);
116
+ const page = node ? pageOfNode(node) : null;
117
+ if (!node || !page || node.type === 'PAGE') {
118
+ throw new Error(`Figma node "${nodeId}" does not exist in this file.`);
119
+ }
120
+ await page.loadAsync();
121
+ return node;
122
+ }
123
+ /**
124
+ * The first other node carrying `tag`, across every page. Scripts start with
125
+ * only the first page loaded, and `findAll` on an unloaded page silently
126
+ * misses content, so each page is loaded first.
127
+ */
128
+ export async function findTagHolder(figma, tag, exceptNodeId) {
129
+ for (const page of figma.root.children) {
130
+ await page.loadAsync();
131
+ const [holder] = page.findAll((candidate) => candidate.id !== exceptNodeId &&
132
+ !isInstanceSublayerId(candidate.id) &&
133
+ candidate.getSharedPluginData(FIGMA_TAG_NAMESPACE, FIGMA_TAG_KEY) === tag);
134
+ if (holder)
135
+ return { node: holder, page };
136
+ }
137
+ return null;
138
+ }
139
+ /**
140
+ * Tag a node, re-checking the live document right before writing: the CLI's
141
+ * REST snapshot can lag behind edits made since it ran.
142
+ */
143
+ export async function setFundusTag(figma, input) {
144
+ const node = await loadTaggableNode(figma, input.nodeId);
145
+ const previous = node.getSharedPluginData(FIGMA_TAG_NAMESPACE, FIGMA_TAG_KEY);
146
+ if (!input.force && previous === input.tag) {
147
+ throw new Error(`Node "${node.name}" (${node.id}) already carries tag "${input.tag}"; rerun prepare-set-tag with --force to rewrite it.`);
148
+ }
149
+ if (!input.force && previous !== '') {
150
+ throw new Error(`Node "${node.name}" (${node.id}) already carries tag "${previous}"; rerun prepare-set-tag with --force to replace it.`);
151
+ }
152
+ const holder = await findTagHolder(figma, input.tag, node.id);
153
+ if (holder) {
154
+ throw new Error(`Tag "${input.tag}" is already used by "${holder.node.name}" (${holder.node.id}) on page "${holder.page.name}"; choose another tag.`);
155
+ }
156
+ node.setSharedPluginData(FIGMA_TAG_NAMESPACE, FIGMA_TAG_KEY, input.tag);
157
+ const annotated = supportsAnnotations(node);
158
+ if (annotated)
159
+ writeFundusAnnotation(node, input.tag);
160
+ return {
161
+ ok: true,
162
+ nodeId: node.id,
163
+ nodeName: node.name,
164
+ tag: input.tag,
165
+ previousTag: previous === '' ? null : previous,
166
+ annotated
167
+ };
168
+ }
169
+ export async function deleteFundusTag(figma, input) {
170
+ const node = await loadTaggableNode(figma, input.nodeId);
171
+ const current = node.getSharedPluginData(FIGMA_TAG_NAMESPACE, FIGMA_TAG_KEY);
172
+ if (current !== input.tag) {
173
+ throw new Error(current === ''
174
+ ? `Node "${node.name}" (${node.id}) carries no Fundus tag.`
175
+ : `Node "${node.name}" (${node.id}) carries tag "${current}", not "${input.tag}"; rerun prepare-delete-tag.`);
176
+ }
177
+ node.setSharedPluginData(FIGMA_TAG_NAMESPACE, FIGMA_TAG_KEY, '');
178
+ let annotationRemoved = false;
179
+ if (supportsAnnotations(node)) {
180
+ const before = node.annotations.length;
181
+ stripFundusAnnotation(node);
182
+ annotationRemoved = node.annotations.length < before;
183
+ }
184
+ else {
185
+ node.setSharedPluginData(FIGMA_TAG_NAMESPACE, FIGMA_ANNOTATION_LABEL_KEY, '');
186
+ }
187
+ return {
188
+ ok: true,
189
+ nodeId: node.id,
190
+ nodeName: node.name,
191
+ removedTag: input.tag,
192
+ annotationRemoved
193
+ };
194
+ }
195
+ const SCRIPT_CONSTANTS = {
196
+ FIGMA_TAG_NAMESPACE,
197
+ FIGMA_TAG_KEY,
198
+ FIGMA_ANNOTATION_LABEL_KEY,
199
+ LEGACY_ANNOTATION_PREFIX
200
+ };
201
+ const SCRIPT_FUNCTIONS = [
202
+ isInstanceSublayerId,
203
+ supportsAnnotations,
204
+ annotationText,
205
+ writableAnnotation,
206
+ isFundusAnnotation,
207
+ writeFundusAnnotation,
208
+ stripFundusAnnotation,
209
+ pageOfNode,
210
+ loadTaggableNode,
211
+ findTagHolder,
212
+ setFundusTag,
213
+ deleteFundusTag
214
+ ];
215
+ /**
216
+ * A self-contained `use_figma` script body: top-level `await` and `return`,
217
+ * with the `figma` global in scope. It returns the operation's JSON result
218
+ * and throws, before mutating anything, when the live document disagrees.
219
+ */
220
+ function buildScript(summary, call) {
221
+ const constants = Object.entries(SCRIPT_CONSTANTS).map(([name, value]) => `const ${name} = ${JSON.stringify(value)};`);
222
+ return [
223
+ `// ${summary}`,
224
+ '// Generated by the fundus CLI — run it unchanged with the Figma MCP `use_figma` tool.',
225
+ ...constants,
226
+ ...SCRIPT_FUNCTIONS.map(String),
227
+ `return await ${call};`,
228
+ ''
229
+ ].join('\n\n');
230
+ }
231
+ export function buildSetTagScript(input) {
232
+ return buildScript(`Set Fundus tag ${JSON.stringify(input.tag)} on Figma node ${JSON.stringify(input.nodeId)}.`, `setFundusTag(figma, ${JSON.stringify(input)})`);
233
+ }
234
+ export function buildDeleteTagScript(input) {
235
+ return buildScript(`Delete Fundus tag ${JSON.stringify(input.tag)} from Figma node ${JSON.stringify(input.nodeId)}.`, `deleteFundusTag(figma, ${JSON.stringify(input)})`);
236
+ }
@@ -3,6 +3,8 @@ export { createEmptyLibrary, parseLibrary, serializeLibrary } from './library.js
3
3
  export { rawFileNameError, rawFilePathError, portableRawPathKey, isFundusTransactionName } from './raw-path.js';
4
4
  export { ROOT_FOLDER, FOLDER_SENTINEL, folderNameError, folderPathError, normalizeFolderPath, portableFolderPathKey, parentFolderPath, folderNameOf, joinFolderPath, folderOfRawPath, rawFileNameOfPath, isFolderOrDescendant, replaceFolderPrefix } from './folder-path.js';
5
5
  export { RESERVED_ASSET_IDS, isValidAssetId, assetIdError, manifestNameError } from './asset-id.js';
6
+ export type { SetFigmaTagInput, SetFigmaTagResult, DeleteFigmaTagInput, DeleteFigmaTagResult } from './figma-tag-script.js';
7
+ export { buildSetTagScript, buildDeleteTagScript, isInstanceSublayerId } from './figma-tag-script.js';
6
8
  export type { ManifestColor } from './manifest-appearance.js';
7
9
  export { MANIFEST_COLORS, MANIFEST_EMOJIS, isManifestColor, isManifestEmoji, resolveManifestColor, manifestColorError, manifestEmojiError } from './manifest-appearance.js';
8
10
  export { acceptedExtensions, acceptedExtensionsForType, typesForFileName, proposeAssetId } from './ingest.js';
@@ -2,6 +2,7 @@ export { createEmptyLibrary, parseLibrary, serializeLibrary } from './library.js
2
2
  export { rawFileNameError, rawFilePathError, portableRawPathKey, isFundusTransactionName } from './raw-path.js';
3
3
  export { ROOT_FOLDER, FOLDER_SENTINEL, folderNameError, folderPathError, normalizeFolderPath, portableFolderPathKey, parentFolderPath, folderNameOf, joinFolderPath, folderOfRawPath, rawFileNameOfPath, isFolderOrDescendant, replaceFolderPrefix } from './folder-path.js';
4
4
  export { RESERVED_ASSET_IDS, isValidAssetId, assetIdError, manifestNameError } from './asset-id.js';
5
+ export { buildSetTagScript, buildDeleteTagScript, isInstanceSublayerId } from './figma-tag-script.js';
5
6
  export { MANIFEST_COLORS, MANIFEST_EMOJIS, isManifestColor, isManifestEmoji, resolveManifestColor, manifestColorError, manifestEmojiError } from './manifest-appearance.js';
6
7
  export { acceptedExtensions, acceptedExtensionsForType, typesForFileName, proposeAssetId } from './ingest.js';
7
8
  export { canonicalizeParameters, compareCodeUnits } from './canonicalize.js';
@@ -6,13 +6,21 @@
6
6
  * The keying algorithm follows the upstream shader: the key color and each
7
7
  * pixel are converted to YCbCr chroma (Cb/Cr), the squared chroma distance is mapped
8
8
  * through a cutoff/feather window (`colorclose`), the resulting mask is blurred with a
9
- * 3×3 neighbourhood + smoothstep-sharpened, the keyed color is despilled (green pulled
10
- * toward the red/blue average, lost luminance re-added), and the output is
9
+ * 3×3 neighbourhood + smoothstep-sharpened, the keyed color is despilled (lost
10
+ * luminance re-added), and the output is
11
11
  * `vec4(color * mask, mask)` — the upstream output formula (tint fixed at white; tinting
12
12
  * is host styling, not asset data) — emitted premultiplied (rgb × alpha) for the canvas
13
13
  * compositor (see the fragColor note).
14
14
  *
15
- * Fundus adds an optional mask pass (`setMask`): a grayscale image
15
+ * Fundus replaces the upstream despill, which hard-codes greenscreen (green pulled toward
16
+ * (2b + r) / 4 — it misses any other key's spill, and tints white and gray pink even for
17
+ * green keys). Fundus's despill follows the key color: the key's dominant channels
18
+ * (`spillChannels`) are clamped down to the pixel's strongest remaining channel, so
19
+ * only excess in the key's direction is removed and neutral colors stay exact. For a
20
+ * green key that is the classic "clamp g to max(r, b)". `despillCoverage` fades the
21
+ * despill out over opaque pixels, so artwork colored like the key survives.
22
+ *
23
+ * Fundus also adds an optional mask pass (`setMask`): a grayscale image
16
24
  * sampled at the same UVs whose luminance multiplies the final alpha — white keeps a
17
25
  * pixel, black drops it, gray fades it (no alpha clip). Absent a mask, a 1×1 white
18
26
  * fallback texture makes the pass a no-op.
@@ -20,7 +28,7 @@
20
28
  * Owns its own GL context on the given canvas. `create` returns null when WebGL2 is
21
29
  * unavailable. Browser-only; the caller drives it per frame with the playing <video>.
22
30
  */
23
- import type { ChromaKeyParameters } from '../core/index.js';
31
+ import { type ChromaKeyParameters } from '../core/index.js';
24
32
  export declare class ChromaKeyGl {
25
33
  #private;
26
34
  /** Returns null when WebGL2 is unavailable or the shader fails to compile. */
@@ -1,3 +1,34 @@
1
+ /**
2
+ * WebGL2 chroma-key video renderer based on the public UnityChromakey shader
3
+ * (https://github.com/otdavies/UnityChromakey, `Shaders/ChromaKey.shader`) so a
4
+ * greenscreen video plays with a transparent background.
5
+ *
6
+ * The keying algorithm follows the upstream shader: the key color and each
7
+ * pixel are converted to YCbCr chroma (Cb/Cr), the squared chroma distance is mapped
8
+ * through a cutoff/feather window (`colorclose`), the resulting mask is blurred with a
9
+ * 3×3 neighbourhood + smoothstep-sharpened, the keyed color is despilled (lost
10
+ * luminance re-added), and the output is
11
+ * `vec4(color * mask, mask)` — the upstream output formula (tint fixed at white; tinting
12
+ * is host styling, not asset data) — emitted premultiplied (rgb × alpha) for the canvas
13
+ * compositor (see the fragColor note).
14
+ *
15
+ * Fundus replaces the upstream despill, which hard-codes greenscreen (green pulled toward
16
+ * (2b + r) / 4 — it misses any other key's spill, and tints white and gray pink even for
17
+ * green keys). Fundus's despill follows the key color: the key's dominant channels
18
+ * (`spillChannels`) are clamped down to the pixel's strongest remaining channel, so
19
+ * only excess in the key's direction is removed and neutral colors stay exact. For a
20
+ * green key that is the classic "clamp g to max(r, b)". `despillCoverage` fades the
21
+ * despill out over opaque pixels, so artwork colored like the key survives.
22
+ *
23
+ * Fundus also adds an optional mask pass (`setMask`): a grayscale image
24
+ * sampled at the same UVs whose luminance multiplies the final alpha — white keeps a
25
+ * pixel, black drops it, gray fades it (no alpha clip). Absent a mask, a 1×1 white
26
+ * fallback texture makes the pass a no-op.
27
+ *
28
+ * Owns its own GL context on the given canvas. `create` returns null when WebGL2 is
29
+ * unavailable. Browser-only; the caller drives it per frame with the playing <video>.
30
+ */
31
+ import { DEFAULT_CHROMA_KEY_PARAMETERS } from '../core/index.js';
1
32
  const VERTEX_SHADER = `#version 300 es
2
33
  in vec2 a_pos;
3
34
  out vec2 v_uv;
@@ -9,8 +40,8 @@ void main() {
9
40
  }`;
10
41
  // The upstream fragment, translated HLSL → GLSL. Kept structurally identical (same
11
42
  // function names, same sample pattern, same constants) so it can be diffed against
12
- // the original; only the texel-size plumbing differs (u_texelSize is (1/w, 1/h),
13
- // Unity's _MainTex_TexelSize packs (1/w, 1/h, w, h)).
43
+ // the original; only the texel-size plumbing (u_texelSize is (1/w, 1/h), Unity's
44
+ // _MainTex_TexelSize packs (1/w, 1/h, w, h)) and the despill block differ.
14
45
  const FRAGMENT_SHADER = `#version 300 es
15
46
  precision highp float;
16
47
 
@@ -28,6 +59,8 @@ uniform float u_maskFeathering;
28
59
  uniform float u_sharpening;
29
60
  uniform float u_despill;
30
61
  uniform float u_despillLuminanceAdd;
62
+ uniform vec3 u_spillChannels;
63
+ uniform float u_despillCoverage;
31
64
 
32
65
  float rgb2y(vec3 c) {
33
66
  return 0.299 * c.r + 0.587 * c.g + 0.114 * c.b;
@@ -85,9 +118,20 @@ void main() {
85
118
  float smoothedMask = smoothstep(u_sharpening, 1.0, mix(c, blurContribution, u_maskFeathering));
86
119
  vec4 result = color * smoothedMask;
87
120
 
88
- // Despill
89
- float v = (2.0 * result.b + result.r) / 4.0;
90
- if (result.g > v) result.g = mix(result.g, v, u_despill);
121
+ // Despill (Fundus, see the header): clamp the spill channels to the strongest other
122
+ // channel. Non-spill channels read as 1.0 in the min and 0.0 in the max (result is
123
+ // in 0–1); a key without spill channels has an all-zero mask and subtracts nothing.
124
+ vec3 spillValues = mix(vec3(1.0), result.rgb, u_spillChannels);
125
+ vec3 otherValues = result.rgb * (1.0 - u_spillChannels);
126
+ float spillExcess = max(
127
+ min(spillValues.r, min(spillValues.g, spillValues.b)) -
128
+ max(otherValues.r, max(otherValues.g, otherValues.b)),
129
+ 0.0
130
+ );
131
+ // Full despill where the mask is at most 0.8 (the keyed edge), coverage-scaled
132
+ // above it.
133
+ float coverage = mix(1.0 - smoothstep(0.8, 1.0, smoothedMask), 1.0, u_despillCoverage);
134
+ result.rgb -= u_spillChannels * spillExcess * u_despill * coverage;
91
135
  vec4 dif = color - result;
92
136
  float desaturatedDif = rgb2y(dif.xyz);
93
137
  result += mix(0.0, desaturatedDif, u_despillLuminanceAdd);
@@ -107,6 +151,15 @@ void main() {
107
151
  // whole canvas rect on iOS.
108
152
  fragColor = vec4(keyed.rgb * keyed.a, keyed.a);
109
153
  }`;
154
+ /**
155
+ * The key color's spill channels as a 0/1 mask: each channel above the key's channel
156
+ * mean (green → g; magenta → r, b; cyan → g, b). A gray key has none, so no despill.
157
+ * Compared as 2c > a + b, which is exact for equal channels.
158
+ */
159
+ function spillChannels(keyColor) {
160
+ const [r, g, b] = keyColor;
161
+ return [2 * r > g + b ? 1 : 0, 2 * g > r + b ? 1 : 0, 2 * b > r + g ? 1 : 0];
162
+ }
110
163
  function compileProgram(gl) {
111
164
  const compile = (type, source) => {
112
165
  const shader = gl.createShader(type);
@@ -202,7 +255,9 @@ export class ChromaKeyGl {
202
255
  maskFeathering: location('u_maskFeathering'),
203
256
  sharpening: location('u_sharpening'),
204
257
  despill: location('u_despill'),
205
- despillLuminanceAdd: location('u_despillLuminanceAdd')
258
+ despillLuminanceAdd: location('u_despillLuminanceAdd'),
259
+ spillChannels: location('u_spillChannels'),
260
+ despillCoverage: location('u_despillCoverage')
206
261
  };
207
262
  gl.uniform1i(gl.getUniformLocation(this.#program, 'u_texture'), 0);
208
263
  gl.uniform1i(gl.getUniformLocation(this.#program, 'u_mask'), 1);
@@ -280,6 +335,8 @@ export class ChromaKeyGl {
280
335
  gl.uniform1f(this.#uniforms.sharpening, parameters.sharpening);
281
336
  gl.uniform1f(this.#uniforms.despill, parameters.despill);
282
337
  gl.uniform1f(this.#uniforms.despillLuminanceAdd, parameters.despillLuminanceAdd);
338
+ gl.uniform3fv(this.#uniforms.spillChannels, spillChannels(parameters.keyColor));
339
+ gl.uniform1f(this.#uniforms.despillCoverage, parameters.despillCoverage ?? DEFAULT_CHROMA_KEY_PARAMETERS.despillCoverage);
283
340
  gl.clear(gl.COLOR_BUFFER_BIT);
284
341
  gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
285
342
  }
@@ -103,7 +103,8 @@
103
103
  maskFeathering: key.maskFeathering,
104
104
  sharpening: key.sharpening,
105
105
  despill: key.despill,
106
- despillLuminanceAdd: key.despillLuminanceAdd
106
+ despillLuminanceAdd: key.despillLuminanceAdd,
107
+ despillCoverage: key.despillCoverage
107
108
  };
108
109
  lifecycle?.setChromaKey(snapshot);
109
110
  });
@@ -12,6 +12,12 @@ export declare const ASSET_SET_HELP = "fundus asset set \u2014 update an asset's
12
12
  export declare const ASSET_RENAME_HELP = "fundus asset rename \u2014 rename an asset id\n\nUsage: fundus asset rename <id> <newId> [--json]\n\nRenames the asset's id (the raw file keeps its name \u2014 ids are the\nstable key) and re-hashes its proxy under the new id via the autosave\nchain. Fails if another asset references <id> \u2014 update the referrers\nfirst \u2014 or if <newId> is taken or invalid.\n\nFlags:\n --json print {\"ok\": true, \"asset\": <AssetStateDto>, \"issues\": [\u2026]}\n --help this help\n\nExample:\n fundus asset rename sidebarPanel navigationPanel --json\n";
13
13
  export declare const ASSET_DELETE_HELP = "fundus asset delete \u2014 delete an asset\n\nUsage: fundus asset delete <id> [--json]\n\nDeletes the library record, its raw file, and its proxies, then\nregenerates the affected manifest modules. Fails if another asset\nreferences it \u2014 remove the references first.\n\nFlags:\n --json print {\"ok\": true, \"deletedAssetId\": \"<id>\", \"issues\": [\u2026]}\n --help this help\n\nExample:\n fundus asset delete obsoleteBanner --json\n";
14
14
  export declare const ASSET_MOVE_HELP = "fundus asset move \u2014 move an asset between folders\n\nUsage: fundus asset move <id> <folder|root> [--json]\n\nMoves the raw file and its current proxy without changing the stable asset id,\nthen runs the autosave chain. The destination folder must already exist; use\nthe literal \"root\" for the implicit root.\n\nFlags:\n --json print {\"ok\": true, \"asset\": <AssetStateDto>, \"issues\": [\u2026]}\n --help this help\n\nExample:\n fundus asset move navigationPanel \"UI/Buttons\" --json\n";
15
+ /**
16
+ * `--figma-file` accepts either a raw Figma file key or a full design/file link.
17
+ * A value that parses as a URL is treated as a link (fileKey extracted via
18
+ * `parseFigmaFileKey`); anything else is passed through as a raw key.
19
+ */
20
+ export declare function resolveFigmaFileKeyFlag(value: string): string;
15
21
  export declare function runAssetCommand(argumentList: string[]): Promise<void>;
16
22
  export declare const FOLDER_HELP = "fundus folder \u2014 manage the mirrored raw/proxy folder tree\n\nUsage: fundus folder <subcommand> \u2026\n\nSubcommands:\n list list registered folders and their direct assets\n create create a persisted folder (fundus folder create <path>)\n rename rename one folder segment and its subtree\n move move a complete folder subtree below another folder\n delete recursively delete a folder subtree and its assets\n\nAll mutations update rawDir and proxyDir together, maintain .gitkeep files,\nand run the full autosave chain. All subcommands accept --json and --help.\n";
17
23
  export declare function runFolderCommand(argumentList: string[]): Promise<void>;
@@ -898,7 +898,7 @@ function parseFigmaScale(value, commandPath) {
898
898
  * A value that parses as a URL is treated as a link (fileKey extracted via
899
899
  * `parseFigmaFileKey`); anything else is passed through as a raw key.
900
900
  */
901
- function resolveFigmaFileKeyFlag(value) {
901
+ export function resolveFigmaFileKeyFlag(value) {
902
902
  try {
903
903
  new URL(value);
904
904
  }
@@ -0,0 +1,2 @@
1
+ export declare const FIGMA_HELP = "fundus figma \u2014 prepare Fundus tag writes in Figma\n\nUsage: fundus figma <subcommand> \u2026 [--json]\n\nThe Figma REST API cannot write plugin data, so Fundus cannot tag nodes by\nitself. These commands check the request against the file (via REST) and the\nlibrary, then print a script. Run that script unchanged with the Figma MCP\n`use_figma` tool on the same file: it re-checks the live document (the set\nscript loads every page, which takes a few seconds on large files), writes\nthe tag and its Dev Mode annotation exactly like the Fundus Figma plugin, and\nreturns the result. It throws without writing when the document changed.\n\nSubcommands:\n prepare-set-tag tag a node (--node-id, --tag)\n prepare-delete-tag remove a node's tag (--tag or --node-id)\n\nRequires import.figma.token in fundus.config.ts.\n";
2
+ export declare function runFigmaCommand(argumentList: string[]): Promise<void>;