diff --git a/web/rs_image-text.js b/web/rs_image-text.js index a60e3a7..dca516d 100644 --- a/web/rs_image-text.js +++ b/web/rs_image-text.js @@ -6,26 +6,26 @@ app.registerExtension({ if (nodeData.name !== "LoadImageWithText") return; const onNodeCreated = nodeType.prototype.onNodeCreated; - - nodeType.prototype.onNodeCreated = function() { + + nodeType.prototype.onNodeCreated = function () { const result = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined; const node = this; - + node.data = { mode: "read", prefix: "Civitai/prompt", text: "", selected_image: "" }; - + const MIN_WIDTH = 360; const MIN_HEIGHT = 520; const PREVIEW_FIXED_HEIGHT = 250; - + node.MIN_WIDTH = MIN_WIDTH; node.MIN_HEIGHT = MIN_HEIGHT; node.setSize([MIN_WIDTH, MIN_HEIGHT]); - + const w_mode = node.widgets?.find(w => w.name === "mode"); const w_prefix = node.widgets?.find(w => w.name === "filename_prefix"); const w_text = node.widgets?.find(w => w.name === "text_input"); @@ -44,13 +44,13 @@ app.registerExtension({ if (w.name === "filename_prefix" && w.value) node.data.prefix = w.value; if (w.name === "text_input" && w.value) node.data.text = w.value; if (w.name === "image" && w.value) node.data.selected_image = w.value; - + w.hidden = true; w.type = "hidden"; w.computeSize = () => [0, 0]; w.computedHeight = 0; - w.draw = function() {}; - + w.draw = function () {}; + if (w.element && w.element.parentNode) { w.element.parentNode.removeChild(w.element); } @@ -65,169 +65,17 @@ app.registerExtension({ }); } - node.syncData = function() { + node.syncData = function () { if (node.graph) { node.graph.setDirtyCanvas(true, true); node.graph.changeTracker?.dispatchEvent(new Event("change")); } }; - node.image = new Image(); node.imageLoaded = false; node.imageLoading = false; - node.dragOver = false; - - node.loadImage = function(imagePath) { - if (!imagePath || node.imageLoading) return; - - node.imageLoading = true; - node.data.selected_image = imagePath; - syncWidget(w_image, 'selected_image'); - - let filename = imagePath; - let subfolder = ""; - if (imagePath.includes("/")) { - const parts = imagePath.split("/"); - subfolder = parts[0]; - filename = parts.slice(1).join("/"); - } - - let imgUrl = `/view?filename=${encodeURIComponent(filename)}&type=input`; - if (subfolder) imgUrl += `&subfolder=${encodeURIComponent(subfolder)}`; - imgUrl += `&t=${Date.now()}`; - - const img = new Image(); - img.onload = () => { - node.image = img; - node.imageLoaded = true; - node.imageLoading = false; - node.syncData(); - }; - img.onerror = () => { - node.imageLoaded = false; - node.imageLoading = false; - node.syncData(); - }; - img.src = imgUrl; - }; - - const uploadFileAndLoad = async (file) => { - if (!file || !file.type.startsWith('image/')) return false; - const formData = new FormData(); - formData.append('image', file); - formData.append('subfolder', ''); - formData.append('type', 'input'); - - try { - const response = await fetch('/upload/image', { method: 'POST', body: formData }); - if (response.ok) { - const result = await response.json(); - const imageName = result.name || result.filename; - const subfolder = result.subfolder || ''; - const finalName = subfolder ? `${subfolder}/${imageName}` : imageName; - node.loadImage(finalName); - return true; - } - } catch (err) { - console.error("[RS Image-Text] Upload error:", err); - } - return false; - }; - - node.triggerFileUpload = function() { - const fileInput = document.createElement('input'); - fileInput.type = 'file'; - fileInput.accept = 'image/png, image/jpeg, image/webp, image/bmp'; - fileInput.onchange = async (e) => { - const file = e.target.files[0]; - if (!file) return; - await uploadFileAndLoad(file); - fileInput.remove(); - }; - fileInput.click(); - }; - - const handleCanvasDrop = (e) => { - if (!e.dataTransfer || !e.dataTransfer.files.length) return; - const rect = app.canvas.canvas.getBoundingClientRect(); - const ds = app.canvas.ds; - const nodeX = rect.left + ((node.pos[0] + ds.offset[0]) * ds.scale); - const nodeY = rect.top + ((node.pos[1] + ds.offset[1]) * ds.scale); - const nodeW = node.size[0] * ds.scale; - const nodeH = node.size[1] * ds.scale; - - if (e.clientX >= nodeX && e.clientX <= nodeX + nodeW && - e.clientY >= nodeY && e.clientY <= nodeY + nodeH) { - e.preventDefault(); - e.stopPropagation(); - const file = e.dataTransfer.files[0]; - if (file && file.type.startsWith('image/')) { - uploadFileAndLoad(file); - } - } - }; - - const handleCanvasDragOver = (e) => { - if (!e.dataTransfer || !e.dataTransfer.files.length) return; - const rect = app.canvas.canvas.getBoundingClientRect(); - const ds = app.canvas.ds; - const nodeX = rect.left + ((node.pos[0] + ds.offset[0]) * ds.scale); - const nodeY = rect.top + ((node.pos[1] + ds.offset[1]) * ds.scale); - const nodeW = node.size[0] * ds.scale; - const nodeH = node.size[1] * ds.scale; - - if (e.clientX >= nodeX && e.clientX <= nodeX + nodeW && - e.clientY >= nodeY && e.clientY <= nodeY + nodeH) { - e.preventDefault(); - e.stopPropagation(); - } - }; - - app.canvas.canvas.addEventListener('dragover', handleCanvasDragOver, { capture: true }); - app.canvas.canvas.addEventListener('drop', handleCanvasDrop, { capture: true }); - const updateUIFromData = () => { - if (prefixInput) prefixInput.value = node.data.prefix || ""; - if (customTextArea) customTextArea.value = node.data.text || ""; - updateModeVisuals(); - }; - - node.onSerialize = function(o) { - syncWidget(w_mode, 'mode'); - syncWidget(w_prefix, 'prefix'); - syncWidget(w_text, 'text'); - syncWidget(w_image, 'selected_image'); - o.data = node.data; - }; - - node.onConfigure = function(o) { - if (o && o.data) { - node.data = { ...node.data, ...o.data }; - } - - node.imageLoading = false; - node.imageLoaded = false; - - if (node.data.selected_image) { - node.loadImage(node.data.selected_image); - } - - updateUIFromData(); - - syncWidget(w_mode, 'mode'); - syncWidget(w_prefix, 'prefix'); - syncWidget(w_text, 'text'); - syncWidget(w_image, 'selected_image'); - }; - - node.visibilityHandler = function() { - if (!document.hidden && node.graph) { - node.graph.setDirtyCanvas(true, true); - node.syncData(); - } - }; - document.addEventListener("visibilitychange", node.visibilityHandler); - + // --- DOM UI --- const mainContainer = document.createElement("div"); mainContainer.style.cssText = ` width: 100%; @@ -240,9 +88,10 @@ app.registerExtension({ pointer-events: none; `; + // Mode buttons const modeContainer = document.createElement("div"); modeContainer.style.cssText = "display:flex; gap:8px; width:100%; pointer-events: auto;"; - + const createModeBtn = (label, value) => { const btn = document.createElement("button"); btn.textContent = label; @@ -259,7 +108,7 @@ app.registerExtension({ const btnWrite = createModeBtn("✏️ WRITE to IMAGE", "write"); modeContainer.appendChild(btnRead); modeContainer.appendChild(btnWrite); - + const updateModeVisuals = () => { const isRead = node.data.mode === "read"; btnRead.style.backgroundColor = isRead ? "#4CAF50" : "#333"; @@ -271,35 +120,37 @@ app.registerExtension({ }; updateModeVisuals(); + // Prefix input const prefixContainer = document.createElement("div"); prefixContainer.style.cssText = "display:flex; align-items:center; gap:8px; width:100%; pointer-events: auto;"; - + const prefixLabel = document.createElement("label"); prefixLabel.textContent = "PREFIX:"; prefixLabel.style.cssText = "color:#aaa; font-size:14px; font-weight:normal; white-space:nowrap; pointer-events: auto; cursor:default;"; - + const prefixInput = document.createElement("input"); prefixInput.type = "text"; prefixInput.value = node.data.prefix; prefixInput.style.cssText = "flex:1; height:28px; padding:0 12px; background:#222; color:#eee; border:1px solid #444; border-radius:4px; font-size:14px; box-sizing:border-box; pointer-events: auto;"; - + prefixInput.oninput = () => { node.data.prefix = prefixInput.value; syncWidget(w_prefix, 'prefix'); node.syncData(); }; - + prefixContainer.appendChild(prefixLabel); prefixContainer.appendChild(prefixInput); + // Text area const textContainer = document.createElement("div"); textContainer.style.cssText = "width:100%; pointer-events: auto;"; - + const customTextArea = document.createElement("textarea"); customTextArea.value = node.data.text; customTextArea.placeholder = "Enter text here..."; customTextArea.style.cssText = "width:100%; min-height:50px; padding:6px; background:#222; color:#eee; border:1px solid #444; border-radius:4px; resize: none; font-family:monospace; font-size:12px; box-sizing:border-box; pointer-events: auto;"; - + customTextArea.oninput = () => { node.data.text = customTextArea.value; syncWidget(w_text, 'text'); @@ -307,16 +158,17 @@ app.registerExtension({ }; textContainer.appendChild(customTextArea); + // Buttons const buttonsContainer = document.createElement("div"); buttonsContainer.style.cssText = "display:flex; gap:8px; width:100%; pointer-events: auto;"; - + const btnUpload = document.createElement("button"); btnUpload.textContent = "📂 UPLOAD IMAGE"; btnUpload.style.cssText = "flex:1; height:28px; background:#2a2a2a; color:#4CAF50; border:2px solid #4CAF50; border-radius:4px; cursor:pointer; font-weight:normal; font-size:12px; transition:all 0.2s; pointer-events: auto;"; btnUpload.onmouseover = () => { btnUpload.style.backgroundColor = "#3a3a3a"; }; btnUpload.onmouseout = () => { btnUpload.style.backgroundColor = "#2a2a2a"; }; btnUpload.onclick = () => node.triggerFileUpload(); - + const btnClear = document.createElement("button"); btnClear.textContent = "🗑️ CLEAR TEXT"; btnClear.style.cssText = "flex:1; height:28px; background:#2a2a2a; color:#dc3545; border:2px solid #dc3545; border-radius:4px; cursor:pointer; font-weight:normal; font-size:12px; transition:all 0.2s; pointer-events: auto;"; @@ -328,52 +180,297 @@ app.registerExtension({ syncWidget(w_text, 'text'); node.syncData(); }; - + buttonsContainer.appendChild(btnUpload); buttonsContainer.appendChild(btnClear); + const previewWrap = document.createElement("div"); + previewWrap.style.cssText = ` + width: 100%; + height: ${PREVIEW_FIXED_HEIGHT}px; + background: #353535; + border: 1px dashed #555; + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + position: relative; + pointer-events: auto; + cursor: pointer; + box-sizing: border-box; + flex-shrink: 0; + margin-top: 28px; + `; + + const placeholder = document.createElement("div"); + placeholder.textContent = "📁 Drop Image Here or Click to Upload"; + placeholder.style.cssText = "color:#888; font-size:14px; font-family:Arial,sans-serif; text-align:center; pointer-events:none; user-select:none;"; + previewWrap.appendChild(placeholder); + + const previewImg = document.createElement("img"); + previewImg.style.cssText = "display:none; width:100%; height:100%; object-fit:contain; pointer-events:none; user-select:none;"; + previewImg.alt = "Image preview"; + previewWrap.appendChild(previewImg); + + const imgInfo = document.createElement("div"); + imgInfo.style.cssText = "width:100%; text-align:center; color:#888; font-size:13px; font-family:sans-serif; pointer-events:none; user-select:none; display:none; margin-top:-1px; margin-bottom:-28px;"; + mainContainer.appendChild(modeContainer); mainContainer.appendChild(prefixContainer); mainContainer.appendChild(textContainer); mainContainer.appendChild(buttonsContainer); - - node.addDOMWidget("custom_widgets", "customtext", mainContainer); + mainContainer.appendChild(imgInfo); + mainContainer.appendChild(previewWrap); + node.addDOMWidget("custom_widgets", "customtext", mainContainer); node.customTextArea = customTextArea; + // --- LOAD / UPLOAD --- + const buildViewUrl = (imagePath) => { + let filename = imagePath; + let subfolder = ""; + if (imagePath.includes("/")) { + const parts = imagePath.split("/"); + subfolder = parts[0]; + filename = parts.slice(1).join("/"); + } + let url = `/view?filename=${encodeURIComponent(filename)}&type=input`; + if (subfolder) url += `&subfolder=${encodeURIComponent(subfolder)}`; + return url + `&t=${Date.now()}`; + }; + + // Запрос текста с бэкенда для режима Read + let promptRequestId = 0; + const fetchTextForRead = async (imagePath) => { + if (node.data.mode !== "read" || !imagePath) return; + + const requestId = ++promptRequestId; + + customTextArea.placeholder = "⏳ Reading text from image..."; + + try { + const response = await fetch(`/rayko/get_prompt?filename=${encodeURIComponent(imagePath)}`); + if (requestId !== promptRequestId) return; + if (!response.ok) throw new Error(`HTTP ${response.status}`); + + const result = await response.json(); + if (requestId !== promptRequestId) return; + + const text = result.prompt || ""; + node.data.text = text; + customTextArea.value = text; + customTextArea.placeholder = text ? "" : "⚠ No text found in this image"; + syncWidget(w_text, 'text'); + node.syncData(); + } catch (err) { + if (requestId !== promptRequestId) return; + customTextArea.placeholder = "⚠ Failed to read text from image"; + console.error("[RS Image-Text] Fetch text error:", err); + } + }; + + node.loadImage = function (imagePath) { + if (!imagePath || node.imageLoading) return; + + node.imageLoading = true; + node.data.selected_image = imagePath; + syncWidget(w_image, 'selected_image'); + + previewImg.onload = () => { + node.imageLoading = false; + node.imageLoaded = true; + previewImg.style.display = "block"; + placeholder.style.display = "none"; + imgInfo.style.display = "block"; + imgInfo.textContent = `${previewImg.naturalWidth} × ${previewImg.naturalHeight}`; + node.syncData(); + + // В режиме Read автоматически извлекаем текст + fetchTextForRead(imagePath); + }; + previewImg.onerror = () => { + node.imageLoading = false; + node.imageLoaded = false; + previewImg.style.display = "none"; + placeholder.style.display = "block"; + imgInfo.style.display = "none"; + node.syncData(); + }; + previewImg.src = buildViewUrl(imagePath); + }; + + const uploadFileAndLoad = async (file) => { + if (!file || !file.type.startsWith('image/')) return false; + const formData = new FormData(); + formData.append('image', file); + formData.append('subfolder', ''); + formData.append('type', 'input'); + + try { + const response = await fetch('/upload/image', { method: 'POST', body: formData }); + if (response.ok) { + const result = await response.json(); + const imageName = result.name || result.filename; + const subfolder = result.subfolder || ''; + const finalName = subfolder ? `${subfolder}/${imageName}` : imageName; + node.loadImage(finalName); + return true; + } + } catch (err) { + console.error("[RS Image-Text] Upload error:", err); + } + return false; + }; + + node.triggerFileUpload = function () { + const fileInput = document.createElement('input'); + fileInput.type = 'file'; + fileInput.accept = 'image/png, image/jpeg, image/webp, image/bmp'; + fileInput.onchange = async (e) => { + const file = e.target.files[0]; + if (!file) return; + await uploadFileAndLoad(file); + fileInput.remove(); + }; + fileInput.click(); + }; + + // --- DRAG & DROP (из примера) --- + const onDocumentDragOver = (e) => { + if (e.dataTransfer && e.dataTransfer.types.includes('Files')) { + e.preventDefault(); + } + }; + const onDocumentDrop = (e) => { + if (e.dataTransfer && e.dataTransfer.types.includes('Files')) { + e.preventDefault(); + } + }; + document.addEventListener('dragover', onDocumentDragOver); + document.addEventListener('drop', onDocumentDrop); + node._docDragOver = onDocumentDragOver; + node._docDrop = onDocumentDrop; + + previewWrap.addEventListener('dragover', (e) => { + if (!e.dataTransfer || !e.dataTransfer.types.includes('Files')) return; + e.preventDefault(); + e.stopPropagation(); + previewWrap.style.borderColor = '#4CAF50'; + previewWrap.style.backgroundColor = '#2a2a2a'; + }); + + previewWrap.addEventListener('dragleave', (e) => { + e.preventDefault(); + e.stopPropagation(); + previewWrap.style.borderColor = '#555'; + previewWrap.style.backgroundColor = '#353535'; + }); + + previewWrap.addEventListener('drop', (e) => { + e.preventDefault(); + e.stopPropagation(); + previewWrap.style.borderColor = '#555'; + previewWrap.style.backgroundColor = '#353535'; + + const file = e.dataTransfer.files[0]; + if (file) { + uploadFileAndLoad(file); + } + }); + + previewWrap.addEventListener('click', () => { + node.triggerFileUpload(); + }); + // -------------------------------------- + + // Serialize / Configure + node.onSerialize = function (o) { + syncWidget(w_mode, 'mode'); + syncWidget(w_prefix, 'prefix'); + syncWidget(w_text, 'text'); + syncWidget(w_image, 'selected_image'); + o.data = node.data; + }; + + node.onConfigure = function (o) { + if (o && o.data) { + node.data = { ...node.data, ...o.data }; + } + + node.imageLoading = false; + node.imageLoaded = false; + + if (node.data.selected_image) { + node.loadImage(node.data.selected_image); + } + + if (prefixInput) prefixInput.value = node.data.prefix || ""; + if (customTextArea) customTextArea.value = node.data.text || ""; + updateModeVisuals(); + + syncWidget(w_mode, 'mode'); + syncWidget(w_prefix, 'prefix'); + syncWidget(w_text, 'text'); + syncWidget(w_image, 'selected_image'); + + requestAnimationFrame(() => { + if (node.size) node.onResize([...node.size]); + }); + }; + + node.visibilityHandler = function () { + if (!document.hidden && node.graph) { + node.graph.setDirtyCanvas(true, true); + node.syncData(); + if (node.size) node.onResize([...node.size]); + } + }; + document.addEventListener("visibilitychange", node.visibilityHandler); + + // Resize const originalOnResize = node.onResize; - node.onResize = function(size) { + node.onResize = function (size) { if (size[0] < MIN_WIDTH) size[0] = MIN_WIDTH; if (size[1] < MIN_HEIGHT) size[1] = MIN_HEIGHT; - + const titleBarHeight = 30; const topPadding = 18; const gap = 8; const bottomPadding = 20; const previewSpacing = 35; - - const fixedElements = titleBarHeight + topPadding + - 28 + gap + 28 + gap + - gap + 28 + - previewSpacing + - PREVIEW_FIXED_HEIGHT + - bottomPadding; - + + const fixedElements = titleBarHeight + topPadding + + 28 + gap + 28 + gap + + gap + 28 + + previewSpacing + + PREVIEW_FIXED_HEIGHT + + bottomPadding; + const textareaHeight = Math.max(50, size[1] - fixedElements); - + if (customTextArea) { customTextArea.style.height = textareaHeight + 'px'; } - + if (originalOnResize) originalOnResize.apply(this, arguments); node.setDirtyCanvas(true, true); }; - - node.onDrawBackground = function(ctx) { + + // Cleanup + const originalOnRemoved = node.onRemoved; + node.onRemoved = function () { + document.removeEventListener("visibilitychange", node.visibilityHandler); + if (node._docDragOver) document.removeEventListener('dragover', node._docDragOver); + if (node._docDrop) document.removeEventListener('drop', node._docDrop); + if (originalOnRemoved) originalOnRemoved.apply(this, arguments); + }; + + // Background only (preview is now DOM) + node.onDrawBackground = function (ctx) { const w = this.size[0]; const h = this.size[1]; const radius = 8; - ctx.beginPath(); ctx.moveTo(radius, 0); ctx.lineTo(w - radius, 0); @@ -385,81 +482,22 @@ app.registerExtension({ ctx.lineTo(0, radius); ctx.quadraticCurveTo(0, 0, radius, 0); ctx.closePath(); - ctx.fillStyle = "#353535"; ctx.fill(); }; - - node.onDrawForeground = function(ctx) { - if (this.flags.collapsed) return; - const [w, h] = this.size; - - const leftRightPadding = 10; - const bottomPadding = 20; - const previewSpacing = 35; - - const previewY = h - bottomPadding - PREVIEW_FIXED_HEIGHT; - const previewWidth = w - (leftRightPadding * 2); - - if (this.imageLoaded && this.image) { - const imgRatio = this.image.width / this.image.height; - let drawW, drawH; - - if (previewWidth / PREVIEW_FIXED_HEIGHT > imgRatio) { - drawH = PREVIEW_FIXED_HEIGHT; - drawW = drawH * imgRatio; - } else { - drawW = previewWidth; - drawH = drawW / imgRatio; - } - - const drawX = leftRightPadding + (previewWidth - drawW) / 2; - const drawY = previewY + (PREVIEW_FIXED_HEIGHT - drawH) / 2; - - ctx.drawImage(this.image, drawX, drawY, drawW, drawH); - ctx.strokeStyle = this.dragOver ? "#4CAF50" : "#444"; - ctx.lineWidth = this.dragOver ? 2 : 1; - ctx.strokeRect(drawX, drawY, drawW, drawH); - - ctx.fillStyle = "#888"; - ctx.font = "15px sans-serif"; - ctx.textAlign = "center"; - ctx.textBaseline = "bottom"; - ctx.fillText(`${this.image.width} × ${this.image.height}`, w / 2, previewY - 5); - } else { - ctx.fillStyle = "#1a1a1a"; - ctx.fillRect(leftRightPadding, previewY, previewWidth, PREVIEW_FIXED_HEIGHT); - ctx.strokeStyle = this.dragOver ? "#4CAF50" : "#333"; - ctx.lineWidth = this.dragOver ? 2 : 1; - ctx.strokeRect(leftRightPadding, previewY, previewWidth, PREVIEW_FIXED_HEIGHT); - - ctx.fillStyle = "#555"; - ctx.font = "14px Arial"; - ctx.textAlign = "center"; - ctx.textBaseline = "middle"; - ctx.fillText("Drop Image Here", w / 2, previewY + PREVIEW_FIXED_HEIGHT / 2); - } - }; - - node.onMouseMove = function() {}; - node.onMouseDown = function() { return false; }; - - const originalOnRemoved = node.onRemoved; - node.onRemoved = function() { - document.removeEventListener("visibilitychange", node.visibilityHandler); - app.canvas.canvas.removeEventListener('dragover', handleCanvasDragOver, { capture: true }); - app.canvas.canvas.removeEventListener('drop', handleCanvasDrop, { capture: true }); - if (originalOnRemoved) originalOnRemoved.apply(this, arguments); - }; - + + node.onDrawForeground = function () {}; + node.onMouseMove = function () {}; + node.onMouseDown = function () { return false; }; + if (node.data.selected_image) { node.loadImage(node.data.selected_image); } - + requestAnimationFrame(() => { node.onResize([MIN_WIDTH, MIN_HEIGHT]); }); - + return result; }; },