diff --git a/dist/immac_style_mixer/main.js b/dist/immac_style_mixer/main.js index b3cc906..52e007e 100644 --- a/dist/immac_style_mixer/main.js +++ b/dist/immac_style_mixer/main.js @@ -1,5 +1,5 @@ const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["immac_style_mixer/StyleMixerPanel-B66F9prP.js","immac_style_mixer/vendor-DPfS0z4g.js"])))=>i.map(i=>d[i]); -import{r as O,a as z,R as M}from"./vendor-DPfS0z4g.js";const U="modulepreload",A=function(y){return"/"+y},$={},L=function(d,_,x){let e=Promise.resolve();if(_&&_.length>0){document.getElementsByTagName("link");const r=document.querySelector("meta[property=csp-nonce]"),i=(r==null?void 0:r.nonce)||(r==null?void 0:r.getAttribute("nonce"));e=Promise.allSettled(_.map(t=>{if(t=A(t),t in $)return;$[t]=!0;const o=t.endsWith(".css"),n=o?'[rel="stylesheet"]':"";if(document.querySelector(`link[href="${t}"]${n}`))return;const s=document.createElement("link");if(s.rel=o?"stylesheet":U,o||(s.as="script"),s.crossOrigin="",s.href=t,i&&s.setAttribute("nonce",i),document.head.appendChild(s),o)return new Promise((p,m)=>{s.addEventListener("load",p),s.addEventListener("error",()=>m(new Error(`Unable to preload CSS for ${t}`)))})}))}function a(r){const i=new Event("vite:preloadError",{cancelable:!0});if(i.payload=r,window.dispatchEvent(i),!i.defaultPrevented)throw r}return e.then(r=>{for(const i of r||[])i.status==="rejected"&&a(i.reason);return d().catch(a)})};var T={exports:{}},E={};/** +import{r as M,a as U,R as T}from"./vendor-DPfS0z4g.js";const A="modulepreload",L=function(f){return"/"+f},C={},B=function(p,w,x){let e=Promise.resolve();if(w&&w.length>0){document.getElementsByTagName("link");const r=document.querySelector("meta[property=csp-nonce]"),i=(r==null?void 0:r.nonce)||(r==null?void 0:r.getAttribute("nonce"));e=Promise.allSettled(w.map(t=>{if(t=L(t),t in C)return;C[t]=!0;const o=t.endsWith(".css"),s=o?'[rel="stylesheet"]':"";if(document.querySelector(`link[href="${t}"]${s}`))return;const n=document.createElement("link");if(n.rel=o?"stylesheet":A,o||(n.as="script"),n.crossOrigin="",n.href=t,i&&n.setAttribute("nonce",i),document.head.appendChild(n),o)return new Promise((y,m)=>{n.addEventListener("load",y),n.addEventListener("error",()=>m(new Error(`Unable to preload CSS for ${t}`)))})}))}function a(r){const i=new Event("vite:preloadError",{cancelable:!0});if(i.payload=r,window.dispatchEvent(i),!i.defaultPrevented)throw r}return e.then(r=>{for(const i of r||[])i.status==="rejected"&&a(i.reason);return p().catch(a)})};var j={exports:{}},E={};/** * @license React * react-jsx-runtime.production.min.js * @@ -7,4 +7,4 @@ import{r as O,a as z,R as M}from"./vendor-DPfS0z4g.js";const U="modulepreload",A * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. - */var B=O,N=Symbol.for("react.element"),D=Symbol.for("react.fragment"),F=Object.prototype.hasOwnProperty,Z=B.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,J={key:!0,ref:!0,__self:!0,__source:!0};function j(y,d,_){var x,e={},a=null,r=null;_!==void 0&&(a=""+_),d.key!==void 0&&(a=""+d.key),d.ref!==void 0&&(r=d.ref);for(x in d)F.call(d,x)&&!J.hasOwnProperty(x)&&(e[x]=d[x]);if(y&&y.defaultProps)for(x in d=y.defaultProps,d)e[x]===void 0&&(e[x]=d[x]);return{$$typeof:N,type:y,key:a,ref:r,props:e,_owner:Z.current}}E.Fragment=D;E.jsx=j;E.jsxs=j;T.exports=E;var P=T.exports,I={},C=z;I.createRoot=C.createRoot,I.hydrateRoot=C.hydrateRoot;const H=M.lazy(()=>L(()=>import("./StyleMixerPanel-B66F9prP.js"),__vite__mapDeps([0,1])));function W(){return new Promise(y=>{function d(){if(window.app){y();return}const _=setInterval(()=>{window.app&&(clearInterval(_),y())},50);setTimeout(()=>{clearInterval(_),console.error("[ImmacStyleMixer] Timeout waiting for ComfyUI app"),y()},5e3)}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",d):d()})}async function q(){if(await W(),!window.app){console.error("[ImmacStyleMixer] ComfyUI app not available");return}window.app.extensionManager.registerSidebarTab({id:"immac-style-mixer",icon:"pi pi-palette",title:"Style Mixer",tooltip:"Immac Style Mixer",type:"custom",render:e=>{const a=document.createElement("div");a.id="immac-style-mixer-root",a.style.position="absolute",a.style.inset="0",a.style.overflow="hidden",e.style.position="relative",e.style.height="100%",e.appendChild(a),I.createRoot(a).render(P.jsx(M.StrictMode,{children:P.jsx(O.Suspense,{fallback:P.jsx("div",{style:{padding:"1rem"},children:"Loading…"}),children:P.jsx(H,{})})}))}});const y="/immac_style_mixer/api/data";async function d(){const e=await fetch(y);if(!e.ok){alert(`Backup failed: HTTP ${e.status}`);return}const a=await e.json(),r=new Blob([JSON.stringify(a,null,2)],{type:"application/json"}),i=URL.createObjectURL(r),t=document.createElement("a");t.href=i,t.download=`style_mixer_backup_${new Date().toISOString().slice(0,10)}.json`,t.click(),URL.revokeObjectURL(i)}function _(){const e=document.createElement("a");e.href="/immac_style_mixer/api/backup.zip",e.download=`style_mixer_backup_${new Date().toISOString().slice(0,10)}.zip`,e.click()}function x(){const e=document.createElement("input");e.type="file",e.accept=".json,.zip,application/json,application/zip",e.onchange=()=>{var i;const a=(i=e.files)==null?void 0:i[0];if(!a)return;if(a.name.endsWith(".zip")||a.type==="application/zip")a.arrayBuffer().then(async t=>{var o,n;try{const s=await fetch("/immac_style_mixer/api/restore.zip",{method:"POST",headers:{"Content-Type":"application/zip"},body:t});if(!s.ok){const m=await s.text();throw new Error(`HTTP ${s.status}: ${m.slice(0,300)}`)}const p=await s.json();try{(n=(o=window.app)==null?void 0:o.toast)==null||n.add({severity:"success",summary:"Style Mixer",detail:`ZIP restored: ${p.styles} styles, ${p.mixes} mixes, ${p.images} images. Reload the Style Mixer panel to see changes.`,life:6e3})}catch{}}catch(s){alert(`Failed to restore ZIP backup: ${s.message}`)}}).catch(t=>alert(`Could not read file: ${t.message}`));else{const t=new FileReader;t.onload=async o=>{var n,s,p;try{const m=JSON.parse((n=o.target)==null?void 0:n.result);if(!Array.isArray(m.styles)||!Array.isArray(m.mixes))throw new Error("Invalid backup: missing styles or mixes arrays.");const g=await fetch(y,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(m)});if(!g.ok)throw new Error(`Save failed: HTTP ${g.status}`);try{(p=(s=window.app)==null?void 0:s.toast)==null||p.add({severity:"success",summary:"Style Mixer",detail:`Backup restored: ${m.styles.length} styles, ${m.mixes.length} mixes. Reload the Style Mixer panel to see changes.`,life:6e3})}catch{}}catch(m){alert(`Failed to restore backup: ${m.message}`)}},t.readAsText(a)}},e.click()}window.app.registerExtension({name:"ImmacStyleMixer",settings:[{id:"ImmacStyleMixer.BackupRestore",name:"Style Mixer — Backup & Restore",type:(e,a,r)=>{const i=document.createElement("div");i.style.cssText="display:flex;gap:8px;align-items:center";const t=document.createElement("button");t.textContent="⬇ Download backup (JSON)",t.title="Save all styles and mixes to a JSON file",t.style.cssText="cursor:pointer;padding:4px 10px;border-radius:4px",t.onclick=()=>d().catch(console.error);const o=document.createElement("button");o.textContent="⬇ Download ZIP (with images)",o.title="Save styles, mixes and all images to a ZIP file",o.style.cssText="cursor:pointer;padding:4px 10px;border-radius:4px",o.onclick=()=>_();const n=document.createElement("button");return n.textContent="⬆ Restore (JSON or ZIP)",n.title="Restore from a JSON or ZIP backup — auto-detected",n.style.cssText="cursor:pointer;padding:4px 10px;border-radius:4px",n.onclick=()=>x(),i.appendChild(t),i.appendChild(o),i.appendChild(n),i},defaultValue:null}],nodeCreated(e){var R;if(e.comfyClass!=="StyleMixImmacStyleMixer"&&e.comfyClass!=="StylePickImmacStyleMixer")return;const a=e.comfyClass==="StylePickImmacStyleMixer",r=a?"style":"mix",i=(R=e.widgets)==null?void 0:R.find(l=>l.name===r);if(!i)return;const t=document.createElement("img");t.style.cssText="width:100%;height:100%;display:none;object-fit:contain;border-radius:4px";const o=document.createElement("div");o.style.cssText="padding:4px;box-sizing:border-box",o.appendChild(t);let n=0;e.addDOMWidget("immac_preview","div",o,{serialize:!1,getMinHeight:()=>n,getHeight:()=>n});function s(l,u){var S,w,k;const f=((S=e.size)==null?void 0:S[0])??300,v=u/l;n=Math.max(80,Math.min(Math.round((f-8)*v),600));const c=(w=e.computeSize)==null?void 0:w.call(e);c&&e.setSize([Math.max(e.size[0],c[0]),Math.max(e.size[1],c[1])]),(k=e.onResize)==null||k.call(e,e.size),e.setDirtyCanvas(!0,!0)}function p(){var l;n=0,t.style.display="none",(l=e.onResize)==null||l.call(e,e.size),e.setDirtyCanvas(!0,!0)}function m(l){t.onload=()=>{t.style.display="block",s(t.naturalWidth,t.naturalHeight)},t.onerror=()=>{p()},t.src=l}async function g(l){var u;try{const f=await fetch("/immac_style_mixer/api/data");if(!f.ok)return;const v=await f.json();if(a){const c=(v.styles??[]).find(w=>w.name===l);if(!(c!=null&&c.image_filename)){p();return}const S=c.image_updated_at?`&t=${c.image_updated_at}`:"";m(`/view?filename=${encodeURIComponent(c.image_filename)}&subfolder=immac_style_mixer%2Fstyles&type=input${S}`)}else{const c=(v.mixes??[]).find(w=>w.name===l);let S="";if(c!=null&&c.image_filename){const w=c.image_updated_at?`&t=${c.image_updated_at}`:"";S=`/view?filename=${encodeURIComponent(c.image_filename)}&subfolder=immac_style_mixer%2Fmixes&type=input${w}`}else if((u=c==null?void 0:c.styles)!=null&&u.length){const w=Object.fromEntries((v.styles??[]).map(h=>[h.id,h])),k=c.styles.filter(h=>h.enabled!==!1).map(h=>w[h.style_id]).find(h=>h==null?void 0:h.image_filename);if(k){const h=k.image_updated_at?`&t=${k.image_updated_at}`:"";S=`/view?filename=${encodeURIComponent(k.image_filename)}&subfolder=immac_style_mixer%2Fstyles&type=input${h}`}}if(!S){p();return}m(S)}}catch(f){console.error("[ImmacStyleMixer] Preview update failed",f)}}const b=i.callback;i.callback=function(...l){var f;b==null||b.apply(this,l);const u=(f=e.widgets)==null?void 0:f.find(v=>v.name===r);g(String((u==null?void 0:u.value)??""))},e._immacUpdatePreview=l=>{var f;const u=(f=e.widgets)==null?void 0:f.find(v=>v.name===r);g(l??String((u==null?void 0:u.value)??""))},e._immacPreviewScheduled=!0,requestAnimationFrame(()=>{var u;if(!e._immacPreviewScheduled)return;const l=(u=e.widgets)==null?void 0:u.find(f=>f.name===r);l!=null&&l.value&&g(String(l.value))})},loadedGraphNode(e){var o;if(typeof e._immacUpdatePreview!="function")return;e._immacPreviewScheduled=!1;const a=e.comfyClass==="StylePickImmacStyleMixer",r=a?"style":"mix",i=(o=e.widgets)==null?void 0:o.find(n=>n.name===r),t=String((i==null?void 0:i.value)??"");requestAnimationFrame(()=>{t==="(no styles saved)"||t==="(no mixes saved)"?fetch("/immac_style_mixer/api/data").then(n=>n.json()).then(n=>{var m,g;const s=a?n.styles??[]:n.mixes??[],p=(m=e.widgets)==null?void 0:m.find(b=>b.name===r);s.length>0&&p&&(p.value=s[0].name,Array.isArray((g=p.options)==null?void 0:g.values)&&(p.options.values=s.map(b=>b.name))),e._immacUpdatePreview()}).catch(()=>e._immacUpdatePreview()):e._immacUpdatePreview(t)})}})}q().catch(console.error);export{P as j}; + */var N=M,D=Symbol.for("react.element"),F=Symbol.for("react.fragment"),Z=Object.prototype.hasOwnProperty,J=N.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,H={key:!0,ref:!0,__self:!0,__source:!0};function z(f,p,w){var x,e={},a=null,r=null;w!==void 0&&(a=""+w),p.key!==void 0&&(a=""+p.key),p.ref!==void 0&&(r=p.ref);for(x in p)Z.call(p,x)&&!H.hasOwnProperty(x)&&(e[x]=p[x]);if(f&&f.defaultProps)for(x in p=f.defaultProps,p)e[x]===void 0&&(e[x]=p[x]);return{$$typeof:D,type:f,key:a,ref:r,props:e,_owner:J.current}}E.Fragment=F;E.jsx=z;E.jsxs=z;j.exports=E;var P=j.exports,R={},O=U;R.createRoot=O.createRoot,R.hydrateRoot=O.hydrateRoot;const q=T.lazy(()=>B(()=>import("./StyleMixerPanel-B66F9prP.js"),__vite__mapDeps([0,1])));function W(){return new Promise(f=>{function p(){if(window.app){f();return}const w=setInterval(()=>{window.app&&(clearInterval(w),f())},50);setTimeout(()=>{clearInterval(w),console.error("[ImmacStyleMixer] Timeout waiting for ComfyUI app"),f()},5e3)}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",p):p()})}async function V(){if(await W(),!window.app){console.error("[ImmacStyleMixer] ComfyUI app not available");return}window.app.extensionManager.registerSidebarTab({id:"immac-style-mixer",icon:"pi pi-palette",title:"Style Mixer",tooltip:"Immac Style Mixer",type:"custom",render:e=>{const a=document.createElement("div");a.id="immac-style-mixer-root",a.style.position="absolute",a.style.inset="0",a.style.overflow="hidden",e.style.position="relative",e.style.height="100%",e.appendChild(a),R.createRoot(a).render(P.jsx(T.StrictMode,{children:P.jsx(M.Suspense,{fallback:P.jsx("div",{style:{padding:"1rem"},children:"Loading…"}),children:P.jsx(q,{})})}))}});const f="/immac_style_mixer/api/data";async function p(){const e=await fetch(f);if(!e.ok){alert(`Backup failed: HTTP ${e.status}`);return}const a=await e.json(),r=new Blob([JSON.stringify(a,null,2)],{type:"application/json"}),i=URL.createObjectURL(r),t=document.createElement("a");t.href=i,t.download=`style_mixer_backup_${new Date().toISOString().slice(0,10)}.json`,t.click(),URL.revokeObjectURL(i)}function w(){const e=document.createElement("a");e.href="/immac_style_mixer/api/backup.zip",e.download=`style_mixer_backup_${new Date().toISOString().slice(0,10)}.zip`,e.click()}function x(){const e=document.createElement("input");e.type="file",e.accept=".json,.zip,application/json,application/zip",e.onchange=()=>{var i;const a=(i=e.files)==null?void 0:i[0];if(!a)return;if(a.name.endsWith(".zip")||a.type==="application/zip")a.arrayBuffer().then(async t=>{var o,s;try{const n=await fetch("/immac_style_mixer/api/restore.zip",{method:"POST",headers:{"Content-Type":"application/zip"},body:t});if(!n.ok){const m=await n.text();throw new Error(`HTTP ${n.status}: ${m.slice(0,300)}`)}const y=await n.json();try{(s=(o=window.app)==null?void 0:o.toast)==null||s.add({severity:"success",summary:"Style Mixer",detail:`ZIP restored: ${y.styles} styles, ${y.mixes} mixes, ${y.images} images. Reload the Style Mixer panel to see changes.`,life:6e3})}catch{}}catch(n){alert(`Failed to restore ZIP backup: ${n.message}`)}}).catch(t=>alert(`Could not read file: ${t.message}`));else{const t=new FileReader;t.onload=async o=>{var s,n,y;try{const m=JSON.parse((s=o.target)==null?void 0:s.result);if(!Array.isArray(m.styles)||!Array.isArray(m.mixes))throw new Error("Invalid backup: missing styles or mixes arrays.");const S=await fetch(f,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(m)});if(!S.ok)throw new Error(`Save failed: HTTP ${S.status}`);try{(y=(n=window.app)==null?void 0:n.toast)==null||y.add({severity:"success",summary:"Style Mixer",detail:`Backup restored: ${m.styles.length} styles, ${m.mixes.length} mixes. Reload the Style Mixer panel to see changes.`,life:6e3})}catch{}}catch(m){alert(`Failed to restore backup: ${m.message}`)}},t.readAsText(a)}},e.click()}window.app.registerExtension({name:"ImmacStyleMixer",settings:[{id:"ImmacStyleMixer.BackupRestore",name:"Style Mixer — Backup & Restore",type:(e,a,r)=>{const i=document.createElement("div");i.style.cssText="display:flex;gap:8px;align-items:center";const t=document.createElement("button");t.textContent="⬇ Download backup (JSON)",t.title="Save all styles and mixes to a JSON file",t.style.cssText="cursor:pointer;padding:4px 10px;border-radius:4px",t.onclick=()=>p().catch(console.error);const o=document.createElement("button");o.textContent="⬇ Download ZIP (with images)",o.title="Save styles, mixes and all images to a ZIP file",o.style.cssText="cursor:pointer;padding:4px 10px;border-radius:4px",o.onclick=()=>w();const s=document.createElement("button");return s.textContent="⬆ Restore (JSON or ZIP)",s.title="Restore from a JSON or ZIP backup — auto-detected",s.style.cssText="cursor:pointer;padding:4px 10px;border-radius:4px",s.onclick=()=>x(),i.appendChild(t),i.appendChild(o),i.appendChild(s),i},defaultValue:null}],nodeCreated(e){var $;if(e.comfyClass!=="StyleMixImmacStyleMixer"&&e.comfyClass!=="StylePickImmacStyleMixer")return;const a=e.comfyClass==="StylePickImmacStyleMixer",r=a?"style":"mix",i=($=e.widgets)==null?void 0:$.find(l=>l.name===r);if(!i)return;const t=document.createElement("img");t.style.cssText="width:100%;height:100%;display:none;object-fit:contain;border-radius:4px";const o=document.createElement("div");o.style.cssText="padding:4px;box-sizing:border-box",o.appendChild(t);let s=0;e.addDOMWidget("immac_preview","div",o,{serialize:!1,getMinHeight:()=>s,getHeight:()=>s});function n(){var l,d,u;(l=e.graph)==null||l.setDirtyCanvas(!0,!0),(u=(d=window.app)==null?void 0:d.canvas)==null||u.setDirty(!0,!0)}function y(l,d){var v,_,k;const u=((v=e.size)==null?void 0:v[0])??300,g=d/l;s=Math.max(80,Math.min(Math.round((u-8)*g),600));const c=(_=e.computeSize)==null?void 0:_.call(e);c&&e.setSize([Math.max(e.size[0],c[0]),Math.max(e.size[1],c[1])]),(k=e.onResize)==null||k.call(e,e.size),n(),requestAnimationFrame(n)}function m(){s=0,t.style.display="none",n()}function S(l){t.onload=()=>{t.style.display="block",y(t.naturalWidth,t.naturalHeight)},t.onerror=()=>{m()},t.src=l}async function b(l){var d;try{const u=await fetch("/immac_style_mixer/api/data");if(!u.ok)return;const g=await u.json();if(a){const c=(g.styles??[]).find(_=>_.name===l);if(!(c!=null&&c.image_filename)){m();return}const v=c.image_updated_at?`&t=${c.image_updated_at}`:"";S(`/view?filename=${encodeURIComponent(c.image_filename)}&subfolder=immac_style_mixer%2Fstyles&type=input${v}`)}else{const c=(g.mixes??[]).find(_=>_.name===l);let v="";if(c!=null&&c.image_filename){const _=c.image_updated_at?`&t=${c.image_updated_at}`:"";v=`/view?filename=${encodeURIComponent(c.image_filename)}&subfolder=immac_style_mixer%2Fmixes&type=input${_}`}else if((d=c==null?void 0:c.styles)!=null&&d.length){const _=Object.fromEntries((g.styles??[]).map(h=>[h.id,h])),k=c.styles.filter(h=>h.enabled!==!1).map(h=>_[h.style_id]).find(h=>h==null?void 0:h.image_filename);if(k){const h=k.image_updated_at?`&t=${k.image_updated_at}`:"";v=`/view?filename=${encodeURIComponent(k.image_filename)}&subfolder=immac_style_mixer%2Fstyles&type=input${h}`}}if(!v){m();return}S(v)}}catch(u){console.error("[ImmacStyleMixer] Preview update failed",u)}}const I=i.callback;i.callback=function(...l){var u;I==null||I.apply(this,l);const d=(u=e.widgets)==null?void 0:u.find(g=>g.name===r);b(String((d==null?void 0:d.value)??""))},e._immacUpdatePreview=l=>{var u;const d=(u=e.widgets)==null?void 0:u.find(g=>g.name===r);b(l??String((d==null?void 0:d.value)??""))},e._immacPreviewScheduled=!0,requestAnimationFrame(()=>{var d;if(!e._immacPreviewScheduled)return;const l=(d=e.widgets)==null?void 0:d.find(u=>u.name===r);l!=null&&l.value&&b(String(l.value))})},loadedGraphNode(e){var o;if(typeof e._immacUpdatePreview!="function")return;e._immacPreviewScheduled=!1;const a=e.comfyClass==="StylePickImmacStyleMixer",r=a?"style":"mix",i=(o=e.widgets)==null?void 0:o.find(s=>s.name===r),t=String((i==null?void 0:i.value)??"");requestAnimationFrame(()=>{t==="(no styles saved)"||t==="(no mixes saved)"?fetch("/immac_style_mixer/api/data").then(s=>s.json()).then(s=>{var m,S;const n=a?s.styles??[]:s.mixes??[],y=(m=e.widgets)==null?void 0:m.find(b=>b.name===r);n.length>0&&y&&(y.value=n[0].name,Array.isArray((S=y.options)==null?void 0:S.values)&&(y.options.values=n.map(b=>b.name))),e._immacUpdatePreview()}).catch(()=>e._immacUpdatePreview()):e._immacUpdatePreview(t)})}})}V().catch(console.error);export{P as j}; diff --git a/ui/src/main.tsx b/ui/src/main.tsx index c5e0aba..cea8b84 100644 --- a/ui/src/main.tsx +++ b/ui/src/main.tsx @@ -228,23 +228,30 @@ async function init(): Promise { getHeight: () => previewHeight, }) + function triggerRedraw() { + // Belt-and-suspenders: both paths in case one is unavailable at call time. + node.graph?.setDirtyCanvas(true, true) + ;(window.app as any)?.canvas?.setDirty(true, true) + } + function applyImageSize(natW: number, natH: number) { const nodeWidth: number = node.size?.[0] ?? 300 const ratio = natH / natW previewHeight = Math.max(80, Math.min(Math.round((nodeWidth - 8) * ratio), 600)) - // node.computeSize reads getMinHeight/getHeight callbacks we just updated + // node.computeSize reads getMinHeight/getHeight callbacks updated above const s = node.computeSize?.() if (s) node.setSize([Math.max(node.size[0], s[0]), Math.max(node.size[1], s[1])]) - // onResize is the canonical signal (matches createImageHost pattern) node.onResize?.(node.size) - node.setDirtyCanvas(true, true) + // Fire immediately, then again on the next frame (canvas may not yet have + // run its first render cycle on page load when onload fires). + triggerRedraw() + requestAnimationFrame(triggerRedraw) } function clearImageSize() { previewHeight = 0 imgEl.style.display = 'none' - node.onResize?.(node.size) - node.setDirtyCanvas(true, true) + triggerRedraw() } function showUrl(url: string) {