Files
tritant-ComfyUI_Layers_Utility/js/layer_system_final.js
T
2025-07-21 23:36:19 +02:00

273 lines
15 KiB
JavaScript

import { app } from "/scripts/app.js";
const BLEND_MODES = ["normal", "multiply", "screen", "overlay", "soft_light", "hard_light", "difference", "color_dodge", "color_burn"];
const RESIZE_MODES = ["stretch", "fit", "cover", "crop"];
app.registerExtension({
name: "LayerSystem.DynamicLayers",
beforeRegisterNodeDef(nodeType, nodeData) {
if (nodeData.name !== "LayerSystem") {
return;
}
const resizeHeight = function() {
if (!this.size) return;
const newSize = this.computeSize();
this.size[1] = newSize[1];
this.onResize?.(this.size);
this.graph.setDirtyCanvas(true, true);
};
const onConfigure = nodeType.prototype.onConfigure;
nodeType.prototype.onConfigure = function (info) {
onConfigure?.apply(this, arguments);
if (info.widgets_values) {
const p_widget = this.widgets.find(w => w.name === "_properties_json");
const p_index = this.widgets.indexOf(p_widget);
if (p_index > -1 && info.widgets_values[p_index]) {
try { this.layer_properties = JSON.parse(info.widgets_values[p_index]); }
catch (e) { this.layer_properties = {}; }
}
}
};
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
onNodeCreated?.apply(this, arguments);
if (!this.layer_properties) { this.layer_properties = {}; }
const p_widget = this.widgets.find(w => w.name === "_properties_json");
if (p_widget) {
p_widget.inputEl.style.display = "none";
p_widget.computeSize = () => [0, -4];
}
setTimeout(() => this.refreshUI(), 0);
};
const onConnectionsChange = nodeType.prototype.onConnectionsChange;
nodeType.prototype.onConnectionsChange = function (side, slot, is_connected, link_info, io_slot) {
onConnectionsChange?.apply(this, arguments);
if (side === 1) {
setTimeout(() => this.refreshUI(), 0);
}
};
nodeType.prototype.refreshUI = function() {
for (const input of this.inputs) {
if (input.link !== null && (input.type === "*" || Array.isArray(input.type))) {
const link = this.graph.links[input.link];
if (link) {
const originNode = this.graph.getNodeById(link.origin_id);
if (originNode?.outputs[link.origin_slot]) {
input.type = originNode.outputs[link.origin_slot].type;
}
}
}
}
this.handleDisconnectedInputs();
this.updateLayerWidgets();
this.ensureWildcardInputs();
this.updatePropertiesJSON();
resizeHeight.call(this);
};
nodeType.prototype.moveLayer = function(layer_index, direction) {
const swap_index = direction === "up" ? layer_index - 1 : layer_index + 1;
const name_A = `layer_${layer_index}`;
const name_B = `layer_${swap_index}`;
const mask_name_A = name_A.replace("layer_", "mask_");
const mask_name_B = name_B.replace("layer_", "mask_");
const props_A = this.layer_properties[name_A];
this.layer_properties[name_A] = this.layer_properties[name_B];
this.layer_properties[name_B] = props_A;
const input_A = this.inputs.find(i => i.name === name_A);
const input_B = this.inputs.find(i => i.name === name_B);
const mask_input_A = this.inputs.find(i => i.name === mask_name_A);
const mask_input_B = this.inputs.find(i => i.name === mask_name_B);
[input_A.link, input_B.link] = [input_B.link, input_A.link];
[mask_input_A.link, mask_input_B.link] = [mask_input_B.link, mask_input_A.link];
if (this.graph.links[input_A.link]) this.graph.links[input_A.link].target_slot = this.inputs.indexOf(input_A);
if (this.graph.links[input_B.link]) this.graph.links[input_B.link].target_slot = this.inputs.indexOf(input_B);
if (this.graph.links[mask_input_A.link]) this.graph.links[mask_input_A.link].target_slot = this.inputs.indexOf(mask_input_A);
if (this.graph.links[mask_input_B.link]) this.graph.links[mask_input_B.link].target_slot = this.inputs.indexOf(mask_input_B);
this.refreshUI();
this.graph.setDirtyCanvas(true, true);
};
nodeType.prototype.addLayerWidgets = function(layer_name) {
if (!this.layer_properties[layer_name]) {
this.layer_properties[layer_name] = {
blend_mode: "normal", opacity: 1.0, enabled: true,
resize_mode: "fit", scale: 1.0, offset_x: 0, offset_y: 0,
brightness: 0.0, contrast: 0.0,
color_r: 1.0, color_g: 1.0, color_b: 1.0,
saturation: 1.0,
invert_mask: false,
color_section_collapsed: true,
layer_collapsed: false, // État du calque principal
};
}
const props = this.layer_properties[layer_name];
const layer_index = parseInt(layer_name.split("_")[1]);
const total_layers = this.inputs.filter(i => i.name.startsWith("layer_") && i.link !== null).length;
if (layer_index > 1) {
const spacer = this.addWidget("text", `spacer_for_${layer_name}`, "", null, {});
spacer.draw = () => {};
spacer.computeSize = () => [0, 20];
}
// --- SECTION PRINCIPALE REPLIABLE ---
const layerToggle = this.addWidget("toggle", `toggle_layer_${layer_name}`, !props.layer_collapsed, (v) => {
props.layer_collapsed = !v;
updateVisibility();
this.updatePropertiesJSON();
}, { on: `▼ Layer ${layer_index} Settings`, off: `▶ Layer ${layer_index} Settings` });
const collapsibleWidgets = [];
const colorWidgets = [];
// Création des widgets et ajout à la liste des éléments repliables
collapsibleWidgets.push(this.addWidget("toggle", `enabled_${layer_name}`, props.enabled, (v) => { props.enabled = v; this.updatePropertiesJSON(); }, {label: `Layer ${layer_index} Enabled`}));
collapsibleWidgets.push(this.addWidget("combo", `blend_mode_${layer_name}`, props.blend_mode, (v) => { props.blend_mode = v; this.updatePropertiesJSON(); }, {values: BLEND_MODES}));
collapsibleWidgets.push(this.addWidget("number", `opacity_${layer_name}`, props.opacity, (v) => { props.opacity = v; this.updatePropertiesJSON(); }, {min: 0.0, max: 1.0, step: 0.1, precision: 2}));
const colorToggle = this.addWidget("toggle", `toggle_color_${layer_name}`, !props.color_section_collapsed, (v) => {
props.color_section_collapsed = !v;
updateVisibility();
this.updatePropertiesJSON();
}, { on: "▼ Color Adjustments", off: "▶ Color Adjustments" });
collapsibleWidgets.push(colorToggle);
colorWidgets.push(this.addWidget("number", `brightness_${layer_name}`, props.brightness, (v) => { props.brightness = v; this.updatePropertiesJSON(); }, { label: "Brightness", min: -1.0, max: 1.0, step: 0.1, precision: 2 }));
colorWidgets.push(this.addWidget("number", `contrast_${layer_name}`, props.contrast, (v) => { props.contrast = v; this.updatePropertiesJSON(); }, { label: "Contrast", min: -1.0, max: 1.0, step: 0.1, precision: 2 }));
colorWidgets.push(this.addWidget("number", `saturation_${layer_name}`, props.saturation, (v) => { props.saturation = v; this.updatePropertiesJSON(); }, { label: "Saturation", min: 0.0, max: 2.0, step: 0.1, precision: 2 }));
colorWidgets.push(this.addWidget("number", `color_r_${layer_name}`, props.color_r, (v) => { props.color_r = v; this.updatePropertiesJSON(); }, { label: "R", min: 0.0, max: 2.0, step: 0.1, precision: 2 }));
colorWidgets.push(this.addWidget("number", `color_g_${layer_name}`, props.color_g, (v) => { props.color_g = v; this.updatePropertiesJSON(); }, { label: "G", min: 0.0, max: 2.0, step: 0.1, precision: 2 }));
colorWidgets.push(this.addWidget("number", `color_b_${layer_name}`, props.color_b, (v) => { props.color_b = v; this.updatePropertiesJSON(); }, { label: "B", min: 0.0, max: 2.0, step: 0.1, precision: 2 }));
collapsibleWidgets.push(...colorWidgets);
const mask_input_name = layer_name.replace("layer_", "mask_");
const mask_input = this.inputs.find(i => i.name === mask_input_name);
if (mask_input && mask_input.link !== null) {
collapsibleWidgets.push(this.addWidget("toggle", `invert_mask_${layer_name}`, !!props.invert_mask, (v) => { props.invert_mask = v; this.updatePropertiesJSON(); }, { label: "Invert Mask" }));
}
collapsibleWidgets.push(this.addWidget("combo", `resize_mode_${layer_name}`, props.resize_mode, (v) => {
props.resize_mode = v;
updateVisibility(); // Mettre à jour la visibilité des contrôles de transformation
this.updatePropertiesJSON();
}, { values: RESIZE_MODES }));
const transformWidgets = [
this.addWidget("number", `scale_${layer_name}`, props.scale, (v) => { props.scale = v; this.updatePropertiesJSON(); }, { min: 0.01, max: 10.0, step: 0.1, precision: 2 }),
this.addWidget("number", `offset_x_${layer_name}`, props.offset_x, (v) => { props.offset_x = v; this.updatePropertiesJSON(); }, { min: -8192, max: 8192, step: 10 }),
this.addWidget("number", `offset_y_${layer_name}`, props.offset_y, (v) => { props.offset_y = v; this.updatePropertiesJSON(); }, { min: -8192, max: 8192, step: 10 })
];
collapsibleWidgets.push(...transformWidgets);
// Sauvegarder la taille originale pour tous les widgets repliables
for (const w of collapsibleWidgets) {
w.originalComputeSize = w.computeSize;
}
// Les boutons Up/Down ne sont PAS repliables
const up_button = this.addWidget("button", "Up", null, () => { this.moveLayer(layer_index, "up"); });
const down_button = this.addWidget("button", "Down", null, () => { this.moveLayer(layer_index, "down"); });
if (layer_index === 1) up_button.disabled = true;
if (layer_index === total_layers) down_button.disabled = true;
// --- LOGIQUE DE VISIBILITÉ ---
const updateVisibility = () => {
const isLayerCollapsed = props.layer_collapsed;
const isColorCollapsed = props.color_section_collapsed;
const showTransformControls = props.resize_mode === 'crop';
for(const w of collapsibleWidgets) {
let isHidden = isLayerCollapsed;
// Logique pour la section couleur
if (colorWidgets.includes(w)) {
isHidden = isLayerCollapsed || isColorCollapsed;
}
if (w === colorToggle) {
isHidden = isLayerCollapsed;
}
// Logique pour la section transformation
if (transformWidgets.includes(w)) {
isHidden = isLayerCollapsed || !showTransformControls;
}
w.hidden = isHidden;
w.computeSize = isHidden ? () => [0, -4] : w.originalComputeSize;
}
resizeHeight.call(this);
};
updateVisibility(); // Appel initial
};
nodeType.prototype.updateLayerWidgets = function() {
this.widgets = this.widgets.filter(w => w.name === "_properties_json");
const connectedInputs = this.inputs.filter(i => i.name.startsWith("layer_") && i.link !== null);
connectedInputs.sort((a, b) => parseInt(a.name.split("_")[1]) - parseInt(b.name.split("_")[1]));
for (const input of connectedInputs) { this.addLayerWidgets(input.name); }
resizeHeight.call(this);
};
nodeType.prototype.handleDisconnectedInputs = function() {
const connected_layer_names = new Set(this.inputs.filter(i => i.name.startsWith("layer_") && i.link !== null).map(i => i.name));
const inputs_to_remove = [];
const props_to_remove = [];
for (const key in this.layer_properties) {
if (!connected_layer_names.has(key)) {
props_to_remove.push(key);
const layer_input = this.inputs.find(i => i.name === key);
if(layer_input) inputs_to_remove.push(layer_input);
const mask_input = this.inputs.find(i => i.name === key.replace("layer_", "mask_"));
if(mask_input) inputs_to_remove.push(mask_input);
}
}
props_to_remove.forEach(key => delete this.layer_properties[key]);
inputs_to_remove.forEach(i => this.removeInput(this.inputs.indexOf(i)));
const new_props = {};
const remaining_layers = this.inputs.filter(i => i.name.startsWith("layer_"));
const remaining_masks = this.inputs.filter(i => i.name.startsWith("mask_"));
remaining_layers.sort((a,b)=>a.name.localeCompare(b.name)).forEach((input, i) => {
const old_name = input.name;
const new_name = `layer_${i + 1}`;
if(this.layer_properties[old_name]) new_props[new_name] = this.layer_properties[old_name];
const old_mask_name = old_name.replace("layer_", "mask_");
const mask_input = remaining_masks.find(m => m.name === old_mask_name);
if(mask_input) mask_input.name = new_name.replace("layer_", "mask_");
input.name = new_name;
});
this.layer_properties = new_props;
};
nodeType.prototype.ensureWildcardInputs = function () {
const layerInputs = this.inputs.filter(i => i.name.startsWith("layer_"));
const lastLayerInput = layerInputs[layerInputs.length - 1];
if (!lastLayerInput || lastLayerInput.link !== null) {
const newIndex = layerInputs.length + 1;
this.addInput(`layer_${newIndex}`, ["IMAGE", "MASK", "*"]);
this.addInput(`mask_${newIndex}`, "MASK");
}
};
nodeType.prototype.updatePropertiesJSON = function() { const p = this.widgets.find(w => w.name === "_properties_json"); if (p) { p.value = JSON.stringify(this.layer_properties); } };
},
});