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.
- package/README.md +27 -0
- package/build/client/_app/immutable/chunks/B8-iUbxt.js +121 -0
- package/build/client/_app/immutable/chunks/B8-iUbxt.js.br +0 -0
- package/build/client/_app/immutable/chunks/B8-iUbxt.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{Cml0-AYv.js → DItbpj5-.js} +2 -2
- package/build/client/_app/immutable/chunks/DItbpj5-.js.br +0 -0
- package/build/client/_app/immutable/chunks/DItbpj5-.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{C4w39LtR.js → DTsuHg02.js} +1 -1
- package/build/client/_app/immutable/chunks/DTsuHg02.js.br +0 -0
- package/build/client/_app/immutable/chunks/DTsuHg02.js.gz +0 -0
- package/build/client/_app/immutable/chunks/DUBuA2Dm.js +1 -0
- package/build/client/_app/immutable/chunks/DUBuA2Dm.js.br +0 -0
- package/build/client/_app/immutable/chunks/DUBuA2Dm.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{DGYcFVZ3.js → Pe5EQzUq.js} +1 -1
- package/build/client/_app/immutable/chunks/Pe5EQzUq.js.br +0 -0
- package/build/client/_app/immutable/chunks/Pe5EQzUq.js.gz +0 -0
- package/build/client/_app/immutable/entry/{app.GgnXYpzJ.js → app.CIuRQnTG.js} +2 -2
- package/build/client/_app/immutable/entry/app.CIuRQnTG.js.br +0 -0
- package/build/client/_app/immutable/entry/app.CIuRQnTG.js.gz +0 -0
- package/build/client/_app/immutable/entry/start.CaIV0LUo.js +1 -0
- package/build/client/_app/immutable/entry/start.CaIV0LUo.js.br +1 -0
- package/build/client/_app/immutable/entry/start.CaIV0LUo.js.gz +0 -0
- package/build/client/_app/immutable/nodes/0.D0OQowHt.js +1 -0
- package/build/client/_app/immutable/nodes/0.D0OQowHt.js.br +0 -0
- package/build/client/_app/immutable/nodes/0.D0OQowHt.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{1.DzAnL2O6.js → 1.CJmLUeSL.js} +1 -1
- package/build/client/_app/immutable/nodes/1.CJmLUeSL.js.br +0 -0
- package/build/client/_app/immutable/nodes/1.CJmLUeSL.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{2.BMN1YTJ4.js → 2.CxPn7qxz.js} +2 -2
- package/build/client/_app/immutable/nodes/2.CxPn7qxz.js.br +0 -0
- package/build/client/_app/immutable/nodes/2.CxPn7qxz.js.gz +0 -0
- package/build/client/_app/immutable/nodes/3.CK4enFv6.js +1 -0
- package/build/client/_app/immutable/nodes/3.CK4enFv6.js.br +0 -0
- package/build/client/_app/immutable/nodes/3.CK4enFv6.js.gz +0 -0
- package/build/client/_app/immutable/nodes/4.DxAj27VZ.js +2 -0
- package/build/client/_app/immutable/nodes/4.DxAj27VZ.js.br +0 -0
- package/build/client/_app/immutable/nodes/4.DxAj27VZ.js.gz +0 -0
- package/build/client/_app/version.json +1 -1
- package/build/client/_app/version.json.br +0 -0
- package/build/client/_app/version.json.gz +0 -0
- package/build/handler.js +7 -7
- package/build/index.js +7 -7
- package/build/server/chunks/chunks/{context.js-4TO_atTF.js → context.js-Bd8bqgno.js} +49 -16
- package/build/server/chunks/chunks/context.js-Bd8bqgno.js.map +1 -0
- package/build/server/chunks/chunks/{contract.js-B6KBK8D9.js → contract.js-CvgjfKhk.js} +7 -7
- package/build/server/chunks/chunks/{contract.js-B6KBK8D9.js.map → contract.js-CvgjfKhk.js.map} +1 -1
- package/build/server/chunks/chunks/{core.js-Bp-ReC60.js → core.js-z3Oyfjut.js} +10 -4
- package/build/server/chunks/chunks/core.js-z3Oyfjut.js.map +1 -0
- package/build/server/chunks/chunks/{exports.js-BAfR8rsb.js → exports.js-DAZwf-PG.js} +2 -2
- package/build/server/chunks/chunks/{exports.js-BAfR8rsb.js.map → exports.js-DAZwf-PG.js.map} +1 -1
- package/build/server/chunks/chunks/{http.js-BTq44nR5.js → http.js-BDlmMiVn.js} +2 -2
- package/build/server/chunks/chunks/{http.js-BTq44nR5.js.map → http.js-BDlmMiVn.js.map} +1 -1
- package/build/server/chunks/chunks/{internal.js-B3hOADoz.js → internal.js-Ct2VLuOf.js} +3 -3
- package/build/server/chunks/chunks/{internal.js-B3hOADoz.js.map → internal.js-Ct2VLuOf.js.map} +1 -1
- package/build/server/chunks/chunks/internal2.js-BlXt2rfE.js.map +1 -1
- package/build/server/chunks/chunks/{manifest-membership.js-Dj3WwZgz.js → manifest-membership.js-dUxJtQsv.js} +2 -2
- package/build/server/chunks/chunks/{manifest-membership.js-Dj3WwZgz.js.map → manifest-membership.js-dUxJtQsv.js.map} +1 -1
- package/build/server/chunks/chunks/{raw-reconciliation.js-mWOAOdgL.js → raw-reconciliation.js-CVpSamHq.js} +3 -3
- package/build/server/chunks/chunks/raw-reconciliation.js-CVpSamHq.js.map +1 -0
- package/build/server/chunks/chunks/{raw-reconciliation2.js-Bo1jJgvd.js → raw-reconciliation2.js-d0VZiZIt.js} +2 -2
- package/build/server/chunks/chunks/{raw-reconciliation2.js-Bo1jJgvd.js.map → raw-reconciliation2.js-d0VZiZIt.js.map} +1 -1
- package/build/server/chunks/chunks/{shared.js-DN477dAr.js → shared.js-7W9eRLAO.js} +2 -2
- package/build/server/chunks/chunks/{shared.js-DN477dAr.js.map → shared.js-7W9eRLAO.js.map} +1 -1
- package/build/server/chunks/chunks/{utils.js-zf3Q16Eh.js → utils.js-DOyEfB14.js} +3 -3
- package/build/server/chunks/chunks/{utils.js-zf3Q16Eh.js.map → utils.js-DOyEfB14.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/_...path_/{_server.ts.js-B_aa_zh7.js → _server.ts.js-D1lzCgKx.js} +6 -6
- package/build/server/chunks/entries/endpoints/api/_...path_/{_server.ts.js-B_aa_zh7.js.map → _server.ts.js-D1lzCgKx.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/assets/_id_/{_server.ts.js-Aa7zYzUB.js → _server.ts.js-B_exlIvG.js} +7 -7
- package/build/server/chunks/entries/endpoints/api/assets/_id_/{_server.ts.js-Aa7zYzUB.js.map → _server.ts.js-B_exlIvG.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/assets/_id_/raw/{_server.ts.js-CM73kt1H.js → _server.ts.js-BVRoG0ks.js} +7 -7
- package/build/server/chunks/entries/endpoints/api/assets/_id_/raw/{_server.ts.js-CM73kt1H.js.map → _server.ts.js-BVRoG0ks.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/assets/_id_/reimport/{_server.ts.js-CyiEsjc3.js → _server.ts.js-CGl_1qOp.js} +7 -7
- 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
- package/build/server/chunks/entries/endpoints/api/assets/_id_/retag/{_server.ts.js-Cpt_vVY0.js → _server.ts.js-Dc5XtD59.js} +7 -7
- 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
- package/build/server/chunks/entries/endpoints/api/assets/{_server.ts.js-BVdeCRy0.js → _server.ts.js-D18kmCae.js} +7 -7
- package/build/server/chunks/entries/endpoints/api/assets/{_server.ts.js-BVdeCRy0.js.map → _server.ts.js-D18kmCae.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/assets/delete/{_server.ts.js-QEa0nYZE.js → _server.ts.js-Dma9az04.js} +7 -7
- package/build/server/chunks/entries/endpoints/api/assets/delete/{_server.ts.js-QEa0nYZE.js.map → _server.ts.js-Dma9az04.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/assets/move/{_server.ts.js-Dk0sFdoM.js → _server.ts.js-Du28ZFF6.js} +7 -7
- package/build/server/chunks/entries/endpoints/api/assets/move/{_server.ts.js-Dk0sFdoM.js.map → _server.ts.js-Du28ZFF6.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/events/{_server.ts.js-CO8RZm3e.js → _server.ts.js-CaQbcAKe.js} +7 -7
- package/build/server/chunks/entries/endpoints/api/events/{_server.ts.js-CO8RZm3e.js.map → _server.ts.js-CaQbcAKe.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/folders/_path_/{_server.ts.js-8RzyUK6g.js → _server.ts.js-BIdR0hXk.js} +7 -7
- package/build/server/chunks/entries/endpoints/api/folders/_path_/{_server.ts.js-8RzyUK6g.js.map → _server.ts.js-BIdR0hXk.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/folders/{_server.ts.js-YWliM590.js → _server.ts.js-DoYqBtnH.js} +7 -7
- package/build/server/chunks/entries/endpoints/api/folders/{_server.ts.js-YWliM590.js.map → _server.ts.js-DoYqBtnH.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/imports/{_server.ts.js-CeXmOrwN.js → _server.ts.js-ydm9S1XX.js} +7 -7
- package/build/server/chunks/entries/endpoints/api/imports/{_server.ts.js-CeXmOrwN.js.map → _server.ts.js-ydm9S1XX.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/manifests/_name_/{_server.ts.js-DQaLSdGl.js → _server.ts.js-BEAasQO4.js} +7 -7
- package/build/server/chunks/entries/endpoints/api/manifests/_name_/{_server.ts.js-DQaLSdGl.js.map → _server.ts.js-BEAasQO4.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/manifests/{_server.ts.js-BE_nYFP_.js → _server.ts.js-CXINoU3b.js} +7 -7
- package/build/server/chunks/entries/endpoints/api/manifests/{_server.ts.js-BE_nYFP_.js.map → _server.ts.js-CXINoU3b.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/raw/adopt/{_server.ts.js-BGxvZ4EV.js → _server.ts.js-CvuzMBDw.js} +7 -7
- package/build/server/chunks/entries/endpoints/api/raw/adopt/{_server.ts.js-BGxvZ4EV.js.map → _server.ts.js-CvuzMBDw.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/raw/edited/accept/{_server.ts.js-Cvwr9a0l.js → _server.ts.js-kgHGsyn-.js} +7 -7
- package/build/server/chunks/entries/endpoints/api/raw/edited/accept/{_server.ts.js-Cvwr9a0l.js.map → _server.ts.js-kgHGsyn-.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/raw/edited/dismiss/{_server.ts.js-DEMDXHsR.js → _server.ts.js-CzS37BdF.js} +7 -7
- package/build/server/chunks/entries/endpoints/api/raw/edited/dismiss/{_server.ts.js-DEMDXHsR.js.map → _server.ts.js-CzS37BdF.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/reconcile/{_server.ts.js-DoeEIUoG.js → _server.ts.js-BXE_mCX3.js} +7 -7
- package/build/server/chunks/entries/endpoints/api/reconcile/{_server.ts.js-DoeEIUoG.js.map → _server.ts.js-BXE_mCX3.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/reveal/{_server.ts.js-0hMtYoB1.js → _server.ts.js-DM8Nj2U6.js} +7 -7
- package/build/server/chunks/entries/endpoints/api/reveal/{_server.ts.js-0hMtYoB1.js.map → _server.ts.js-DM8Nj2U6.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/state/{_server.ts.js-B4YWmlkC.js → _server.ts.js-C0a_L0JL.js} +7 -7
- package/build/server/chunks/entries/endpoints/api/state/{_server.ts.js-B4YWmlkC.js.map → _server.ts.js-C0a_L0JL.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/usage/{_server.ts.js-dNLtLO2U.js → _server.ts.js-CavHY0Ie.js} +7 -7
- package/build/server/chunks/entries/endpoints/api/usage/{_server.ts.js-dNLtLO2U.js.map → _server.ts.js-CavHY0Ie.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/files/_...path_/{_server.ts.js-DkYQlG9U.js → _server.ts.js-c03akA1c.js} +6 -6
- package/build/server/chunks/entries/endpoints/files/_...path_/{_server.ts.js-DkYQlG9U.js.map → _server.ts.js-c03akA1c.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/files/proxy/_...path_/{_server.ts.js-BtCkf-hk.js → _server.ts.js-CKKJ-v3P.js} +7 -7
- 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
- package/build/server/chunks/entries/endpoints/files/raw/_...path_/{_server.ts.js-CozrPtRv.js → _server.ts.js-DiZ3D46Z.js} +7 -7
- package/build/server/chunks/entries/endpoints/files/raw/_...path_/{_server.ts.js-CozrPtRv.js.map → _server.ts.js-DiZ3D46Z.js.map} +1 -1
- package/build/server/chunks/entries/{hooks.server.js-Cnsi5KsE.js → hooks.server.js-DwnveOre.js} +6 -6
- package/build/server/chunks/entries/{hooks.server.js-Cnsi5KsE.js.map → hooks.server.js-DwnveOre.js.map} +1 -1
- package/build/server/chunks/{handler-BN27c-Da.js → handler-ZL0S9fLH.js} +5 -5
- package/build/server/chunks/{handler-BN27c-Da.js.map → handler-ZL0S9fLH.js.map} +1 -1
- package/build/server/chunks/{index.js-C1f-Qrf0.js → index.js-Ddbaaikg.js} +5 -5
- package/build/server/chunks/{index.js-C1f-Qrf0.js.map → index.js-Ddbaaikg.js.map} +1 -1
- package/build/server/chunks/{manifest.js-CFkuYTzu.js → manifest.js-D3J7G7mh.js} +31 -31
- package/build/server/chunks/{manifest.js-CFkuYTzu.js.map → manifest.js-D3J7G7mh.js.map} +1 -1
- package/build/server/chunks/nodes/{0.js-B1i6yC_e.js → 0.js-DjvM5GIw.js} +2 -2
- package/build/server/chunks/nodes/{0.js-B1i6yC_e.js.map → 0.js-DjvM5GIw.js.map} +1 -1
- package/build/server/chunks/nodes/1.js-D_KuDyBR.js +7 -0
- package/build/server/chunks/nodes/{1.js-Bv64OO0t.js.map → 1.js-D_KuDyBR.js.map} +1 -1
- package/build/server/chunks/nodes/{2.js-DivaYfS1.js → 2.js-CEThjyv1.js} +2 -2
- package/build/server/chunks/nodes/{2.js-DivaYfS1.js.map → 2.js-CEThjyv1.js.map} +1 -1
- package/build/server/chunks/nodes/3.js-7gq-At5G.js +7 -0
- package/build/server/chunks/nodes/{3.js-CUAbHOxb.js.map → 3.js-7gq-At5G.js.map} +1 -1
- package/build/server/chunks/nodes/4.js-DsoSGC53.js +7 -0
- package/build/server/chunks/nodes/{4.js-Ceg0ylPI.js.map → 4.js-DsoSGC53.js.map} +1 -1
- package/dist/cli.js +9 -0
- package/dist/core/chroma-key.d.ts +11 -5
- package/dist/core/chroma-key.js +11 -3
- package/dist/core/figma-tag-script.d.ts +141 -0
- package/dist/core/figma-tag-script.js +236 -0
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +1 -0
- package/dist/runtime/chroma-key-gl.d.ts +12 -4
- package/dist/runtime/chroma-key-gl.js +63 -6
- package/dist/runtime/components/ChromaKeyVideo.svelte +2 -1
- package/dist/server/cli-commands.d.ts +6 -0
- package/dist/server/cli-commands.js +1 -1
- package/dist/server/cli-figma-tags.d.ts +2 -0
- package/dist/server/cli-figma-tags.js +222 -0
- package/dist/server/figma-tags.d.ts +61 -0
- package/dist/server/figma-tags.js +153 -0
- package/dist/server/importers/figma.d.ts +17 -0
- package/dist/server/importers/figma.js +43 -18
- package/package.json +1 -1
- package/build/client/_app/immutable/chunks/C4w39LtR.js.br +0 -1
- package/build/client/_app/immutable/chunks/C4w39LtR.js.gz +0 -0
- package/build/client/_app/immutable/chunks/Cml0-AYv.js.br +0 -0
- package/build/client/_app/immutable/chunks/Cml0-AYv.js.gz +0 -0
- package/build/client/_app/immutable/chunks/DGYcFVZ3.js.br +0 -0
- package/build/client/_app/immutable/chunks/DGYcFVZ3.js.gz +0 -0
- package/build/client/_app/immutable/chunks/DGy-bMvO.js +0 -1
- package/build/client/_app/immutable/chunks/DGy-bMvO.js.br +0 -0
- package/build/client/_app/immutable/chunks/DGy-bMvO.js.gz +0 -0
- package/build/client/_app/immutable/chunks/WV4B0d_0.js +0 -108
- package/build/client/_app/immutable/chunks/WV4B0d_0.js.br +0 -0
- package/build/client/_app/immutable/chunks/WV4B0d_0.js.gz +0 -0
- package/build/client/_app/immutable/entry/app.GgnXYpzJ.js.br +0 -0
- package/build/client/_app/immutable/entry/app.GgnXYpzJ.js.gz +0 -0
- package/build/client/_app/immutable/entry/start.O9RwuDjf.js +0 -1
- package/build/client/_app/immutable/entry/start.O9RwuDjf.js.br +0 -0
- package/build/client/_app/immutable/entry/start.O9RwuDjf.js.gz +0 -0
- package/build/client/_app/immutable/nodes/0.D1g1plhI.js +0 -1
- package/build/client/_app/immutable/nodes/0.D1g1plhI.js.br +0 -0
- package/build/client/_app/immutable/nodes/0.D1g1plhI.js.gz +0 -0
- package/build/client/_app/immutable/nodes/1.DzAnL2O6.js.br +0 -2
- package/build/client/_app/immutable/nodes/1.DzAnL2O6.js.gz +0 -0
- package/build/client/_app/immutable/nodes/2.BMN1YTJ4.js.br +0 -0
- package/build/client/_app/immutable/nodes/2.BMN1YTJ4.js.gz +0 -0
- package/build/client/_app/immutable/nodes/3.DL_TiE5x.js +0 -1
- package/build/client/_app/immutable/nodes/3.DL_TiE5x.js.br +0 -0
- package/build/client/_app/immutable/nodes/3.DL_TiE5x.js.gz +0 -0
- package/build/client/_app/immutable/nodes/4.D7FHmbgq.js +0 -2
- package/build/client/_app/immutable/nodes/4.D7FHmbgq.js.br +0 -0
- package/build/client/_app/immutable/nodes/4.D7FHmbgq.js.gz +0 -0
- package/build/server/chunks/chunks/context.js-4TO_atTF.js.map +0 -1
- package/build/server/chunks/chunks/core.js-Bp-ReC60.js.map +0 -1
- package/build/server/chunks/chunks/raw-reconciliation.js-mWOAOdgL.js.map +0 -1
- package/build/server/chunks/nodes/1.js-Bv64OO0t.js +0 -7
- package/build/server/chunks/nodes/3.js-CUAbHOxb.js +0 -7
- 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
|
+
}
|
package/dist/core/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/core/index.js
CHANGED
|
@@ -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 (
|
|
10
|
-
*
|
|
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
|
|
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
|
|
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
|
|
13
|
-
//
|
|
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
|
-
|
|
90
|
-
|
|
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>;
|