Files
2026-09-06 20:59:27 +03:00

593 lines
22 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { app } from "../../../scripts/app.js";
import { IToolsUI } from "./itools_ui.js";
const DEFAULT_TEMPLATES = [
{
id: "bg_swap",
title: "🖼️ Background Swap",
text: `Background replacement. Replace the environment with [NEW BG] while maintaining absolute consistency
of the subject's facial features, outfit, pose, and original camera perspective. Seamless edge blending.`,
},
{
id: "env_mood",
title: "🌙 Time & Mood",
text: `Environmental relighting. Change the scene atmosphere to [TIME | WEATHER] with
[LIGHTING STYLE]. Preserve the original subject geometry and composition layout.`,
},
{
id: "style_trans",
title: "🎨 Style Transform",
text: `Neural style transfer. Convert the image into [STYLE].
Retain the global composition, spatial structure, and core [SUBJECT] design elements.`,
},
{
id: "mat_change",
title: "🧶 Material/Texture",
text: `Physically-based rendering (PBR) texture modification. Change the surface material of [SUBJECT] to [MATERIAL].
Maintain identical mesh topology, shadows, and proportions.`,
},
{
id: "pose_mod",
title: "🏃 Pose Modification",
text: `Rigging and pose adjustment. Modify the subject's pose to [NEW POSE].
Ensure strict character consistency, maintaining the exact outfit details, face ID, and environmental lighting.`,
},
{
id: "expr_edit",
title: "😊 Expression Edit",
text: `Facial expression synthesis. Alter the expression to [EXPRESSION].
Keep the bone structure, eye color, and head orientation (yaw, pitch, roll) unchanged.`,
},
{
id: "outfit_chg",
title: "👕 Wardrobe Change",
text: `Garment replacement. Change the outfit to [NEW OUTFIT].
Maintain the subject's anatomical proportions, identity (Face ID), and current action/pose.`,
},
{
id: "logo_mock",
title: "☕ Branding Mockup",
text: `Precision logo mapping. Overlay the provided logo onto [OBJECT].
Apply realistic surface displacement, texture mapping, and ambient occlusion for a natural integrated look.`,
},
{
id: "logo_3d",
title: "💎 3D Logo Effect",
text: `3D extrusion and shading. Transform the 2D logo into a 3D emblem with [MATERIAL].
Maintain the exact vector silhouette with realistic specular highlights and depth of field.`,
},
{
id: "color_grade",
title: "🌈 Color Grading",
text: `Professional color science. Apply a [MOOD] LUT-based palette.
Adjust highlights, midtones, and shadows without altering the underlying image data or shapes.`,
},
{
id: "add_prop",
title: "➕ Add Element",
text: `In-painting and object insertion. Synthesize [ITEM] into the scene.
Match the global illumination, shadows, and noise profile of the original image for perfect integration.`,
},
{
id: "rem_obj",
title: "➖ Remove Element",
text: `Object removal and generative fill. Delete [ITEM] and reconstruct the underlying background
using context-aware synthesis, ensuring no artifacts remain.`,
},
{
id: "high_retouch",
title: "✨ Pro Retouch",
text: `High-end frequency separation. Apply subtle skin retouching: eliminate blemishes, balance skin tones,
and reduce oily specular highlights while preserving micro-texture and pores.`,
},
{
id: "upscale_fix",
title: "🔍 Detail Enhancer",
text: `Generative upscaling. Increase the resolution and inject high-frequency details into [SUBJECT / SCENE].
Improve sharpness and clarity without changing the original design.`,
},
{
id: "lighting_fix",
title: "💡 Light Correction",
text: `Advanced relighting. Adjust the scene's [BRIGHTNESS | CONTRAST | COLOR TEMP] using global illumination principles.
Preserve shadow density, specular reflections, and the original subject's structural integrity.`,
},
{
id: "background_blur",
title: "💧 Depth Blur",
text: `Optical depth-of-field simulation. Apply a [BACKGROUND | FOREGROUND] Gaussian or lens blur.
Maintain sub-pixel subject sharpness and render a realistic bokeh diaphragm effect.`,
},
{
id: "reflections_add",
title: "🪞 Add Reflection",
text: `Ray-traced reflection synthesis. Generate realistic reflections of [SUBJECT] on [SURFACE].
Account for surface roughness, Fresnel effects, and accurate perspective vanishing points.`,
},
{
id: "weather_effect",
title: "☔ Weather Effects",
text: `Atmospheric particle simulation. Superimpose [RAIN | SNOW | FOG] onto the scene.
Synchronize volumetric lighting, occlusion, and surface wetness/accumulation with the environment.`,
},
{
id: "shadow_overlay",
title: "🌒 Shadow Overlay",
text: `Light masking and GOBOS. Overlay a [PATTERN | SHADOW TYPE] shadow onto [FACE | OBJECT].
Adjust the penumbra softness and opacity to match the environment's ambient occlusion and light source direction.`,
},
{
id: "lens_effects",
title: "🎥 Lens Effects",
text: `Cinematic optical artifacts. Apply [FLARE | BOKEH | DISTORTION] mimicking high-end prime lenses.
Maintain chromatic accuracy and natural geometric perspective.`,
},
{
id: "cam_angle",
title: "📸 Camera Angle",
text: `Virtual cinematography. Re-render the scene from a [ANGLE] perspective.
Maintain 'Temporal Consistency' of all textures and character details while shifting the focal point.`,
},
{
id: "environment_map",
title: "🏞️ Environment Map",
text: `HDRI environment remapping. Generate a cohesive lighting wrap for [SCENE | SUBJECT].
Synchronize all specular highlights, ambient light bounce, and environmental reflections.`,
},
{
id: "motion_blur",
title: "🍃 Motion Blur",
text: `Kinetic blur synthesis. Add directional motion blur to [SUBJECT | OBJECT] based on a virtual movement vector.
Ensure temporal edge blending while keeping static background elements perfectly sharp.`,
},
];
app.registerExtension({
name: "iTools.instructorNode",
async nodeCreated(node) {
if (node.comfyClass !== "iToolsInstructorNode") return;
let selectedItems = new Set();
let dynamicData = {};
let showSelectedOnly = false;
const getUserTemplates = () =>
JSON.parse(localStorage.getItem("iTools_userTemplates") || "[]");
const saveUserTemplates = (templates) =>
localStorage.setItem("iTools_userTemplates", JSON.stringify(templates));
const getTemplateOrder = () =>
JSON.parse(localStorage.getItem("iTools_templateOrder") || "[]");
const saveTemplateOrder = (order) =>
localStorage.setItem("iTools_templateOrder", JSON.stringify(order));
const getMergedTemplates = () => {
const userTemplates = getUserTemplates();
const merged = DEFAULT_TEMPLATES.map((dt) => {
const userVersion = userTemplates.find((ut) => ut.id === dt.id);
return userVersion || dt;
});
const newOnes = userTemplates.filter(
(ut) => !DEFAULT_TEMPLATES.find((dt) => dt.id === ut.id),
);
const all = [...merged, ...newOnes];
const order = getTemplateOrder();
if (order.length > 0) {
all.sort((a, b) => {
const idxA = order.indexOf(a.id);
const idxB = order.indexOf(b.id);
if (idxA === -1 && idxB === -1) return 0;
if (idxA === -1) return 1;
if (idxB === -1) return -1;
return idxA - idxB;
});
}
return all;
};
const showToast = (severity, summary, detail) => {
app.extensionManager.toast.add({ severity, summary, detail, life: 2000 });
};
const container = document.createElement("div");
container.style.cssText = `
display: flex;
flex-direction: column;
gap: 8px;
padding: 5px;
height: 100%;
background: #1c1c1c;
border-radius: 8px;
color: white;
font-family: sans-serif;
`;
const header = document.createElement("div");
header.style.cssText = `display: flex; gap: 4px; min-width: 0;`;
const searchInput = document.createElement("input");
searchInput.placeholder = "Search instructions...";
searchInput.style.cssText = `flex: 1 1 0; min-width: 0; width: 0; padding: 5px; border-radius: 4px; border: 1px solid #444; background: #333; color: white; outline: none;`;
const addBtn = document.createElement("button");
addBtn.innerHTML = "+";
addBtn.title = "Add Instruction";
addBtn.style.cssText = `flex: 0 0 auto; min-width: 30px; padding: 0 8px; background: #444; border: none; border-radius: 4px; color: white; cursor: pointer;`;
const toggleFilterBtn = document.createElement("button");
const inactiveEmoji = "⛶";
const activeEmoji = "☐";
const updateFilterBtnUI = () => {
toggleFilterBtn.innerHTML = showSelectedOnly
? activeEmoji
: inactiveEmoji;
toggleFilterBtn.style.background = showSelectedOnly ? "#888" : "#444";
};
updateFilterBtnUI();
toggleFilterBtn.title = "Show Selected Only";
toggleFilterBtn.style.cssText = `flex: 0 0 auto; min-width: 30px; padding: 0 8px; background: #444; border: none; border-radius: 4px; color: white; cursor: pointer; font-size: 14px;`;
// Logic for Toggle Filter Button
toggleFilterBtn.onclick = () => {
// If user tries to filter but nothing is selected
if (!showSelectedOnly && selectedItems.size === 0) {
showToast(
"warn",
"Selection Required",
"Please select at least one instruction from the list.",
);
return; // Just exit, don't change state
}
showSelectedOnly = !showSelectedOnly;
updateFilterBtnUI();
renderList(searchInput.value);
};
header.appendChild(searchInput);
header.appendChild(toggleFilterBtn);
header.appendChild(addBtn);
const listContainer = document.createElement("div");
listContainer.style.cssText = `flex: 1; overflow-y: auto; border: 1px solid #444; border-radius: 4px; background: #252525;`;
const editTemplate = async (existing = null) => {
const isDefault =
existing && DEFAULT_TEMPLATES.some((dt) => dt.id === existing.id);
let title = existing ? existing.title : "";
// Only prompt for title if it's NOT a default template
if (!isDefault) {
title = await IToolsUI.prompt({
title: "Template Settings",
message: "Enter Instruction Title:",
default: title,
});
if (title === null) return;
if (title === "") {
showToast("error", "Error", "Instruction title cannot be empty.");
return;
}
}
const text = await IToolsUI.prompt({
title: "Template Settings",
message: isDefault ? `Edit Instruction:` : "Enter Instruction Text:",
type: "textarea",
hint: "ⓘ Hint: Use your own variables inside brackets [].",
default: existing ? existing.text : "",
});
if (text === null) return;
if (text === "") {
showToast("error", "Error", "Instruction text cannot be empty.");
return;
}
let userTemplates = getUserTemplates();
const templateId = existing ? existing.id : "user_" + Date.now();
const idx = userTemplates.findIndex((t) => t.id === templateId);
if (idx > -1) {
userTemplates[idx] = { ...userTemplates[idx], title, text };
} else {
userTemplates.push({ id: templateId, title, text, isUser: !isDefault });
}
saveUserTemplates(userTemplates);
showToast(
"success",
"Saved",
`${title} ${isDefault ? "updated" : "added"}.`,
);
renderList(searchInput.value);
};
const deleteOrReset = async (template) => {
const isDefault = DEFAULT_TEMPLATES.some((dt) => dt.id === template.id);
const actionText = isDefault ? "reset" : "delete";
const wantAction = await IToolsUI.confirm({
title: `${isDefault ? "Reset" : "Delete"} Template`,
message: `Are you sure you want to ${actionText} ${template.title} ?`,
type: isDefault ? "reset" : "delete",
});
if (wantAction) {
let userTemplates = getUserTemplates();
const updated = userTemplates.filter((ut) => ut.id !== template.id);
saveUserTemplates(updated);
showToast("success", "Success", `${template.title} ${actionText}ed.`);
renderList(searchInput.value);
}
};
const renderList = (filter = "") => {
listContainer.innerHTML = "";
const allTemplates = getMergedTemplates();
const userTemplates = getUserTemplates();
allTemplates
.filter((t) => {
const matchesSearch = t.title
.toLowerCase()
.includes(filter.toLowerCase());
const matchesSelection = !showSelectedOnly || selectedItems.has(t.id);
return matchesSearch && matchesSelection;
})
.forEach((template) => {
const itemContainer = document.createElement("div");
itemContainer.style.cssText = `border-bottom: 1px solid #333; padding: 8px; display: flex; flex-direction: column; gap: 5px; position: relative; transition: background 0.2s;`;
// Drop targets stay on the container
itemContainer.addEventListener("dragover", (e) => {
e.preventDefault();
itemContainer.style.background = "#2a2a2a";
});
itemContainer.addEventListener("dragleave", () => {
itemContainer.style.background = "transparent";
});
itemContainer.addEventListener("drop", (e) => {
e.preventDefault();
itemContainer.style.background = "transparent";
const draggedId = e.dataTransfer.getData("text/plain");
const targetId = template.id;
if (draggedId === targetId) return;
const all = getMergedTemplates();
const order = all.map((t) => t.id);
const fromIdx = order.indexOf(draggedId);
const toIdx = order.indexOf(targetId);
order.splice(fromIdx, 1);
order.splice(toIdx, 0, draggedId);
saveTemplateOrder(order);
renderList(searchInput.value);
});
const row = document.createElement("div");
row.style.cssText = `display: flex; align-items: center; gap: 8px; cursor: pointer; padding-right: 55px;`;
const dragHandle = document.createElement("div");
dragHandle.innerHTML = "⋮⋮";
dragHandle.title = "Drag to reorder";
dragHandle.style.cssText = `cursor: grab; color: #555; font-size: 14px; margin-right: 2px; user-select: none;`;
dragHandle.draggable = true;
dragHandle.addEventListener("dragstart", (e) => {
e.dataTransfer.setData("text/plain", template.id);
if (e.dataTransfer.setDragImage) {
e.dataTransfer.setDragImage(itemContainer, 15, 15);
}
itemContainer.style.opacity = "0.4";
itemContainer.style.background = "#333";
});
dragHandle.addEventListener("dragend", () => {
itemContainer.style.opacity = "1";
itemContainer.style.background = "transparent";
});
const cb = document.createElement("input");
cb.type = "checkbox";
cb.checked = selectedItems.has(template.id);
cb.style.pointerEvents = "none";
const label = document.createElement("span");
label.textContent = template.title;
label.style.fontSize = "12px";
label.style.fontWeight = "bold";
// Action Buttons
const actions = document.createElement("div");
actions.style.cssText = `position: absolute; right: 8px; top: 8px; display: flex; gap: 10px; align-items: center;`;
const editBtn = document.createElement("div");
editBtn.innerHTML = "✎";
editBtn.style.cssText = `cursor: pointer; color: #888; font-size: 14px;`;
editBtn.onclick = async (e) => {
e.stopPropagation();
editTemplate(template);
};
actions.appendChild(editBtn);
// Delete/Reset logic
const isUserCreated = userTemplates.find(
(ut) => ut.id === template.id,
)?.isUser;
const isEditedDefault =
DEFAULT_TEMPLATES.some((dt) => dt.id === template.id) &&
userTemplates.some((ut) => ut.id === template.id);
if (isUserCreated) {
const delBtn = document.createElement("div");
delBtn.innerHTML = "✕";
delBtn.title = "Delete";
delBtn.style.cssText = `cursor: pointer; color: #ff5555; font-size: 14px; font-weight: bold;`;
delBtn.onclick = (e) => {
e.stopPropagation();
deleteOrReset(template);
};
actions.appendChild(delBtn);
} else if (isEditedDefault) {
const resetBtn = document.createElement("div");
resetBtn.innerHTML = "↺";
resetBtn.title = "Reset to Default";
resetBtn.style.cssText = `cursor: pointer; color: #55aaff; font-size: 16px;`;
resetBtn.onclick = (e) => {
e.stopPropagation();
deleteOrReset(template);
};
actions.appendChild(resetBtn);
}
row.appendChild(dragHandle);
row.appendChild(cb);
row.appendChild(label);
itemContainer.appendChild(row);
itemContainer.appendChild(actions);
const inputContainer = document.createElement("div");
inputContainer.style.cssText = `display: ${cb.checked ? "flex" : "none"}; flex-direction: column; gap: 5px; margin-left: 22px;`;
const placeholders = template.text.match(/\[#?([^\]]+)\]/g) || [];
const shouldShow = cb.checked && placeholders.length > 0;
inputContainer.style.cssText = `display: ${shouldShow ? "flex" : "none"}; flex-direction: column; gap: 5px; margin-left: 22px;`;
placeholders.forEach((placeholder) => {
const cleanName = placeholder.replace(/[\[\]]/g, "");
// Container for input + clear button
const inputWrapper = document.createElement("div");
inputWrapper.style.cssText = `position: relative; display: flex; align-items: center;`;
const input = document.createElement("input");
input.placeholder = cleanName;
input.value = dynamicData[template.id]?.[cleanName] || "";
input.style.cssText = `background: #111; border: 1px solid #444; color: #eee; padding: 3px 20px 3px 6px; border-radius: 3px; font-size: 10px; outline: none; width: 100%;`;
// The X button inside the input
const clearX = document.createElement("div");
clearX.innerHTML = "×";
clearX.style.cssText = `position: absolute; right: 5px; cursor: pointer; color: #666; font-size: 14px; line-height: 1; display: ${input.value ? "block" : "none"};`;
// Logic to clear input
clearX.onclick = (ev) => {
ev.stopPropagation();
input.value = "";
if (dynamicData[template.id]) {
delete dynamicData[template.id][cleanName];
}
clearX.style.display = "none";
};
input.oninput = (ev) => {
ev.stopPropagation();
if (!dynamicData[template.id]) dynamicData[template.id] = {};
dynamicData[template.id][cleanName] = input.value;
// Show/hide X based on content
clearX.style.display = input.value ? "block" : "none";
};
inputWrapper.appendChild(input);
inputWrapper.appendChild(clearX);
inputContainer.appendChild(inputWrapper);
});
itemContainer.appendChild(inputContainer);
row.onclick = () => {
const isChecked = !selectedItems.has(template.id);
if (isChecked) {
selectedItems.add(template.id);
} else {
selectedItems.delete(template.id);
// Auto-disable filter if no items are left selected
if (showSelectedOnly && selectedItems.size === 0) {
showSelectedOnly = false;
updateFilterBtnUI();
// Add this line to show everything again
renderList(searchInput.value);
return; // Exit here since renderList already handled the update
}
}
cb.checked = isChecked;
inputContainer.style.display =
isChecked && placeholders.length > 0 ? "flex" : "none";
// Re-render if filtering is active to remove the unchecked item from view
if (showSelectedOnly) {
renderList(searchInput.value);
}
};
listContainer.appendChild(itemContainer);
});
};
addBtn.onclick = () => editTemplate();
searchInput.oninput = (e) => renderList(e.target.value);
container.appendChild(header);
container.appendChild(listContainer);
// init node size
node.size = [310, 380];
let widget = node.addDOMWidget("InstructorWidget", "custom", container, {
tooltip: 'Instructor',
getValue: () => {
const finalStrings = [];
const allTemplates = getMergedTemplates();
// Follow the list order instead of selection order
allTemplates.forEach((template) => {
if (selectedItems.has(template.id)) {
let processedText = template.text.replace(/\s+/g, " ");
const placeholders = template.text.match(/\[#?([^\]]+)\]/g) || [];
placeholders.forEach((placeholder) => {
const cleanName = placeholder.replace(/[\[\]]/g, "");
const val = dynamicData[template.id]?.[cleanName] || placeholder;
processedText = processedText.replace(placeholder, val);
});
finalStrings.push(processedText);
}
});
return {
selected: Array.from(selectedItems),
dynamicData: dynamicData,
showSelectedOnly: showSelectedOnly,
finalText: finalStrings.join("\n\n"),
};
},
setValue: (v) => {
if (v) {
selectedItems = new Set(v.selected || []);
dynamicData = v.dynamicData || {};
showSelectedOnly = !!v.showSelectedOnly;
updateFilterBtnUI();
renderList(searchInput.value);
}
},
});
widget.tooltip = 'Instructor'
renderList();
node.setDirtyCanvas(true, true);
node.onResize = () => {
if (node.size[0] < 310) {
node.size[0] = 310;
}
};
node.onRemoved = () => {
widget = null;
};
},
});