fundus 0.26.0 → 0.28.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 (197) hide show
  1. package/README.md +18 -4
  2. package/build/client/_app/immutable/assets/{3.OulZ54CG.css → 3.Dfl33dj0.css} +1 -1
  3. package/build/client/_app/immutable/assets/3.Dfl33dj0.css.br +0 -0
  4. package/build/client/_app/immutable/assets/3.Dfl33dj0.css.gz +0 -0
  5. package/build/client/_app/immutable/chunks/{B_4qwt4o.js → BSzEzOBI.js} +1 -1
  6. package/build/client/_app/immutable/chunks/BSzEzOBI.js.br +0 -0
  7. package/build/client/_app/immutable/chunks/BSzEzOBI.js.gz +0 -0
  8. package/build/client/_app/immutable/chunks/CRFKGnag.js +121 -0
  9. package/build/client/_app/immutable/chunks/CRFKGnag.js.br +0 -0
  10. package/build/client/_app/immutable/chunks/CRFKGnag.js.gz +0 -0
  11. package/build/client/_app/immutable/chunks/apKHI4vq.js +2 -0
  12. package/build/client/_app/immutable/chunks/apKHI4vq.js.br +0 -0
  13. package/build/client/_app/immutable/chunks/apKHI4vq.js.gz +0 -0
  14. package/build/client/_app/immutable/chunks/g2rma9Ym.js +1 -0
  15. package/build/client/_app/immutable/chunks/g2rma9Ym.js.br +0 -0
  16. package/build/client/_app/immutable/chunks/g2rma9Ym.js.gz +0 -0
  17. package/build/client/_app/immutable/chunks/{DS1jTxvm.js → rtFCvv3j.js} +1 -1
  18. package/build/client/_app/immutable/chunks/rtFCvv3j.js.br +0 -0
  19. package/build/client/_app/immutable/chunks/rtFCvv3j.js.gz +0 -0
  20. package/build/client/_app/immutable/entry/{app.DgW6Eg19.js → app.BiYJKHq5.js} +2 -2
  21. package/build/client/_app/immutable/entry/app.BiYJKHq5.js.br +0 -0
  22. package/build/client/_app/immutable/entry/app.BiYJKHq5.js.gz +0 -0
  23. package/build/client/_app/immutable/entry/start.BGYYpgAJ.js +1 -0
  24. package/build/client/_app/immutable/entry/start.BGYYpgAJ.js.br +0 -0
  25. package/build/client/_app/immutable/entry/start.BGYYpgAJ.js.gz +0 -0
  26. package/build/client/_app/immutable/nodes/0.kL9OGUbr.js +1 -0
  27. package/build/client/_app/immutable/nodes/0.kL9OGUbr.js.br +0 -0
  28. package/build/client/_app/immutable/nodes/0.kL9OGUbr.js.gz +0 -0
  29. package/build/client/_app/immutable/nodes/{1.CoM7ng4c.js → 1.DQGC5ZQg.js} +1 -1
  30. package/build/client/_app/immutable/nodes/1.DQGC5ZQg.js.br +0 -0
  31. package/build/client/_app/immutable/nodes/1.DQGC5ZQg.js.gz +0 -0
  32. package/build/client/_app/immutable/nodes/{2.CV-vWxFI.js → 2.DXo6NZ8U.js} +2 -2
  33. package/build/client/_app/immutable/nodes/2.DXo6NZ8U.js.br +0 -0
  34. package/build/client/_app/immutable/nodes/2.DXo6NZ8U.js.gz +0 -0
  35. package/build/client/_app/immutable/nodes/3.DkyIyoNd.js +1 -0
  36. package/build/client/_app/immutable/nodes/3.DkyIyoNd.js.br +0 -0
  37. package/build/client/_app/immutable/nodes/3.DkyIyoNd.js.gz +0 -0
  38. package/build/client/_app/immutable/nodes/4.D1JY4Ttk.js +2 -0
  39. package/build/client/_app/immutable/nodes/4.D1JY4Ttk.js.br +0 -0
  40. package/build/client/_app/immutable/nodes/4.D1JY4Ttk.js.gz +0 -0
  41. package/build/client/_app/version.json +1 -1
  42. package/build/client/_app/version.json.br +0 -0
  43. package/build/client/_app/version.json.gz +0 -0
  44. package/build/handler.js +7 -7
  45. package/build/index.js +7 -7
  46. package/build/server/chunks/chunks/{context.js-mE8mFnsg.js → context.js-BZSZNuAd.js} +5 -5
  47. package/build/server/chunks/chunks/{context.js-mE8mFnsg.js.map → context.js-BZSZNuAd.js.map} +1 -1
  48. package/build/server/chunks/chunks/{contract.js-B6KBK8D9.js → contract.js-B4JdXFgm.js} +33 -38
  49. package/build/server/chunks/chunks/contract.js-B4JdXFgm.js.map +1 -0
  50. package/build/server/chunks/chunks/{core.js-Bp-ReC60.js → core.js-BHxa3VBo.js} +40 -5
  51. package/build/server/chunks/chunks/core.js-BHxa3VBo.js.map +1 -0
  52. package/build/server/chunks/chunks/{exports.js-CDl9URFG.js → exports.js-Cncv1qoG.js} +2 -2
  53. package/build/server/chunks/chunks/{exports.js-CDl9URFG.js.map → exports.js-Cncv1qoG.js.map} +1 -1
  54. package/build/server/chunks/chunks/{http.js-BTq44nR5.js → http.js-DYYKvppy.js} +2 -2
  55. package/build/server/chunks/chunks/{http.js-BTq44nR5.js.map → http.js-DYYKvppy.js.map} +1 -1
  56. package/build/server/chunks/chunks/{internal.js-Bz3sw0lq.js → internal.js-C2FKw_9R.js} +3 -3
  57. package/build/server/chunks/chunks/{internal.js-Bz3sw0lq.js.map → internal.js-C2FKw_9R.js.map} +1 -1
  58. package/build/server/chunks/chunks/internal2.js-BlXt2rfE.js.map +1 -1
  59. package/build/server/chunks/chunks/{manifest-membership.js-Dj3WwZgz.js → manifest-membership.js-BkrPsM_r.js} +63 -4
  60. package/build/server/chunks/chunks/manifest-membership.js-BkrPsM_r.js.map +1 -0
  61. package/build/server/chunks/chunks/{raw-reconciliation.js-mWOAOdgL.js → raw-reconciliation.js-DPnNXe0b.js} +3 -3
  62. package/build/server/chunks/chunks/raw-reconciliation.js-DPnNXe0b.js.map +1 -0
  63. package/build/server/chunks/chunks/{raw-reconciliation2.js-Bo1jJgvd.js → raw-reconciliation2.js-DKOpJ8if.js} +2 -2
  64. package/build/server/chunks/chunks/{raw-reconciliation2.js-Bo1jJgvd.js.map → raw-reconciliation2.js-DKOpJ8if.js.map} +1 -1
  65. package/build/server/chunks/chunks/{shared.js-DwJl8ZNw.js → shared.js-BGsWd7z-.js} +2 -2
  66. package/build/server/chunks/chunks/{shared.js-DwJl8ZNw.js.map → shared.js-BGsWd7z-.js.map} +1 -1
  67. package/build/server/chunks/chunks/{utils.js-DoU5Jolh.js → utils.js-C0ADATvf.js} +3 -3
  68. package/build/server/chunks/chunks/{utils.js-DoU5Jolh.js.map → utils.js-C0ADATvf.js.map} +1 -1
  69. package/build/server/chunks/entries/endpoints/api/_...path_/{_server.ts.js-B_aa_zh7.js → _server.ts.js-Dw2MW5Tm.js} +6 -6
  70. package/build/server/chunks/entries/endpoints/api/_...path_/{_server.ts.js-B_aa_zh7.js.map → _server.ts.js-Dw2MW5Tm.js.map} +1 -1
  71. package/build/server/chunks/entries/endpoints/api/assets/_id_/{_server.ts.js-CM6PedpD.js → _server.ts.js-GQDuJGug.js} +7 -7
  72. package/build/server/chunks/entries/endpoints/api/assets/_id_/{_server.ts.js-CM6PedpD.js.map → _server.ts.js-GQDuJGug.js.map} +1 -1
  73. package/build/server/chunks/entries/endpoints/api/assets/_id_/raw/{_server.ts.js-BpfOlmKN.js → _server.ts.js-JbLSv7Xx.js} +7 -7
  74. package/build/server/chunks/entries/endpoints/api/assets/_id_/raw/{_server.ts.js-BpfOlmKN.js.map → _server.ts.js-JbLSv7Xx.js.map} +1 -1
  75. package/build/server/chunks/entries/endpoints/api/assets/_id_/reimport/{_server.ts.js-UvDSeD5O.js → _server.ts.js-BPae-BYF.js} +7 -7
  76. package/build/server/chunks/entries/endpoints/api/assets/_id_/reimport/{_server.ts.js-UvDSeD5O.js.map → _server.ts.js-BPae-BYF.js.map} +1 -1
  77. package/build/server/chunks/entries/endpoints/api/assets/_id_/retag/{_server.ts.js-D4kq3W1-.js → _server.ts.js-BafrrUxu.js} +7 -7
  78. package/build/server/chunks/entries/endpoints/api/assets/_id_/retag/{_server.ts.js-D4kq3W1-.js.map → _server.ts.js-BafrrUxu.js.map} +1 -1
  79. package/build/server/chunks/entries/endpoints/api/assets/{_server.ts.js-CcV2BTCH.js → _server.ts.js-UOlSNEi9.js} +7 -7
  80. package/build/server/chunks/entries/endpoints/api/assets/{_server.ts.js-CcV2BTCH.js.map → _server.ts.js-UOlSNEi9.js.map} +1 -1
  81. package/build/server/chunks/entries/endpoints/api/assets/delete/{_server.ts.js-Cb0Dn_a2.js → _server.ts.js-QE1vUuAx.js} +7 -7
  82. package/build/server/chunks/entries/endpoints/api/assets/delete/{_server.ts.js-Cb0Dn_a2.js.map → _server.ts.js-QE1vUuAx.js.map} +1 -1
  83. package/build/server/chunks/entries/endpoints/api/assets/move/{_server.ts.js-ChlfFRqR.js → _server.ts.js-DIVfppGt.js} +7 -7
  84. package/build/server/chunks/entries/endpoints/api/assets/move/{_server.ts.js-ChlfFRqR.js.map → _server.ts.js-DIVfppGt.js.map} +1 -1
  85. package/build/server/chunks/entries/endpoints/api/events/{_server.ts.js-DnrgWGff.js → _server.ts.js-B6R_kqP8.js} +7 -7
  86. package/build/server/chunks/entries/endpoints/api/events/{_server.ts.js-DnrgWGff.js.map → _server.ts.js-B6R_kqP8.js.map} +1 -1
  87. package/build/server/chunks/entries/endpoints/api/folders/_path_/{_server.ts.js-CFM83bne.js → _server.ts.js-CertbDsH.js} +7 -7
  88. package/build/server/chunks/entries/endpoints/api/folders/_path_/{_server.ts.js-CFM83bne.js.map → _server.ts.js-CertbDsH.js.map} +1 -1
  89. package/build/server/chunks/entries/endpoints/api/folders/{_server.ts.js-BtJIovEG.js → _server.ts.js-DEIF3Hu2.js} +7 -7
  90. package/build/server/chunks/entries/endpoints/api/folders/{_server.ts.js-BtJIovEG.js.map → _server.ts.js-DEIF3Hu2.js.map} +1 -1
  91. package/build/server/chunks/entries/endpoints/api/imports/{_server.ts.js-BAHE77r2.js → _server.ts.js-DNFsNGr4.js} +7 -7
  92. package/build/server/chunks/entries/endpoints/api/imports/{_server.ts.js-BAHE77r2.js.map → _server.ts.js-DNFsNGr4.js.map} +1 -1
  93. package/build/server/chunks/entries/endpoints/api/manifests/_name_/{_server.ts.js-Cypw-CjT.js → _server.ts.js-DDW8XRPe.js} +7 -7
  94. package/build/server/chunks/entries/endpoints/api/manifests/_name_/{_server.ts.js-Cypw-CjT.js.map → _server.ts.js-DDW8XRPe.js.map} +1 -1
  95. package/build/server/chunks/entries/endpoints/api/manifests/{_server.ts.js-FrntF4Gn.js → _server.ts.js-C222-xli.js} +7 -7
  96. package/build/server/chunks/entries/endpoints/api/manifests/{_server.ts.js-FrntF4Gn.js.map → _server.ts.js-C222-xli.js.map} +1 -1
  97. package/build/server/chunks/entries/endpoints/api/raw/adopt/{_server.ts.js-BeEHDBep.js → _server.ts.js-CI1P1uyd.js} +7 -7
  98. package/build/server/chunks/entries/endpoints/api/raw/adopt/{_server.ts.js-BeEHDBep.js.map → _server.ts.js-CI1P1uyd.js.map} +1 -1
  99. package/build/server/chunks/entries/endpoints/api/raw/edited/accept/{_server.ts.js-Di1kfrl2.js → _server.ts.js-DpiUwaSh.js} +7 -7
  100. package/build/server/chunks/entries/endpoints/api/raw/edited/accept/{_server.ts.js-Di1kfrl2.js.map → _server.ts.js-DpiUwaSh.js.map} +1 -1
  101. package/build/server/chunks/entries/endpoints/api/raw/edited/dismiss/{_server.ts.js-CrLHlnap.js → _server.ts.js-DT0VPqPT.js} +7 -7
  102. package/build/server/chunks/entries/endpoints/api/raw/edited/dismiss/{_server.ts.js-CrLHlnap.js.map → _server.ts.js-DT0VPqPT.js.map} +1 -1
  103. package/build/server/chunks/entries/endpoints/api/reconcile/{_server.ts.js-BcqKQTDk.js → _server.ts.js-DBpHM42g.js} +7 -7
  104. package/build/server/chunks/entries/endpoints/api/reconcile/{_server.ts.js-BcqKQTDk.js.map → _server.ts.js-DBpHM42g.js.map} +1 -1
  105. package/build/server/chunks/entries/endpoints/api/reveal/{_server.ts.js-oFUf-T_H.js → _server.ts.js-Cxu5b6Au.js} +7 -7
  106. package/build/server/chunks/entries/endpoints/api/reveal/{_server.ts.js-oFUf-T_H.js.map → _server.ts.js-Cxu5b6Au.js.map} +1 -1
  107. package/build/server/chunks/entries/endpoints/api/state/{_server.ts.js-D46dsJCB.js → _server.ts.js-W4dhMo8Y.js} +7 -7
  108. package/build/server/chunks/entries/endpoints/api/state/{_server.ts.js-D46dsJCB.js.map → _server.ts.js-W4dhMo8Y.js.map} +1 -1
  109. package/build/server/chunks/entries/endpoints/api/usage/{_server.ts.js-BlKjsU-v.js → _server.ts.js-DJ_adxbT.js} +7 -7
  110. package/build/server/chunks/entries/endpoints/api/usage/{_server.ts.js-BlKjsU-v.js.map → _server.ts.js-DJ_adxbT.js.map} +1 -1
  111. package/build/server/chunks/entries/endpoints/files/_...path_/{_server.ts.js-DkYQlG9U.js → _server.ts.js-BisoHihD.js} +6 -6
  112. package/build/server/chunks/entries/endpoints/files/_...path_/{_server.ts.js-DkYQlG9U.js.map → _server.ts.js-BisoHihD.js.map} +1 -1
  113. package/build/server/chunks/entries/endpoints/files/proxy/_...path_/{_server.ts.js-DafDgMis.js → _server.ts.js-D4jLLuko.js} +7 -7
  114. package/build/server/chunks/entries/endpoints/files/proxy/_...path_/{_server.ts.js-DafDgMis.js.map → _server.ts.js-D4jLLuko.js.map} +1 -1
  115. package/build/server/chunks/entries/endpoints/files/raw/_...path_/{_server.ts.js-6Vn2UuF6.js → _server.ts.js-Dfv2ItjT.js} +7 -7
  116. package/build/server/chunks/entries/endpoints/files/raw/_...path_/{_server.ts.js-6Vn2UuF6.js.map → _server.ts.js-Dfv2ItjT.js.map} +1 -1
  117. package/build/server/chunks/entries/{hooks.server.js-CSypLuCa.js → hooks.server.js-oHJX5INm.js} +6 -6
  118. package/build/server/chunks/entries/{hooks.server.js-CSypLuCa.js.map → hooks.server.js-oHJX5INm.js.map} +1 -1
  119. package/build/server/chunks/{handler-C0NXVVDu.js → handler-uc4GLvWI.js} +5 -5
  120. package/build/server/chunks/{handler-C0NXVVDu.js.map → handler-uc4GLvWI.js.map} +1 -1
  121. package/build/server/chunks/{index.js-D3rzqgKi.js → index.js-BTlSiK4q.js} +5 -5
  122. package/build/server/chunks/{index.js-D3rzqgKi.js.map → index.js-BTlSiK4q.js.map} +1 -1
  123. package/build/server/chunks/{manifest.js-Dw3Fvt_0.js → manifest.js-BB8iIVbZ.js} +31 -31
  124. package/build/server/chunks/{manifest.js-Dw3Fvt_0.js.map → manifest.js-BB8iIVbZ.js.map} +1 -1
  125. package/build/server/chunks/nodes/{0.js-D1uRRjFL.js → 0.js-CccZDEdX.js} +2 -2
  126. package/build/server/chunks/nodes/{0.js-D1uRRjFL.js.map → 0.js-CccZDEdX.js.map} +1 -1
  127. package/build/server/chunks/nodes/1.js-_Ms77u64.js +7 -0
  128. package/build/server/chunks/nodes/{1.js-sYnc9kZu.js.map → 1.js-_Ms77u64.js.map} +1 -1
  129. package/build/server/chunks/nodes/{2.js-B3OCWo8p.js → 2.js-BpyFsilO.js} +2 -2
  130. package/build/server/chunks/nodes/{2.js-B3OCWo8p.js.map → 2.js-BpyFsilO.js.map} +1 -1
  131. package/build/server/chunks/nodes/3.js-D7RUEviu.js +7 -0
  132. package/build/server/chunks/nodes/{3.js-BWkr9ibc.js.map → 3.js-D7RUEviu.js.map} +1 -1
  133. package/build/server/chunks/nodes/4.js-Cfc3G2Gv.js +7 -0
  134. package/build/server/chunks/nodes/{4.js-gujLHNff.js.map → 4.js-Cfc3G2Gv.js.map} +1 -1
  135. package/dist/core/asset-id.d.ts +3 -2
  136. package/dist/core/asset-id.js +63 -2
  137. package/dist/core/chroma-key.d.ts +11 -5
  138. package/dist/core/chroma-key.js +11 -3
  139. package/dist/core/figma-tag-script.d.ts +19 -6
  140. package/dist/core/figma-tag-script.js +35 -23
  141. package/dist/core/index.d.ts +2 -2
  142. package/dist/core/index.js +1 -1
  143. package/dist/core/manifest-entry.d.ts +7 -0
  144. package/dist/core/plugins/slice.d.ts +12 -0
  145. package/dist/core/plugins/slice.js +26 -0
  146. package/dist/runtime/chroma-key-gl.d.ts +12 -4
  147. package/dist/runtime/chroma-key-gl.js +63 -6
  148. package/dist/runtime/components/ChromaKeyVideo.svelte +2 -1
  149. package/dist/runtime/geometry.d.ts +4 -2
  150. package/dist/runtime/geometry.js +14 -12
  151. package/dist/server/cli-figma-tags.js +12 -5
  152. package/dist/server/figma-tags.js +32 -6
  153. package/dist/server/plugins/contract.d.ts +5 -2
  154. package/dist/server/plugins/slice.js +14 -31
  155. package/dist/server/processing/processor.d.ts +1 -1
  156. package/dist/server/processing/processor.js +3 -3
  157. package/package.json +1 -1
  158. package/build/client/_app/immutable/assets/3.OulZ54CG.css.br +0 -0
  159. package/build/client/_app/immutable/assets/3.OulZ54CG.css.gz +0 -0
  160. package/build/client/_app/immutable/chunks/B_4qwt4o.js.br +0 -0
  161. package/build/client/_app/immutable/chunks/B_4qwt4o.js.gz +0 -0
  162. package/build/client/_app/immutable/chunks/Cml0-AYv.js +0 -2
  163. package/build/client/_app/immutable/chunks/Cml0-AYv.js.br +0 -0
  164. package/build/client/_app/immutable/chunks/Cml0-AYv.js.gz +0 -0
  165. package/build/client/_app/immutable/chunks/DGYcFVZ3.js +0 -1
  166. package/build/client/_app/immutable/chunks/DGYcFVZ3.js.br +0 -0
  167. package/build/client/_app/immutable/chunks/DGYcFVZ3.js.gz +0 -0
  168. package/build/client/_app/immutable/chunks/DS1jTxvm.js.br +0 -2
  169. package/build/client/_app/immutable/chunks/DS1jTxvm.js.gz +0 -0
  170. package/build/client/_app/immutable/chunks/p2q3f3Ml.js +0 -108
  171. package/build/client/_app/immutable/chunks/p2q3f3Ml.js.br +0 -0
  172. package/build/client/_app/immutable/chunks/p2q3f3Ml.js.gz +0 -0
  173. package/build/client/_app/immutable/entry/app.DgW6Eg19.js.br +0 -0
  174. package/build/client/_app/immutable/entry/app.DgW6Eg19.js.gz +0 -0
  175. package/build/client/_app/immutable/entry/start.D3J5OM6w.js +0 -1
  176. package/build/client/_app/immutable/entry/start.D3J5OM6w.js.br +0 -1
  177. package/build/client/_app/immutable/entry/start.D3J5OM6w.js.gz +0 -0
  178. package/build/client/_app/immutable/nodes/0.DdcKoZu5.js +0 -1
  179. package/build/client/_app/immutable/nodes/0.DdcKoZu5.js.br +0 -0
  180. package/build/client/_app/immutable/nodes/0.DdcKoZu5.js.gz +0 -0
  181. package/build/client/_app/immutable/nodes/1.CoM7ng4c.js.br +0 -0
  182. package/build/client/_app/immutable/nodes/1.CoM7ng4c.js.gz +0 -0
  183. package/build/client/_app/immutable/nodes/2.CV-vWxFI.js.br +0 -0
  184. package/build/client/_app/immutable/nodes/2.CV-vWxFI.js.gz +0 -0
  185. package/build/client/_app/immutable/nodes/3.B9mDsYGS.js +0 -1
  186. package/build/client/_app/immutable/nodes/3.B9mDsYGS.js.br +0 -0
  187. package/build/client/_app/immutable/nodes/3.B9mDsYGS.js.gz +0 -0
  188. package/build/client/_app/immutable/nodes/4.CFfIs-6D.js +0 -2
  189. package/build/client/_app/immutable/nodes/4.CFfIs-6D.js.br +0 -0
  190. package/build/client/_app/immutable/nodes/4.CFfIs-6D.js.gz +0 -0
  191. package/build/server/chunks/chunks/contract.js-B6KBK8D9.js.map +0 -1
  192. package/build/server/chunks/chunks/core.js-Bp-ReC60.js.map +0 -1
  193. package/build/server/chunks/chunks/manifest-membership.js-Dj3WwZgz.js.map +0 -1
  194. package/build/server/chunks/chunks/raw-reconciliation.js-mWOAOdgL.js.map +0 -1
  195. package/build/server/chunks/nodes/1.js-sYnc9kZu.js +0 -7
  196. package/build/server/chunks/nodes/3.js-BWkr9ibc.js +0 -7
  197. package/build/server/chunks/nodes/4.js-gujLHNff.js +0 -7
