はじめに
前回の記事では、Figmaにガイドロック機能が存在しないことを踏まえ、Pluginを使ってガイドロックらしき挙動を再現する方法を検討しました。
今回はその続編として、実際にガイドロックの代替案としてのガイド補正を試してみた様子と、そこから見えてきた課題についてまとめてみます。
検証環境
この記事には以下の環境を使用しています。
- Figma 125.7.5 (デスクトップアプリ)
- Windows 11 Home 24H2
- Node.js v18.20.0
- 6.6.87.2-microsoft-standard-WSL2
- Ubuntu 24.04.2
Figma Pluginで補正処理を動かすには
-
ガイドの座標を記録する
Figmaでは、ページ全体のガイド情報をfigma.currentPage.guidesから取得できます。これは以下のような構造を持つ配列です。
[{ axis: 'X', offset: 100 }, { axis: 'Y', offset: 200 }]
Plugin起動時にこの配列を読み取り専用として保持しておくことで、後から「元のガイド位置」として参照できます。 -
ガイドを再設定する
ガイドの補正は、記録しておいた配列をfigma.currentPage.guidesに再代入することで実現できます。ガイドの値は操作できませんが、再代入することで「ロックされた状態に戻す」ような挙動になります。
補正が行われたことをユーザーに通知するには、figma.notify()を使ってみます。 -
補正タイミングの検討
補正処理をいつ実行するかによって、UXや精度が大きく変わると考えられます。今回は次セクションで紹介する3つのタイミングを検討しました。
また、通常Figmaのプラグインは動作後figma.closePlugin();で閉じる必要がありますが、バックグラウンドで動作するプラグインは作成できないという制約があるようでしたので今回は補正タイミングに関わらずすべてUIごと表示させたまま試しました。
補正タイミングの比較検証
1. ボタン操作による補正
// ボタンでガイドチェックする
document.getElementById('guides-check').onclick = () => {
parent.postMessage({ pluginMessage: { type: 'guides-check' } }, '*');
};
// もし、ガイドが動かされたら、ガイドを補正する
if (msg.type === 'guides-check') {
if (originalGuides === undefined || originalGuides.length === 0) {
// ロックされてないときは、何もしない
figma.notify("ガイドがロックされていません");
} else if (originalGuides !== figma.currentPage.guides) {
// ガイドが変更されていたら、元に戻す
figma.currentPage.guides = originalGuides;
figma.notify("ガイドを補正しました");
} else {
// ガイドが変更されていなかったら、何もしない
figma.notify("ガイドは変更されていません");
}
}
まず、UIに「ガイドをロック」「位置チェック」「ロック解除」の3つのボタンを設置し、明示的に補正を実行できるようにしてみました。
-
メリット:
- 補正のタイミングが明確で補正されたとわかりやすい
- ユーザが意図的に操作できる
-
問題点:
- 毎回ボタンを押すのがとても面倒
- 気づかないうちにガイドがずれている可能性がある
2. 定期実行による補正
// ガイドチェックの定期実行を開始
guideCheckIntervalId = setInterval(() => {
parent.postMessage({ pluginMessage: { type: 'guides-check' } }, '*');
}, 5000);
// 定期チェックを解除
if (guideCheckIntervalId !== null) {
clearInterval(guideCheckIntervalId);
guideCheckIntervalId = null;
};
わざわざボタンを押さなくていいように、setInterval() を使って、一定時間ごとにガイドの位置をチェックするようにしてみました。ロック時に guideCheckIntervalId を保存し、解除時に clearInterval() で停止することで、重複や無駄な処理を防ぎます。
-
メリット:
- 誤操作したガイドをいちいち手動で戻さなくてよい
-
問題点:
- 間隔を長めにすると(例.10秒間隔)、補正までのラグが気になる(その間にずれたガイドに揃えてしまうかも)
- 補正までの時間が気にならないくらいに短めにすると(例.1秒間隔)、
figma.notify()の通知が頻繁すぎて動作が重くて追いつかなくなる
→ボタンの時のように 補正を通知しようとすると通知と補正が1~2分ずれてしまいました...
3. ユーザ操作をトリガーにした補正
// Nodeの選択が変更されたらガイドをチェックする
figma.on('selectionchange', () => {
if (originalGuides === undefined || originalGuides.length === 0) {
return;
} else if (JSON.stringify(figma.currentPage.guides) !== JSON.stringify(originalGuides)) {
figma.currentPage.guides = originalGuides;
figma.notify("ガイドを補正しました(選択変更)");
}
});
Figma Plugin API の selectionchange イベントを使って、Nodeの選択変更をトリガーに補正処理を実行する方法も試してみました。
-
メリット:
- 定期実行より回数が少ないので動きが軽い
-
問題点:
- ガイドはNodeではないため、選択しても
selectionchangeは発火しない - 「Node → Guide → 同じ Node」への選択遷移では検知することができない
- ガイドはNodeではないため、選択しても
=> 2の定期実行と3を併用して、選択変更ですばやく補正しつつ、ガイド誤操作を見逃さないようにするのがよいかもしれません。
ガイド補正の限界と課題
補正で代用しようとした場合に共通してみられた問題点です。
問題点①:ロック中のガイド追加が反映されない
figma.currentPage.guides は配列の再代入によって補正されるため、ロック後に追加されたガイドは補正時に消えてしまいます。ガイド補正でロックを補うと上書きとして扱われるため、意図的に追加したガイドも失われる可能性があります。
問題点②:Page全体のガイドしか取得できない
guides: ReadonlyArray<[Guide](https://www.figma.com/plugin-docs/api/Guide/)>
Array of
Guideused inside the frame. Note that each frame has its own guides, separate from the canvas-wide guides. For help on how to change this value, see Editing Properties.(Figma Developers より)
figma.currentPage.guidesはページ全体のガイドしか含んでいないようでした。Figma内に別に存在している FrameNode.guides というプロパティが各フレーム内の個別のガイドを指しますが、こちらは現時点では Plugin API から取得することはできません。ページ全体のガイドはフレーム内から動かしてしまうとフレーム内の個別ガイドに変換されるため、再代入の際にずらしてしまった元のガイドは上書きできず残ったままになってしまうことがわかりました。
フレーム内の個別ガイドは補正対象にできないため、仮想ガイドなどで代用するしかなさそうです。
おわりに
今回の検証を通じて、Figma Pluginでガイド補正を実現することは可能である一方、完全なロック機能に近づくのにはAPIの制限によってかなり限界があることもわかりました。
今後のFigma APIの拡張や、より柔軟なイベント検知機能に期待しつつ、引き続き改善を重ねていきたいと思います。
今回の試作で使用したコード
ui.htmlとcode.ts以外は公式テンプレートをベースに使用しています。
2の定期実行部分は3のユーザ操作と干渉するので一時的にコメントアウトしてあります。
<h2>ガイド補正</h2>
<button id="guides-lock">ガイドをロック</button>
<button id="guides-check">位置チェック</button>
<button id="guides-unlock">ロックを解除</button>
<script>
let guideCheckIntervalId = null;
document.getElementById('guides-lock').onclick = () => {
parent.postMessage({ pluginMessage: { type: 'guides-lock' } }, '*');
// // すでに定期実行が動いていたら止める(重複防止)
// if (guideCheckIntervalId !== null) {
// clearInterval(guideCheckIntervalId);
// }
// // ガイドチェックの定期実行を開始
// guideCheckIntervalId = setInterval(() => {
// parent.postMessage({ pluginMessage: { type: 'guides-check' } }, '*');
// }, 3000);
};
// ボタンでガイドチェックする場合
document.getElementById('guides-check').onclick = () => {
parent.postMessage({ pluginMessage: { type: 'guides-check' } }, '*');
};
document.getElementById('guides-unlock').onclick = () => {
parent.postMessage({ pluginMessage: { type: 'guides-unlock' } }, '*');
// 定期チェックを解除
// if (guideCheckIntervalId !== null) {
// clearInterval(guideCheckIntervalId);
// guideCheckIntervalId = null;
// };
};
</script>
if (figma.editorType === 'figma') {
figma.showUI(__html__);
let originalGuides: readonly Guide[];
figma.ui.onmessage = (msg: { type: string }) => {
if (msg.type === 'guides-lock') {
// ガイドを記録する
originalGuides = figma.currentPage.guides;
console.log(originalGuides);
console.log(figma.currentPage.guides);
figma.notify("ガイドをロックしました");
}
// もし、ガイドが動かされたら、ガイドを補正する
if (msg.type === 'guides-check') {
if (originalGuides === undefined || originalGuides.length === 0) {
// ロックされてないときは、何もしない
figma.notify("ガイドがロックされていません");
} else if (originalGuides !== figma.currentPage.guides) {
// ガイドが変更されていたら、元に戻す
figma.currentPage.guides = originalGuides;
figma.notify("ガイドを補正しました");
} else {
// ガイドが変更されていなかったら、何もしない
figma.notify("ガイドは変更されていません");
}
}
if (msg.type === 'guides-unlock') {
// ガイドのロックを解除する
if (originalGuides === undefined || originalGuides.length === 0) {
figma.notify("すでにガイドのロックは解除されています");
return;
}
originalGuides = [];
figma.notify("ガイドのロックを解除しました");
}
};
// Nodeの選択が変更されたらガイドをチェックする
figma.on('selectionchange', () => {
if (originalGuides === undefined || originalGuides.length === 0) {
return;
} else if (JSON.stringify(figma.currentPage.guides) !== JSON.stringify(originalGuides)) {
figma.currentPage.guides = originalGuides;
figma.notify("ガイドを補正しました(選択変更)");
}
});
};
参考
https://www.figma.com/plugin-docs/api/FrameNode/
https://www.sejuku.net/blog/24425