diff --git a/Example/RS Image-Prompt.json b/Example/RS Image-Prompt.json new file mode 100644 index 0000000..2e967db --- /dev/null +++ b/Example/RS Image-Prompt.json @@ -0,0 +1 @@ +{"id":"e1f1bf07-8bcb-4aac-9148-db917300217f","revision":0,"last_node_id":108,"last_link_id":121,"nodes":[{"id":9,"type":"SaveImage","pos":[810,0],"size":[630,810],"flags":{},"order":7,"mode":0,"inputs":[{"localized_name":"images","name":"images","type":"IMAGE","link":80},{"localized_name":"filename_prefix","name":"filename_prefix","type":"STRING","widget":{"name":"filename_prefix"},"link":null}],"outputs":[{"localized_name":"images","name":"images","type":"IMAGE","links":null}],"properties":{"cnr_id":"comfy-core","ver":"0.3.64","Node name for S&R":"SaveImage","enableTabs":false,"tabWidth":65,"tabXOffset":10,"hasSecondTab":false,"secondTabText":"Send Back","secondTabOffset":80,"secondTabWidth":65,"ue_properties":{"widget_ue_connectable":{},"version":"7.8","input_ue_unconnectable":{}}},"widgets_values":["ComfyUI"],"widgets_values_named":{"filename_prefix":"ComfyUI"}},{"id":65,"type":"VAEDecode","pos":[620,-10],"size":[230,100],"flags":{"collapsed":true},"order":6,"mode":0,"inputs":[{"localized_name":"samples","name":"samples","type":"LATENT","link":73},{"localized_name":"vae","name":"vae","type":"VAE","link":119}],"outputs":[{"localized_name":"IMAGE","name":"IMAGE","type":"IMAGE","slot_index":0,"links":[80]}],"properties":{"cnr_id":"comfy-core","ver":"0.3.64","Node name for S&R":"VAEDecode","enableTabs":false,"tabWidth":65,"tabXOffset":10,"hasSecondTab":false,"secondTabText":"Send Back","secondTabOffset":80,"secondTabWidth":65,"ue_properties":{"widget_ue_connectable":{},"version":"7.8","input_ue_unconnectable":{}}}},{"id":69,"type":"ModelSamplingAuraFlow","pos":[50,-50],"size":[240,60],"flags":{},"order":2,"mode":0,"inputs":[{"localized_name":"model","name":"model","type":"MODEL","link":117},{"localized_name":"shift","name":"shift","type":"FLOAT","widget":{"name":"shift"},"link":null}],"outputs":[{"localized_name":"MODEL","name":"MODEL","type":"MODEL","slot_index":0,"links":[75]}],"properties":{"cnr_id":"comfy-core","ver":"0.3.64","Node name for S&R":"ModelSamplingAuraFlow","enableTabs":false,"tabWidth":65,"tabXOffset":10,"hasSecondTab":false,"secondTabText":"Send Back","secondTabOffset":80,"secondTabWidth":65,"ue_properties":{"widget_ue_connectable":{},"version":"7.8","input_ue_unconnectable":{}}},"widgets_values":[3],"widgets_values_named":{"shift":3}},{"id":82,"type":"RSPrompts","pos":[10,70],"size":[370,350],"flags":{},"order":4,"mode":0,"inputs":[{"localized_name":"clip","name":"clip","type":"CLIP","link":118},{"localized_name":"text_input","name":"text_input","shape":7,"type":"STRING","link":121},{"color_off":"transparent","color_on":"transparent","localized_name":"text","name":"text","type":"STRING","widget":{"name":"text"},"link":null},{"localized_name":"enable_text_input","name":"enable_text_input","type":"BOOLEAN","widget":{"name":"enable_text_input"},"link":null},{"localized_name":"pause_for_edit","name":"pause_for_edit","type":"BOOLEAN","widget":{"name":"pause_for_edit"},"link":null},{"localized_name":"instance_uid","name":"instance_uid","shape":7,"type":"STRING","widget":{"name":"instance_uid"},"link":null}],"outputs":[{"localized_name":"POSITIVE","name":"POSITIVE","type":"CONDITIONING","links":[90]},{"localized_name":"NEGATIVE","name":"NEGATIVE","type":"CONDITIONING","links":[91]},{"localized_name":"PROMPT_STRING","name":"PROMPT_STRING","type":"STRING","links":null}],"properties":{"cnr_id":"ComfyUI_RaykoStudio","ver":"dd9fb78ac6030d4b4699c13d4a50577aa8905919","Node name for S&R":"RSPrompts","rs_instance_uid":"rs_inst_0914f2172ba24dbb817d4e5aef6734e8","rs_pause_state":false,"rs_enable_state":true,"rs_is_waiting":false,"rs_waiting_prompt":"","rs_waiting_timestamp":0,"ue_properties":{"widget_ue_connectable":{},"input_ue_unconnectable":{},"version":"7.8"}},"widgets_values":["A masterpiece cinematic 8k shot of a hilarious and chaotic cyberpunk party scene inside a dark futuristic bar. A huge, heavily drunk silverback gorilla in a ripped white tank top is sitting on the floor, doing a messy group hug with a glowing neon tiger and a fiery-orange neon fox. The fox's fur pulses with bright orange and gold laser light strips, and she is wearing funny glowing neon shutter shades tilted sideways. The fox is comically passed out across the gorilla's lap, holding a half-eaten salted herring fish like a plush toy. The gorilla is grinning with a goofy, cross-eyed expression, holding a tiny frosted shot glass of vodka with two fingers. The neon tiger, glowing in cyan and pink, has its paw around the gorilla's shoulder, balancing a piece of black rye bread on its head. The dark sci-fi background is filled with volumetric smoke, neon graffiti, glowing signs, and spilled puddles of alcohol reflecting the chaotic purple and orange light. Photorealistic fur textures, extreme details on the animals' expressions, funny adult comedy style.",true,false,"rs_inst_0914f2172ba24dbb817d4e5aef6734e8",""],"widgets_values_named":{"text":"A masterpiece cinematic 8k shot of a hilarious and chaotic cyberpunk party scene inside a dark futuristic bar. A huge, heavily drunk silverback gorilla in a ripped white tank top is sitting on the floor, doing a messy group hug with a glowing neon tiger and a fiery-orange neon fox. The fox's fur pulses with bright orange and gold laser light strips, and she is wearing funny glowing neon shutter shades tilted sideways. The fox is comically passed out across the gorilla's lap, holding a half-eaten salted herring fish like a plush toy. The gorilla is grinning with a goofy, cross-eyed expression, holding a tiny frosted shot glass of vodka with two fingers. The neon tiger, glowing in cyan and pink, has its paw around the gorilla's shoulder, balancing a piece of black rye bread on its head. The dark sci-fi background is filled with volumetric smoke, neon graffiti, glowing signs, and spilled puddles of alcohol reflecting the chaotic purple and orange light. Photorealistic fur textures, extreme details on the animals' expressions, funny adult comedy style.","enable_text_input":true,"pause_for_edit":false,"instance_uid":"rs_inst_0914f2172ba24dbb817d4e5aef6734e8","prompt_ui":""},"color":"#009933"},{"id":70,"type":"KSampler","pos":[440,50],"size":[310,290],"flags":{},"order":5,"mode":0,"inputs":[{"localized_name":"model","name":"model","type":"MODEL","link":75},{"localized_name":"positive","name":"positive","type":"CONDITIONING","link":90},{"localized_name":"negative","name":"negative","type":"CONDITIONING","link":91},{"localized_name":"latent_image","name":"latent_image","type":"LATENT","link":92},{"localized_name":"seed","name":"seed","type":"INT","widget":{"name":"seed"},"link":null},{"localized_name":"steps","name":"steps","type":"INT","widget":{"name":"steps"},"link":null},{"localized_name":"cfg","name":"cfg","type":"FLOAT","widget":{"name":"cfg"},"link":null},{"localized_name":"sampler_name","name":"sampler_name","type":"COMBO","widget":{"name":"sampler_name"},"link":null},{"localized_name":"scheduler","name":"scheduler","type":"COMBO","widget":{"name":"scheduler"},"link":null},{"localized_name":"denoise","name":"denoise","type":"FLOAT","widget":{"name":"denoise"},"link":null}],"outputs":[{"localized_name":"LATENT","name":"LATENT","type":"LATENT","slot_index":0,"links":[73]}],"properties":{"cnr_id":"comfy-core","ver":"0.3.64","Node name for S&R":"KSampler","enableTabs":false,"tabWidth":65,"tabXOffset":10,"hasSecondTab":false,"secondTabText":"Send Back","secondTabOffset":80,"secondTabWidth":65,"ue_properties":{"widget_ue_connectable":{},"version":"7.8","input_ue_unconnectable":{}}},"widgets_values":[773386409897344,"randomize",8,1,"res_multistep","simple",1],"widgets_values_named":{"seed":773386409897344,"control_after_generate":"randomize","steps":8,"cfg":1,"sampler_name":"res_multistep","scheduler":"simple","denoise":1},"color":"#009933"},{"id":83,"type":"RS_Image_MultiLatent","pos":[50,480],"size":[260,310],"flags":{},"order":3,"mode":0,"inputs":[{"localized_name":"vae","name":"vae","type":"VAE","link":120},{"localized_name":"size_mode","name":"size_mode","type":"COMBO","widget":{"name":"size_mode"},"link":null},{"localized_name":"active_preset","name":"active_preset","type":"COMBO","widget":{"name":"active_preset"},"link":null},{"localized_name":"preset_standard","name":"preset_standard","type":"COMBO","widget":{"name":"preset_standard"},"link":null},{"localized_name":"preset_qwen","name":"preset_qwen","type":"COMBO","widget":{"name":"preset_qwen"},"link":null},{"localized_name":"preset_krea2","name":"preset_krea2","type":"COMBO","widget":{"name":"preset_krea2"},"link":null},{"localized_name":"width","name":"width","type":"INT","widget":{"name":"width"},"link":null},{"localized_name":"height","name":"height","type":"INT","widget":{"name":"height"},"link":null},{"localized_name":"megapixels","name":"megapixels","type":"FLOAT","widget":{"name":"megapixels"},"link":null},{"localized_name":"aspect_ratio","name":"aspect_ratio","type":"COMBO","widget":{"name":"aspect_ratio"},"link":null},{"localized_name":"batch_size","name":"batch_size","type":"INT","widget":{"name":"batch_size"},"link":null}],"outputs":[{"localized_name":"latent","name":"latent","type":"LATENT","links":[92]},{"localized_name":"width_px","name":"width_px","type":"INT","links":null},{"localized_name":"height_px","name":"height_px","type":"INT","links":null}],"properties":{"cnr_id":"ComfyUI_RaykoStudio","ver":"dd9fb78ac6030d4b4699c13d4a50577aa8905919","Node name for S&R":"RS_Image_MultiLatent","ue_properties":{"widget_ue_connectable":{},"input_ue_unconnectable":{},"version":"7.8"}},"widgets_values":["Preset","Standard","Portrait - 832x1216 (13:19)","Square - 1008x1008 (1:1)","Square - 1024x1024 (1:1)",1024,1024,1,"1:1",1,""],"widgets_values_named":{"size_mode":"Preset","active_preset":"Standard","preset_standard":"Portrait - 832x1216 (13:19)","preset_qwen":"Square - 1008x1008 (1:1)","preset_krea2":"Square - 1024x1024 (1:1)","width":1024,"height":1024,"megapixels":1,"aspect_ratio":"1:1","batch_size":1,"preset_ui":""}},{"id":107,"type":"RaykoModelsLoader","pos":[-910,0],"size":[400,440],"flags":{},"order":0,"mode":0,"inputs":[{"localized_name":"unet_name","name":"unet_name","type":"COMBO","widget":{"name":"unet_name"},"link":null},{"localized_name":"weight_dtype","name":"weight_dtype","type":"COMBO","widget":{"name":"weight_dtype"},"link":null},{"localized_name":"use_clip2","name":"use_clip2","type":"BOOLEAN","widget":{"name":"use_clip2"},"link":null},{"localized_name":"clip_name","name":"clip_name","type":"COMBO","widget":{"name":"clip_name"},"link":null},{"localized_name":"clip_name2","name":"clip_name2","type":"COMBO","widget":{"name":"clip_name2"},"link":null},{"localized_name":"clip_type","name":"clip_type","type":"COMBO","widget":{"name":"clip_type"},"link":null},{"localized_name":"clip_device","name":"clip_device","type":"COMBO","widget":{"name":"clip_device"},"link":null},{"localized_name":"vae_name","name":"vae_name","type":"COMBO","widget":{"name":"vae_name"},"link":null},{"localized_name":"lora_data","name":"lora_data","type":"STRING","widget":{"name":"lora_data"},"link":null}],"outputs":[{"localized_name":"MODEL","name":"MODEL","type":"MODEL","links":[117]},{"localized_name":"CLIP","name":"CLIP","type":"CLIP","links":[118]},{"localized_name":"VAE","name":"VAE","type":"VAE","links":[119,120]}],"properties":{"cnr_id":"ComfyUI_RaykoStudio","ver":"d4cb57d666cd7bdc1e7a306d5158d735b8e49ce5","Node name for S&R":"RaykoModelsLoader","ue_properties":{"widget_ue_connectable":{},"input_ue_unconnectable":{},"version":"7.8"},"lora_rows":"[]","manual_size":true,"scrollOffset":0},"widgets_values":["KREA2\\Turbo fp8.safetensors","default",false,"KREA2\\qwen3vl_4b_uncensored_int8_convrot.safetensors","ACE\\qwen_0.6b_ace15.safetensors","krea2","default","wan2.1-vae.safetensors","[]","","",""],"widgets_values_named":{"unet_name":"KREA2\\Turbo fp8.safetensors","weight_dtype":"default","use_clip2":false,"clip_name":"KREA2\\qwen3vl_4b_uncensored_int8_convrot.safetensors","clip_name2":"ACE\\qwen_0.6b_ace15.safetensors","clip_type":"krea2","clip_device":"default","vae_name":"wan2.1-vae.safetensors","lora_data":"[]","presets_ui":"","✔️ Update LoRA list":"","➕ Add LoRA":""}},{"id":108,"type":"RS_ImagePrompt","pos":[-460,110],"size":[390,550],"flags":{},"order":1,"mode":0,"inputs":[{"localized_name":"image","name":"image","type":"STRING","widget":{"name":"image"},"link":null},{"localized_name":"prompt_preview","name":"prompt_preview","type":"STRING","widget":{"name":"prompt_preview"},"link":null}],"outputs":[{"localized_name":"prompt","name":"prompt","type":"STRING","links":[121]}],"properties":{"cnr_id":"ComfyUI_RaykoStudio","ver":"d4cb57d666cd7bdc1e7a306d5158d735b8e49ce5","Node name for S&R":"RS_ImagePrompt","ue_properties":{"widget_ue_connectable":{},"input_ue_unconnectable":{},"version":"7.8"}},"widgets_values":["photo_2026-08-02_05-07-13.jpg","",""],"widgets_values_named":{"image":"photo_2026-08-02_05-07-13.jpg","prompt_preview":"","custom_widgets":""},"data":{"selected_image":"photo_2026-08-02_05-07-13.jpg","prompt":""}}],"links":[[73,70,0,65,0,"LATENT"],[75,69,0,70,0,"MODEL"],[80,65,0,9,0,"IMAGE"],[90,82,0,70,1,"CONDITIONING"],[91,82,1,70,2,"CONDITIONING"],[92,83,0,70,3,"LATENT"],[117,107,0,69,0,"MODEL"],[118,107,1,82,0,"CLIP"],[119,107,2,65,1,"VAE"],[120,107,2,83,0,"VAE"],[121,108,0,82,1,"STRING"]],"groups":[],"config":{},"extra":{"ds":{"scale":0.9229599817706416,"offset":[1018.2131646946362,256.0305268911027]},"ue_links":[],"links_added_by_ue":[],"frontendVersion":"1.49.3","VHS_latentpreview":false,"VHS_latentpreviewrate":0,"VHS_MetadataImage":true,"VHS_KeepIntermediate":true},"version":0.4} \ No newline at end of file diff --git a/Rayko_Image_Text .py b/Rayko_Image_Text .py index 30eb4c8..16f1412 100644 --- a/Rayko_Image_Text .py +++ b/Rayko_Image_Text .py @@ -94,6 +94,8 @@ class LoadImageWithText: print(f"[WRITE] Saved in: {output_path}") + os.makedirs(os.path.dirname(output_path), exist_ok=True) + if pil_img.mode not in ('RGB', 'RGBA'): pil_img = pil_img.convert('RGB') @@ -120,5 +122,5 @@ NODE_CLASS_MAPPINGS = { "LoadImageWithText": LoadImageWithText, } NODE_DISPLAY_NAME_MAPPINGS = { - "LoadImageWithText": "🦊 RS Image-Text ", + "LoadImageWithText": "🦊 RS Image-Text", } \ No newline at end of file diff --git a/web/image_selector.js b/web/image_selector.js index 37e00a3..2ed12b9 100644 --- a/web/image_selector.js +++ b/web/image_selector.js @@ -6,6 +6,9 @@ app.registerExtension({ async beforeRegisterNodeDef(nodeType, nodeData, app) { if (nodeData.name !== "RS Image Selector") return; + const MIN_WIDTH = 300; + const MIN_HEIGHT = 400; + const onNodeCreated = nodeType.prototype.onNodeCreated; nodeType.prototype.onNodeCreated = function () { if (onNodeCreated) onNodeCreated.apply(this, arguments); @@ -16,7 +19,8 @@ app.registerExtension({ this.userResized = false; this.heartbeatInterval = null; - this.setSize([450, 500]); + this.setSize([MIN_WIDTH, MIN_HEIGHT]); + this.min_size = [MIN_WIDTH, MIN_HEIGHT]; this.buttons = [ { label: "➕ SELECT ALL", key: "select_all", color: "#2196F3", hover: false, row: 0, col: 0 }, @@ -58,6 +62,10 @@ app.registerExtension({ const onResize = nodeType.prototype.onResize; nodeType.prototype.onResize = function(size) { if (onResize) onResize.apply(this, arguments); + + if (this.size[0] < MIN_WIDTH) this.size[0] = MIN_WIDTH; + if (this.size[1] < MIN_HEIGHT) this.size[1] = MIN_HEIGHT; + this.userResized = true; }; @@ -93,9 +101,9 @@ app.registerExtension({ margin: 12, topBarH: 10, bottomBarH: 85, - minWidth: 400, + minWidth: MIN_WIDTH, maxWidth: 1200, - minHeight: 300, + minHeight: MIN_HEIGHT, maxHeight: 900 }; @@ -222,7 +230,7 @@ app.registerExtension({ const titleBarHeight = LiteGraph.NODE_TITLE_HEIGHT || 30; const padding = 10; - const footerHeight = 70; + const footerHeight = 65; let widgetsTotalHeight = 0; if (this.widgets) { @@ -242,10 +250,10 @@ app.registerExtension({ const titleBarHeight = LiteGraph.NODE_TITLE_HEIGHT || 30; const padding = 10; - const footerHeight = 70; + const footerHeight = 65; const btnH = 28; const btnGap = 5; - const btnY = this.size[1] - 65; + const btnY = this.size[1] - 70; let widgetsTotalHeight = 0; if (this.widgets) { @@ -351,7 +359,7 @@ app.registerExtension({ ctx.fillText("Render Error", 10, 10); } - const totalBtnW = this.size[0] - 50; + const totalBtnW = this.size[0] - 30; const btnW = (totalBtnW - btnGap) / 2; this.btnRects = []; diff --git a/web/rayko_lora_widget.js b/web/rayko_lora_widget.js index cc0f35b..2d0548f 100644 --- a/web/rayko_lora_widget.js +++ b/web/rayko_lora_widget.js @@ -699,7 +699,7 @@ app.registerExtension({ this.clickZones = []; const startY = this.getLoraListStartY(); const padding = 10; - const rightPanelWidth = 180; + const rightPanelWidth = 170; const availableHeight = this.size[1] - startY - 10; const maxVisibleStyles = Math.max(1, Math.floor(availableHeight / this.rowHeight)); @@ -756,7 +756,7 @@ app.registerExtension({ this.clickZones.push({ type: "info", index: dataIdx, x: infoX, y: y, w: infoW, h: h }); const nameX = infoX + infoW + 5; - const nameW = this.size[0] - (padding * 2) - 50 - rightPanelWidth - 20 - infoW - 5; + const nameW = this.size[0] - (padding * 2) - 1 - rightPanelWidth - 20 - infoW - 5; ctx.fillStyle = row.enabled ? "#fff" : "#777"; ctx.font = "12px sans-serif"; let displayName = row.name; diff --git a/web/rs_any_switch.js b/web/rs_any_switch.js index 6239e66..01ff0c3 100644 --- a/web/rs_any_switch.js +++ b/web/rs_any_switch.js @@ -65,13 +65,19 @@ app.registerExtension({ widget.slotName = conn.name; widget.draw = function(ctx, node, w, y, h) { ctx.fillStyle = "#222"; - ctx.fillRect(0, y, w, h); + ctx.beginPath(); + if (ctx.roundRect) { + ctx.roundRect(10, y, w - 20, h, 6); + } else { + ctx.rect(10, y, w - 20, h); + } + ctx.fill(); ctx.fillStyle = "#aaa"; ctx.font = "13px Arial"; ctx.textAlign = "left"; - ctx.fillText(this.name, 10, y + h / 2 + 1); + ctx.fillText(this.name, 20, y + h / 2 + 1); const toggleW = 30, toggleH = 14; - const toggleX = w - toggleW - 10; + const toggleX = w - toggleW - 20; const toggleY = y + (h - toggleH) / 2; ctx.fillStyle = this.value ? "#4a4" : "#666"; ctx.beginPath(); @@ -200,8 +206,8 @@ app.registerExtension({ ctx.strokeStyle = this.value === "OFF" ? "#f44336" : "#4caf50"; ctx.lineWidth = 2; ctx.beginPath(); - if (ctx.roundRect) ctx.roundRect(1, y, w - 2, h, 6); - else ctx.rect(1, y, w - 2, h); + if (ctx.roundRect) ctx.roundRect(10, y, w - 20, h, 6); + else ctx.rect(10, y, w - 20, h); ctx.stroke(); ctx.fillStyle = "#fff"; ctx.font = "bold 13px Arial"; @@ -270,13 +276,19 @@ app.registerExtension({ widget.slotName = inputName; widget.draw = function(ctx, node, w, y, h) { ctx.fillStyle = "#222"; - ctx.fillRect(0, y, w, h); + ctx.beginPath(); + if (ctx.roundRect) { + ctx.roundRect(10, y, w - 20, h, 6); + } else { + ctx.rect(10, y, w - 20, h); + } + ctx.fill(); ctx.fillStyle = "#aaa"; ctx.font = "14px Arial"; ctx.textAlign = "left"; - ctx.fillText(this.name, 10, y + h / 2 + 1); + ctx.fillText(this.name, 20, y + h / 2 + 1); const toggleW = 30, toggleH = 14; - const toggleX = w - toggleW - 10; + const toggleX = w - toggleW - 20; const toggleY = y + (h - toggleH) / 2; ctx.fillStyle = this.value ? "#4a4" : "#666"; ctx.beginPath(); diff --git a/web/rs_compare.js b/web/rs_compare.js index 2efa608..65a885b 100644 --- a/web/rs_compare.js +++ b/web/rs_compare.js @@ -22,17 +22,17 @@ function drawImageFit(ctx, img, areaX, areaY, areaW, areaH, zoom, panX, panY) { ctx.restore(); } -function drawRoundedRect(ctx, x, y, w, h, r) { +function drawRoundRect(ctx, x, y, w, h, r) { ctx.beginPath(); ctx.moveTo(x + r, y); ctx.lineTo(x + w - r, y); - ctx.arcTo(x + w, y, x + w, y + r, r); + ctx.quadraticCurveTo(x + w, y, x + w, y + r); ctx.lineTo(x + w, y + h - r); - ctx.arcTo(x + w, y + h, x + w - r, y + h, r); + ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h); ctx.lineTo(x + r, y + h); - ctx.arcTo(x, y + h, x, y + h - r, r); + ctx.quadraticCurveTo(x, y + h, x, y + h - r); ctx.lineTo(x, y + r); - ctx.arcTo(x, y, x + r, y, r); + ctx.quadraticCurveTo(x, y, x + r, y); ctx.closePath(); } @@ -40,7 +40,7 @@ function getImageArea(node) { const headerHeight = 50; const padding = 10; const yStart = headerHeight; - const widgetsHeight = 112; + const widgetsHeight = 122; // Обновлено под новые отступы (3*24 + 2*6 + 6 + 22 + 10) const widgetsGap = 10; const yEnd = node.size[1] - widgetsHeight - widgetsGap - padding; const w = Math.max(10, node.size[0] - padding * 2); @@ -50,7 +50,7 @@ function getImageArea(node) { function getWidgetsArea(node) { const padding = 10; - const widgetsHeight = 112; + const widgetsHeight = 122; // Обновлено const y = node.size[1] - widgetsHeight - padding; return { y, height: widgetsHeight }; } @@ -89,6 +89,7 @@ app.registerExtension({ this.rs_hideImg2 = false; this.rs_dragTarget = null; this.rs_widgetRects = []; + this.rs_sliderHover = []; this.setSize([450, 450]); this.min_size = [450, 380]; @@ -202,94 +203,92 @@ app.registerExtension({ } const ROW_HEIGHT = 24; - const ROW_GAP = 4; - const LABEL_WIDTH = 50; - const INFO_WIDTH = 35; - const RESET_WIDTH = 15; - const CORNER_RADIUS = 6; - const INNER_PADDING = 8; - const SLIDER_HEIGHT = 4; - const HANDLE_SIZE = 6; + const ROW_GAP = 6; // Уменьшено до 6 (как отступ до кнопки сброса) this.rs_widgetRects = []; const widgets = [ - { name: 'zoom', icon: '🔎', label: 'Zoom', value: zoom, format: (v) => v.toFixed(1), default: 1.0, min: 1.0, max: 10.0, step: 0.05 }, - { name: 'pan_x', icon: '↔️', label: 'Pan H', value: panPercentX, format: (v) => Math.round(v).toString(), default: 0, min: -100, max: 100, step: 0.5 }, - { name: 'pan_y', icon: '️↕️', label: 'Pan V', value: panPercentY, format: (v) => Math.round(v).toString(), default: 0, min: -100, max: 100, step: 0.5 } + { name: 'zoom', label: 'ZOOM', value: zoom, format: (v) => v.toFixed(1), default: 1.0, min: 1.0, max: 10.0, step: 0.05 }, + { name: 'pan_x', label: 'PAN H', value: panPercentX, format: (v) => Math.round(v).toString(), default: 0, min: -100, max: 100, step: 0.5 }, + { name: 'pan_y', label: 'PAN V', value: panPercentY, format: (v) => Math.round(v).toString(), default: 0, min: -100, max: 100, step: 0.5 } ]; - const buttonWidth = w; - const sliderWidth = buttonWidth - LABEL_WIDTH - INFO_WIDTH - RESET_WIDTH - 6 * INNER_PADDING; - + const widgetX = padding; + const widgetW = w; + widgets.forEach((widget, i) => { const rowY = widgetsY + i * (ROW_HEIGHT + ROW_GAP); - - const labelX = padding + INNER_PADDING; - const sliderX = labelX + LABEL_WIDTH + INNER_PADDING * 2; - const infoX = sliderX + sliderWidth + INNER_PADDING; - const resetX = infoX + INFO_WIDTH + INNER_PADDING; - - drawRoundedRect(ctx, padding, rowY, buttonWidth, ROW_HEIGHT, CORNER_RADIUS); - ctx.fillStyle = "#252525"; - ctx.fill(); - ctx.strokeStyle = "#444444"; + const isHover = this.rs_sliderHover?.[i] || false; + const isActive = this.rs_dragTarget?.widget === widget.name; + + ctx.fillStyle = isHover ? "#2a2a2a" : "#252525"; + ctx.strokeStyle = isActive ? "#4CAF50" : (isHover ? "#4CAF50" : "#444"); ctx.lineWidth = 1; + drawRoundRect(ctx, widgetX, rowY, widgetW, ROW_HEIGHT, 4); + ctx.fill(); ctx.stroke(); - - ctx.font = "bold 12px Arial"; - ctx.fillStyle = "#C0C0C0"; + + ctx.fillStyle = "#aaa"; + ctx.font = "10px sans-serif"; ctx.textAlign = "left"; ctx.textBaseline = "middle"; - const labelText = `${widget.icon} ${widget.label}`; - ctx.fillText(labelText, labelX, rowY + ROW_HEIGHT / 2); - - const sliderTrackY = rowY + ROW_HEIGHT / 2; + ctx.fillText(widget.label, widgetX + 8, rowY + ROW_HEIGHT / 2); + + // Gap уменьшен на 40: было 90, стало 50 + const trackX = widgetX + 50; + // Ширина трека увеличена на 40: было 190, стало 150 + const trackW = widgetW - 142; + const trackY = rowY + ROW_HEIGHT / 2; + const trackH = 3; + + ctx.fillStyle = "#444"; + ctx.fillRect(trackX, trackY - trackH / 2, trackW, trackH); + + const ratio = Math.max(0, Math.min(1, (widget.value - widget.min) / (widget.max - widget.min))); + const fillW = trackW * ratio; + ctx.fillStyle = "#4CAF50"; + ctx.fillRect(trackX, trackY - trackH / 2, fillW, trackH); + + const handleX = trackX + fillW; + const handleSize = 12; + ctx.fillStyle = "#fff"; ctx.beginPath(); - ctx.moveTo(sliderX, sliderTrackY); - ctx.lineTo(sliderX + sliderWidth, sliderTrackY); - ctx.strokeStyle = "#444444"; - ctx.lineWidth = SLIDER_HEIGHT; - ctx.stroke(); - - const normalizedValue = (widget.value - widget.min) / (widget.max - widget.min); - const handleX = sliderX + normalizedValue * sliderWidth; - - ctx.beginPath(); - ctx.moveTo(handleX, sliderTrackY - HANDLE_SIZE); - ctx.lineTo(handleX + HANDLE_SIZE, sliderTrackY); - ctx.lineTo(handleX, sliderTrackY + HANDLE_SIZE); - ctx.lineTo(handleX - HANDLE_SIZE, sliderTrackY); - ctx.closePath(); - ctx.fillStyle = "#000000"; + ctx.arc(handleX, trackY, handleSize / 2, 0, Math.PI * 2); ctx.fill(); - ctx.strokeStyle = "#ffffff"; - ctx.lineWidth = 1; - ctx.stroke(); - - const infoRect = { x: infoX, y: rowY + 2, w: INFO_WIDTH, h: ROW_HEIGHT - 4 }; - drawRoundedRect(ctx, infoX, rowY + 2, INFO_WIDTH, ROW_HEIGHT - 4, 4); - ctx.fillStyle = "#3B3B3B"; + + const valueW = 50; + const valueX = widgetX + widgetW - valueW - 30; + ctx.fillStyle = "#222"; + ctx.strokeStyle = "#444"; + drawRoundRect(ctx, valueX, rowY + 3, valueW, ROW_HEIGHT - 6, 3); ctx.fill(); - - ctx.font = "12px monospace"; - ctx.fillStyle = "#e0e0e0"; + ctx.stroke(); + + ctx.fillStyle = "#4CAF50"; + ctx.font = "10px monospace"; + ctx.textAlign = "center"; + ctx.fillText(widget.format(widget.value), valueX + valueW / 2, rowY + ROW_HEIGHT / 2); + + const resetBtnSize = 24; + const resetBtnX = widgetX + widgetW - resetBtnSize; + const resetBtnY = rowY + (ROW_HEIGHT - resetBtnSize) / 2; + ctx.fillStyle = "#252525"; + ctx.strokeStyle = "#444"; + drawRoundRect(ctx, resetBtnX, resetBtnY, resetBtnSize, resetBtnSize, 4); + ctx.fill(); + ctx.stroke(); + ctx.fillStyle = "#888"; + ctx.font = "14px sans-serif"; ctx.textAlign = "center"; ctx.textBaseline = "middle"; - ctx.fillText(widget.format(widget.value), infoX + INFO_WIDTH / 2, rowY + ROW_HEIGHT / 2); - - const resetRect = { x: resetX, y: rowY, w: RESET_WIDTH, h: ROW_HEIGHT }; - ctx.font = "12px Arial"; - ctx.textAlign = "center"; - ctx.fillText("🔃", resetX + RESET_WIDTH / 2, rowY + ROW_HEIGHT / 2); - + ctx.fillText("🔄", resetBtnX + resetBtnSize / 2, resetBtnY + resetBtnSize / 2); + this.rs_widgetRects.push({ widget: widget.name, - sliderRect: { x: sliderX - HANDLE_SIZE, y: rowY - HANDLE_SIZE, w: sliderWidth + HANDLE_SIZE * 2, h: ROW_HEIGHT + HANDLE_SIZE * 2 }, - sliderTrackX: sliderX, - sliderTrackWidth: sliderWidth, - infoRect, - resetRect, + sliderRect: { x: trackX - 6, y: rowY, w: trackW + 12, h: ROW_HEIGHT }, + sliderTrackX: trackX, + sliderTrackWidth: trackW, + resetRect: { x: resetBtnX, y: resetBtnY, w: resetBtnSize, h: resetBtnSize }, default: widget.default, min: widget.min, max: widget.max, @@ -299,11 +298,12 @@ app.registerExtension({ const resetAllY = widgetsY + 2 * (ROW_HEIGHT + ROW_GAP) + ROW_HEIGHT + 6; const resetAllHeight = 22; + const buttonWidth = w; - drawRoundedRect(ctx, padding, resetAllY, buttonWidth, resetAllHeight, CORNER_RADIUS); + drawRoundRect(ctx, padding, resetAllY, buttonWidth, resetAllHeight, 4); ctx.fillStyle = "#252525"; ctx.fill(); - ctx.strokeStyle = "#444444"; + ctx.strokeStyle = "#444"; ctx.lineWidth = 1; ctx.stroke(); @@ -400,6 +400,26 @@ app.registerExtension({ const onMouseMove = nodeType.prototype.onMouseMove; nodeType.prototype.onMouseMove = function (e, pos, graphcanvas) { + if (this.rs_dragTarget && e && e.buttons === 0) { + this.rs_dragTarget = null; + graphcanvas.setDirty(true, true); + } + if (this.rs_widgetRects && pos && !this.rs_dragTarget) { + let hoverChanged = false; + for (let i = 0; i < this.rs_widgetRects.length; i++) { + const rect = this.rs_widgetRects[i]; + const isOver = pos[0] >= rect.sliderRect.x && pos[0] <= rect.sliderRect.x + rect.sliderRect.w && + pos[1] >= rect.sliderRect.y && pos[1] <= rect.sliderRect.y + rect.sliderRect.h; + if ((this.rs_sliderHover[i] || false) !== isOver) { + this.rs_sliderHover[i] = isOver; + hoverChanged = true; + } + } + if (hoverChanged) { + graphcanvas.setDirty(true, true); + } + } + if (this.rs_dragTarget) { const delta = pos[0] - this.rs_dragTarget.startX; const deltaValue = (delta / this.rs_dragTarget.sliderWidth) * (this.rs_dragTarget.max - this.rs_dragTarget.min); diff --git a/web/rs_image-text.js b/web/rs_image-text.js new file mode 100644 index 0000000..a60e3a7 --- /dev/null +++ b/web/rs_image-text.js @@ -0,0 +1,467 @@ +import { app } from "../../scripts/app.js"; + +app.registerExtension({ + name: "RaykoStudio.ImageText", + async beforeRegisterNodeDef(nodeType, nodeData, app) { + if (nodeData.name !== "LoadImageWithText") return; + + const onNodeCreated = nodeType.prototype.onNodeCreated; + + 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"); + const w_image = node.widgets?.find(w => w.name === "image"); + + const syncWidget = (widget, dataKey) => { + if (widget) { + widget.value = node.data[dataKey]; + widget.serializeValue = () => node.data[dataKey]; + } + }; + + if (node.widgets) { + node.widgets.forEach(w => { + if (w.name === "mode" && w.value) node.data.mode = w.value; + 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() {}; + + if (w.element && w.element.parentNode) { + w.element.parentNode.removeChild(w.element); + } + + w.serializeValue = () => { + if (w.name === "mode") return node.data.mode; + if (w.name === "filename_prefix") return node.data.prefix; + if (w.name === "text_input") return node.data.text; + if (w.name === "image") return node.data.selected_image; + return w.value; + }; + }); + } + + 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); + + const mainContainer = document.createElement("div"); + mainContainer.style.cssText = ` + width: 100%; + display: flex; + flex-direction: column; + gap: 8px; + padding: 0 !important; + margin: 0 !important; + box-sizing: border-box; + pointer-events: none; + `; + + 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; + btn.style.cssText = `flex:1; height:28px; padding:0 12px; border:2px solid #555; border-radius:4px; font-weight:normal; cursor:pointer; transition:all 0.2s; font-size:12px; pointer-events: auto;`; + btn.onclick = () => { + node.data.mode = value; + syncWidget(w_mode, 'mode'); + updateModeVisuals(); + node.syncData(); + }; + return btn; + }; + const btnRead = createModeBtn("📄 READ from IMAGE", "read"); + 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"; + btnRead.style.borderColor = isRead ? "#81C784" : "#555"; + btnRead.style.color = isRead ? "#fff" : "#aaa"; + btnWrite.style.backgroundColor = !isRead ? "#2196F3" : "#333"; + btnWrite.style.borderColor = !isRead ? "#64B5F6" : "#555"; + btnWrite.style.color = !isRead ? "#fff" : "#aaa"; + }; + updateModeVisuals(); + + 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); + + 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'); + node.syncData(); + }; + textContainer.appendChild(customTextArea); + + 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;"; + btnClear.onmouseover = () => { btnClear.style.backgroundColor = "#3a3a3a"; }; + btnClear.onmouseout = () => { btnClear.style.backgroundColor = "#2a2a2a"; }; + btnClear.onclick = () => { + node.data.text = ""; + customTextArea.value = ""; + syncWidget(w_text, 'text'); + node.syncData(); + }; + + buttonsContainer.appendChild(btnUpload); + buttonsContainer.appendChild(btnClear); + + mainContainer.appendChild(modeContainer); + mainContainer.appendChild(prefixContainer); + mainContainer.appendChild(textContainer); + mainContainer.appendChild(buttonsContainer); + + node.addDOMWidget("custom_widgets", "customtext", mainContainer); + + node.customTextArea = customTextArea; + + const originalOnResize = node.onResize; + 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 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) { + const w = this.size[0]; + const h = this.size[1]; + const radius = 8; + + ctx.beginPath(); + ctx.moveTo(radius, 0); + ctx.lineTo(w - radius, 0); + ctx.quadraticCurveTo(w, 0, w, radius); + ctx.lineTo(w, h - radius); + ctx.quadraticCurveTo(w, h, w - radius, h); + ctx.lineTo(radius, h); + ctx.quadraticCurveTo(0, h, 0, h - radius); + 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); + }; + + if (node.data.selected_image) { + node.loadImage(node.data.selected_image); + } + + requestAnimationFrame(() => { + node.onResize([MIN_WIDTH, MIN_HEIGHT]); + }); + + return result; + }; + }, + setup() {} +}); \ No newline at end of file diff --git a/web/rs_styles.js b/web/rs_styles.js index 9c5206d..eac4e51 100644 --- a/web/rs_styles.js +++ b/web/rs_styles.js @@ -368,11 +368,15 @@ app.registerExtension({ ctx.fill(); }; + // --- ИЗМЕНЕННАЯ ФУНКЦИЯ drawButton с закруглением --- this.drawButton = function(ctx, text, x, y, w, h, iconColor = "#fff") { + const radius = 5; ctx.fillStyle = "#242427"; - ctx.fillRect(x, y, w, h); + this.drawRoundedRect(ctx, x, y, w, h, radius); + ctx.fill(); ctx.strokeStyle = "#3a3a3c"; - ctx.strokeRect(x, y, w, h); + this.drawRoundedRect(ctx, x, y, w, h, radius); + ctx.stroke(); ctx.font = "bold 11px sans-serif"; ctx.textAlign = "center"; @@ -388,6 +392,7 @@ app.registerExtension({ ctx.fillStyle = "#fff"; ctx.fillText(label, x + w/2 + 8, y + h/2); }; + // ---------------------------------------------- this.drawRoundedRect = function(ctx, x, y, w, h, r) { ctx.beginPath(); diff --git a/web/spline_mask.js b/web/spline_mask.js index 26a90c5..f8c26ab 100644 --- a/web/spline_mask.js +++ b/web/spline_mask.js @@ -253,8 +253,10 @@ app.registerExtension({ const canvasRect = canvasEl.getBoundingClientRect(); const ds = app.canvas.ds; const btnY = this.size[1] - 45; - const btnW = (this.size[0] - 50) / 3; - const btnX = 15 + (buttonIndex * (btnW + 5)); + const padding = 15; + const gap = 10; + const btnW = (this.size[0] - 2 * padding - 2 * gap) / 3; + const btnX = padding + (buttonIndex * (btnW + gap)); const nodeScreenX = canvasRect.left + ((this.pos[0] + ds.offset[0]) * ds.scale); const nodeScreenY = canvasRect.top + ((this.pos[1] + ds.offset[1]) * ds.scale); @@ -282,6 +284,7 @@ app.registerExtension({ syncData(); }; + // ---- ФУНКЦИЯ РАСЧЁТА ПОЛОЖЕНИЯ ИЗОБРАЖЕНИЯ (с учётом смещения вниз) ---- const calculateImageRect = () => { if (!app.canvas) return null; @@ -302,11 +305,13 @@ app.registerExtension({ } const titleBarHeight = LiteGraph.NODE_TITLE_HEIGHT || 30; - const padding = 10; + const leftRightPadding = 10; + const topPadding = 18; // 10 (базовый) + 8 (дополнительный сдвиг вниз) + const bottomPadding = 10; const footerHeight = 50; - const availableHeight = (node.size[1] * scale) - (titleBarHeight * scale) - (widgetsHeight * scale) - (footerHeight * scale) - (padding * 2 * scale); - const availableWidth = (node.size[0] * scale) - (padding * 2 * scale); + const availableHeight = (node.size[1] * scale) - (titleBarHeight * scale) - (widgetsHeight * scale) - (footerHeight * scale) - (topPadding * scale) - (bottomPadding * scale); + const availableWidth = (node.size[0] * scale) - (leftRightPadding * 2 * scale); if (availableWidth <= 0 || availableHeight <= 0) return null; @@ -326,11 +331,12 @@ app.registerExtension({ contentScale = drawW / node.image.width; } - const drawX = nodeScreenX + (padding * scale) + ((availableWidth - drawW) / 2); - const drawY = nodeScreenY + (titleBarHeight * scale) + (widgetsHeight * scale) + (padding * scale) + ((availableHeight - drawH) / 2); + const drawX = nodeScreenX + (leftRightPadding * scale) + ((availableWidth - drawW) / 2); + const drawY = nodeScreenY + (titleBarHeight * scale) + (widgetsHeight * scale) + (topPadding * scale) + ((availableHeight - drawH) / 2); return { left: drawX, top: drawY, width: drawW, height: drawH, scale: contentScale }; }; + // ---------------------------------------------------------------- const syncPosition = () => { if (!_overlayCanvas) return; @@ -478,6 +484,7 @@ app.registerExtension({ updateLoop(); }; + // ---- ОТРИСОВКА КНОПОК И ИЗОБРАЖЕНИЯ (с учётом смещения) ---- node.onDrawForeground = function(ctx) { if (this.flags.collapsed) return; @@ -490,16 +497,22 @@ app.registerExtension({ } const titleBarHeight = LiteGraph.NODE_TITLE_HEIGHT || 30; - const padding = 10; + const leftRightPadding = 10; + const topPadding = 18; // 10 + 8 + const bottomPadding = 10; const footerHeight = 50; const btnH = 28; const btnY = h - 45; - const btnW = (w - 50) / 3; + + // Параметры кнопок (расстояние +10, закругление) + const btnPadding = 15; + const btnGap = 10; + const btnW = (w - 2 * btnPadding - 2 * btnGap) / 3; // === РАЗМЕРЫ ИЗОБРАЖЕНИЯ НАД ПРЕВЬЮ === if (this.imageLoaded && this.image) { ctx.fillStyle = "#888"; - ctx.font = "11px sans-serif"; + ctx.font = "15px sans-serif"; ctx.textAlign = "center"; ctx.textBaseline = "top"; ctx.fillText( @@ -510,9 +523,9 @@ app.registerExtension({ } // ==================================== - const startY = titleBarHeight + widgetsHeight + padding; - const availableHeight = h - startY - footerHeight - padding; - const availableWidth = w - (padding * 2); + const startY = titleBarHeight + widgetsHeight + topPadding; + const availableHeight = h - startY - footerHeight - bottomPadding; + const availableWidth = w - (leftRightPadding * 2); if (this.imageLoaded && this.image) { const imgRatio = this.image.width / this.image.height; @@ -526,7 +539,7 @@ app.registerExtension({ drawH = drawW / imgRatio; } - const drawX = padding + (availableWidth - drawW) / 2; + const drawX = leftRightPadding + (availableWidth - drawW) / 2; const drawY = startY + (availableHeight - drawH) / 2; ctx.drawImage(this.image, drawX, drawY, drawW, drawH); @@ -534,7 +547,7 @@ app.registerExtension({ ctx.strokeRect(drawX, drawY, drawW, drawH); } else { ctx.fillStyle = "#222"; - ctx.fillRect(padding, startY, availableWidth, availableHeight); + ctx.fillRect(leftRightPadding, startY, availableWidth, availableHeight); ctx.fillStyle = "#555"; ctx.font = "14px Arial"; ctx.textAlign = "center"; @@ -542,17 +555,37 @@ app.registerExtension({ ctx.fillText("Select Image...", w / 2, startY + availableHeight / 2); } + // ---- РИСУЕМ КНОПКИ (скруглённые, с увеличенным расстоянием) ---- for (let i = 0; i < this.buttons.length; i++) { const btn = this.buttons[i]; - btn.x = 15 + (i * (btnW + 5)); + btn.x = btnPadding + (i * (btnW + btnGap)); btn.y = btnY; btn.w = btnW; btn.h = btnH; + // Фон со скруглением + const radius = 4; + const x = btn.x, y = btn.y, w = btn.w, h = btn.h; ctx.fillStyle = btn.hover ? "#444" : "#2a2a2a"; - ctx.fillRect(btn.x, btn.y, btn.w, btn.h); + ctx.beginPath(); + ctx.moveTo(x + radius, y); + ctx.lineTo(x + w - radius, y); + ctx.quadraticCurveTo(x + w, y, x + w, y + radius); + ctx.lineTo(x + w, y + h - radius); + ctx.quadraticCurveTo(x + w, y + h, x + w - radius, y + h); + ctx.lineTo(x + radius, y + h); + ctx.quadraticCurveTo(x, y + h, x, y + h - radius); + ctx.lineTo(x, y + radius); + ctx.quadraticCurveTo(x, y, x + radius, y); + ctx.closePath(); + ctx.fill(); + + // Обводка ctx.strokeStyle = btn.color; - ctx.strokeRect(btn.x, btn.y, btn.w, btn.h); + ctx.lineWidth = 1.5; + ctx.stroke(); + + // Текст ctx.fillStyle = btn.color; ctx.font = "bold 11px Arial"; ctx.textAlign = "center"; @@ -560,6 +593,7 @@ app.registerExtension({ ctx.fillText(btn.label, btn.x + btn.w / 2, btn.y + btn.h / 2); } }; + // ------------------------------------------------------------ node.onMouseMove = function(event, pos) { const [x, y] = pos;