@@ -121,20 +121,32 @@ export async function loadTaggableNode(figma, nodeId) {
121
121
  return node;
122
122
  }
123
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.
124
+ * Every other node carrying `tag`, across every page. Scripts start with only
125
+ * the first page loaded, and `findAll` on an unloaded page silently misses
126
+ * content, so each page is loaded first.
127
127
  */
128
- export async function findTagHolder(figma, tag, exceptNodeId) {
128
+ export async function findTagHolders(figma, tag, exceptNodeId) {
129
+ const holders = [];
129
130
  for (const page of figma.root.children) {
130
131
  await page.loadAsync();
131
- const [holder] = page.findAll((candidate) => candidate.id !== exceptNodeId &&
132
+ const found = page.findAll((candidate) => candidate.id !== exceptNodeId &&
132
133
  !isInstanceSublayerId(candidate.id) &&
133
134
  candidate.getSharedPluginData(FIGMA_TAG_NAMESPACE, FIGMA_TAG_KEY) === tag);
134
- if (holder)
135
- return { node: holder, page };
135
+ for (const node of found)
136
+ holders.push({ node, page });
136
137
  }
137
- return null;
138
+ return holders;
139
+ }
140
+ /** Clear the node's tag and its Fundus annotation; returns whether an annotation was removed. */
141
+ export function clearFundusTag(node) {
142
+ node.setSharedPluginData(FIGMA_TAG_NAMESPACE, FIGMA_TAG_KEY, '');
143
+ if (!supportsAnnotations(node)) {
144
+ node.setSharedPluginData(FIGMA_TAG_NAMESPACE, FIGMA_ANNOTATION_LABEL_KEY, '');
145
+ return false;
146
+ }
147
+ const before = node.annotations.length;
148
+ stripFundusAnnotation(node);
149
+ return node.annotations.length < before;
138
150
  }
139
151
  /**
140
152
  * Tag a node, re-checking the live document right before writing: the CLI's
@@ -149,10 +161,13 @@ export async function setFundusTag(figma, input) {
149
161
  if (!input.force && previous !== '') {
150
162
  throw new Error(`Node "${node.name}" (${node.id}) already carries tag "${previous}"; rerun prepare-set-tag with --force to replace it.`);
151
163
  }
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.`);
164
+ const holders = await findTagHolders(figma, input.tag, node.id);
165
+ if (!input.force && holders.length > 0) {
166
+ const [holder] = holders;
167
+ throw new Error(`Tag "${input.tag}" is already used by "${holder.node.name}" (${holder.node.id}) on page "${holder.page.name}"; choose another tag, or rerun prepare-set-tag with --force to move it here.`);
155
168
  }
169
+ for (const holder of holders)
170
+ clearFundusTag(holder.node);
156
171
  node.setSharedPluginData(FIGMA_TAG_NAMESPACE, FIGMA_TAG_KEY, input.tag);
157
172
  const annotated = supportsAnnotations(node);
158
173
  if (annotated)
@@ -163,7 +178,12 @@ export async function setFundusTag(figma, input) {
163
178
  nodeName: node.name,
164
179
  tag: input.tag,
165
180
  previousTag: previous === '' ? null : previous,
166
- annotated
181
+ annotated,
182
+ movedFrom: holders.map((holder) => ({
183
+ nodeId: holder.node.id,
184
+ nodeName: holder.node.name,
185
+ page: holder.page.name
186
+ }))
167
187
  };
168
188
  }
169
189
  export async function deleteFundusTag(figma, input) {
@@ -174,16 +194,7 @@ export async function deleteFundusTag(figma, input) {
174
194
  ? `Node "${node.name}" (${node.id}) carries no Fundus tag.`
175
195
  : `Node "${node.name}" (${node.id}) carries tag "${current}", not "${input.tag}"; rerun prepare-delete-tag.`);
176
196
  }
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
- }
197
+ const annotationRemoved = clearFundusTag(node);
187
198
  return {
188
199
  ok: true,
189
200
  nodeId: node.id,
@@ -208,7 +219,8 @@ const SCRIPT_FUNCTIONS = [
208
219
  stripFundusAnnotation,
209
220
  pageOfNode,
210
221
  loadTaggableNode,
211
- findTagHolder,
222
+ findTagHolders,
223
+ clearFundusTag,
212
224
  setFundusTag,
213
225
  deleteFundusTag
214
226
  ];
@@ -3,7 +3,7 @@ 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';
6
+ export type { SetFigmaTagInput, SetFigmaTagResult, FigmaTagHolder, DeleteFigmaTagInput, DeleteFigmaTagResult } from './figma-tag-script.js';
7
7
  export { buildSetTagScript, buildDeleteTagScript, isInstanceSublayerId } from './figma-tag-script.js';
8
8
  export type { ManifestColor } from './manifest-appearance.js';
9
9
  export { MANIFEST_COLORS, MANIFEST_EMOJIS, isManifestColor, isManifestEmoji, resolveManifestColor, manifestColorError, manifestEmojiError } from './manifest-appearance.js';
@@ -31,7 +31,7 @@ export type { AssetTypePluginDescriptor } from './plugins/contract.js';
31
31
  export type { ImageFormat, ImagePreset, ImagePluginConfig, ImageParameters } from './plugins/image.js';
32
32
  export { imagePlugin, validateImagePreset, IMAGE_BUILT_IN_PRESETS, RASTER_IMAGE_EXTENSIONS } from './plugins/image.js';
33
33
  export type { SliceCompression, SliceParameters, SlicePluginConfig } from './plugins/slice.js';
34
- export { processedSliceInsets, slicePlugin, SLICE_BUILT_IN_PRESETS } from './plugins/slice.js';
34
+ export { processedSliceInsets, sliceEntryGeometry, slicePlugin, SLICE_BUILT_IN_PRESETS } from './plugins/slice.js';
35
35
  export type { CustomFfmpegVideoPreset, FfmpegArgument, FfmpegArgumentTokens, FfmpegCommandPaths, FfmpegPathToken, ResolvedCustomFfmpegVideoPreset, ResolvedVideoPreset, StructuredVideoPreset, VideoPreset, VideoPluginConfig, VideoParameters } from './plugins/video.js';
36
36
  export { isCustomFfmpegVideoPreset, materializeFfmpegCommand, videoPlugin, validateVideoPreset, validateVideoParameters, VIDEO_BUILT_IN_PRESETS, VIDEO_EXTENSIONS } from './plugins/video.js';
37
37
  export type { ChromaKeyVideoParameters } from './plugins/chroma-key-video.js';
@@ -15,7 +15,7 @@ export { isAssetReference } from './asset-reference.js';
15
15
  export { collectAllReferences, referenceErrors, referenceClosure, topologicalReferenceOrder } from './reference-graph.js';
16
16
  export { computeLibraryMembership, allDeliveredAssetIds } from './manifest-membership.js';
17
17
  export { imagePlugin, validateImagePreset, IMAGE_BUILT_IN_PRESETS, RASTER_IMAGE_EXTENSIONS } from './plugins/image.js';
18
- export { processedSliceInsets, slicePlugin, SLICE_BUILT_IN_PRESETS } from './plugins/slice.js';
18
+ export { processedSliceInsets, sliceEntryGeometry, slicePlugin, SLICE_BUILT_IN_PRESETS } from './plugins/slice.js';
19
19
  export { isCustomFfmpegVideoPreset, materializeFfmpegCommand, videoPlugin, validateVideoPreset, validateVideoParameters, VIDEO_BUILT_IN_PRESETS, VIDEO_EXTENSIONS } from './plugins/video.js';
20
20
  export { chromaKeyVideoPlugin } from './plugins/chroma-key-video.js';
21
21
  export { audioPlugin, validateAudioPreset, AUDIO_BUILT_IN_PRESETS, AUDIO_EXTENSIONS } from './plugins/audio.js';
@@ -43,6 +43,13 @@ export interface SliceManifestEntry extends AssetIdentified {
43
43
  /** Pixel dimensions of the delivered image the insets are measured in. */
44
44
  sourceWidth: number;
45
45
  sourceHeight: number;
46
+ /**
47
+ * Pixel dimensions before stretch compression; present only when compression
48
+ * shrank the delivered image. Layout sizing uses these, painting samples the
49
+ * delivered `sourceWidth` × `sourceHeight`.
50
+ */
51
+ uncompressedWidth?: number;
52
+ uncompressedHeight?: number;
46
53
  }
47
54
  export interface VideoManifestEntry extends AssetIdentified {
48
55
  kind: 'video';
@@ -5,6 +5,7 @@
5
5
  * preset). No `targetWidth`: fixed caps keep their authored source-pixel size.
6
6
  */
7
7
  import type { AssetTypePluginDescriptor } from './contract.js';
8
+ import type { SliceManifestEntry } from '../manifest-entry.js';
8
9
  import { type SliceInsets, type SliceMode } from '../slice-data.js';
9
10
  import type { ImagePluginConfig, ImagePreset } from './image.js';
10
11
  export interface SliceParameters {
@@ -39,8 +40,19 @@ export declare function processedSliceInsets(options: {
39
40
  width: number;
40
41
  height: number;
41
42
  }): SliceInsets;
43
+ interface Dimensions {
44
+ width: number;
45
+ height: number;
46
+ }
47
+ /**
48
+ * The geometry fields of a slice entry delivered at `delivered` pixels from a
49
+ * `raw` original — shared by codegen and the editor preview so both describe
50
+ * the image identically.
51
+ */
52
+ export declare function sliceEntryGeometry(parameters: SliceParameters, delivered: Dimensions, raw: Dimensions): Pick<SliceManifestEntry, 'slicing' | 'sourceWidth' | 'sourceHeight' | 'uncompressedWidth' | 'uncompressedHeight'>;
42
53
  /** Same shape as the image plugin's config: named output-format presets. */
43
54
  export type SlicePluginConfig = ImagePluginConfig;
44
55
  /** Built-in presets, always available; host config merges over them. */
45
56
  export declare const SLICE_BUILT_IN_PRESETS: Record<string, ImagePreset>;
46
57
  export declare const slicePlugin: AssetTypePluginDescriptor<SliceParameters, SlicePluginConfig>;
58
+ export {};
@@ -30,6 +30,32 @@ export function processedSliceInsets(options) {
30
30
  const bottom = clamp(desiredBottom, 0, options.height - top);
31
31
  return { top, right, bottom, left };
32
32
  }
33
+ function hasAnySide(insets) {
34
+ if (!insets)
35
+ return false;
36
+ return insets.top > 0 || insets.right > 0 || insets.bottom > 0 || insets.left > 0;
37
+ }
38
+ /**
39
+ * The geometry fields of a slice entry delivered at `delivered` pixels from a
40
+ * `raw` original — shared by codegen and the editor preview so both describe
41
+ * the image identically.
42
+ */
43
+ export function sliceEntryGeometry(parameters, delivered, raw) {
44
+ // Slices are never resized, so differing dimensions mean compression shrank the image.
45
+ const compressed = delivered.width !== raw.width || delivered.height !== raw.height;
46
+ return {
47
+ slicing: {
48
+ mode: parameters.mode,
49
+ insets: processedSliceInsets({ ...parameters, ...delivered }),
50
+ pixelRatio: parameters.pixelRatio,
51
+ // An all-zero overdraw is semantically "none" — keep the entry minimal.
52
+ ...(hasAnySide(parameters.overdraw) ? { overdraw: parameters.overdraw } : {})
53
+ },
54
+ sourceWidth: delivered.width,
55
+ sourceHeight: delivered.height,
56
+ ...(compressed ? { uncompressedWidth: raw.width, uncompressedHeight: raw.height } : {})
57
+ };
58
+ }
33
59
  /** Built-in presets, always available; host config merges over them. */
34
60
  // Deliberately separate from image presets: each namespace feeds its own job hash.
35
61
  export const SLICE_BUILT_IN_PRESETS = {
@@ -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
  });
@@ -27,7 +27,7 @@ import type { SliceInsets, SliceManifestEntry } from '../core/index.js';
27
27
  * The manifest-entry fields the pure layout math needs — a structural subset
28
28
  * of `SliceManifestEntry`, so hand-built fixtures stay minimal.
29
29
  */
30
- export type SliceLayoutEntry = Pick<SliceManifestEntry, 'sourceWidth' | 'sourceHeight' | 'slicing'>;
30
+ export type SliceLayoutEntry = Pick<SliceManifestEntry, 'sourceWidth' | 'sourceHeight' | 'uncompressedWidth' | 'uncompressedHeight' | 'slicing'>;
31
31
  /**
32
32
  * The insets that actually apply for the entry's mode: a 3-slice mode zeroes out
33
33
  * the axis it does not use (and `one` uses none at all), so callers never have
@@ -53,7 +53,9 @@ export interface SliceLayoutGeometry {
53
53
  * image minus its baked-in overdraw, divided by the export `pixelRatio`. The
54
54
  * core is what fills the layout box when `Slice` renders the entry; the
55
55
  * overdraw band paints outside the box (like a CSS `box-shadow`), so these are
56
- * the sizes to lay the element out at.
56
+ * the sizes to lay the element out at. Stretch compression is a delivery
57
+ * optimization, so the authored `uncompressedWidth` / `uncompressedHeight`
58
+ * take precedence over the delivered source size.
57
59
  *
58
60
  * The math is identical for all modes; the mode decides how a consumer should
59
61
  * use the result:
@@ -35,15 +35,15 @@ export function effectiveOverdraw(entry) {
35
35
  return { top, right, bottom, left };
36
36
  }
37
37
  /**
38
- * The layout box — the image minus its effective overdraw — in **source pixels**
39
- * Never smaller than one source pixel per axis (`effectiveOverdraw` guarantees
40
- * a remaining box).
38
+ * The layout box — an image of `width × height` pixels minus the entry's
39
+ * effective overdraw — in those pixels. Never smaller than one pixel per axis
40
+ * (`effectiveOverdraw` guarantees a remaining box).
41
41
  */
42
- function sourceLayoutSize(entry) {
42
+ function coreSize(entry, width, height) {
43
43
  const overdraw = effectiveOverdraw(entry);
44
44
  return {
45
- width: Math.max(1, entry.sourceWidth - overdraw.left - overdraw.right),
46
- height: Math.max(1, entry.sourceHeight - overdraw.top - overdraw.bottom)
45
+ width: Math.max(1, width - overdraw.left - overdraw.right),
46
+ height: Math.max(1, height - overdraw.top - overdraw.bottom)
47
47
  };
48
48
  }
49
49
  /**
@@ -51,7 +51,9 @@ function sourceLayoutSize(entry) {
51
51
  * image minus its baked-in overdraw, divided by the export `pixelRatio`. The
52
52
  * core is what fills the layout box when `Slice` renders the entry; the
53
53
  * overdraw band paints outside the box (like a CSS `box-shadow`), so these are
54
- * the sizes to lay the element out at.
54
+ * the sizes to lay the element out at. Stretch compression is a delivery
55
+ * optimization, so the authored `uncompressedWidth` / `uncompressedHeight`
56
+ * take precedence over the delivered source size.
55
57
  *
56
58
  * The math is identical for all modes; the mode decides how a consumer should
57
59
  * use the result:
@@ -72,12 +74,12 @@ function sourceLayoutSize(entry) {
72
74
  * no float artifacts leak into style output.
73
75
  */
74
76
  export function sliceLayoutGeometry(entry) {
75
- const sourceLayout = sourceLayoutSize(entry);
77
+ const core = coreSize(entry, entry.uncompressedWidth ?? entry.sourceWidth, entry.uncompressedHeight ?? entry.sourceHeight);
76
78
  const pixelRatio = entry.slicing.pixelRatio > 0 ? entry.slicing.pixelRatio : 1;
77
79
  return {
78
- width: sourceLayout.width / pixelRatio,
79
- height: sourceLayout.height / pixelRatio,
80
- aspectRatio: `${sourceLayout.width} / ${sourceLayout.height}`
80
+ width: core.width / pixelRatio,
81
+ height: core.height / pixelRatio,
82
+ aspectRatio: `${core.width} / ${core.height}`
81
83
  };
82
84
  }
83
85
  /**
@@ -94,7 +96,7 @@ export function computeOverdrawDestination(entry, boxWidth, boxHeight) {
94
96
  return overdraw;
95
97
  }
96
98
  const pixelRatio = entry.slicing.pixelRatio > 0 ? entry.slicing.pixelRatio : 1;
97
- const sourceLayout = sourceLayoutSize(entry);
99
+ const sourceLayout = coreSize(entry, entry.sourceWidth, entry.sourceHeight);
98
100
  let horizontalScale;
99
101
  let verticalScale;
100
102
  if (entry.slicing.mode === 'three-horizontal') {
@@ -32,16 +32,23 @@ const PREPARE_SET_TAG_HELP = `fundus figma prepare-set-tag — prepare a use_fig
32
32
  Usage: fundus figma prepare-set-tag --node-id <id> --tag <tag>
33
33
  [--file <key|link>] [--force] [--json]
34
34
 
35
- Refuses, listing the file's existing tags, when another node already carries
36
- the tag or a Fundus asset with that id exists from another source. Refuses
37
- when the node already carries a tag (the same one or another) unless --force.
35
+ Refuses unless --force, listing the file's existing tags, when another node
36
+ already carries the tag; --force moves the tag (and its annotation) from those
37
+ nodes to this one, so it stays unique. Also refuses unless --force when the
38
+ node already carries a tag (the same one or another) or a Fundus asset with
39
+ that id exists from another source. An asset imported from this very node by
40
+ node id is not a conflict: tagging prepares its move to the tag
41
+ (\`fundus asset replace <tag> --from figma --figma-tag <tag>\` afterwards keeps
42
+ its id, parameters, and manifests).
38
43
 
39
44
  Flags:
40
45
  --node-id <id> the node to tag: 12:34 or 12-34
41
46
  --tag <tag> the Fundus asset id to store on the node (camelCase)
42
47
  --file <key|link> Figma file key or link; default:
43
48
  import.figma.defaultFileKey from fundus.config.ts
44
- --force replace the node's current tag, or rewrite the same tag
49
+ --force always set the tag: replace the node's current tag,
50
+ rewrite the same tag, move it from other nodes, or tag
51
+ despite a Fundus asset from another source
45
52
  --json print {"ok": true, "fileKey", "nodeId", "tag",
46
53
  "previousTag", "warnings", "next", "code"}; a refusal
47
54
  prints {"ok": false, "error": {"message"}, "existingTags"}
@@ -50,7 +57,7 @@ Flags:
50
57
 
51
58
  Without --json the script goes to stdout, everything else to stderr.
52
59
  The script returns {"ok": true, "nodeId", "nodeName", "tag", "previousTag",
53
- "annotated"}. Afterwards: \`fundus asset ingest --from figma --figma-tag <tag>\`.
60
+ "annotated", "movedFrom": [{"nodeId", "nodeName", "page"}]}. Afterwards: \`fundus asset ingest --from figma --figma-tag <tag>\`.
54
61
 
55
62
  Example:
56
63
  fundus figma prepare-set-tag --file 'https://www.figma.com/design/abc/UI' \\
@@ -37,6 +37,13 @@ function tagListing(nodes) {
37
37
  function describeNode(node) {
38
38
  return `"${node.nodeName}" (${node.nodeId}) on page "${node.page}"`;
39
39
  }
40
+ /** Whether `record` is a node-mode Figma import of `nodeId` in `fileKey`. */
41
+ function isNodeImportOf(record, fileKey, nodeId) {
42
+ if (record.importSource?.importer !== 'figma')
43
+ return false;
44
+ const source = validateFigmaSourceParameters(record.importSource.parameters);
45
+ return source.mode !== 'tag' && source.fileKey === fileKey && source.nodeId === nodeId;
46
+ }
40
47
  /** The library asset imported from `tag` in `fileKey`, if any. */
41
48
  function tagImportedAsset(library, fileKey, tag) {
42
49
  const record = library.assets[tag];
@@ -55,12 +62,31 @@ export function planSetFigmaTag(request, { library, document }) {
55
62
  const nodes = figmaDocumentNodes(document);
56
63
  const existingTags = tagListing(nodes);
57
64
  const warnings = [];
58
- const holder = nodes.find((node) => node.tag === tag && node.nodeId !== nodeId);
59
- if (holder) {
60
- throw new FigmaTagConflictError(`Tag "${tag}" is already used by ${describeNode(holder)} — choose another tag (existing tags are listed).`, existingTags);
65
+ const holders = nodes.filter((node) => node.tag === tag && node.nodeId !== nodeId);
66
+ if (holders.length > 0) {
67
+ const described = holders.map(describeNode).join(', ');
68
+ if (!force) {
69
+ throw new FigmaTagConflictError(`Tag "${tag}" is already used by ${described} — choose another tag, or pass --force to move it to node ${nodeId} (existing tags are listed).`, existingTags);
70
+ }
71
+ warnings.push(`Moves tag "${tag}" from ${described} to node ${nodeId}.`);
61
72
  }
62
- if (Object.hasOwn(library.assets, tag) && tagImportedAsset(library, fileKey, tag) === null) {
63
- throw new FigmaTagConflictError(`A Fundus asset "${tag}" already exists and is not imported from this tag in file "${fileKey}" — \`fundus asset ingest --figma-tag ${tag}\` would collide; choose another tag.`, existingTags);
73
+ const existing = Object.hasOwn(library.assets, tag) ? library.assets[tag] : null;
74
+ if (existing && tagImportedAsset(library, fileKey, tag) === null) {
75
+ // A replace without --scale falls back to the config default and resets a
76
+ // slice's pixelRatio to it, so keep the scale a Figma asset was imported at.
77
+ const savedScale = existing.importSource?.importer === 'figma'
78
+ ? ` --scale ${validateFigmaSourceParameters(existing.importSource.parameters).scale}`
79
+ : '';
80
+ const replace = `\`fundus asset replace ${tag} --from figma --figma-tag ${tag} --figma-file ${fileKey}${savedScale}\``;
81
+ if (isNodeImportOf(existing, fileKey, nodeId)) {
82
+ warnings.push(`Fundus asset "${tag}" is imported from this node by id — afterwards run ${replace} to move it to the tag (id, parameters, folder, and manifests stay).`);
83
+ }
84
+ else if (!force) {
85
+ throw new FigmaTagConflictError(`A Fundus asset "${tag}" already exists and is imported neither from this tag nor from this node in file "${fileKey}" — choose another tag, or pass --force to tag the node anyway and then run ${replace}, which replaces the asset's current source.`, existingTags);
86
+ }
87
+ else {
88
+ warnings.push(`Fundus asset "${tag}" already exists from another source — afterwards run ${replace} to switch it to this tag; that replaces its current source.`);
89
+ }
64
90
  }
65
91
  const node = nodes.find((candidate) => candidate.nodeId === nodeId) ?? null;
66
92
  if (!node) {
@@ -83,7 +109,7 @@ export function planSetFigmaTag(request, { library, document }) {
83
109
  }
84
110
  warnings.push(`Replaces tag "${previousTag}" on node ${nodeId}.${followUp}`);
85
111
  }
86
- if (tagImportedAsset(library, fileKey, tag) !== null && !nodes.some((n) => n.tag === tag)) {
112
+ if (tagImportedAsset(library, fileKey, tag) !== null && previousTag !== tag) {
87
113
  warnings.push(`Fundus asset "${tag}" is imported from this tag; its next \`fundus asset reimport ${tag}\` resolves to this node.`);
88
114
  }
89
115
  return {
@@ -15,6 +15,9 @@ export interface ProxyOutputInfo {
15
15
  height?: number;
16
16
  /** Media duration; present for video and audio proxies. */
17
17
  durationMs?: number;
18
+ /** Raw source pixel dimensions; present for slice proxies (pre-compression layout size). */
19
+ rawWidth?: number;
20
+ rawHeight?: number;
18
21
  }
19
22
  /** One delivered asset's current proxy: file identity plus output metadata. */
20
23
  export interface AssetProxyInfo extends ProxyOutputInfo {
@@ -101,7 +104,7 @@ export interface ServerAssetTypePlugin<TParameters = unknown, TPreset = unknown>
101
104
  /** Run the processing job (sharp/ffmpeg): write the proxy, report its metadata. */
102
105
  process(job: ServerProcessingJob<TParameters, TPreset>): Promise<ProxyOutputInfo>;
103
106
  /** Probe an existing proxy file's output metadata (the up-to-date path). */
104
- readProxyInfo(proxyPath: string, config: ResolvedFundusConfig): Promise<ProxyOutputInfo>;
107
+ readProxyInfo(proxyPath: string, config: ResolvedFundusConfig, rawPath: string): Promise<ProxyOutputInfo>;
105
108
  /**
106
109
  * Build the generated manifest entry. Reference fields stay
107
110
  * `{ $asset }` markers — codegen's generic inline walk resolves them.
@@ -113,4 +116,4 @@ export type ServerPluginRegistry = Record<string, ServerAssetTypePlugin>;
113
116
  /** Resolve a named preset out of a plugin's merged preset map, or throw listing the known names. */
114
117
  export declare function resolveNamedPreset<TPreset>(label: string, presetName: string, presets: Record<string, TPreset>): TPreset;
115
118
  /** A proxy-info field a given entry kind cannot do without. */
116
- export declare function requireProxyField(proxy: AssetProxyInfo, field: 'width' | 'height' | 'durationMs'): number;
119
+ export declare function requireProxyField(proxy: AssetProxyInfo, field: keyof ProxyOutputInfo): number;
@@ -4,7 +4,7 @@
4
4
  * redundant pixels from the stretchable regions. The slicing data itself is
5
5
  * entry-only.
6
6
  */
7
- import { SLICE_BUILT_IN_PRESETS, defaultInsetsForSize, processedSliceInsets, slicePlugin, validateImagePreset } from '../../core/index.js';
7
+ import { SLICE_BUILT_IN_PRESETS, defaultInsetsForSize, sliceEntryGeometry, slicePlugin, validateImagePreset } from '../../core/index.js';
8
8
  import { readImageDimensions } from '../processing/image-processing.js';
9
9
  import { processSliceImageFile } from '../processing/slice-processing.js';
10
10
  import { extensionForImageFormat } from './image.js';
@@ -21,10 +21,10 @@ export const sliceConfigDefinition = {
21
21
  })
22
22
  })
23
23
  };
24
- function hasAnySide(insets) {
25
- if (!insets)
26
- return false;
27
- return insets.top > 0 || insets.right > 0 || insets.bottom > 0 || insets.left > 0;
24
+ /** Proxy info plus the raw dimensions the entry's pre-compression layout size needs. */
25
+ async function withRawDimensions(output, rawPath) {
26
+ const [info, raw] = await Promise.all([output, readImageDimensions(rawPath)]);
27
+ return { ...info, rawWidth: raw.width, rawHeight: raw.height };
28
28
  }
29
29
  /**
30
30
  * Best-effort guess of the asset's export scale from a `@2x` / `@3x` suffix in
@@ -53,7 +53,7 @@ export const sliceServerPlugin = {
53
53
  pixelRatio: detectPixelRatio(rawFileName)
54
54
  };
55
55
  },
56
- process: (job) => processSliceImageFile({
56
+ process: (job) => withRawDimensions(processSliceImageFile({
57
57
  sourcePath: job.sourcePath,
58
58
  targetPath: job.targetPath,
59
59
  preset: job.preset,
@@ -61,29 +61,12 @@ export const sliceServerPlugin = {
61
61
  insets: job.parameters.insets,
62
62
  overdraw: job.parameters.overdraw,
63
63
  compression: job.parameters.compression
64
- }),
65
- readProxyInfo: (proxyPath) => readImageDimensions(proxyPath),
66
- buildManifestEntry: ({ parameters, src, proxy }) => {
67
- const sourceWidth = requireProxyField(proxy, 'width');
68
- const sourceHeight = requireProxyField(proxy, 'height');
69
- const slicing = {
70
- mode: parameters.mode,
71
- insets: processedSliceInsets({
72
- ...parameters,
73
- width: sourceWidth,
74
- height: sourceHeight
75
- }),
76
- pixelRatio: parameters.pixelRatio,
77
- // An all-zero overdraw is semantically "none" — keep the entry minimal.
78
- ...(hasAnySide(parameters.overdraw) ? { overdraw: parameters.overdraw } : {})
79
- };
80
- return {
81
- kind: 'slice',
82
- src,
83
- byteSize: proxy.fileSize,
84
- slicing,
85
- sourceWidth,
86
- sourceHeight
87
- };
88
- }
64
+ }), job.sourcePath),
65
+ readProxyInfo: (proxyPath, _config, rawPath) => withRawDimensions(readImageDimensions(proxyPath), rawPath),
66
+ buildManifestEntry: ({ parameters, src, proxy }) => ({
67
+ kind: 'slice',
68
+ src,
69
+ byteSize: proxy.fileSize,
70
+ ...sliceEntryGeometry(parameters, { width: requireProxyField(proxy, 'width'), height: requireProxyField(proxy, 'height') }, { width: requireProxyField(proxy, 'rawWidth'), height: requireProxyField(proxy, 'rawHeight') })
71
+ })
89
72
  };