
【.ai用試作機】AiからPsのアクション実行できないかなって試験機
目指したのはアクションの擬似スクリプト化
Illustrator側からここまでする必要はあるのか、と根本的な問いは脇に置きます
バッチ処理的なこともできるようになったらいいなとチャッピー先生とClaide先生に頑張ってもらいました
まだまだ改善・検証の余地が残るこの子ですが一旦アップ
アクションの組み合わせ方次第で面白いことができるかも?
説明書はClaude製です
…bridgetalk使うようなscriptでもアクションに組み込めていれば機能する、はず
…あまりPhotoshopに明るくないものだからこんなテストしてないだろ!ってケースがあれば教えてください
※処理対象のファイルを複製の上、使用感をお楽しみください

Photoshopを起動した状態で実行してください

もしかして複雑なソースコードをバイブコーディングするより、アクション実行できた方が色々柔軟に対応できるのではと思いついてバイブコーディングした子。
(実用性はともかく)複数のアクションを連続実行できたら何かと有用なのではと思い立って、セットリストを跨いで連続実行できるようにしました。…そのはず。
ただし検証や細かい挙動(バッチ処理的な挙動とか…)の精査は不十分な試作機となっております。
ざっくり使い方
あらかじめPhotoshopを起動しておく※起動していない場合、起動を促すアラートが出て終了します
処理対象が配置されている.aiドキュメントを開く
処理対象を選択or無選択でスクリプトを実行
実行するアクションセットを選択
アクション一覧から実行するアクションを選択。この際、選択順に "実行順序" リストへ追加されていきます
"選択したアクションを実行" ボタンで対象のファイルをPhotoshopへ送信し、アクションを実行
規定のアクションであれば書き出し・読み込みで書き出すと再利用性が上がる。はず
イラレを落とすまではある程度履歴を残してくれます。履歴を選択して復元でどうぞ
注意事項(試験機運用前提)
本番データではなく必ず複製でテストしてください。Photoshop側で app.doAction() を実行するため、アクション内容次第でファイルが変更・保存されます。
Photoshop未起動時は起動しません(試験機でも誤操作防止のため、先にPhotoshopを起動)。
Photoshop側はダイアログを抑制して実行します。途中で確認が必要なアクション(停止・入力待ち等)は想定どおりに動かない可能性があります。
対象拡張子は限定されています(psd/psb/tif/tiff/jpg/jpeg/png/eps/pdf)。想定外の拡張子は「除外」扱いになります。
実行前にアクション存在チェックを行い、Photoshop側に無い組み合わせが混ざっていると停止します。試験機ではまず「再読込→存在確認」をルーチン化推奨。
履歴・実行リストはIllustrator終了まで保持されます。試験機の使い回し時は「履歴クリア」「実行リストをクリア」を適宜使って事故防止。
通信はタイムアウトありです。重いファイルや大量処理では分割実行を推奨(2分待機でタイムアウト)。
// @target illustrator
#target illustrator
#targetengine "ai_PhotoshopActionPanel"
/* ============================================================================
ai_PhotoshopActionPanel.jsx (v6.6 - 実行リストI/O + 履歴保持)
Illustrator 2022+ / ExtendScript(ECMA-3)
----------------------------------------------------------------------------
v6.6 追加:
・実行順(セット名 / アクション名)をテキストで書き出し/読み込み
・Illustrator終了まで、実行履歴&前回の実行リストを保持(targetengine + $.global)
============================================================================ */
(function () {
// =========================================================
// 設定
// =========================================================
var UI_TITLE = "Photoshop Action";
var UI_W = 520;
var UI_H = 620;
var ALLOWED_EXT = {
"psd": true, "psb": true,
"tif": true, "tiff": true,
"jpg": true, "jpeg": true,
"png": true,
"eps": true,
"pdf": true
};
var DEBUG = false;
// --- 履歴保持(Illustrator終了まで)
var STORE_KEY = "__ai_PhotoshopActionPanel_STORE__v1";
var store = $.global[STORE_KEY];
if (!store) {
store = {
lastActionSelectionOrder: [],
history: [],
historyMax: 20,
lastIOFolderFsName: "" // 最後にI/Oしたフォルダ
};
$.global[STORE_KEY] = store;
}
// =========================================================
// 入口(AIドキュメント)
// =========================================================
if (app.documents.length === 0) {
alert("ドキュメントが開かれていません。");
return;
}
var targetDoc = null;
try { targetDoc = app.activeDocument; } catch (_) { targetDoc = null; }
if (!targetDoc) {
alert("アクティブなドキュメントを取得できません。");
return;
}
// =========================================================
// Photoshop 起動チェック(起動していなければ、ここで終了)
// =========================================================
if (typeof BridgeTalk === "undefined") {
alert("BridgeTalk が使用できないため、Photoshop に送信できません。");
return;
}
var selectedPhotoshopSpec = detectRunningPhotoshopSpecifier();
if (!selectedPhotoshopSpec) {
alert("Photoshop が起動していません。\nPhotoshop を起動してから、再度スクリプトを実行してください。");
return;
}
// =========================================================
// 変数
// =========================================================
var psActionData = null;
var __btPool = [];
var executeParams = null;
// アクション選択順(セットをまたいで保持)
var actionSelectionOrder = cloneActionList(store.lastActionSelectionOrder || []);
var lastClickTime = 0;
// =========================================================
// UI 構築(モーダルダイアログ)
// =========================================================
var win = new Window("dialog", UI_TITLE);
win.orientation = "column";
win.alignChildren = ["fill", "top"];
win.spacing = 6;
win.margins = 10;
win.preferredSize = [UI_W, UI_H];
// --- 上:セット行
var topGrp = win.add("group");
topGrp.orientation = "row";
topGrp.alignChildren = ["fill", "center"];
var ddSets = topGrp.add("dropdownlist", undefined, []);
ddSets.preferredSize.width = 280;
var btnReload = topGrp.add("button", undefined, "再読込");
btnReload.preferredSize.width = 70;
// --- 中段:アクションエリア(左右2列)
var actionAreaGrp = win.add("group");
actionAreaGrp.orientation = "row";
actionAreaGrp.alignChildren = ["fill", "top"];
actionAreaGrp.spacing = 10;
// 左:アクション一覧
var leftPanel = actionAreaGrp.add("panel", undefined, "アクション一覧(複数選択可)");
leftPanel.orientation = "column";
leftPanel.alignChildren = ["fill", "top"];
leftPanel.margins = 10;
leftPanel.preferredSize = [240, 260];
var lbActions = leftPanel.add("listbox", undefined, [], { multiselect: true });
lbActions.preferredSize.height = 220;
// 右:選択したアクション(実行順)
var rightPanel = actionAreaGrp.add("panel", undefined, "実行順序(セット横断)");
rightPanel.orientation = "column";
rightPanel.alignChildren = ["fill", "top"];
rightPanel.margins = 10;
rightPanel.preferredSize = [240, 260];
var lbSelectedActions = rightPanel.add("listbox", undefined, []);
lbSelectedActions.preferredSize.height = 170;
var actionBtnGrp = rightPanel.add("group");
actionBtnGrp.orientation = "row";
actionBtnGrp.alignChildren = ["fill", "center"];
actionBtnGrp.spacing = 8;
var btnClearThisSet = actionBtnGrp.add("button", undefined, "このセットをクリア");
btnClearThisSet.preferredSize.width = 120;
var btnClearExecList = actionBtnGrp.add("button", undefined, "実行リストをクリア");
btnClearExecList.preferredSize.width = 120;
// --- I/O & 履歴
var ioGrp = win.add("group");
ioGrp.orientation = "row";
ioGrp.alignChildren = ["left", "center"];
ioGrp.spacing = 8;
var btnExportActions = ioGrp.add("button", undefined, "書き出し");
btnExportActions.preferredSize.width = 70;
var btnImportActions = ioGrp.add("button", undefined, "読み込み");
btnImportActions.preferredSize.width = 70;
var ddHistory = ioGrp.add("dropdownlist", undefined, []);
ddHistory.preferredSize.width = 260;
var btnApplyHistory = ioGrp.add("button", undefined, "履歴を復元");
btnApplyHistory.preferredSize.width = 80;
var btnClearHistory = ioGrp.add("button", undefined, "履歴クリア");
btnClearHistory.preferredSize.width = 70;
// --- 処理対象(件数+更新ボタン)
var targetGrp = win.add("group");
targetGrp.orientation = "row";
targetGrp.alignChildren = ["left", "center"];
var stTargetLabel = targetGrp.add("statictext", undefined, "処理対象:");
stTargetLabel.characters = 8;
var stTargetCount = targetGrp.add("statictext", undefined, "対象 0 / 除外 0 / 切れ 0");
stTargetCount.characters = 30;
var btnRefreshTarget = targetGrp.add("button", undefined, "更新");
btnRefreshTarget.preferredSize.width = 50;
// --- 対象リスト
var pnlTargets = win.add("panel", undefined, "対象ファイル(複数選択可)");
pnlTargets.orientation = "column";
pnlTargets.alignChildren = ["fill", "top"];
pnlTargets.margins = 10;
var lbTargets = pnlTargets.add("listbox", undefined, "", {
numberOfColumns: 3,
showHeaders: true,
columnTitles: ["状態", "ファイル", "リンク"],
columnWidths: [70, 350, 60],
multiselect: true
});
lbTargets.preferredSize.height = 140;
// --- 状態表示
var stInfo = win.add("statictext", undefined, "未読込");
stInfo.characters = 80;
// --- ボタン行
var btnGrp = win.add("group");
btnGrp.orientation = "row";
btnGrp.alignChildren = ["fill", "center"];
btnGrp.spacing = 10;
var btnExecute = btnGrp.add("button", undefined, "選択したアクションを実行");
btnExecute.preferredSize.height = 30;
btnExecute.enabled = false;
var btnClose = btnGrp.add("button", undefined, "閉じる");
btnClose.preferredSize.width = 80;
// =========================================================
// UI イベント
// =========================================================
btnReload.onClick = function () {
reloadFromPhotoshop();
};
ddSets.onChange = function () {
updateActionListBySelectedSet();
try { applySelectionStateForSet(ddSets.selection ? ddSets.selection.text : ""); } catch (_) {}
updateExecuteButton();
};
lbActions.onChange = function () {
var currentTime = new Date().getTime();
lastClickTime = currentTime;
win.update();
setTimeout(function () {
if (new Date().getTime() - lastClickTime >= 45) {
updateActionSelectionOrderImmediate();
updateExecuteButton();
}
}, 50);
};
lbActions.addEventListener("click", function () {
setTimeout(function () {
updateActionSelectionOrderImmediate();
updateExecuteButton();
}, 10);
});
btnClearThisSet.onClick = function () {
clearCurrentSetSelection();
updateExecuteButton();
};
btnClearExecList.onClick = function () {
clearExecuteListAll();
updateExecuteButton();
};
btnExportActions.onClick = function () {
exportActionList();
};
btnImportActions.onClick = function () {
importActionList();
};
btnApplyHistory.onClick = function () {
applySelectedHistory();
};
btnClearHistory.onClick = function () {
clearHistoryAll();
};
btnRefreshTarget.onClick = function () {
updateTargetUI(true);
};
btnExecute.onClick = function () {
executeSelectedAction();
};
btnClose.onClick = function () {
win.close();
};
win.addEventListener("keydown", function (k) {
try {
if (k && k.keyName === "Escape") win.close();
} catch (_) {}
});
// =========================================================
// 初期表示(履歴&実行順)
// =========================================================
updateSelectedActionsDisplay();
refreshHistoryDropdown();
// =========================================================
// 初期化
// =========================================================
reloadFromPhotoshop();
updateTargetUI(true);
var result = win.show();
if (executeParams) {
executeActionAfterClose(executeParams.actions, executeParams.files);
}
// =========================================================
// Photoshop 起動/検出(起動していない場合は中止)
// =========================================================
function ensurePhotoshopRunning() {
if (!selectedPhotoshopSpec) {
selectedPhotoshopSpec = detectRunningPhotoshopSpecifier();
}
if (!selectedPhotoshopSpec) {
alert("Photoshop が起動していません。\nPhotoshop を起動してから再度お試しください。");
return false;
}
var running = false;
try { running = BridgeTalk.isRunning(selectedPhotoshopSpec); } catch (_) { running = false; }
// specifierでisRunningが取れない環境向け保険
if (!running) {
try { running = BridgeTalk.isRunning("photoshop"); } catch (_) { running = false; }
}
if (!running) {
alert("Photoshop が起動していません。\nPhotoshop を起動してから再度お試しください。");
return false;
}
return true;
}
// =========================================================
// 実行順リスト(セット横断保持)
// =========================================================
function updateActionSelectionOrderImmediate() {
if (!ddSets.selection) return;
var setName = ddSets.selection.text;
// 現在のセットで選択されているアクション名
var currentSelected = [];
for (var i = 0; i < lbActions.items.length; i++) {
if (lbActions.items[i].selected) currentSelected.push(lbActions.items[i].text);
}
// まず「他セット」の順序は保持
var newOrder = [];
for (var j = 0; j < actionSelectionOrder.length; j++) {
var it = actionSelectionOrder[j];
if (it.setName !== setName) newOrder.push(it);
}
// 「現セット」は既存順序を保ちつつ、まだ選択されているものだけ残す
for (var k = 0; k < actionSelectionOrder.length; k++) {
var oldItem = actionSelectionOrder[k];
if (oldItem.setName !== setName) continue;
var found = false;
for (var m = 0; m < currentSelected.length; m++) {
if (currentSelected[m] === oldItem.actionName) { found = true; break; }
}
if (found) newOrder.push(oldItem);
}
// 現セットで新規に選択されたものを末尾へ追加
for (var n = 0; n < currentSelected.length; n++) {
var aName = currentSelected[n];
var exists = false;
for (var p = 0; p < newOrder.length; p++) {
if (newOrder[p].setName === setName && newOrder[p].actionName === aName) {
exists = true; break;
}
}
if (!exists) newOrder.push({ setName: setName, actionName: aName });
}
actionSelectionOrder = newOrder;
persistLastSelection();
updateSelectedActionsDisplay();
}
function updateSelectedActionsDisplay() {
lbSelectedActions.removeAll();
for (var i = 0; i < actionSelectionOrder.length; i++) {
var item = actionSelectionOrder[i];
var displayText = (i + 1) + ". " + item.actionName + " (" + item.setName + ")";
// PS側のアクション取得済みなら存在チェックして表示に反映
if (psActionData && psActionData.sets) {
if (!isActionExists(psActionData, item.setName, item.actionName)) {
displayText += " [未検出]";
}
}
lbSelectedActions.add("item", displayText);
}
win.update();
}
function applySelectionStateForSet(setName) {
if (!setName) return;
// 現セットで実行順に入っているアクションをマップ化
var map = {};
for (var i = 0; i < actionSelectionOrder.length; i++) {
var it = actionSelectionOrder[i];
if (it.setName === setName) map[it.actionName] = true;
}
// ListBox 側の選択状態を復元
for (var j = 0; j < lbActions.items.length; j++) {
var nm = lbActions.items[j].text;
lbActions.items[j].selected = !!map[nm];
}
}
function clearCurrentSetSelection() {
if (!ddSets.selection) return;
var setName = ddSets.selection.text;
// UI上の選択を解除(現セットのみ)
for (var i = 0; i < lbActions.items.length; i++) {
lbActions.items[i].selected = false;
}
// 実行順から「現セット分」だけ削除
var newOrder = [];
for (var j = 0; j < actionSelectionOrder.length; j++) {
if (actionSelectionOrder[j].setName !== setName) newOrder.push(actionSelectionOrder[j]);
}
actionSelectionOrder = newOrder;
persistLastSelection();
updateSelectedActionsDisplay();
}
function clearExecuteListAll() {
actionSelectionOrder = [];
// 現在表示中のセットはUI上も解除
for (var i = 0; i < lbActions.items.length; i++) {
lbActions.items[i].selected = false;
}
persistLastSelection();
updateSelectedActionsDisplay();
}
function updateExecuteButton() {
var enabled = (!!psActionData) && (actionSelectionOrder.length > 0);
btnExecute.enabled = enabled;
}
function persistLastSelection() {
store.lastActionSelectionOrder = cloneActionList(actionSelectionOrder);
}
// =========================================================
// 実行リスト:書き出し / 読み込み
// =========================================================
function exportActionList() {
if (!actionSelectionOrder || actionSelectionOrder.length === 0) {
alert("書き出す実行リストがありません。");
return;
}
var baseFolder = null;
if (store.lastIOFolderFsName) {
try { baseFolder = new Folder(store.lastIOFolderFsName); } catch (_) { baseFolder = null; }
}
var f = null;
try {
f = File.saveDialog("実行アクションリストを書き出し", "Text:*.txt");
} catch (_) {
f = null;
}
if (!f) return;
try {
if (f.parent) store.lastIOFolderFsName = String(f.parent.fsName || "");
} catch (_) {}
var lines = [];
lines.push("# ai_PhotoshopActionPanel action-list v1");
lines.push("# Saved: " + formatDate(new Date()));
lines.push("# Format: setName<TAB>actionName");
lines.push("# Count: " + actionSelectionOrder.length);
lines.push("");
for (var i = 0; i < actionSelectionOrder.length; i++) {
var it = actionSelectionOrder[i];
lines.push(String(it.setName) + "\t" + String(it.actionName));
}
var text = lines.join("\n");
try {
f.encoding = "UTF-8";
f.lineFeed = "Unix";
if (!f.open("w")) {
alert("ファイルを書き込めませんでした。");
return;
}
// BOM付きで書き出し
f.write("\uFEFF" + text);
f.close();
alert("書き出しが完了しました。\n" + f.fsName);
} catch (e) {
try { if (f && f.opened) f.close(); } catch (_) {}
alert("書き出しに失敗しました:\n" + e);
}
}
function importActionList() {
var f = null;
try {
f = File.openDialog("実行アクションリストを読み込み", "Text:*.txt");
} catch (_) {
f = null;
}
if (!f) return;
try {
if (f.parent) store.lastIOFolderFsName = String(f.parent.fsName || "");
} catch (_) {}
var text = "";
try {
f.encoding = "UTF-8";
if (!f.open("r")) {
alert("ファイルを読み込めませんでした。");
return;
}
text = f.read();
f.close();
} catch (e) {
try { if (f && f.opened) f.close(); } catch (_) {}
alert("読み込みに失敗しました:\n" + e);
return;
}
text = stripBom(text);
var list = parseActionListText(text);
if (!list || list.length === 0) {
alert("読み込みデータから有効な行を取得できませんでした。");
return;
}
actionSelectionOrder = cloneActionList(list);
persistLastSelection();
updateSelectedActionsDisplay();
// 現在のセット表示に合わせて左ListBoxも復元
try {
if (ddSets.selection) applySelectionStateForSet(ddSets.selection.text);
} catch (_) {}
updateExecuteButton();
alert("読み込みが完了しました。\nアクション数: " + actionSelectionOrder.length);
}
function parseActionListText(text) {
var lines = String(text || "").split(/\r\n|\r|\n/);
var list = [];
for (var i = 0; i < lines.length; i++) {
var line = trimStr(lines[i]);
if (!line) continue;
if (line.indexOf("#") === 0) continue;
var parts = line.split("\t");
if (!parts || parts.length < 2) continue;
var setName = trimStr(parts[0]);
var actionName = trimStr(parts.slice(1).join("\t")); // タブ混入に一応対応
if (!setName || !actionName) continue;
list.push({ setName: setName, actionName: actionName });
}
return list;
}
function stripBom(s) {
s = String(s || "");
if (s.length > 0 && s.charCodeAt(0) === 0xFEFF) return s.substring(1);
return s;
}
// =========================================================
// 履歴(Illustrator終了まで保持)
// =========================================================
function pushHistory(actions, fileCount) {
var item = {
ts: (new Date()).getTime(),
docName: safeDocName(targetDoc),
actions: cloneActionList(actions),
fileCount: fileCount
};
store.history.unshift(item);
// 最大件数
if (store.history.length > store.historyMax) {
store.history.length = store.historyMax;
}
refreshHistoryDropdown();
}
function refreshHistoryDropdown() {
ddHistory.removeAll();
if (!store.history || store.history.length === 0) {
ddHistory.add("item", "(履歴なし)");
ddHistory.selection = 0;
ddHistory.enabled = false;
btnApplyHistory.enabled = false;
btnClearHistory.enabled = false;
return;
}
ddHistory.enabled = true;
btnApplyHistory.enabled = true;
btnClearHistory.enabled = true;
for (var i = 0; i < store.history.length; i++) {
var h = store.history[i];
var d = new Date(h.ts);
var label = formatDate(d) +
" / " + (h.actions ? h.actions.length : 0) + "アクション" +
" / " + (h.fileCount || 0) + "ファイル" +
" / " + (h.docName || "");
var it = ddHistory.add("item", label);
it._index = i;
}
ddHistory.selection = 0;
}
function applySelectedHistory() {
if (!store.history || store.history.length === 0) return;
if (!ddHistory.selection || ddHistory.selection._index === undefined) return;
var idx = ddHistory.selection._index;
var h = store.history[idx];
if (!h || !h.actions || h.actions.length === 0) {
alert("履歴の内容が空です。");
return;
}
actionSelectionOrder = cloneActionList(h.actions);
persistLastSelection();
updateSelectedActionsDisplay();
try {
if (ddSets.selection) applySelectionStateForSet(ddSets.selection.text);
} catch (_) {}
updateExecuteButton();
}
function clearHistoryAll() {
store.history = [];
refreshHistoryDropdown();
}
function safeDocName(doc) {
try {
if (doc && doc.name) return String(doc.name);
} catch (_) {}
return "";
}
// =========================================================
// アクション一覧取得
// =========================================================
function reloadFromPhotoshop() {
if (!ensurePhotoshopRunning()) return;
stInfo.text = "Photoshop からアクション一覧を取得中... (" + getDisplayNameSafe(selectedPhotoshopSpec) + ")";
btnReload.enabled = false;
btnExecute.enabled = false;
ddSets.enabled = false;
lbActions.enabled = false;
var ps = buildPsListActionsScript();
sendToPhotoshop(ps,
function (body) {
try {
psActionData = parseJsonSafe(body);
if (!psActionData) {
stInfo.text = "応答の解析に失敗";
alert("Photoshop からの応答を解析できませんでした。\n\n応答:\n" + String(body).substring(0, 500));
ddSets.removeAll();
lbActions.removeAll();
return;
}
if (psActionData.error) {
stInfo.text = "PS側でエラー発生";
alert("Photoshop 側でエラーが発生しました:\n" + psActionData.error);
ddSets.removeAll();
lbActions.removeAll();
return;
}
if (!psActionData.sets || psActionData.sets.length === 0) {
stInfo.text = "アクションが見つかりません";
alert("アクションが見つかりません。\n\nPhotoshop でアクションが読み込まれているか確認してください。");
ddSets.removeAll();
lbActions.removeAll();
return;
}
fillSetDropdown(psActionData.sets);
updateActionListBySelectedSet();
// 現セットの選択復元
try { applySelectionStateForSet(ddSets.selection ? ddSets.selection.text : ""); } catch (_) {}
// 実行順表示も更新(未検出表示を反映)
updateSelectedActionsDisplay();
stInfo.text = "読込完了(セット数: " + psActionData.sets.length + ")";
} catch (e) {
stInfo.text = "読込失敗";
alert("アクション一覧の解析に失敗しました:\n" + e + "\n\n応答:\n" + String(body).substring(0, 300));
} finally {
btnReload.enabled = true;
ddSets.enabled = true;
lbActions.enabled = true;
updateExecuteButton();
try { app.bringToFront(); } catch (_) {}
}
},
function (errBody) {
btnReload.enabled = true;
ddSets.enabled = true;
lbActions.enabled = true;
stInfo.text = "読込失敗";
alert("Photoshop 通信エラー:\n" + errBody);
try { app.bringToFront(); } catch (_) {}
}
);
}
function fillSetDropdown(sets) {
ddSets.removeAll();
for (var i = 0; i < sets.length; i++) {
ddSets.add("item", sets[i].name);
}
if (ddSets.items.length > 0) ddSets.selection = 0;
}
function updateActionListBySelectedSet() {
lbActions.removeAll();
if (!psActionData || !psActionData.sets) return;
if (!ddSets.selection) return;
var setName = ddSets.selection.text;
var actions = findActionsBySetName(psActionData.sets, setName);
if (!actions || actions.length === 0) {
stInfo.text = "セット内にアクションがありません: " + setName;
return;
}
for (var i = 0; i < actions.length; i++) {
lbActions.add("item", actions[i]);
}
}
function findActionsBySetName(sets, setName) {
for (var i = 0; i < sets.length; i++) {
if (sets[i].name === setName) return sets[i].actions;
}
return [];
}
function isActionExists(data, setName, actionName) {
if (!data || !data.sets) return false;
for (var i = 0; i < data.sets.length; i++) {
if (data.sets[i].name !== setName) continue;
var acts = data.sets[i].actions || [];
for (var j = 0; j < acts.length; j++) {
if (acts[j] === actionName) return true;
}
return false;
}
return false;
}
// =========================================================
// 対象収集&UI更新
// =========================================================
function updateTargetUI(force) {
try {
if (!targetDoc) return;
if (force) {
try { app.redraw(); } catch (_) {}
}
var info = collectTargetInfo(targetDoc);
stTargetCount.text =
"対象 " + info.okFiles +
" / 除外 " + info.skipFiles +
" / 切れ " + info.missFiles;
lbTargets.removeAll();
for (var i = 0; i < info.entries.length; i++) {
var e = info.entries[i];
var row = lbTargets.add("item", e.statusJa);
row.subItems[0].text = e.name;
row.subItems[1].text = String(e.linkCount);
try {
row.helpTip = e.fsName + (e.reason ? ("\n" + e.reason) : "");
} catch (_) {}
}
// 「対象」のファイルをデフォルトで選択
for (var j = 0; j < info.entries.length; j++) {
if (info.entries[j].status === "OK") lbTargets.items[j].selected = true;
}
} catch (e) {
if (DEBUG) $.writeln("updateTargetUI error: " + e);
}
}
function collectTargetInfo(doc) {
var entriesMap = {};
var entries = [];
var placedItems = collectPlacedItems(doc);
for (var i = 0; i < placedItems.length; i++) {
var it = placedItems[i];
if (!it) continue;
var f = null;
try { f = it.file; } catch (_) { f = null; }
if (!f) continue;
var fs = String(f.fsName || "");
if (!fs) continue;
if (!entriesMap[fs]) {
var name = "";
try {
// File.name ではなく fsName から末尾のファイル名を抽出
name = fileNameFromFsName(f.fsName);
} catch (_) {
name = "";
}
var ext = getExtLower(name);
var exists = false;
try { exists = !!f.exists; } catch (_) { exists = false; }
var status = "OK";
var statusJa = "対象";
var reason = "";
if (!exists) {
status = "MISSING";
statusJa = "リンク切れ";
reason = "ファイルが存在しません。";
} else if (!ALLOWED_EXT[ext]) {
status = "SKIP";
statusJa = "除外";
reason = "拡張子が対象外です: " + ext;
}
entriesMap[fs] = {
fsName: fs,
name: name,
ext: ext,
exists: exists,
status: status,
statusJa: statusJa,
reason: reason,
linkCount: 0
};
entries.push(entriesMap[fs]);
}
entriesMap[fs].linkCount++;
}
entries.sort(function (a, b) {
var pa = statusPriority(a.status);
var pb = statusPriority(b.status);
if (pa !== pb) return pa - pb;
var an = a.name.toLowerCase();
var bn = b.name.toLowerCase();
if (an < bn) return -1;
if (an > bn) return 1;
return 0;
});
var okFiles = 0, skipFiles = 0, missFiles = 0;
for (var k = 0; k < entries.length; k++) {
if (entries[k].status === "OK") okFiles++;
else if (entries[k].status === "SKIP") skipFiles++;
else if (entries[k].status === "MISSING") missFiles++;
}
return {
entries: entries,
okFiles: okFiles,
skipFiles: skipFiles,
missFiles: missFiles
};
}
function statusPriority(s) {
if (s === "OK") return 0;
if (s === "SKIP") return 1;
return 2;
}
// =========================================================
// fsName からファイル名を安全に取り出す
// =========================================================
function decodeSafe(s) {
try { return decodeURI(s); } catch (_) { return s; }
}
function fileNameFromFsName(fsName) {
var s = decodeSafe(String(fsName || ""));
var p = Math.max(s.lastIndexOf("/"), s.lastIndexOf("\\"));
return (p >= 0) ? s.substring(p + 1) : s;
}
function getExtLower(name) {
var m = String(name || "").match(/\.([^.]+)$/);
return m ? String(m[1]).toLowerCase() : "";
}
// =========================================================
// アクション実行準備
// =========================================================
function executeSelectedAction() {
if (!psActionData) {
alert("アクションが読み込まれていません。\n「再読込」ボタンをクリックしてください。");
return;
}
if (actionSelectionOrder.length === 0) {
alert("アクションを選択してください。");
return;
}
if (!ensurePhotoshopRunning()) {
return;
}
// 未検出アクションが混ざっている場合は事前に止める
var missing = [];
for (var i = 0; i < actionSelectionOrder.length; i++) {
var it = actionSelectionOrder[i];
if (!isActionExists(psActionData, it.setName, it.actionName)) {
missing.push(it.setName + " / " + it.actionName);
}
}
if (missing.length > 0) {
var msg = "以下のアクションが Photoshop 側で見つかりません。\n\n" +
missing.slice(0, 15).join("\n");
if (missing.length > 15) msg += "\n...(他 " + (missing.length - 15) + " 件)";
msg += "\n\n実行リストを修正するか、再読込してください。";
alert(msg);
return;
}
var info = null;
try {
info = collectTargetInfo(targetDoc);
} catch (e) {
alert("処理対象の収集に失敗しました:\n" + e);
return;
}
var files = [];
var selectedIndices = [];
for (var i2 = 0; i2 < lbTargets.items.length; i2++) {
if (lbTargets.items[i2].selected) selectedIndices.push(i2);
}
if (selectedIndices.length === 0) {
for (var j = 0; j < info.entries.length; j++) {
if (info.entries[j].status === "OK") files.push(info.entries[j].fsName);
}
} else {
for (var k = 0; k < selectedIndices.length; k++) {
var idx = selectedIndices[k];
if (info.entries[idx] && info.entries[idx].status === "OK") {
files.push(info.entries[idx].fsName);
}
}
}
if (!files.length) {
alert("処理対象ファイルがありません。\n「対象」状態のファイルを選択してください。");
return;
}
// 履歴へ記録(Illustrator終了まで保持)
pushHistory(actionSelectionOrder, files.length);
executeParams = {
actions: actionSelectionOrder,
files: files
};
win.close();
}
// =========================================================
// プログレスバー付きアクション実行(ダイアログクローズ後)
// =========================================================
function executeActionAfterClose(actions, files) {
// プログレスバーウィンドウ作成
var progressWin = new Window("palette", "アクション実行中");
progressWin.orientation = "column";
progressWin.alignChildren = ["fill", "top"];
progressWin.spacing = 10;
progressWin.margins = 15;
progressWin.preferredSize = [420, 130];
var actionInfo = progressWin.add("statictext", undefined, "アクション数: " + actions.length);
var fileInfo = progressWin.add("statictext", undefined, "対象ファイル数: " + files.length);
var progressBar = progressWin.add("progressbar", undefined, 0, 100);
progressBar.preferredSize = [400, 20];
var statusText = progressWin.add("statictext", undefined, "Photoshop と通信中...");
statusText.preferredSize.width = 400;
progressWin.show();
progressWin.update();
// Photoshop を前面化(実行時のみ)
try {
var btActivate = new BridgeTalk();
btActivate.target = selectedPhotoshopSpec;
btActivate.body = "app.bringToFront();";
btActivate.send();
$.sleep(300);
} catch (_) {}
var ps = buildPsRunMultipleActionsScript(actions, files);
// BridgeTalk送信(同期待ち)
var completed = false;
var resultBody = null;
var errorBody = null;
var bt = new BridgeTalk();
bt.target = selectedPhotoshopSpec;
bt.body = ps;
bt.onResult = function (res) {
completed = true;
resultBody = res.body;
};
bt.onError = function (e) {
completed = true;
errorBody = e && e.body ? e.body : String(e);
};
try {
bt.send(120); // 2分タイムアウト(送信側)
} catch (e) {
try { progressWin.close(); } catch (_) {}
try { app.bringToFront(); } catch (_) {}
alert("BridgeTalk送信エラー: " + e);
return;
}
// 待機しつつプログレス表示
var progress = 0;
var maxWaitTime = 120000; // 最大2分待機
var startTime = new Date().getTime();
while (!completed) {
var elapsed = new Date().getTime() - startTime;
if (elapsed > maxWaitTime) {
try { progressWin.close(); } catch (_) {}
try { app.bringToFront(); } catch (_) {}
alert("タイムアウト: Photoshop からの応答がありません。");
return;
}
progress = (progress + 2) % 100;
progressBar.value = progress;
statusText.text = "処理中... (" + Math.floor(elapsed / 1000) + "秒経過)";
progressWin.update();
$.sleep(100);
}
try { progressWin.close(); } catch (_) {}
// Illustratorへ制御を戻す(前面化)
try { app.bringToFront(); } catch (_) {}
if (errorBody) {
alert("Photoshop 通信エラー:\n" + errorBody);
return;
}
if (resultBody) {
var lines = String(resultBody || "").split(/\r\n|\r|\n/);
var ok = 0, openFail = 0, actFail = 0, err = 0;
var details = [];
for (var i = 0; i < lines.length; i++) {
var s = lines[i];
if (s.indexOf("ok\t") === 0) ok++;
else if (s.indexOf("openFail\t") === 0) { openFail++; details.push(s); }
else if (s.indexOf("actionFail\t") === 0) { actFail++; details.push(s); }
else if (s.indexOf("error\t") === 0) { err++; details.push(s); }
}
try { app.redraw(); } catch (_) {}
var msg =
"アクション実行が完了しました。\n\n" +
"成功: " + ok + "\n" +
"開けない: " + openFail + "\n" +
"アクション失敗: " + actFail + "\n" +
"その他エラー: " + err;
if (details.length > 0) {
msg += "\n\n--- 詳細(先頭10件) ---\n";
for (var j = 0; j < Math.min(10, details.length); j++) {
msg += details[j] + "\n";
}
if (details.length > 10) {
msg += "...(他 " + (details.length - 10) + " 件)";
}
}
alert(msg);
}
}
// =========================================================
// PlacedItems収集
// =========================================================
function getSelectionPageItemsArray(doc) {
var sel = null;
try { sel = doc.selection; } catch (_) { sel = null; }
if (!sel) return [];
try {
if (sel.typename && sel.typename === "TextRange") return [];
} catch (_) {}
try {
if (sel instanceof Array) return sel;
} catch (_) {}
try {
if (typeof sel.length === "number" && sel.length > 0 && sel[0] && sel[0].typename) {
var arr = [];
for (var i = 0; i < sel.length; i++) arr.push(sel[i]);
return arr;
}
} catch (_) {}
try {
if (sel.typename) return [sel];
} catch (_) {}
return [];
}
function collectPlacedItems(doc) {
var out = [];
var selArr = getSelectionPageItemsArray(doc);
if (selArr.length > 0) {
for (var i = 0; i < selArr.length; i++) {
collectPlacedItemsRecursive(selArr[i], 0, out);
}
} else {
for (var j = 0; j < doc.placedItems.length; j++) {
try { out.push(doc.placedItems[j]); } catch (_) {}
}
}
var filtered = [];
for (var k = 0; k < out.length; k++) {
try {
var it = out[k];
if (it && it.typename === "PlacedItem" && !it.embedded) {
var f = it.file;
if (f) filtered.push(it);
}
} catch (_) {}
}
return filtered;
}
function collectPlacedItemsRecursive(item, depth, out) {
if (depth > 20) return;
if (!item) return;
try {
if (item.typename === "PlacedItem") {
out.push(item);
return;
}
} catch (_) {}
try {
if (item.pageItems && item.pageItems.length) {
for (var i = 0; i < item.pageItems.length; i++) {
collectPlacedItemsRecursive(item.pageItems[i], depth + 1, out);
}
}
} catch (_) {}
}
// =========================================================
// BridgeTalk 送信
// =========================================================
function sendToPhotoshop(psBody, onOk, onErr) {
var bt = new BridgeTalk();
if (!ensurePhotoshopRunning()) {
if (onErr) onErr("Photoshop が起動していません。");
return;
}
bt.target = selectedPhotoshopSpec;
bt.body = psBody;
__btPool.push(bt);
function releaseBt(obj) {
for (var i = __btPool.length - 1; i >= 0; i--) {
if (__btPool[i] === obj) {
__btPool.splice(i, 1);
break;
}
}
}
bt.onResult = function (res) {
releaseBt(bt);
try {
if (onOk) onOk(res.body);
} catch (e) {
alert("受信結果の処理でエラー:\n" + e);
}
};
bt.onError = function (e) {
releaseBt(bt);
try {
if (onErr) onErr(e && e.body ? e.body : String(e));
} catch (_) {
alert("Photoshop 通信エラー");
}
};
try {
bt.send(60);
} catch (e) {
if (onErr) onErr("BridgeTalk送信エラー: " + e);
releaseBt(bt);
return;
}
}
// =========================================================
// Photoshop側:アクション列挙スクリプト
// =========================================================
function buildPsListActionsScript() {
var ps = [];
ps.push("(function(){");
ps.push("try {");
ps.push(" function cID(s){ return charIDToTypeID(s); }");
ps.push(" function sID(s){ return stringIDToTypeID(s); }");
ps.push(" function getActionSetCount(){");
ps.push(" try {");
ps.push(" var ref = new ActionReference();");
ps.push(" ref.putProperty(cID('Prpr'), sID('numberOfActionSets'));");
ps.push(" ref.putEnumerated(cID('capp'), cID('Ordn'), cID('Trgt'));");
ps.push(" var desc = executeActionGet(ref);");
ps.push(" if (desc.hasKey(sID('numberOfActionSets'))) return desc.getInteger(sID('numberOfActionSets'));");
ps.push(" } catch(e1) {}");
ps.push(" var count = 0;");
ps.push(" for (var i = 1; i <= 200; i++) {");
ps.push(" try {");
ps.push(" var r = new ActionReference();");
ps.push(" r.putIndex(cID('ASet'), i);");
ps.push(" executeActionGet(r);");
ps.push(" count = i;");
ps.push(" } catch(e) { break; }");
ps.push(" }");
ps.push(" return count;");
ps.push(" }");
ps.push(" function getActionSetInfo(setIndex){");
ps.push(" var ref = new ActionReference();");
ps.push(" ref.putIndex(cID('ASet'), setIndex);");
ps.push(" var desc = executeActionGet(ref);");
ps.push(" var obj = { name: '', count: 0 };");
ps.push(" try { obj.name = desc.getString(cID('Nm ')); } catch(e) { obj.name = '(Set ' + setIndex + ')'; }");
ps.push(" try { obj.count = desc.getInteger(cID('NmbC')); } catch(e) { obj.count = 0; }");
ps.push(" return obj;");
ps.push(" }");
ps.push(" function getActionName(setIndex, actionIndex){");
ps.push(" var ref = new ActionReference();");
ps.push(" ref.putIndex(cID('Actn'), actionIndex);");
ps.push(" ref.putIndex(cID('ASet'), setIndex);");
ps.push(" var desc = executeActionGet(ref);");
ps.push(" try { return desc.getString(cID('Nm ')); } catch(e) { return '(Action ' + actionIndex + ')'; }");
ps.push(" }");
ps.push(" var sets = [];");
ps.push(" var setCount = getActionSetCount();");
ps.push(" for (var i = 1; i <= setCount; i++) {");
ps.push(" try {");
ps.push(" var setInfo = getActionSetInfo(i);");
ps.push(" var actions = [];");
ps.push(" for (var j = 1; j <= setInfo.count; j++) {");
ps.push(" try { actions.push(getActionName(i, j)); } catch(e2) {}");
ps.push(" }");
ps.push(" sets.push({ name: setInfo.name, actions: actions });");
ps.push(" } catch(e3) {}");
ps.push(" }");
ps.push(" var result = { sets: sets };");
ps.push(" if (typeof JSON !== 'undefined' && JSON.stringify) return JSON.stringify(result);");
ps.push(" return result.toSource();");
ps.push("} catch(mainError) {");
ps.push(" var errObj = { error: String(mainError.message || mainError), sets: [] };");
ps.push(" if (typeof JSON !== 'undefined' && JSON.stringify) return JSON.stringify(errObj);");
ps.push(" return errObj.toSource();");
ps.push("}");
ps.push("})();");
return ps.join("\n");
}
// =========================================================
// Photoshop側:複数アクション実行スクリプト
// =========================================================
function buildPsRunMultipleActionsScript(actions, fsNameList) {
var ps = [];
ps.push("(function(){");
ps.push("try {");
ps.push(" var startTime = new Date();");
ps.push(" var actionsList = [];");
for (var i = 0; i < actions.length; i++) {
ps.push(" actionsList.push({");
ps.push(" setName: \"" + escJs(actions[i].setName) + "\",");
ps.push(" actionName: \"" + escJs(actions[i].actionName) + "\"");
ps.push(" });");
}
ps.push(" var filePaths = [];");
for (var j = 0; j < fsNameList.length; j++) {
ps.push(" filePaths.push(\"" + escJs(fsNameList[j]) + "\");");
}
ps.push(" var results = [];");
ps.push(" var __oldDlg = app.displayDialogs;");
ps.push(" try { app.displayDialogs = DialogModes.NO; } catch(_) {}");
ps.push(" for (var i = 0; i < filePaths.length; i++) {");
ps.push(" var fPath = filePaths[i];");
ps.push(" var f = null;");
ps.push(" var doc = null;");
ps.push(" try {");
ps.push(" f = new File(fPath);");
ps.push(" if (!f.exists) {");
ps.push(" results.push('openFail\\t' + fPath + '\\tFile not found');");
ps.push(" continue;");
ps.push(" }");
ps.push(" try {");
ps.push(" doc = app.open(f);");
ps.push(" } catch(eOpen) {");
ps.push(" results.push('openFail\\t' + fPath + '\\t' + (eOpen.message || String(eOpen)));");
ps.push(" continue;");
ps.push(" }");
ps.push(" var allActionsSuccess = true;");
ps.push(" for (var a = 0; a < actionsList.length; a++) {");
ps.push(" try {");
ps.push(" app.doAction(actionsList[a].actionName, actionsList[a].setName);");
ps.push(" } catch(eAct) {");
ps.push(" results.push('actionFail\\t' + fPath + '\\t' + actionsList[a].setName + ' / ' + actionsList[a].actionName + ': ' + (eAct.message || String(eAct)));");
ps.push(" allActionsSuccess = false;");
ps.push(" break;");
ps.push(" }");
ps.push(" }");
ps.push(" if (!allActionsSuccess) {");
ps.push(" try { doc.close(SaveOptions.DONOTSAVECHANGES); } catch(_) {}");
ps.push(" continue;");
ps.push(" }");
ps.push(" try { doc.save(); } catch(_s) {}");
ps.push(" try {");
ps.push(" doc.close(SaveOptions.SAVECHANGES);");
ps.push(" } catch(_c) {");
ps.push(" try { doc.close(SaveOptions.DONOTSAVECHANGES); } catch(__) {}");
ps.push(" }");
ps.push(" results.push('ok\\t' + fPath + '\\t(' + actionsList.length + ' actions)');");
ps.push(" } catch(e) {");
ps.push(" results.push('error\\t' + fPath + '\\t' + (e.message || String(e)));");
ps.push(" try {");
ps.push(" if (app.documents.length > 0) {");
ps.push(" app.activeDocument.close(SaveOptions.DONOTSAVECHANGES);");
ps.push(" }");
ps.push(" } catch(__c) {}");
ps.push(" }");
ps.push(" }");
ps.push(" try { app.displayDialogs = __oldDlg; } catch(_) {}");
ps.push(" var endTime = new Date();");
ps.push(" var elapsed = (endTime - startTime) / 1000;");
ps.push(" results.push('info\\tCompleted in ' + elapsed.toFixed(1) + ' seconds');");
ps.push(" return results.join(\"\\n\");");
ps.push("} catch(mainError) {");
ps.push(" return 'error\\tmainError\\t' + (mainError.message || String(mainError));");
ps.push("}");
ps.push("})();");
return ps.join("\n");
}
// =========================================================
// Photoshop 起動判定(「起動しているPS」を拾う)
// =========================================================
function detectRunningPhotoshopSpecifier() {
var specs = [];
var seen = {};
function addSpec(s) {
if (!s) return;
if (seen[s]) return;
seen[s] = true;
specs.push(s);
}
var targets = null;
try { targets = BridgeTalk.getTargets(null, null); } catch (_) { targets = null; }
if (!targets || !targets.length) {
try { targets = BridgeTalk.getTargets(null); } catch (_) { targets = null; }
}
if (!targets || !targets.length) {
try { targets = BridgeTalk.getTargets(); } catch (_) { targets = null; }
}
if (targets && targets.length) {
for (var i = 0; i < targets.length; i++) {
var t = "";
try { t = String(targets[i] || ""); } catch (_) { t = ""; }
if (!t) continue;
var tl = t.toLowerCase();
if (tl.indexOf("photoshop-") === 0) {
addSpec(t);
}
}
}
addSpec("photoshop");
var running = [];
for (var j = 0; j < specs.length; j++) {
var sp = specs[j];
var isRun = false;
try { isRun = BridgeTalk.isRunning(sp); } catch (_) { isRun = false; }
if (isRun) running.push(sp);
}
if (!running.length) {
var baseRun = false;
try { baseRun = BridgeTalk.isRunning("photoshop"); } catch (_) { baseRun = false; }
if (baseRun) return "photoshop";
return null;
}
if (running.length === 1) return running[0];
running.sort(function (a, b) {
var va = parseVersionFromSpecifier(a);
var vb = parseVersionFromSpecifier(b);
if (va > vb) return -1;
if (va < vb) return 1;
if (a < b) return -1;
if (a > b) return 1;
return 0;
});
return selectFromRunningPhotoshop(running);
}
function parseVersionFromSpecifier(spec) {
var s = String(spec || "");
var p = s.indexOf("-");
if (p < 0) return 0;
s = s.substring(p + 1);
var m = s.match(/^([0-9]+(?:\.[0-9]+)?)/);
if (!m) return 0;
var v = parseFloat(m[1]);
if (isNaN(v)) return 0;
return v;
}
function getDisplayNameSafe(spec) {
try {
var dn = BridgeTalk.getDisplayName(spec);
if (dn) return String(dn);
} catch (_) {}
return String(spec);
}
function selectFromRunningPhotoshop(runningSpecs) {
var w = new Window("dialog", "起動中のPhotoshopを選択");
w.orientation = "column";
w.alignChildren = ["fill", "top"];
w.spacing = 10;
w.margins = 15;
var info = w.add("statictext", undefined, "複数の Photoshop が起動しています。使用するものを選択してください:");
info.preferredSize.width = 420;
var lb = w.add("listbox", undefined, []);
lb.preferredSize = [420, 160];
for (var i = 0; i < runningSpecs.length; i++) {
var sp = runningSpecs[i];
var nm = getDisplayNameSafe(sp);
var it = lb.add("item", nm + " [" + sp + "]");
it._spec = sp;
}
lb.selection = 0;
var g = w.add("group");
g.orientation = "row";
g.alignChildren = ["center", "center"];
var ok = g.add("button", undefined, "OK");
var cancel = g.add("button", undefined, "キャンセル");
var selected = null;
ok.onClick = function () {
if (lb.selection) selected = lb.selection._spec;
w.close();
};
cancel.onClick = function () {
selected = null;
w.close();
};
w.show();
return selected;
}
// =========================================================
// JSON パース / エスケープ
// =========================================================
function parseJsonSafe(s) {
if (s === null || s === undefined) return null;
s = String(s);
try {
if (typeof JSON !== "undefined" && JSON.parse) return JSON.parse(s);
} catch (_) {}
try {
return eval("(" + s + ")");
} catch (e) {
throw e;
}
}
function escJs(s) {
var r = String(s);
r = r.replace(/\\/g, "\\\\");
r = r.replace(/"/g, '\\"');
r = r.replace(/\r/g, "\\r").replace(/\n/g, "\\n");
return r;
}
// =========================================================
// ユーティリティ
// =========================================================
function cloneActionList(list) {
var out = [];
if (!list || !list.length) return out;
for (var i = 0; i < list.length; i++) {
var it = list[i];
if (!it) continue;
var s = (it.setName !== undefined) ? String(it.setName) : "";
var a = (it.actionName !== undefined) ? String(it.actionName) : "";
if (!s || !a) continue;
out.push({ setName: s, actionName: a });
}
return out;
}
function trimStr(s) {
s = String(s === undefined || s === null ? "" : s);
return s.replace(/^\s+/, "").replace(/\s+$/, "");
}
function pad2(n) {
n = Math.floor(n);
return (n < 10 ? "0" : "") + String(n);
}
function formatDate(d) {
try {
return d.getFullYear() + "-" + pad2(d.getMonth() + 1) + "-" + pad2(d.getDate()) +
" " + pad2(d.getHours()) + ":" + pad2(d.getMinutes()) + ":" + pad2(d.getSeconds());
} catch (_) {
return "";
}
}
// =========================================================
// setTimeout実装(ExtendScript用)
// =========================================================
function setTimeout(func, delay) {
var startTime = new Date().getTime();
while (new Date().getTime() - startTime < delay) {
// ビジーウェイト
}
func();
}
})();