w-flowchart 1.0.3 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  /*!
2
- * w-flowchart v1.0.3
2
+ * w-flowchart v1.0.5
3
3
  * (c) 2018-2021 yuda-lyu(semisphere)
4
4
  * Released under the MIT License.
5
5
  */
6
- !function(e,n){"object"==typeof exports&&"undefined"!=typeof module?module.exports=n(require("playwright"),require("fs"),require("url"),require("path")):"function"==typeof define&&define.amd?define(["playwright","fs","url","path"],n):(e="undefined"!=typeof globalThis?globalThis:e||self)["w-flowchart"]=n(e.playwright,e.fs,e.url,e.path)}(this,function(e,n,t,r){"use strict";var o="undefined"!=typeof document?document.currentScript:null,a=Array.isArray,i="object"==typeof global&&global&&global.Object===Object&&global,s="object"==typeof self&&self&&self.Object===Object&&self,l=i||s||Function("return this")(),c=l.Symbol,d=Object.prototype,p=d.hasOwnProperty,f=d.toString,u=c?c.toStringTag:void 0;var h=Object.prototype.toString;var g=c?c.toStringTag:void 0;function y(e){return null==e?void 0===e?"[object Undefined]":"[object Null]":g&&g in Object(e)?function(e){var n=p.call(e,u),t=e[u];try{e[u]=void 0;var r=!0}catch(e){}var o=f.call(e);return r&&(n?e[u]=t:delete e[u]),o}(e):function(e){return h.call(e)}(e)}function b(e){return null!=e&&"object"==typeof e}function m(e){return"symbol"==typeof e||b(e)&&"[object Symbol]"==y(e)}var x=/\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/,w=/^\w*$/;function v(e){var n=typeof e;return null!=e&&("object"==n||"function"==n)}function k(e){if(!v(e))return!1;var n=y(e);return"[object Function]"==n||"[object GeneratorFunction]"==n||"[object AsyncFunction]"==n||"[object Proxy]"==n}var _,E=l["__core-js_shared__"],S=(_=/[^.]+$/.exec(E&&E.keys&&E.keys.IE_PROTO||""))?"Symbol(src)_1."+_:"";var O=Function.prototype.toString;function I(e){if(null!=e){try{return O.call(e)}catch(e){}try{return e+""}catch(e){}}return""}var j=/^\[object .+?Constructor\]$/,A=Function.prototype,M=Object.prototype,L=A.toString,P=M.hasOwnProperty,B=RegExp("^"+L.call(P).replace(/[\\^$.*+?()[\]{}|]/g,"\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g,"$1.*?")+"$");function F(e){return!(!v(e)||(n=e,S&&S in n))&&(k(e)?B:j).test(I(e));var n}function D(e,n){var t=function(e,n){return null==e?void 0:e[n]}(e,n);return F(t)?t:void 0}var N=D(Object,"create");var $=Object.prototype.hasOwnProperty;var G=Object.prototype.hasOwnProperty;function T(e){var n=-1,t=null==e?0:e.length;for(this.clear();++n<t;){var r=e[n];this.set(r[0],r[1])}}function C(e,n){return e===n||e!=e&&n!=n}function R(e,n){for(var t=e.length;t--;)if(C(e[t][0],n))return t;return-1}T.prototype.clear=function(){this.__data__=N?N(null):{},this.size=0},T.prototype.delete=function(e){var n=this.has(e)&&delete this.__data__[e];return this.size-=n?1:0,n},T.prototype.get=function(e){var n=this.__data__;if(N){var t=n[e];return"__lodash_hash_undefined__"===t?void 0:t}return $.call(n,e)?n[e]:void 0},T.prototype.has=function(e){var n=this.__data__;return N?void 0!==n[e]:G.call(n,e)},T.prototype.set=function(e,n){var t=this.__data__;return this.size+=this.has(e)?0:1,t[e]=N&&void 0===n?"__lodash_hash_undefined__":n,this};var H=Array.prototype.splice;function z(e){var n=-1,t=null==e?0:e.length;for(this.clear();++n<t;){var r=e[n];this.set(r[0],r[1])}}z.prototype.clear=function(){this.__data__=[],this.size=0},z.prototype.delete=function(e){var n=this.__data__,t=R(n,e);return!(t<0)&&(t==n.length-1?n.pop():H.call(n,t,1),--this.size,!0)},z.prototype.get=function(e){var n=this.__data__,t=R(n,e);return t<0?void 0:n[t][1]},z.prototype.has=function(e){return R(this.__data__,e)>-1},z.prototype.set=function(e,n){var t=this.__data__,r=R(t,e);return r<0?(++this.size,t.push([e,n])):t[r][1]=n,this};var W=D(l,"Map");function q(e,n){var t,r,o=e.__data__;return("string"==(r=typeof(t=n))||"number"==r||"symbol"==r||"boolean"==r?"__proto__"!==t:null===t)?o["string"==typeof n?"string":"hash"]:o.map}function J(e){var n=-1,t=null==e?0:e.length;for(this.clear();++n<t;){var r=e[n];this.set(r[0],r[1])}}J.prototype.clear=function(){this.size=0,this.__data__={hash:new T,map:new(W||z),string:new T}},J.prototype.delete=function(e){var n=q(this,e).delete(e);return this.size-=n?1:0,n},J.prototype.get=function(e){return q(this,e).get(e)},J.prototype.has=function(e){return q(this,e).has(e)},J.prototype.set=function(e,n){var t=q(this,e),r=t.size;return t.set(e,n),this.size+=t.size==r?0:1,this};function U(e,n){if("function"!=typeof e||null!=n&&"function"!=typeof n)throw new TypeError("Expected a function");var t=function(){var r=arguments,o=n?n.apply(this,r):r[0],a=t.cache;if(a.has(o))return a.get(o);var i=e.apply(this,r);return t.cache=a.set(o,i)||a,i};return t.cache=new(U.Cache||J),t}U.Cache=J;var X,Y,V,Z=/[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g,K=/\\(\\)?/g,Q=(X=function(e){var n=[];return 46===e.charCodeAt(0)&&n.push(""),e.replace(Z,function(e,t,r,o){n.push(r?o.replace(K,"$1"):t||e)}),n},Y=U(X,function(e){return 500===V.size&&V.clear(),e}),V=Y.cache,Y),ee=Q;var ne=c?c.prototype:void 0,te=ne?ne.toString:void 0;function re(e){if("string"==typeof e)return e;if(a(e))return function(e,n){for(var t=-1,r=null==e?0:e.length,o=Array(r);++t<r;)o[t]=n(e[t],t,e);return o}(e,re)+"";if(m(e))return te?te.call(e):"";var n=e+"";return"0"==n&&1/e==-1/0?"-0":n}function oe(e,n){return a(e)?e:function(e,n){if(a(e))return!1;var t=typeof e;return!("number"!=t&&"symbol"!=t&&"boolean"!=t&&null!=e&&!m(e))||w.test(e)||!x.test(e)||null!=n&&e in Object(n)}(e,n)?[e]:ee(function(e){return null==e?"":re(e)}(e))}function ae(e){if("string"==typeof e||m(e))return e;var n=e+"";return"0"==n&&1/e==-1/0?"-0":n}function ie(e,n,t){var r=null==e?void 0:function(e,n){for(var t=0,r=(n=oe(n,e)).length;null!=e&&t<r;)e=e[ae(n[t++])];return t&&t==r?e:void 0}(e,n);return void 0===r?t:r}function se(e){var n=this.__data__=new z(e);this.size=n.size}se.prototype.clear=function(){this.__data__=new z,this.size=0},se.prototype.delete=function(e){var n=this.__data__,t=n.delete(e);return this.size=n.size,t},se.prototype.get=function(e){return this.__data__.get(e)},se.prototype.has=function(e){return this.__data__.has(e)},se.prototype.set=function(e,n){var t=this.__data__;if(t instanceof z){var r=t.__data__;if(!W||r.length<199)return r.push([e,n]),this.size=++t.size,this;t=this.__data__=new J(r)}return t.set(e,n),this.size=t.size,this};var le=function(){try{var e=D(Object,"defineProperty");return e({},"",{}),e}catch(e){}}(),ce=le;function de(e,n,t){"__proto__"==n&&ce?ce(e,n,{configurable:!0,enumerable:!0,value:t,writable:!0}):e[n]=t}var pe=Object.prototype.hasOwnProperty;function fe(e,n,t){var r=e[n];pe.call(e,n)&&C(r,t)&&(void 0!==t||n in e)||de(e,n,t)}function ue(e,n,t,r){var o=!t;t||(t={});for(var a=-1,i=n.length;++a<i;){var s=n[a],l=r?r(t[s],e[s],s,t,e):void 0;void 0===l&&(l=e[s]),o?de(t,s,l):fe(t,s,l)}return t}function he(e){return b(e)&&"[object Arguments]"==y(e)}var ge=Object.prototype,ye=ge.hasOwnProperty,be=ge.propertyIsEnumerable,me=he(function(){return arguments}())?he:function(e){return b(e)&&ye.call(e,"callee")&&!be.call(e,"callee")},xe=me;var we="object"==typeof exports&&exports&&!exports.nodeType&&exports,ve=we&&"object"==typeof module&&module&&!module.nodeType&&module,ke=ve&&ve.exports===we?l.Buffer:void 0,_e=(ke?ke.isBuffer:void 0)||function(){return!1},Ee=/^(?:0|[1-9]\d*)$/;function Se(e,n){var t=typeof e;return!!(n=null==n?9007199254740991:n)&&("number"==t||"symbol"!=t&&Ee.test(e))&&e>-1&&e%1==0&&e<n}function Oe(e){return"number"==typeof e&&e>-1&&e%1==0&&e<=9007199254740991}var Ie={};function je(e){return function(n){return e(n)}}Ie["[object Float32Array]"]=Ie["[object Float64Array]"]=Ie["[object Int8Array]"]=Ie["[object Int16Array]"]=Ie["[object Int32Array]"]=Ie["[object Uint8Array]"]=Ie["[object Uint8ClampedArray]"]=Ie["[object Uint16Array]"]=Ie["[object Uint32Array]"]=!0,Ie["[object Arguments]"]=Ie["[object Array]"]=Ie["[object ArrayBuffer]"]=Ie["[object Boolean]"]=Ie["[object DataView]"]=Ie["[object Date]"]=Ie["[object Error]"]=Ie["[object Function]"]=Ie["[object Map]"]=Ie["[object Number]"]=Ie["[object Object]"]=Ie["[object RegExp]"]=Ie["[object Set]"]=Ie["[object String]"]=Ie["[object WeakMap]"]=!1;var Ae="object"==typeof exports&&exports&&!exports.nodeType&&exports,Me=Ae&&"object"==typeof module&&module&&!module.nodeType&&module,Le=Me&&Me.exports===Ae&&i.process,Pe=function(){try{var e=Me&&Me.require&&Me.require("util").types;return e||Le&&Le.binding&&Le.binding("util")}catch(e){}}(),Be=Pe&&Pe.isTypedArray,Fe=Be?je(Be):function(e){return b(e)&&Oe(e.length)&&!!Ie[y(e)]},De=Object.prototype.hasOwnProperty;function Ne(e,n){var t=a(e),r=!t&&xe(e),o=!t&&!r&&_e(e),i=!t&&!r&&!o&&Fe(e),s=t||r||o||i,l=s?function(e,n){for(var t=-1,r=Array(e);++t<e;)r[t]=n(t);return r}(e.length,String):[],c=l.length;for(var d in e)!n&&!De.call(e,d)||s&&("length"==d||o&&("offset"==d||"parent"==d)||i&&("buffer"==d||"byteLength"==d||"byteOffset"==d)||Se(d,c))||l.push(d);return l}var $e=Object.prototype;function Ge(e){var n=e&&e.constructor;return e===("function"==typeof n&&n.prototype||$e)}function Te(e,n){return function(t){return e(n(t))}}var Ce=Te(Object.keys,Object),Re=Object.prototype.hasOwnProperty;function He(e){return null!=e&&Oe(e.length)&&!k(e)}function ze(e){return He(e)?Ne(e):function(e){if(!Ge(e))return Ce(e);var n=[];for(var t in Object(e))Re.call(e,t)&&"constructor"!=t&&n.push(t);return n}(e)}var We=Object.prototype.hasOwnProperty;function qe(e){if(!v(e))return function(e){var n=[];if(null!=e)for(var t in Object(e))n.push(t);return n}(e);var n=Ge(e),t=[];for(var r in e)("constructor"!=r||!n&&We.call(e,r))&&t.push(r);return t}function Je(e){return He(e)?Ne(e,!0):qe(e)}var Ue="object"==typeof exports&&exports&&!exports.nodeType&&exports,Xe=Ue&&"object"==typeof module&&module&&!module.nodeType&&module,Ye=Xe&&Xe.exports===Ue?l.Buffer:void 0,Ve=Ye?Ye.allocUnsafe:void 0;function Ze(){return[]}var Ke=Object.prototype.propertyIsEnumerable,Qe=Object.getOwnPropertySymbols,en=Qe?function(e){return null==e?[]:(e=Object(e),function(e,n){for(var t=-1,r=null==e?0:e.length,o=0,a=[];++t<r;){var i=e[t];n(i,t,e)&&(a[o++]=i)}return a}(Qe(e),function(n){return Ke.call(e,n)}))}:Ze,nn=en;function tn(e,n){for(var t=-1,r=n.length,o=e.length;++t<r;)e[o+t]=n[t];return e}var rn=Te(Object.getPrototypeOf,Object),on=Object.getOwnPropertySymbols?function(e){for(var n=[];e;)tn(n,nn(e)),e=rn(e);return n}:Ze,an=on;function sn(e,n,t){var r=n(e);return a(e)?r:tn(r,t(e))}function ln(e){return sn(e,ze,nn)}function cn(e){return sn(e,Je,an)}var dn=D(l,"DataView"),pn=D(l,"Promise"),fn=D(l,"Set"),un=D(l,"WeakMap"),hn="[object Map]",gn="[object Promise]",yn="[object Set]",bn="[object WeakMap]",mn="[object DataView]",xn=I(dn),wn=I(W),vn=I(pn),kn=I(fn),_n=I(un),En=y;(dn&&En(new dn(new ArrayBuffer(1)))!=mn||W&&En(new W)!=hn||pn&&En(pn.resolve())!=gn||fn&&En(new fn)!=yn||un&&En(new un)!=bn)&&(En=function(e){var n=y(e),t="[object Object]"==n?e.constructor:void 0,r=t?I(t):"";if(r)switch(r){case xn:return mn;case wn:return hn;case vn:return gn;case kn:return yn;case _n:return bn}return n});var Sn=En,On=Object.prototype.hasOwnProperty;var In=l.Uint8Array;function jn(e){var n=new e.constructor(e.byteLength);return new In(n).set(new In(e)),n}var An=/\w*$/;var Mn=c?c.prototype:void 0,Ln=Mn?Mn.valueOf:void 0;function Pn(e,n,t){var r,o=e.constructor;switch(n){case"[object ArrayBuffer]":return jn(e);case"[object Boolean]":case"[object Date]":return new o(+e);case"[object DataView]":return function(e,n){var t=n?jn(e.buffer):e.buffer;return new e.constructor(t,e.byteOffset,e.byteLength)}(e,t);case"[object Float32Array]":case"[object Float64Array]":case"[object Int8Array]":case"[object Int16Array]":case"[object Int32Array]":case"[object Uint8Array]":case"[object Uint8ClampedArray]":case"[object Uint16Array]":case"[object Uint32Array]":return function(e,n){var t=n?jn(e.buffer):e.buffer;return new e.constructor(t,e.byteOffset,e.length)}(e,t);case"[object Map]":case"[object Set]":return new o;case"[object Number]":case"[object String]":return new o(e);case"[object RegExp]":return function(e){var n=new e.constructor(e.source,An.exec(e));return n.lastIndex=e.lastIndex,n}(e);case"[object Symbol]":return r=e,Ln?Object(Ln.call(r)):{}}}var Bn=Object.create,Fn=function(){function e(){}return function(n){if(!v(n))return{};if(Bn)return Bn(n);e.prototype=n;var t=new e;return e.prototype=void 0,t}}(),Dn=Fn;var Nn=Pe&&Pe.isMap,$n=Nn?je(Nn):function(e){return b(e)&&"[object Map]"==Sn(e)};var Gn=Pe&&Pe.isSet,Tn=Gn?je(Gn):function(e){return b(e)&&"[object Set]"==Sn(e)},Cn="[object Arguments]",Rn="[object Function]",Hn="[object Object]",zn={};function Wn(e,n,t,r,o,i){var s,l=1&n,c=2&n,d=4&n;if(t&&(s=o?t(e,r,o,i):t(e)),void 0!==s)return s;if(!v(e))return e;var p=a(e);if(p){if(s=function(e){var n=e.length,t=new e.constructor(n);return n&&"string"==typeof e[0]&&On.call(e,"index")&&(t.index=e.index,t.input=e.input),t}(e),!l)return function(e,n){var t=-1,r=e.length;for(n||(n=Array(r));++t<r;)n[t]=e[t];return n}(e,s)}else{var f=Sn(e),u=f==Rn||"[object GeneratorFunction]"==f;if(_e(e))return function(e,n){if(n)return e.slice();var t=e.length,r=Ve?Ve(t):new e.constructor(t);return e.copy(r),r}(e,l);if(f==Hn||f==Cn||u&&!o){if(s=c||u?{}:function(e){return"function"!=typeof e.constructor||Ge(e)?{}:Dn(rn(e))}(e),!l)return c?function(e,n){return ue(e,an(e),n)}(e,function(e,n){return e&&ue(n,Je(n),e)}(s,e)):function(e,n){return ue(e,nn(e),n)}(e,function(e,n){return e&&ue(n,ze(n),e)}(s,e))}else{if(!zn[f])return o?e:{};s=Pn(e,f,l)}}i||(i=new se);var h=i.get(e);if(h)return h;i.set(e,s),Tn(e)?e.forEach(function(r){s.add(Wn(r,n,t,r,e,i))}):$n(e)&&e.forEach(function(r,o){s.set(o,Wn(r,n,t,o,e,i))});var g=p?void 0:(d?c?cn:ln:c?Je:ze)(e);return function(e,n){for(var t=-1,r=null==e?0:e.length;++t<r&&!1!==n(e[t],t,e););}(g||e,function(r,o){g&&(r=e[o=r]),fe(s,o,Wn(r,n,t,o,e,i))}),s}zn[Cn]=zn["[object Array]"]=zn["[object ArrayBuffer]"]=zn["[object DataView]"]=zn["[object Boolean]"]=zn["[object Date]"]=zn["[object Float32Array]"]=zn["[object Float64Array]"]=zn["[object Int8Array]"]=zn["[object Int16Array]"]=zn["[object Int32Array]"]=zn["[object Map]"]=zn["[object Number]"]=zn[Hn]=zn["[object RegExp]"]=zn["[object Set]"]=zn["[object String]"]=zn["[object Symbol]"]=zn["[object Uint8Array]"]=zn["[object Uint8ClampedArray]"]=zn["[object Uint16Array]"]=zn["[object Uint32Array]"]=!0,zn["[object Error]"]=zn[Rn]=zn["[object WeakMap]"]=!1;function qn(e){return"[object String]"===Object.prototype.toString.call(e)}function Jn(e){return!(!qn(e)||""===e)}function Un(e){return"[object Object]"===Object.prototype.toString.call(e)}function Xn(e){return"[object Array]"===Object.prototype.toString.call(e)}function Yn(e){return!!function(e){return"[object Undefined]"===Object.prototype.toString.call(e)}(e)||(!!function(e){return"[object Null]"===Object.prototype.toString.call(e)}(e)||(!!function(e){if(Un(e)){for(let n in e)return!1;return!0}return!1}(e)||(!!function(e){return!(!qn(e)||""!==e)}(e)||(!!function(e){return!!Xn(e)&&0===e.length}(e)||!!function(e){return e!=e}(e)))))}const Vn={p1:()=>Promise.resolve().then(function(){return ft}),p2:()=>Promise.resolve().then(function(){return yt}),p3:()=>Promise.resolve().then(function(){return vt}),p4:()=>Promise.resolve().then(function(){return At}),p5:()=>Promise.resolve().then(function(){return Ct}),p6:()=>Promise.resolve().then(function(){return Xt}),p7:()=>Promise.resolve().then(function(){return er}),p8:()=>Promise.resolve().then(function(){return cr}),p9:()=>Promise.resolve().then(function(){return fr}),p10:()=>Promise.resolve().then(function(){return yr})},Zn=["p1","p2","p3","p4","p5","p6","p7","p8","p10"];const Kn={blue:{fill:"#eef4fb",stroke:"#3f6fb0",text:"#1c2b36",shape:"box"},green:{fill:"#eaf4ef",stroke:"#348a5c",text:"#1c2b36",shape:"box"},orange:{fill:"#fcf0e2",stroke:"#c46e1a",text:"#1c2b36",shape:"box"},purple:{fill:"#f4f0fa",stroke:"#8163ad",text:"#23303a",shape:"box"},red:{fill:"#fbecea",stroke:"#c0392b",text:"#23303a",shape:"box"},done:{fill:"#eaf4ef",stroke:"#256046",text:"#256046",shape:"box"},diamond:{fill:"#fcf0e2",stroke:"#c46e1a",text:"#23303a",shape:"diamond"},blueG:{fill:"#f5f9fe",stroke:"#3f6fb0",text:"#3f6fb0",group:!0},blueG2:{fill:"#eef4fb",stroke:"#3f6fb0",text:"#3f6fb0",group:!0},greenG:{fill:"#f0f7f3",stroke:"#348a5c",text:"#348a5c",group:!0},greenG2:{fill:"#eaf4ef",stroke:"#348a5c",text:"#348a5c",group:!0},orangeG:{fill:"#fdf6ec",stroke:"#c46e1a",text:"#c46e1a",group:!0},purpleG:{fill:"#faf7fd",stroke:"#8163ad",text:"#8163ad",group:!0}},Qn={line:"#44505a",text:"#4a5560",textBg:"transparent",haloColor:"#ffffff",haloWidth:3.5},et="'Microsoft JhengHei', sans-serif",nt=e=>/G2?$/.test(e||""),tt=e=>Kn[e]||Kn.blue,rt=r.dirname(t.fileURLToPath("undefined"==typeof document&&"undefined"==typeof location?require("url").pathToFileURL(__filename).href:"undefined"==typeof document?location.href:o&&"SCRIPT"===o.tagName.toUpperCase()&&o.src||new URL("w-flowchart.umd.js",document.baseURI).href));function ot(e){let t=rt;for(let o=0;o<10;o++){const o=r.join(t,"node_modules",e);if(n.existsSync(o))return o;const a=r.dirname(t);if(a===t)break;t=a}throw new Error("找不到本機套件檔案 node_modules/"+e)}function at(e){return n.readFileSync(ot(e),"utf8").replace(/<\/script/gi,"<\\/script")}function it(e){return n.readFileSync(ot(e),"utf8")}const st=at("mermaid10/dist/mermaid.min.js"),lt=e=>String(e).replace(/\n/g,"<br/>");function ct(e,n){return"diamond"===e.cls?`${n(e.id)}{"${lt(e.label)}"}`:`${n(e.id)}["${lt(e.label)}"]`}function dt(e){const{dir:n,nodes:t,edges:r}=e,o=new Set,a={},i=e=>{if(a[e])return a[e];const n="n_"+String(e).replace(/[^A-Za-z0-9_]/g,"_");let t=n,r=2;for(;o.has(t);)t=n+"_"+r++;return o.add(t),a[e]=t,t},s={};for(const e of t)s[e.id]=e;new Set(t.filter(e=>nt(e.cls)).map(e=>e.id));const l=new Set(t.map(e=>e.cls)),c=[];for(const e of l){const n=Kn[e]||Kn.blue;nt(e)?c.push(` classDef ${e} fill:${n.fill},stroke:${n.stroke},stroke-width:1.8px,color:${n.text}`):"diamond"===e?c.push(` classDef ${e} fill:${n.fill},stroke:${n.stroke},stroke-width:1.6px,color:${n.text}`):c.push(` classDef ${e} fill:${n.fill},stroke:${n.stroke},stroke-width:1.5px,color:${n.text}`)}const d={};for(const e of t)d[e.cls]||(d[e.cls]=[]),d[e.cls].push(e.id);const p=[];for(const[e,n]of Object.entries(d))p.push(` class ${n.map(i).join(",")} ${e}`);const f=t.filter(e=>nt(e.cls)&&!e.group).map(e=>e.id),u=t.filter(e=>!nt(e.cls)&&!e.group);function h(e,n){const r=s[e],o=[];o.push(`${n}subgraph ${i(e)}["${String(r.label).replace(/\n/g," ")}"]`);const a=t.filter(n=>n.group===e&&!nt(n.cls));for(const e of a)o.push(`${n} ${ct(e,i)}`);const l=t.filter(n=>nt(n.cls)&&n.group===e);for(const e of l)o.push(...h(e.id,n+" "));return o.push(`${n}end`),o}const g=[];for(const e of r){const n="dashed"===e.kind?"-..->":"--\x3e",t=e.label?`|"${lt(e.label)}"| `:"";g.push(` ${i(e.from)} ${n} ${t}${i(e.to)}`)}const y=[`flowchart ${n}`];for(const e of u)y.push(` ${ct(e,i)}`);for(const e of f)y.push(...h(e," "));return y.push(...g),y.push(...c),y.push(...p),y.join("\n")}async function pt(n){const t=await e.chromium.launch();try{const e=await t.newPage({deviceScaleFactor:4});await e.setContent(function(e){return`<!doctype html><html lang="zh-Hant"><head><meta charset="utf-8">\n<script>${st}<\/script>\n</head><body style="margin:0;background:#fff;font-family:'微軟正黑體','Microsoft JhengHei',sans-serif">\n<div id="box" style="display:inline-block;padding:24px"></div>\n<script>\nmermaid.initialize({ startOnLoad:false, theme:'base', themeVariables:{ fontFamily:"'Microsoft JhengHei','微軟正黑體',sans-serif", fontSize:'17px', primaryColor:'#eef4fb', primaryBorderColor:'#3f6fb0', lineColor:'#44505a', primaryTextColor:'#1c2b36' }, themeCSS: '.flowchart-link{stroke:#44505a !important;stroke-width:2.5px !important;} .edgePaths .path{stroke:#44505a !important;stroke-width:2.5px !important;} .marker,.marker path{fill:#44505a !important;stroke:#44505a !important;} .edgeLabel .label{background:transparent !important;} .edgeLabel{background:transparent !important;} .edgeLabel rect{fill:transparent !important;opacity:0 !important;} .label foreignObject{overflow:visible;} .cluster-label text,.cluster-label span,.cluster-label div,.edgeLabel span{font-family:"Microsoft JhengHei","微軟正黑體",sans-serif !important;}' });\n(async () => {\n try {\n const { svg } = await mermaid.render('g', ${JSON.stringify(e)})\n document.getElementById('box').innerHTML = svg\n const DK = '#333333', W = '3px'\n document.querySelectorAll('#box .edgePaths path, #box .edgePath path, #box path.flowchart-link, #box g.edgePaths path').forEach((p) => { p.style.setProperty('stroke', DK, 'important'); p.style.setProperty('stroke-width', W, 'important') })\n document.querySelectorAll('#box marker path, #box .marker, #box .marker path').forEach((m) => { m.style.setProperty('fill', DK, 'important'); m.style.setProperty('stroke', DK, 'important') })\n document.querySelectorAll('#box .node rect, #box .node polygon, #box .node circle, #box .node path').forEach((n) => { n.style.setProperty('stroke-width', '2px', 'important') })\n document.querySelectorAll('#box .cluster rect, #box .cluster polygon, #box .cluster path').forEach((c) => { c.style.setProperty('stroke', '#777777', 'important'); c.style.setProperty('stroke-width', '1.5px', 'important') })\n // 修正 A: 邊標籤背景透明 + 白色光暈(透過 foreignObject 內 span 的 CSS)\n document.querySelectorAll('#box .edgeLabel rect, #box .edgeLabel .label').forEach((el) => {\n el.style.setProperty('fill', 'transparent', 'important')\n el.style.setProperty('background', 'transparent', 'important')\n el.style.setProperty('opacity', '1', 'important')\n })\n document.querySelectorAll('#box .edgeLabel foreignObject').forEach((fo) => {\n fo.style.setProperty('overflow', 'visible', 'important')\n })\n document.querySelectorAll('#box .edgeLabel span, #box .edgeLabel .edgeLabel').forEach((sp) => {\n sp.style.setProperty('background', 'transparent', 'important')\n sp.style.setProperty('color', '#4a5560', 'important')\n // 字型覆蓋: 邊標籤 span 可能繼承系統字型, 強制指定\n sp.style.setProperty('font-family', "'Microsoft JhengHei', '微軟正黑體', sans-serif", 'important')\n // 白色光暈: CSS text-shadow 多向\n sp.style.setProperty('text-shadow', '-1.5px -1.5px 0 #fff, 1.5px -1.5px 0 #fff, -1.5px 1.5px 0 #fff, 1.5px 1.5px 0 #fff, 0 -2px 0 #fff, 0 2px 0 #fff, -2px 0 0 #fff, 2px 0 0 #fff', 'important')\n // 修正 B: 英數識別字不攔腰斷\n sp.style.setProperty('word-break', 'normal', 'important')\n sp.style.setProperty('overflow-wrap', 'break-word', 'important')\n })\n // 字型覆蓋: cluster(subgraph)標題文字 — mermaid@10 的 .cluster-label text/span 不繼承 fontFamily themeVariable\n document.querySelectorAll('#box .cluster-label text, #box .cluster-label span, #box .cluster-label div').forEach((el) => {\n el.style.setProperty('font-family', "'Microsoft JhengHei', '微軟正黑體', sans-serif", 'important')\n })\n document.title = 'DONE'\n }\n catch (e) {\n document.title = 'FAIL'\n console.log('mmErr', e.message)\n }\n})()\n<\/script></body></html>`}(n),{waitUntil:"load"}),await e.waitForFunction(()=>"DONE"===document.title||"FAIL"===document.title,{timeout:3e4});if("DONE"!==await e.title()){const n=await e.evaluate(()=>{const e=document.getElementById("box");return e?e.innerText:""});throw new Error(`mermaid render failed: ${n}`)}return await e.evaluate(()=>document.fonts.ready),{browser:t,page:e}}catch(e){throw await t.close(),e}}var ft=Object.freeze({__proto__:null,genPng:async function(e,n={}){const{browser:t,page:r}=await pt(dt(e));try{return await r.locator("#box svg").screenshot()}finally{await t.close()}},genSvg:async function(e,n={}){const{browser:t,page:r}=await pt(dt(e));try{return(await r.evaluate(()=>{const e=document.querySelector("#box svg"),n=e.viewBox.baseVal;return e.style.maxWidth="none",e.setAttribute("width",Math.ceil(n.width)),e.setAttribute("height",Math.ceil(n.height)),document.getElementById("box").innerHTML})).replace(/&nbsp;/g,"&#160;").trim()}finally{await t.close()}}});const ut="https://pkg.local/";function ht(e){const n=e.dir||"TB",t="TB"===n?"LR":"TB",r=new Set,o={},a=e=>{if(o[e])return o[e];const n="n_"+String(e).replace(/[^A-Za-z0-9_]/g,"_");let t=n,a=2;for(;r.has(t);)t=n+"_"+a++;return r.add(t),o[e]=t,t},i={},s={};for(const n of e.nodes)s[n.id]=n,n.group&&(i[n.group]||(i[n.group]=[]),i[n.group].push(n.id));const l=e.nodes.filter(e=>!e.group),c=[];function d(e,n){const r=" ".repeat(n);if(nt(e.cls)){c.push(`${r}subgraph ${a(e.id)}["${e.label}"]`),c.push(`${r} direction ${t}`);const o=i[e.id]||[];for(const e of o)d(s[e],n+1);c.push(`${r}end`)}else{const n="diamond"===e.cls?`{${e.label}}`:`["${e.label}"]`;c.push(`${r}${a(e.id)}${n}`)}}c.push(`flowchart ${n}`);for(const e of l)d(e,1);for(const n of e.edges){const e="dashed"===n.kind?"-.->":"--\x3e",t=n.label?`|"${n.label}"|`:"";c.push(` ${a(n.from)} ${e}${t} ${a(n.to)}`)}const p=new Set(e.nodes.map(e=>e.cls)),f={};for(const n of e.nodes)f[n.cls]||(f[n.cls]=[]),f[n.cls].push(n.id);for(const e of p){const n=Kn[e]||Kn.blue,t=n.fill,r=n.stroke,o=n.text,a=(nt(e),"1.5px");c.push(` classDef ${e} fill:${t},stroke:${r},stroke-width:${a},color:${o};`)}for(const[e,n]of Object.entries(f))c.push(` class ${n.map(a).join(",")} ${e};`);return c.join("\n")}async function gt(t){const r="---\nconfig:\n layout: elk\n---\n"+ht(t),o=await e.chromium.launch();try{const e=await o.newPage({deviceScaleFactor:2});await async function(e){await e.route(ut+"**",e=>{const t=decodeURIComponent(e.request().url().slice(18).split("?")[0]);try{e.fulfill({body:n.readFileSync(ot(t)),contentType:t.endsWith(".css")?"text/css":"text/javascript"})}catch(n){e.fulfill({status:404,body:String(n&&n.message||n)})}})}(e),await e.setContent("<!doctype html><html lang=\"zh-Hant\"><head><meta charset=\"utf-8\">\n<style>\n /* 修正 A: 邊標籤光暈 — paint-order + stroke 白邊讓線段透出、文字仍清晰 */\n .edgeLabel text, .edgeLabel tspan {\n paint-order: stroke fill;\n stroke: #ffffff;\n stroke-width: 3.5px;\n stroke-linejoin: round;\n }\n /* 修正 A: 邊標籤背景框透明(移除不透明白底, 否則會遮斷轉折線) */\n .edgeLabel rect, .edgeLabel .label-container {\n fill: transparent !important;\n fill-opacity: 0 !important;\n stroke: none !important;\n }\n /* 修正 B: 節點標籤禁止英數識別字被攔腰斷行 */\n .node foreignObject div, .node foreignObject span,\n .label foreignObject div, .label foreignObject span {\n word-break: keep-all;\n overflow-wrap: break-word;\n white-space: normal;\n }\n /* 修正 C: 字型全覆蓋 — subgraph/容器標題(cluster-label)強制 Microsoft JhengHei */\n .cluster-label foreignObject div, .cluster-label foreignObject span,\n .cluster-label text, .cluster-label tspan,\n .nodeLabel, .nodeLabel span, text, tspan {\n font-family: 'Microsoft JhengHei','微軟正黑體',sans-serif !important;\n }\n</style>\n</head>\n<body style=\"margin:0;background:#fff;font-family:'微軟正黑體','Microsoft JhengHei',sans-serif\">\n<div id=\"box\" style=\"display:inline-block;padding:24px\"></div>\n<script type=\"module\">\ntry {\n const mermaid = (await import('https://pkg.local/mermaid11/dist/mermaid.esm.min.mjs')).default\n const elk = (await import('https://pkg.local/@mermaid-js/layout-elk/dist/mermaid-layout-elk.esm.min.mjs')).default\n mermaid.registerLayoutLoaders(elk)\n mermaid.initialize({ startOnLoad:false, theme:'base', flowchart:{ useMaxWidth:false }, themeVariables:{ fontFamily:\"'微軟正黑體','Microsoft JhengHei',sans-serif\", fontSize:'17px', primaryColor:'#eef4fb', primaryBorderColor:'#3f6fb0', lineColor:'#44505a', primaryTextColor:'#1c2b36' } })\n window.renderFig = async (code, idx) => {\n try {\n const { svg } = await mermaid.render('g'+idx, code)\n const box = document.getElementById('box'); box.innerHTML = svg\n const svgEl = box.querySelector('svg')\n try {\n const vb = svgEl.viewBox && svgEl.viewBox.baseVal\n let w = vb && vb.width, h = vb && vb.height\n if (!w || !h) { const bb = svgEl.getBBox(); w = bb.width; h = bb.height }\n svgEl.style.maxWidth = 'none'\n svgEl.setAttribute('width', Math.ceil(w))\n svgEl.setAttribute('height', Math.ceil(h))\n } catch(e){}\n const DK='#333333', W='3px'\n box.querySelectorAll('.edgePaths path, .edgePath path, path.flowchart-link, g.edgePaths path').forEach(p=>{p.style.setProperty('stroke',DK,'important');p.style.setProperty('stroke-width',W,'important')})\n box.querySelectorAll('marker path, .marker, .marker path').forEach(m=>{m.style.setProperty('fill',DK,'important');m.style.setProperty('stroke',DK,'important')})\n box.querySelectorAll('.node rect, .node polygon, .node circle, .node path').forEach(n=>n.style.setProperty('stroke-width','2px','important'))\n box.querySelectorAll('.cluster rect, .cluster polygon, .cluster path').forEach(c=>{c.style.setProperty('stroke','#777','important');c.style.setProperty('stroke-width','1.5px','important')})\n // 修正 A (JS 層): 邊標籤背景矩形透明 + 文字白色光暈(JS inline style 強制覆蓋 mermaid 注入的樣式)\n box.querySelectorAll('.edgeLabel rect, .edgeLabel .label-container').forEach(r=>{\n r.style.setProperty('fill','transparent','important')\n r.style.setProperty('fill-opacity','0','important')\n r.style.setProperty('stroke','none','important')\n })\n box.querySelectorAll('.edgeLabel text, .edgeLabel tspan').forEach(t=>{\n t.style.setProperty('paint-order','stroke fill','important')\n t.style.setProperty('stroke','#ffffff','important')\n t.style.setProperty('stroke-width','3.5px','important')\n t.style.setProperty('stroke-linejoin','round','important')\n })\n // 修正 C (JS 層): 字型全覆蓋 — subgraph/容器標題 cluster-label + 所有 SVG text/tspan\n const FF = \"'Microsoft JhengHei','微軟正黑體',sans-serif\"\n box.querySelectorAll('.cluster-label foreignObject div, .cluster-label foreignObject span').forEach(el=>{\n el.style.setProperty('font-family',FF,'important')\n })\n box.querySelectorAll('text, tspan').forEach(el=>{\n el.style.setProperty('font-family',FF,'important')\n })\n if (document.fonts && document.fonts.ready) await document.fonts.ready\n return { ok:true }\n } catch(e){ return { ok:false, err:(e&&(e.message||e.stack))||String(e) } }\n }\n window.__ready = true\n} catch(e){ window.__setupErr = (e&&(e.message||e.stack))||String(e) }\n<\/script></body></html>",{waitUntil:"load"}),await e.waitForFunction(()=>window.__ready||window.__setupErr,{timeout:9e4}).catch(()=>{});const t=await e.evaluate(()=>window.__setupErr);if(t)throw new Error(t);const a=await e.evaluate(([e,n])=>window.renderFig(e,n),[r,0]);if(!a.ok)throw new Error(a.err);return await e.waitForTimeout(250),{browser:o,page:e}}catch(e){throw await o.close(),e}}var yt=Object.freeze({__proto__:null,genPng:async function(e,n={}){const{browser:t,page:r}=await gt(e);try{return await r.locator("#box svg").screenshot()}finally{await t.close()}},genSvg:async function(e,n={}){const{browser:t,page:r}=await gt(e);try{return(await r.evaluate(()=>{const e=document.querySelector("#box svg"),n=Array.from(document.querySelectorAll("head style")).map(e=>e.textContent).join("\n"),t=document.createElementNS("http://www.w3.org/2000/svg","style");t.textContent=n,e.insertBefore(t,e.firstChild);const r=document.getElementById("box").innerHTML;return t.remove(),r})).replace(/&nbsp;/g,"&#160;").trim()}finally{await t.close()}},translate:ht});function bt(e){return String(e).toLowerCase().replace(/[^a-z]/g,e=>"2"===e?"b":"g")}function mt(e){const n="LR"===e.dir?"right":"down",t=function(){const e=[];for(const[n,t]of Object.entries(Kn))"diamond"!==n&&e.push(`#.${bt(n)}: fill=${t.fill} stroke=${t.stroke}`);return e.join("\n")}(),r={};for(const n of e.nodes)r[n.id]=n;const o=e=>String(e).replace(/\n/g,";"),a=e=>String(e).replace(/\n/g," "),i=e=>"diamond"===e.cls?a(e.label):o(e.label);return`#direction: ${n}\n#font: Microsoft JhengHei\n${t}\n${e.nodes.map(e=>{return`[${n=e.cls,"diamond"===n?"<choice>":`<${bt(n)}>`}${i(e)}]`;var n}).join("\n")}\n${e.nodes.filter(e=>e.group&&r[e.group]).map(e=>`[${i(r[e.group])}] --\x3e [${i(e)}]`).join("\n")}\n${e.edges.map(e=>{const n=r[e.from]?i(r[e.from]):o(e.from),t=r[e.to]?i(r[e.to]):o(e.to),s="dashed"===e.kind?"--\x3e":"->";return`[${n}]${e.label?` ${a(e.label)} `:" "}${s} [${t}]`}).join("\n")}`}const xt=`<!doctype html><html lang="zh-Hant"><head><meta charset="utf-8">\n<script>${at("graphre/dist/graphre.js")}<\/script>\n<script>${at("nomnoml/dist/nomnoml.js")}<\/script>\n</head><body style="margin:0;background:#fff;font-family:'Microsoft JhengHei',sans-serif">\n<div id="box" style="display:inline-block;padding:20px"></div>\n<script>\nvar HALO_COLOR = ${JSON.stringify(Qn.haloColor)}, HALO_W = ${Qn.haloWidth}\nfunction applyEdgeLabelHalo(box){\n // 邊標籤 = <text> 無 data-name(節點內標籤皆帶 data-name)。\n var svg = box.querySelector('svg')\n var ts = Array.prototype.slice.call(box.querySelectorAll('svg text'))\n ts.forEach(function(t){\n if (t.hasAttribute('data-name')) return // 節點標籤, 不處理\n // (A) 白描邊 + paint-order:stroke 使描邊在字下方\n t.setAttribute('paint-order','stroke')\n t.setAttribute('stroke', HALO_COLOR)\n t.setAttribute('stroke-width', HALO_W)\n t.setAttribute('stroke-linejoin','round')\n // (B) z-order: 移至 svg 尾端 → 最後繪製, 永不被節點 rect/容器填色蓋住\n svg.appendChild(t)\n })\n}\nwindow.renderAndDetect = function(src){\n try {\n var svg = window.nomnoml.renderSvg(src)\n var box = document.getElementById('box'); box.innerHTML = svg\n applyEdgeLabelHalo(box)\n var texts = Array.prototype.slice.call(box.querySelectorAll('svg text'))\n var R = texts.map(function(t){ return t.getBoundingClientRect() })\n // 只計「節點×節點」文字重疊驅動間距(原則2/3): 節點標籤帶 data-name; 邊標籤無 data-name。\n // 凡牽涉邊標籤之重疊(邊標籤×節點 / 邊標籤×邊標籤)一律「不算碰撞」——其可讀性已由\n // halo(白描邊)+ z-order(提至最上層)保證, 不該用「加大間距」處理(否則 進度回報×Worker Thread\n // 這類邊標籤壓節點之 bbox 重疊會把圖撐爆)。唯有節點本體互疊才是 z-order 無法解、須間距處理者。\n var isNode = texts.map(function(t){ return t.hasAttribute('data-name') })\n var col = 0, details = []\n for (var i=0;i<R.length;i++) for (var j=i+1;j<R.length;j++){\n if (!(isNode[i] && isNode[j])) continue // 任一方為邊標籤 → 交給 halo+z-order, 不驅動間距\n var a=R[i], b=R[j]\n var ix = Math.min(a.right,b.right)-Math.max(a.left,b.left)\n var iy = Math.min(a.bottom,b.bottom)-Math.max(a.top,b.top)\n if (ix>2 && iy>2){ col++; if(details.length<3) details.push(((texts[i].textContent||'').trim().slice(0,6))+'×'+((texts[j].textContent||'').trim().slice(0,6))) }\n }\n return { ok:true, collisions: col, details: details }\n } catch(e){ return { ok:false, err:(e&&(e.message||e.stack))||String(e) } }\n}\nwindow.__ready = true\n<\/script></body></html>`;async function wt(n){const t=await e.chromium.launch();try{const e=await t.newPage({deviceScaleFactor:2});await e.setContent(xt,{waitUntil:"load"}),await e.waitForFunction(()=>window.__ready,{timeout:6e4}).catch(()=>{});const r=mt(n),o=await async function(e,n){const t=[40,60,85];let r=null;for(const o of t){const t=await e.evaluate(e=>window.renderAndDetect(e),"#spacing: "+o+"\n"+n);if(!t.ok)return{ok:!1,err:String(t.err).split("\n")[0]};if((null===r||t.collisions<r.collisions)&&(r={spacing:o,collisions:t.collisions,details:t.details}),0===t.collisions){r={spacing:o,collisions:0,details:[]};break}}return{ok:!0,...r}}(e,r);if(!o.ok)throw new Error(o.err);return await e.evaluate(e=>window.renderAndDetect(e),"#spacing: "+o.spacing+"\n"+r),await e.evaluate(()=>document.fonts&&document.fonts.ready),await e.waitForTimeout(180),{browser:t,page:e}}catch(e){throw await t.close(),e}}var vt=Object.freeze({__proto__:null,genPng:async function(e,n={}){const{browser:t,page:r}=await wt(e);try{return await r.locator("#box svg").screenshot()}finally{await t.close()}},genSvg:async function(e,n={}){const{browser:t,page:r}=await wt(e);try{return(await r.evaluate(()=>document.getElementById("box").innerHTML)).replace(/&nbsp;/g,"&#160;").trim()}finally{await t.close()}}});const kt=at("cytoscape/dist/cytoscape.min.js"),_t=at("dagre/dist/dagre.min.js"),Et=at("cytoscape-dagre/dist/cytoscape-dagre.js"),St=at("cytoscape-svg/cytoscape-svg.js");function Ot(e){const n=[];for(const t of e.nodes)n.push({data:{id:t.id,label:t.label,parent:t.group},classes:t.cls});for(const t of e.edges)n.push({data:{source:t.from,target:t.to,label:t.label},classes:"dashed"===t.kind?"dashed":void 0});return n}const It=`<!doctype html><html lang="zh-Hant"><head><meta charset="utf-8">\n<script>${kt}<\/script>\n<script>${_t}<\/script>\n<script>${Et}<\/script>\n<script>${St}<\/script>\n</head><body style="margin:0;background:#fff">\n<div id="cy" style="width:1400px;height:1400px;background:#fff"></div>\n<script>\nif (window.cytoscapeDagre) { try { cytoscape.use(window.cytoscapeDagre) } catch(e){} }\nif (window.cytoscapeSvg) { try { cytoscape.use(window.cytoscapeSvg) } catch(e){} }\n// mode: 'png'(預設) 或 'svg' — 版面/元素/樣式建置流程共用, 僅末端輸出格式不同(genPng 行為不變)\nwindow.renderFig = function(els, styleJson, rankDir, mode){ return new Promise(function(resolve){\n try {\n var cy = cytoscape({ container: document.getElementById('cy'), elements: els, style: JSON.parse(styleJson) })\n els.forEach(function(x){ if (x.classes && x.data && x.data.id) cy.getElementById(x.data.id).addClass(x.classes) })\n var l = cy.layout({ name:'dagre', rankDir:rankDir, nodeSep:42, rankSep:55, edgeSep:18 })\n l.promiseOn('layoutstop').then(function(){\n setTimeout(function(){\n try {\n var bb=cy.elements().boundingBox()\n if (mode === 'svg') { window.__svg = cy.svg({ full:true, scale:2, bg:'#ffffff' }) }\n else { window.__png = cy.png({ full:true, scale:2, bg:'#ffffff', output:'base64' }) }\n resolve({ ok:true, w:Math.round(bb.w), h:Math.round(bb.h) })\n }\n catch(e){ resolve({ ok:false, err:String(e.message||e) }) }\n }, 250)\n })\n l.run()\n } catch(e){ resolve({ ok:false, err:String(e.message||e) }) }\n})}\nwindow.__ready = true\n<\/script></body></html>`;async function jt(n,t){const r=await e.chromium.launch();try{const e=await r.newPage({deviceScaleFactor:1});await e.setContent(It,{waitUntil:"load"}),await e.waitForFunction(()=>window.__ready,{timeout:6e4}).catch(()=>{}),await e.evaluate(()=>document.fonts&&document.fonts.ready);const o=await e.evaluate(([e,n,t,r])=>window.renderFig(e,n,t,r),[Ot(n),'[\n { "selector":"node", "style":{ "shape":"round-rectangle","background-color":"#eef4fb","border-color":"#3f6fb0","border-width":1.8,"label":"data(label)","text-valign":"center","text-halign":"center","font-family":"Microsoft JhengHei, sans-serif","font-size":14,"color":"#1c2b36","width":"label","height":"label","padding":"10px","text-wrap":"wrap","text-max-width":"360px" } },\n { "selector":"node.green", "style":{ "background-color":"#eaf4ef","border-color":"#348a5c" } },\n { "selector":"node.orange", "style":{ "background-color":"#fcf0e2","border-color":"#c46e1a" } },\n { "selector":"node.purple", "style":{ "background-color":"#f4f0fa","border-color":"#8163ad" } },\n { "selector":"node.red", "style":{ "background-color":"#fbecea","border-color":"#c0392b" } },\n { "selector":"node.done", "style":{ "background-color":"#eaf4ef","border-color":"#256046","color":"#256046" } },\n { "selector":"node.diamond", "style":{ "shape":"diamond","background-color":"#fcf0e2","border-color":"#c46e1a","padding":"34px","text-max-width":"120px" } },\n { "selector":":parent", "style":{ "text-valign":"top","text-halign":"center","font-family":"Microsoft JhengHei, sans-serif","font-weight":"bold","padding":"16px","background-opacity":0.35,"border-width":2,"z-compound-depth":"bottom" } },\n { "selector":"node.blueG", "style":{ "background-color":"#f5f9fe","border-color":"#3f6fb0","color":"#3f6fb0" } },\n { "selector":"node.blueG2", "style":{ "background-color":"#eef4fb","border-color":"#3f6fb0","color":"#3f6fb0" } },\n { "selector":"node.greenG", "style":{ "background-color":"#f0f7f3","border-color":"#348a5c","color":"#348a5c" } },\n { "selector":"node.greenG2", "style":{ "background-color":"#eaf4ef","border-color":"#348a5c","color":"#348a5c" } },\n { "selector":"node.orangeG", "style":{ "background-color":"#fdf6ec","border-color":"#c46e1a","color":"#c46e1a" } },\n { "selector":"node.purpleG", "style":{ "background-color":"#faf7fd","border-color":"#8163ad","color":"#8163ad" } },\n { "selector":"edge", "style":{ "width":2.2,"line-color":"#44505a","target-arrow-color":"#44505a","target-arrow-shape":"triangle","curve-style":"bezier","label":"data(label)","font-family":"Microsoft JhengHei, sans-serif","font-size":12.5,"color":"#4a5560","text-background-opacity":0,"text-outline-color":"#ffffff","text-outline-width":3,"text-margin-y":-2,"text-wrap":"wrap" } },\n { "selector":"edge.dashed", "style":{ "line-style":"dashed" } }\n]',n.dir,t]);if(!o.ok)throw new Error(o.err);return await e.evaluate(e=>"svg"===e?window.__svg:window.__png,t)}finally{await r.close()}}var At=Object.freeze({__proto__:null,genPng:async function(e,n={}){const t=await jt(e,"png");return Buffer.from(t,"base64")},genSvg:async function(e,n={}){let t=await jt(e,"svg");return t=t.replace(/&nbsp;/g,"&#160;"),/\sxmlns=/.test(t)||(t=t.replace("<svg",'<svg xmlns="http://www.w3.org/2000/svg"')),/font-family/.test(t)||(t=t.replace(">","><style>text{font-family:'Microsoft JhengHei',sans-serif}</style>")),t}});const Mt='"Microsoft JhengHei","Microsoft YaHei",sans-serif',Lt=`<style>text,.text-bold,.text-italic,.text-underline,tspan{font-family:${Mt} !important;}.text-bold{font-weight:bold !important;}</style>`;const Pt="<style>.text-italic{paint-order:stroke;stroke:#ffffff;stroke-width:3.5px;}</style>";const Bt=e=>String(e).replace(/\$/g,"\\$").replace(/\n/g,"\\n"),Ft={TB:"down",LR:"right",BT:"up",RL:"left"};function Dt(e,n){const t=tt(e),r=[];return r.push(` style.fill: "${t.fill}"`),r.push(` style.stroke: "${t.stroke}"`),t.text&&(n||"done"===e)&&r.push(` style.font-color: "${t.text}"`),n&&r.push(" style.bold: true"),"diamond"===t.shape&&r.push(" shape: diamond"),r}function Nt(e,n,t){const r=[];let o=e;for(;o;)r.unshift(o),o=n[o];return r.map(t).join(".")}function $t(e,n,t,r,o,a){const i=" ".repeat(o),s=[];if(nt(e.cls)){s.push(`${i}${a(e.id)}: "${Bt(e.label)}" {`);const l=n[e.id]||[];l.length>0&&s.push(`${i} direction: ${Ft[r]||"down"}`);for(const n of Dt(e.cls,!0))s.push(`${i}${n}`);for(const e of l)s.push($t(e,n,t,r,o+2,a));s.push(`${i}}`)}else{s.push(`${i}${a(e.id)}: "${Bt(e.label)}" {`);for(const n of Dt(e.cls,!1))s.push(`${i}${n}`);s.push(`${i}}`)}return s.join("\n")}const Gt={layout:"dagre",pad:40,themeID:0};async function Tt(e){const{D2:n}=await import("@terrastruct/d2"),t=new n;try{const n=function(e){const n=new Set,t={},r=e=>{if(t[e])return t[e];const r="n_"+String(e).replace(/[^A-Za-z0-9_]/g,"_");let o=r,a=2;for(;n.has(o);)o=r+"_"+a++;return n.add(o),t[e]=o,o},o={},a={};for(const n of e.nodes)n.group&&(o[n.id]=n.group,a[n.group]||(a[n.group]=[]),a[n.group].push(n));const i=e.nodes.filter(e=>!e.group),s=[];s.push(`direction: ${Ft[e.dir]||"down"}`);for(const n of i)s.push($t(n,a,o,e.dir,0,r));for(const n of e.edges){let e=`${Nt(n.from,o,r)} -> ${Nt(n.to,o,r)}`;n.label&&(e+=`: "${Bt(n.label)}"`),"dashed"===n.kind&&(e+=" { style.stroke-dash: 4 }"),s.push(e)}return s.join("\n\n")}(e),r=await t.compile({fs:{index:n},options:Gt});let o=await t.render(r.diagram,r.renderOptions);return o=function(e){const n=(e=e.replace(/<mask\b([^>]*)>([\s\S]*?)<\/mask>/g,(e,n,t)=>`<mask${n}>${t.replace(/<rect[^>]*fill="black"[^>]*><\/rect>/g,"")}</mask>`)).indexOf("<style");if(n>=0)return e.slice(0,n)+Pt+e.slice(n);const t=e.indexOf(">");return e.slice(0,t+1)+Pt+e.slice(t+1)}(o),o=function(e){const n=(e=(e=e.replace(/font-family\s*:\s*[^;}<]+/gi,`font-family:${Mt}`)).replace(/font-family\s*=\s*"[^"]*"/gi,`font-family="${Mt}"`)).indexOf("<style");if(n>=0)return e.slice(0,n)+Lt+e.slice(n);const t=e.indexOf(">");return e.slice(0,t+1)+Lt+e.slice(t+1)}(o),o}finally{await t.ready.catch(()=>{}),t.worker&&t.worker.terminate&&await t.worker.terminate()}}var Ct=Object.freeze({__proto__:null,genPng:async function(e,n={}){const{default:t}=await import("sharp"),r=await Tt(e),o=r.match(/viewBox="0 0 (\d+(?:\.\d+)?) (\d+(?:\.\d+)?)"/),a=o?parseFloat(o[1]):1e3,i=96*Math.min(2.4,Math.max(1.4,1700/a));return await t(Buffer.from(r),{density:i}).png().toBuffer()},genSvg:async function(e,n={}){return await Tt(e)}});const Rt=at("@antv/g6/dist/g6.min.js"),Ht=at("@antv/g-lite/dist/index.umd.min.js"),zt=at("@antv/g-svg/dist/index.umd.min.js");function Wt(e){const n=String(e).split("\n");return[Math.round(Math.max(...n.map(e=>[...e].reduce((e,n)=>e+(/[一-鿿()/]/.test(n)?15.5:8.5),0)))+34),Math.round(26*n.length+20)]}function qt(e){const n=[],t=[],r=new Set(e.nodes.filter(e=>nt(e.cls)).map(e=>e.id)),o={};for(const n of e.nodes)n.group&&(o[n.group]=o[n.group]||[]).push(n);const a=e=>{for(const n of o[e]||[]){if(!nt(n.cls))return n.id;const e=a(n.id);if(e)return e}return null},i=e=>r.has(e)&&a(e)||e;for(const r of e.nodes){const e=tt(r.cls);if(nt(r.cls)){t.push({id:r.id,combo:r.group,type:"rect",style:{zIndex:-10,labelText:r.label,labelPlacement:"top",labelFontFamily:et,labelFontSize:15,labelFontWeight:700,labelFill:e.text,fill:e.fill,fillOpacity:.3,stroke:e.stroke,lineWidth:2}});continue}const o="diamond"===e.shape;let[a,i]=Wt(r.label);o&&(a+=60,i+=50),n.push({id:r.id,combo:r.group,type:o?"diamond":"rect",style:{size:[a,i],radius:o?0:6,lineWidth:1.8,fill:e.fill,stroke:e.stroke,labelText:r.label,labelPlacement:"center",labelFill:e.text,labelFontSize:14,labelFontFamily:et}})}let s=0;return{nodes:n,combos:t,edges:e.edges.map(e=>{s++;const n="dashed"===e.kind;return{id:"e"+s,source:i(e.from),target:i(e.to),style:{zIndex:100,lineWidth:2.2,endArrow:!0,stroke:n?tt("orange").stroke:Qn.line,lineDash:n?[7,5]:void 0,labelText:e.label||"",labelFontFamily:et,labelFontSize:12.5,labelFill:Qn.text,labelBackground:!1,labelStroke:Qn.haloColor,labelLineWidth:Qn.haloWidth,labelPaintOrder:"stroke",labelAutoRotate:!1}}})}}const Jt=`<!doctype html><html lang="zh-Hant"><head><meta charset="utf-8">\n<script>${Rt}<\/script>\n</head><body style="margin:0;background:#fff">\n<div id="g6" style="background:#fff"></div>\n<script>\nwindow.renderFig = async function(g, rankdir, nodesep, ranksep, canvas, font, comboZ, edgeZ){\n try {\n const el = document.getElementById('g6')\n el.style.width = canvas + 'px'; el.style.height = canvas + 'px'\n if (window.__graph) { try { window.__graph.destroy() } catch(e){} }\n const graph = new G6.Graph({\n container: el, width: canvas, height: canvas, autoFit:'view',\n data: { nodes: g.nodes, edges: g.edges, combos: g.combos },\n layout: { type:'antv-dagre', rankdir: rankdir, nodesep: nodesep, ranksep: ranksep, sortByCombo: (g.combos && g.combos.length>0) },\n // 原則2:combo 墊底、edge 最上層之 graph 預設(各元素 style 已設, 此處兜底)\n combo: { type:'rect', style:{ zIndex: comboZ, padding:[30,18,18,18], labelPlacement:'top', labelFontFamily: font } },\n edge: { type:'polyline', style:{ zIndex: edgeZ, lineWidth:2.2, endArrow:true, labelFontFamily: font } },\n // 原則1:node/edge/combo 三層 graph 預設字型統一(JhengHei), 全覆蓋兜底\n node: { style:{ labelFontFamily: font } },\n })\n window.__graph = graph\n await graph.render()\n await new Promise(r=>setTimeout(r,300))\n return { ok:true }\n } catch(e){ return { ok:false, err:String(e&&(e.stack||e.message)||e).slice(0,400) } }\n}\n// 以目前已渲染之圖截圖\nwindow.snap = async function(){\n try {\n await new Promise(r=>setTimeout(r,200))\n const png = await window.__graph.toDataURL({ mode:'overall' })\n return { ok:true, png }\n } catch(e){ return { ok:false, err:String(e&&(e.stack||e.message)||e).slice(0,400) } }\n}\nwindow.__ready=true\n<\/script></body></html>`;const Ut=`<!doctype html><html lang="zh-Hant"><head><meta charset="utf-8">\n<script>${Ht}<\/script>\n<script>${zt}<\/script>\n<script>${Rt}<\/script>\n</head><body style="margin:0;background:#fff">\n<div id="g6" style="background:#fff"></div>\n<script>\nwindow.renderFigSvg = async function(g, rankdir, nodesep, ranksep, canvas, font, comboZ, edgeZ){\n try {\n const el = document.getElementById('g6')\n el.style.width = canvas + 'px'; el.style.height = canvas + 'px'\n if (window.__graph) { try { window.__graph.destroy() } catch(e){} }\n const graph = new G6.Graph({\n container: el, width: canvas, height: canvas, autoFit:'view',\n renderer: () => new window.G.SVG.Renderer(),\n data: { nodes: g.nodes, edges: g.edges, combos: g.combos },\n layout: { type:'antv-dagre', rankdir: rankdir, nodesep: nodesep, ranksep: ranksep, sortByCombo: (g.combos && g.combos.length>0) },\n combo: { type:'rect', style:{ zIndex: comboZ, padding:[30,18,18,18], labelPlacement:'top', labelFontFamily: font } },\n edge: { type:'polyline', style:{ zIndex: edgeZ, lineWidth:2.2, endArrow:true, labelFontFamily: font } },\n node: { style:{ labelFontFamily: font } },\n })\n window.__graph = graph\n await graph.render()\n await new Promise(r=>setTimeout(r,300))\n return { ok:true }\n } catch(e){ return { ok:false, err:String(e&&(e.stack||e.message)||e).slice(0,400) } }\n}\n// 取 main layer 之 SVG DOM(SVG renderer 下唯一承載節點/邊/combo 之 layer, background/label/transient 三層無內容),\n// 以 canvas.getBounds() 對齊 toDataURL({mode:'overall'}) 之緊裁切邊界改寫 viewBox/width/height, 補白底矩形。\nwindow.snapSvg = async function(){\n try {\n await new Promise(r=>setTimeout(r,200))\n const canvas = window.__graph.getCanvas()\n const bounds = canvas.getBounds()\n const minX = bounds.min[0], minY = bounds.min[1]\n const w = bounds.max[0] - minX, h = bounds.max[1] - minY\n const mainEl = canvas.getLayer('main').getContextService().getDomElement()\n const svg = mainEl.cloneNode(true)\n // canvas.getBounds() 回傳世界座標(排版原始座標, 未經 camera 縮放平移); 但複製之 DOM 內容掛在\n // #g-svg-camera 群組下, 帶有互動視圖之 camera transform(autoFit:'view' 縮放平移)。\n // 兩者座標系不同, 混用會裁切錯位, 故先歸零 camera transform, 使內容回到與 bounds 相同之世界座標。\n const cameraEl = svg.querySelector('#g-svg-camera')\n if (cameraEl) cameraEl.removeAttribute('transform')\n svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg')\n svg.setAttribute('viewBox', minX + ' ' + minY + ' ' + w + ' ' + h)\n svg.setAttribute('width', String(Math.ceil(w)))\n svg.setAttribute('height', String(Math.ceil(h)))\n svg.removeAttribute('tabindex')\n svg.style.background = ''\n svg.style.pointerEvents = ''\n svg.style.gridArea = ''\n svg.style.outline = ''\n const bgRect = document.createElementNS('http://www.w3.org/2000/svg', 'rect')\n bgRect.setAttribute('x', String(minX)); bgRect.setAttribute('y', String(minY))\n bgRect.setAttribute('width', String(w)); bgRect.setAttribute('height', String(h))\n bgRect.setAttribute('fill', '#fff')\n svg.insertBefore(bgRect, svg.firstChild)\n const out = svg.outerHTML.replace(/&nbsp;/g, '&#160;')\n return { ok:true, svg: out }\n } catch(e){ return { ok:false, err:String(e&&(e.stack||e.message)||e).slice(0,400) } }\n}\nwindow.__readySvg=true\n<\/script></body></html>`;var Xt=Object.freeze({__proto__:null,genPng:async function(n,t={}){const r=await e.chromium.launch();try{const e=await r.newPage({deviceScaleFactor:2}),t=[];e.on("pageerror",e=>t.push(e.message)),e.on("console",e=>{"error"===e.type()&&t.push("c:"+e.text())}),await e.setContent(Jt,{waitUntil:"load"}),await e.waitForFunction(()=>window.__ready,{timeout:6e4}).catch(()=>{}),await e.evaluate(()=>document.fonts&&document.fonts.ready);const o=qt(n),a=await e.evaluate(([e,n,t,r,o,a,i,s])=>window.renderFig(e,n,t,r,o,a,i,s),[o,n.dir,28,60,2200,et,-10,100]);if(!a.ok)throw new Error("renderFig failed :: "+String(a.err).split("\n")[0]);const i=await e.evaluate(()=>window.snap());if(!i.ok)throw new Error("snap failed :: "+i.err);return Buffer.from(i.png.split(",")[1],"base64")}finally{await r.close()}},genSvg:async function(n,t={}){const r=await e.chromium.launch();try{const e=await r.newPage(),t=[];e.on("pageerror",e=>t.push(e.message)),e.on("console",e=>{"error"===e.type()&&t.push("c:"+e.text())}),await e.setContent(Ut,{waitUntil:"load"}),await e.waitForFunction(()=>window.__readySvg,{timeout:6e4}).catch(()=>{}),await e.evaluate(()=>document.fonts&&document.fonts.ready);const o=qt(n),a=await e.evaluate(([e,n,t,r,o,a,i,s])=>window.renderFigSvg(e,n,t,r,o,a,i,s),[o,n.dir,28,60,2200,et,-10,100]);if(!a.ok)throw new Error("renderFigSvg failed :: "+String(a.err).split("\n")[0]);const i=await e.evaluate(()=>window.snapSvg());if(!i.ok)throw new Error("snapSvg failed :: "+i.err);return i.svg}finally{await r.close()}}});const Yt=at("@joint/core/dist/joint.min.js"),Vt=at("dagre/dist/dagre.min.js"),Zt=Object.fromEntries(Object.entries(Kn).map(([e,n])=>{const t={fill:n.fill,stroke:n.stroke,color:n.text};return"diamond"===n.shape&&(t.shape="diamond"),n.group&&(t.group=!0),[e,t]}));const Kt=`<!doctype html><html lang="zh-Hant"><head><meta charset="utf-8">\n<script>${Yt}<\/script>\n<script>${Vt}<\/script>\n</head><body style="margin:0;background:#fff">\n<div id="paper" style="background:#fff"></div>\n<script>\nvar CLS = ${JSON.stringify(Zt)}\nvar FONT = 'Microsoft JhengHei, "Microsoft JhengHei", sans-serif'\nvar FS = 14, FSG = 14.5, FSL = 12.5\n\n// 量測文字寬度(canvas)\nvar __cv = document.createElement('canvas'); var __cx = __cv.getContext('2d')\nfunction measure(text, fs){ __cx.font = fs + 'px ' + FONT; return __cx.measureText(text).width }\n\n// 依字寬自動換行(回傳 lines 陣列 + 最大寬)\n// label 內 \\n 為強制換行: 先依 \\n 切段、各段再逐字寬斷行——渲染端以 join('\\n') 交給 JointJS,\n// 若量測不切 \\n 會令實際行數多於估算行數, 框高低估、文字上下溢出框\nfunction wrapText(text, fs, maxW){\n var lines = []\n String(text).split('\\n').forEach(function(seg){\n var cur = ''\n for (var i = 0; i < seg.length; i++){\n var ch = seg[i]\n var test = cur + ch\n if (measure(test, fs) > maxW && cur !== ''){ lines.push(cur); cur = ch }\n else cur = test\n }\n lines.push(cur) // 空段亦保留(連續 \\n 表空行)\n })\n if (lines.length === 1 && lines[0] === '') lines = [] // 空字串輸入維持原行為(零行)\n var w = 0; lines.forEach(function(l){ w = Math.max(w, measure(l, fs)) })\n return { lines: lines, w: w }\n}\n\nfunction renderFig(els, rankDir, sep){\n return new Promise(function(resolve){\n try {\n var NODESEP = (sep && sep.nodesep) || 32 // 同層節點間距(緊湊固定基準)\n var RANKSEP = (sep && sep.ranksep) || 48 // 層間距(緊湊固定基準)\n var nodes = els.filter(function(x){ return x.id !== undefined })\n var edges = els.filter(function(x){ return x.source !== undefined })\n var nodeById = {}; nodes.forEach(function(nn){ nodeById[nn.id] = nn })\n\n // 分類:群組(parent)與一般節點\n var groups = nodes.filter(function(nn){ return CLS[nn.cls] && CLS[nn.cls].group })\n var groupIds = {}; groups.forEach(function(g){ groupIds[g.id] = true })\n var leaves = nodes.filter(function(nn){ return !groupIds[nn.id] })\n\n // 量測 leaf 節點尺寸(含換行)\n var maxLeafW = 480\n var sizing = {}\n leaves.forEach(function(nn){\n var st = CLS[nn.cls] || CLS.blue\n var wr = wrapText(nn.label, FS, maxLeafW)\n var w, h\n if (st.shape === 'diamond'){\n var tw = wr.w + 24, th = wr.lines.length * (FS + 6) + 16\n w = Math.max(tw * 1.9, 130); h = Math.max(th * 1.9, 90)\n } else {\n w = Math.max(wr.w + 34, 90); h = wr.lines.length * (FS + 7) + 24\n }\n sizing[nn.id] = { w: Math.round(w), h: Math.round(h), lines: wr.lines }\n })\n\n // ── 兩段式 layout(避開 dagre compound 對「群組為邊端點」的 rank bug)──\n // 巢狀關係:childrenOf[parentId] = [子節點...];topLevel = 無 parent 者\n var GPAD = 16 // 群組內距(標題列高 titleH 依群組各自動態計算)\n var childrenOf = {}\n nodes.forEach(function(nn){ var p = nn.parent || '__root'; (childrenOf[p] = childrenOf[p] || []).push(nn) })\n var grpById = {}; groups.forEach(function(gp){ grpById[gp.id] = gp })\n var parentOf = {}; nodes.forEach(function(nn){ parentOf[nn.id] = nn.parent || '__root' })\n // 將節點往上抬升,直到其 parent 為指定容器(回傳該容器之直接子);無則回 null\n function liftTo(nodeId, containerId){\n var cur = nodeId\n while (cur != null){\n if (parentOf[cur] === containerId) return cur\n cur = (parentOf[cur] === '__root') ? null : parentOf[cur]\n }\n return null\n }\n\n // size[id] = {w,h}:leaf 直接取量測;group 需先排內部子圖才得知\n var size = {}\n leaves.forEach(function(nn){ size[nn.id] = { w: sizing[nn.id].w, h: sizing[nn.id].h } })\n // 群組內每個子節點相對群組內容左上角的偏移\n var innerPos = {} // innerPos[childId] = {x,y} 相對其 parent 內容區\n var groupContentW = {}, groupContentH = {}\n var titleH = {}, titleLines = {} // 每個群組標題列高度與換行\n\n // 對一個容器(root 或 group)內的直接子節點做 dagre 排版,回傳 {w,h} 內容尺寸\n function layoutContainer(containerId){\n var kids = (childrenOf[containerId] || [])\n // 先遞迴排子群組,取得其尺寸\n kids.forEach(function(k){ if (grpById[k.id]) { var s = layoutContainer(k.id); size[k.id] = { w: s.w, h: s.h } } })\n var dg = new dagre.graphlib.Graph()\n dg.setGraph({ rankdir: rankDir, nodesep: NODESEP, ranksep: RANKSEP, marginx: 0, marginy: 0, ranker: 'tight-tree' })\n dg.setDefaultEdgeLabel(function(){ return {} })\n var kidSet = {}; kids.forEach(function(k){ kidSet[k.id] = true; dg.setNode(k.id, { width: size[k.id].w, height: size[k.id].h }) })\n // 納入「抬升至本容器直接子」後兩端相異之邊(含跨群組邊),避免重複\n var seen = {}\n edges.forEach(function(ed){\n var s = liftTo(ed.source, containerId), t = liftTo(ed.target, containerId)\n if (s && t && s !== t && kidSet[s] && kidSet[t]){\n var key = s + '' + t\n if (!seen[key]){ seen[key] = true; dg.setEdge(s, t) }\n }\n })\n dagre.layout(dg)\n var minX = 1e9, minY = 1e9, maxX = -1e9, maxY = -1e9\n kids.forEach(function(k){\n var p = dg.node(k.id)\n minX = Math.min(minX, p.x - p.width/2); minY = Math.min(minY, p.y - p.height/2)\n maxX = Math.max(maxX, p.x + p.width/2); maxY = Math.max(maxY, p.y + p.height/2)\n })\n if (kids.length === 0){ minX = minY = maxX = maxY = 0 }\n kids.forEach(function(k){\n var p = dg.node(k.id)\n innerPos[k.id + '@' + containerId] = { x: p.x - p.width/2 - minX, y: p.y - p.height/2 - minY }\n })\n var cw = (maxX - minX), ch = (maxY - minY)\n if (containerId !== '__root'){\n cw += GPAD * 2\n // 標題依群組內寬換行,動態決定標題列高度\n var gp = grpById[containerId]\n var twr = wrapText(gp.label, FSG, cw - GPAD * 2)\n var th = twr.lines.length * (FSG + 4) + 12\n titleH[containerId] = th; titleLines[containerId] = twr.lines\n ch += GPAD * 2 + th // 群組外框含內距與標題列\n }\n groupContentW[containerId] = cw; groupContentH[containerId] = ch\n return { w: cw, h: ch }\n }\n layoutContainer('__root')\n\n // 由 root 內容起,遞迴攤平出每個節點的絕對 (x,y)=中心點\n var pos = {} // pos[id] = {x,y,w,h}(x,y 為中心)\n function place(containerId, originX, originY){\n // originX/originY = 此容器內容區左上角絕對座標\n var kids = (childrenOf[containerId] || [])\n kids.forEach(function(k){\n var off = innerPos[k.id + '@' + containerId]\n var w = size[k.id].w, h = size[k.id].h\n var topLeftX = originX + off.x, topLeftY = originY + off.y\n pos[k.id] = { x: topLeftX + w/2, y: topLeftY + h/2, w: w, h: h }\n if (grpById[k.id]){\n // 群組內容區左上角 = 群組左上角 + 內距 +(標題列)\n place(k.id, topLeftX + GPAD, topLeftY + GPAD + titleH[k.id])\n }\n })\n }\n place('__root', 0, 0)\n\n // 建 JointJS graph\n var graph = new joint.dia.Graph({}, { cellNamespace: joint.shapes })\n var cells = []\n var cellById = {}\n\n // 群組容器(先畫,置底);標題用獨立 TextBlock 置於頂部中央\n var groupTitleCells = []\n groups.forEach(function(gp){\n var p = pos[gp.id]\n var st = CLS[gp.cls]\n var x = p.x - p.w/2, y = p.y - p.h/2\n var r = new joint.shapes.standard.Rectangle()\n r.position(x, y); r.resize(p.w, p.h)\n r.attr({\n body: { fill: st.fill, stroke: st.stroke, strokeWidth: 2, rx: 8, ry: 8, fillOpacity: 0.5 },\n label: { text: '' }\n })\n cells.push(r); cellById[gp.id] = r\n // 標題(獨立元件,置頂中央)\n var th = titleH[gp.id]\n var tb = new joint.shapes.standard.TextBlock()\n tb.resize(p.w - 16, th)\n tb.position(x + 8, y + 4)\n tb.attr({\n body: { fill: 'none', stroke: 'none' },\n // TextBlock 以 foreignObject<div> 渲染: font-family 屬性對 HTML div 無效(會落回瀏覽器預設 Times New Roman),\n // 字型必須寫進 style(JointJS 將 style keys 複製為 div inline CSS)才會套用 → 群組標題與全圖統一 JhengHei\n label: { text: (titleLines[gp.id] || [gp.label]).join('\\n'), fontFamily: FONT, fontSize: FSG, fontWeight: 'bold', fill: st.color,\n style: { color: st.color, fontWeight: 'bold', fontFamily: FONT } }\n })\n groupTitleCells.push(tb)\n })\n\n // leaf 節點\n leaves.forEach(function(nn){\n var p = pos[nn.id]\n var st = CLS[nn.cls] || CLS.blue\n var sz = sizing[nn.id]\n var cell\n if (st.shape === 'diamond'){\n cell = new joint.shapes.standard.Polygon()\n cell.attr('body/refPoints', '50,0 100,50 50,100 0,50')\n } else {\n cell = new joint.shapes.standard.Rectangle()\n cell.attr('body/rx', 6); cell.attr('body/ry', 6)\n }\n cell.position(p.x - p.w/2, p.y - p.h/2)\n cell.resize(p.w, p.h)\n cell.attr('body/fill', st.fill)\n cell.attr('body/stroke', st.stroke)\n cell.attr('body/strokeWidth', 1.8)\n cell.attr('label/text', sz.lines.join('\\n'))\n cell.attr('label/fontFamily', FONT)\n cell.attr('label/fontSize', FS)\n cell.attr('label/fill', st.color)\n cell.attr('label/lineHeight', (FS + 7) + 'px')\n cells.push(cell); cellById[nn.id] = cell\n })\n\n // 邊\n edges.forEach(function(ed){\n var s = cellById[ed.source], t = cellById[ed.target]\n if (!s || !t) return\n var l = new joint.shapes.standard.Link()\n l.source(s); l.target(t)\n var dashed = ed.cls === 'dashed'\n l.attr({\n line: {\n stroke: '#44505a', strokeWidth: 2.2,\n strokeDasharray: dashed ? '6,4' : null,\n targetMarker: { type: 'path', d: 'M 10 -5 0 0 10 5 z', fill: '#44505a' }\n }\n })\n l.router('normal'); l.connector('rounded', { radius: 6 })\n if (ed.label){\n // 邊標籤距離: 預設置中(0.5); 虛線(跨群組/驗證類)靠來源端;\n // 跨群組邊(端點落在不同容器)把標籤推向「位於容器外」的一端, 避免疊到群組內節點。\n var srcIn = parentOf[ed.source] && parentOf[ed.source] !== '__root'\n var tgtIn = parentOf[ed.target] && parentOf[ed.target] !== '__root'\n var dist = 0.5\n if (dashed) dist = 0.32\n else if (tgtIn && !srcIn) dist = 0.26 // 進入群組: 標籤靠來源(群組外)\n else if (srcIn && !tgtIn) dist = 0.74 // 離開群組: 標籤靠目標(群組外)\n // 全域修正 A: 背景框透明(不遮轉折線), 文字以白色光暈/描邊保持清晰\n // SVG paint-order:stroke → 先描白邊再填字 → 線段能透出、文字仍清晰\n l.labels([{\n markup: [{ tagName: 'text', selector: 'lbl' }],\n attrs: {\n lbl: { text: ed.label, fontFamily: FONT, fontSize: FSL, fill: '#4a5560',\n stroke: '#ffffff', strokeWidth: 3.5, paintOrder: 'stroke', strokeLinejoin: 'round',\n textAnchor: 'middle', textVerticalAnchor: 'middle' }\n },\n position: { distance: dist }\n }])\n }\n cells.push(l)\n })\n\n // 群組標題置於最上層\n groupTitleCells.forEach(function(tb){ cells.push(tb) })\n\n var paper = document.getElementById('paper')\n var P = new joint.dia.Paper({\n el: paper, model: graph,\n width: 100, height: 100, background: { color: '#ffffff' },\n cellViewNamespace: joint.shapes\n })\n graph.resetCells(cells)\n // 確保群組在底層\n groups.forEach(function(gp){ cellById[gp.id].toBack() })\n P.fitToContent({ padding: 28, allowNewOrigin: 'any', useModelGeometry: true })\n\n // ── 碰撞偵測(通用機制, 對所有圖一致) ─────────────────────────────\n // 量測「節點 bounding box」與「邊標籤 bounding box」, 兩兩偵測重疊(>2px 計碰撞)。\n // 群組容器刻意包住其子節點, 故排除「容器 ↔ 其後代」之合法重疊; 其餘皆計入。\n // 座標一律取 getBoundingClientRect()(同一螢幕座標系, 跨節點/標籤可直接比對, paper 未縮放故相對關係即真實版面)。\n // 容器後代集合(供排除合法包含)\n var descOf = {}\n function collectDesc(gid){\n var set = {}; var stack = (childrenOf[gid] || []).slice()\n while (stack.length){ var c = stack.pop(); set[c.id] = true; if (grpById[c.id]) (childrenOf[c.id] || []).forEach(function(k){ stack.push(k) }) }\n return set\n }\n groups.forEach(function(gp){ descOf[gp.id] = collectDesc(gp.id) })\n function toRect(dom, kind, id){\n var r = dom.getBoundingClientRect()\n return { kind: kind, id: id, x: r.left, y: r.top, w: r.width, h: r.height }\n }\n // 收集矩形: 節點(cellView body 之 client rect), 群組亦納入(偵測容器 ↔ 無關節點/容器)\n var rects = []\n nodes.forEach(function(nn){\n var c = cellById[nn.id]; if (!c) return\n var cv = P.findViewByModel(c); if (!cv) return\n // 群組量「外框 body」, 不含其內子節點; 一般節點量整個 cell\n var dom = grpById[nn.id] ? (cv.el.querySelector('[joint-selector="body"], rect') || cv.el) : cv.el\n rects.push(toRect(dom, grpById[nn.id] ? 'group' : 'node', nn.id))\n })\n // 邊標籤: 取其 label <text>(同一連線之 halo+fill 為單一 text, 不重複計)\n var labelRects = []\n cells.forEach(function(c){\n if (!(c.isLink && c.isLink())) return\n if (!(c.attributes.labels && c.attributes.labels.length)) return\n var lv = P.findViewByModel(c); if (!lv) return\n var tnodes = lv.el.querySelectorAll('[joint-selector="lbl"]')\n if (!tnodes.length) tnodes = lv.el.querySelectorAll('.labels text, .label text')\n for (var ti = 0; ti < tnodes.length; ti++){\n var r = tnodes[ti].getBoundingClientRect()\n if (r.width < 1) continue\n labelRects.push({ kind: 'label', id: c.id + ':' + ti, x: r.left, y: r.top, w: r.width, h: r.height })\n }\n })\n function overlapPx(a, b){\n var ix = Math.min(a.x + a.w, b.x + b.w) - Math.max(a.x, b.x)\n var iy = Math.min(a.y + a.h, b.y + b.h) - Math.max(a.y, b.y)\n return { ix: ix, iy: iy }\n }\n var allRects = rects.concat(labelRects)\n var collisions = 0, minGap = 1e9, details = []\n for (var i = 0; i < allRects.length; i++) for (var j = i + 1; j < allRects.length; j++){\n var A = allRects[i], B = allRects[j]\n // 排除合法包含: 容器 ↔ 其後代節點\n if (A.kind === 'group' && descOf[A.id] && descOf[A.id][B.id]) continue\n if (B.kind === 'group' && descOf[B.id] && descOf[B.id][A.id]) continue\n // 排除「群組外框 ↔ 邊標籤」: 群組為半透明色塊背景(swimlane), 標籤帶白色光暈疊於其上仍清晰,\n // 且跨群組邊必然穿越群組邊界 → 不計為碰撞(真正要避免的是標籤疊到節點文字/其他標籤)\n if ((A.kind === 'group' && B.kind === 'label') || (B.kind === 'group' && A.kind === 'label')) continue\n var ov = overlapPx(A, B)\n if (ov.ix > 2 && ov.iy > 2){\n collisions++\n if (details.length < 4) details.push((A.kind[0]) + A.id.slice(0, 6) + '×' + (B.kind[0]) + B.id.slice(0, 6))\n } else if (ov.ix > -200 && ov.iy > -200){\n // 估「最近可見間隙」: 兩矩形在分離軸上的最小正向距離(僅在另一軸有重疊時才算同列/同行相鄰)\n if (ov.ix <= 2 && ov.iy > 2) minGap = Math.min(minGap, -ov.ix)\n if (ov.iy <= 2 && ov.ix > 2) minGap = Math.min(minGap, -ov.iy)\n }\n }\n if (!isFinite(minGap)) minGap = 999\n resolve({ ok: true, w: Math.round(P.options.width), h: Math.round(P.options.height),\n collisions: collisions, minGap: Math.round(minGap), details: details })\n } catch(e){ resolve({ ok: false, err: String(e.message || e), stack: String(e.stack) }) }\n })\n}\nwindow.renderFig = renderFig\nwindow.__ready = true\n<\/script></body></html>`;async function Qt(n){const t=await e.chromium.launch();try{const e=await t.newPage({deviceScaleFactor:2}),r=[];e.on("pageerror",e=>r.push(e.message)),e.on("console",e=>{"error"===e.type()&&r.push("c:"+e.text())}),await e.setContent(Kt,{waitUntil:"load"}),await e.waitForFunction(()=>window.__ready,{timeout:6e4}).catch(()=>{}),await e.evaluate(()=>document.fonts&&document.fonts.ready);const o={nodesep:32,ranksep:48},{els:a,rankDir:i}=function(e){const n=[];for(const t of e.nodes)n.push({id:t.id,label:t.label,cls:t.cls||"blue",parent:t.group||null});for(const t of e.edges)n.push({source:t.from,target:t.to,label:t.label||"",cls:"dashed"===t.kind?"dashed":""});return{els:n,rankDir:e.dir}}(n),s=await e.evaluate(([e,n,t])=>window.renderFig(e,n,t),[a,i,o]);if(!s.ok)throw new Error(s.err+(s.stack?"\n"+s.stack:""));return{browser:t,page:e,res:s}}catch(e){throw await t.close(),e}}var er=Object.freeze({__proto__:null,genPng:async function(e,n={}){const{browser:t,page:r}=await Qt(e);try{const e=await r.$("#paper svg");return await e.screenshot()}finally{await t.close()}},genSvg:async function(e,n={}){const{browser:t,page:r,res:o}=await Qt(e);try{let e=await r.$eval("#paper svg",e=>e.outerHTML);return e=e.replace(/^<svg([^>]*)>/,(e,n)=>{let t=n;return/\bxmlns=/.test(t)||(t=' xmlns="http://www.w3.org/2000/svg"'+t),t=t.replace(/\swidth="100%"/,` width="${o.w}"`),t=t.replace(/\sheight="100%"/,` height="${o.h}"`),t=t.replace(/position:\s*absolute;\s*inset:\s*[^;]*;\s*/,""),`<svg${t}>`}),e.replace(/&nbsp;/g,"&#160;").trim()}finally{await t.close()}}});const nr=it("@logicflow/core/dist/index.css"),tr=at("@logicflow/core/dist/index.min.js"),rr=at("dagre/dist/dagre.min.js");const or=Object.fromEntries(Object.entries(Kn).map(([e,n])=>[e,{fill:n.fill,stroke:n.stroke,text:n.text}])),ar=JSON.stringify(or),ir=JSON.stringify(Qn),sr=`<!doctype html><html lang="zh-Hant"><head><meta charset="utf-8">\n<style>${nr}</style>\n<script>${tr}<\/script>\n<script>${rr}<\/script>\n<style>\n * { font-family: ${et}; }\n html,body{ margin:0; background:#fff; }\n #app { background:#fff; }\n .lf-grid { display:none !important; }\n</style>\n</head><body>\n<div id="app"></div>\n<script>\nvar PAL = ${ar}\nvar EDGE = ${ir}\nvar FONT = ${JSON.stringify(`14px ${et}`)} // canvas 量測用之 font 短語\nvar FONT_FAMILY = ${JSON.stringify(et)} // 純 font-family(供 SVG text 明確指定, 字型全覆蓋)\n\n// ---- 通用版面常數(非逐圖; 皆為與內容無關之全域預設) ----\nvar FONT_SIZE = 14, LINE_H = 22 // 節點字級 / 行高\nvar NODE_MAXW = 230, DIAMOND_MAXW = 110 // 換行最大寬度(矩形 / 菱形)\nvar PAD_X = 28, PAD_Y = 22 // 矩形內距\nvar DIA_PAD_X = 60, DIA_PAD_Y = 46 // 菱形內距(對角需較大留白)\nvar NODE_MINW = 120, NODE_MINH = 56 // 矩形最小尺寸\nvar DIA_MINW = 150, DIA_MINH = 110 // 菱形最小尺寸\nvar MARGIN = 20 // dagre 邊距\nvar GROUP_TITLE = 26 // 群組容器標題列高\nvar CANVAS_MARGIN = 60 // 畫布外緣留白\nvar LABEL_GAP = 28, LABEL_BIAS = 0.62 // 跨界邊標籤離容器框間隙 / 偏容器側權重\n\n// ---- 緊湊固定間距(通用機制, 對所有圖一致; 不做「掃描放大 until 零碰撞」) ----\n// 一律以單一緊湊間距排版, 維持報告可用的緊湊尺寸; 不因避碰撞而把圖撐大。\n// 文字遮蔽改由繪製順序(z-order)解決(群組墊底 / 節點居中 / 邊與邊標籤置頂), 不靠加大間距。\n// LR 層距略大於 TB(水平向標籤較長), 仍為固定值。\nvar SEP_TB = [36, 55] // [nodesep, ranksep] — 上下向緊湊間距\nvar SEP_LR = [36, 82] // [nodesep, ranksep] — 左右向緊湊間距\nvar COLLIDE_TOL = 2 // 重疊 >此像素 才計為碰撞(僅供回報用, 不再驅動間距放大)\n\n// 量測文字寬度\nvar _cv = document.createElement('canvas'); var _cx = _cv.getContext('2d')\nfunction measure(t, font){ _cx.font = font; return _cx.measureText(t).width }\n\n// 將長 label 依最大寬度斷行: 中文逐字可斷; 連續英數識別字(token)視為不可攔腰斷之整體, 整段超寬才獨佔一行。\nfunction wrap(label, maxW, font){\n _cx.font = font\n // 先切成 token: 連續英數(含 . _ / 等識別字常見字元)為一個 token, 其餘(中文/標點/空白)逐字\n var chars = Array.from(label)\n var tokens = [], buf = ''\n function isWordChar(c){ return /[0-9A-Za-z._/\\-]/.test(c) }\n for (var i=0;i<chars.length;i++){\n if (isWordChar(chars[i])){ buf += chars[i] }\n else { if (buf){ tokens.push(buf); buf='' } tokens.push(chars[i]) }\n }\n if (buf) tokens.push(buf)\n var lines = [], cur = ''\n for (var k=0;k<tokens.length;k++){\n var tk = tokens[k]\n var test = cur + tk\n if (_cx.measureText(test).width > maxW && cur.length>0){ lines.push(cur); cur = tk }\n else cur = test\n }\n if (cur) lines.push(cur)\n // 去除純空白造成的行首空白\n return lines.map(function(l){ return l.replace(/^\\s+/, '') }).filter(function(l){ return l.length>0 })\n}\n\n// 依 label 自動推算節點尺寸(寬: 依最寬一行字寬 + 內距; 高: 依行數 + 內距; 皆套最小值)\nfunction sizeOf(label, isDiamond){\n var maxW = isDiamond ? DIAMOND_MAXW : NODE_MAXW\n var lines = wrap(label, maxW, FONT)\n if (!lines.length) lines = [label]\n var w = 0\n lines.forEach(function(l){ w = Math.max(w, measure(l, FONT)) })\n var padX = isDiamond ? DIA_PAD_X : PAD_X, padY = isDiamond ? DIA_PAD_Y : PAD_Y\n var W = Math.ceil(w + padX*2)\n var H = Math.ceil(lines.length*LINE_H + padY*2)\n if (isDiamond){ W = Math.max(W, DIA_MINW); H = Math.max(H, DIA_MINH) }\n else { W = Math.max(W, NODE_MINW); H = Math.max(H, NODE_MINH) }\n return { w: W, h: H, text: lines.join('\\n') }\n}\n\n// 兩矩形重疊量(回傳 {ix,iy} 為水平/垂直交集像素, 皆>0 才重疊)\nfunction overlap(a, b){\n var ix = Math.min(a.x+a.w/2, b.x+b.w/2) - Math.max(a.x-a.w/2, b.x-b.w/2)\n var iy = Math.min(a.y+a.h/2, b.y+b.h/2) - Math.max(a.y-a.h/2, b.y-b.h/2)\n return { ix: ix, iy: iy }\n}\n\n// ---- 解析 fig 為內部結構 + 量測尺寸(只做一次, 不隨間距改變) ----\nfunction prepare(fig){\n var els = fig.els\n var nodes = els.filter(function(x){ return !('source' in x) })\n var edges = els.filter(function(x){ return ('source' in x) })\n var byId = {}; nodes.forEach(function(nd){ byId[nd.id] = nd })\n var groupIds = {}; nodes.forEach(function(nd){ if (nd.parent) groupIds[nd.parent] = true })\n nodes.forEach(function(nd){\n if (groupIds[nd.id]) return\n var sz = sizeOf(nd.label, nd.cls === 'diamond')\n nd._w = sz.w; nd._h = sz.h; nd._text = sz.text\n })\n return { nodes: nodes, edges: edges, byId: byId, groupIds: groupIds, rankDir: fig.rankDir }\n}\n\n// ---- 以給定間距做 dagre compound 排版, 回傳座標/群組框/邊標籤座標(供偵測與渲染) ----\nfunction layout(P, nodeSep, rankSep){\n var nodes = P.nodes, edges = P.edges, byId = P.byId, groupIds = P.groupIds\n var g = new dagre.graphlib.Graph({ compound:true })\n g.setGraph({ rankdir: P.rankDir, nodesep: nodeSep, ranksep: rankSep, marginx: MARGIN, marginy: MARGIN })\n g.setDefaultEdgeLabel(function(){ return {} })\n Object.keys(groupIds).forEach(function(gid){ g.setNode(gid, { label: gid }) })\n Object.keys(groupIds).forEach(function(gid){\n var gp = byId[gid] && byId[gid].parent\n if (gp && groupIds[gp]) g.setParent(gid, gp)\n })\n nodes.forEach(function(nd){\n if (groupIds[nd.id]) return\n g.setNode(nd.id, { width: nd._w, height: nd._h })\n if (nd.parent) g.setParent(nd.id, nd.parent)\n })\n function repLeaf(gid){\n var c = nodes.filter(function(x){ return x.parent === gid && !groupIds[x.id] })\n return c.length ? c[0].id : null\n }\n edges.forEach(function(ed){\n var sG = groupIds[ed.source], tG = groupIds[ed.target]\n if (!sG && !tG){ g.setEdge(ed.source, ed.target); return }\n var s = sG ? repLeaf(ed.source) : ed.source\n var t = tG ? repLeaf(ed.target) : ed.target\n if (s && t) g.setEdge(s, t)\n })\n dagre.layout(g)\n\n // leaf 節點座標\n var lfNodes = []\n nodes.forEach(function(nd){\n if (groupIds[nd.id]) return\n var p = g.node(nd.id)\n var pal = PAL[nd.cls] || PAL.blue\n var type = nd.cls === 'diamond' ? 'diamond' : 'rect'\n lfNodes.push({ id: nd.id, type: type, x: Math.round(p.x), y: Math.round(p.y), text: nd._text, _pal: pal, _w: nd._w, _h: nd._h })\n })\n // 群組容器框(dagre cluster bbox)\n function depth(gid){ var d=0,p=byId[gid]&&byId[gid].parent; while(p){ d++; p=byId[p]&&byId[p].parent } return d }\n var lfGroupNodes = []\n Object.keys(groupIds).sort(function(a,b){ return depth(a)-depth(b) }).forEach(function(gid){\n var gn = g.node(gid)\n var pal = PAL[byId[gid].cls] || PAL.blue\n // 群組標題帶為固定高(GROUP_TITLE), label 內 \\n 降級為空格(兩行標題會壓到成員節點)\n lfGroupNodes.push({ id: gid, label: String(byId[gid].label).replace(/\\n/g, ' '), _pal: pal,\n x: Math.round(gn.x), y: Math.round(gn.y), _w: Math.round(gn.width), _h: Math.round(gn.height) })\n })\n\n // 幾何查詢\n var geo = {}\n lfNodes.forEach(function(nd){ geo[nd.id]={ x:nd.x, y:nd.y, w:nd._w, h:nd._h } })\n lfGroupNodes.forEach(function(b){ geo[b.id]={ x:b.x, y:b.y, w:b._w, h:b._h } })\n var parentBox = {}\n nodes.forEach(function(nd){ if (nd.parent && geo[nd.parent]) parentBox[nd.id] = geo[nd.parent] })\n function inGroup(id){ return !!parentBox[id] }\n\n // 跨界邊標籤座標(落在外部節點與群組框之間淨空)\n function labelPos(ed){\n if (!ed.label) return null\n var sIn = inGroup(ed.source) || groupIds[ed.source]\n var tIn = inGroup(ed.target) || groupIds[ed.target]\n var outId = null, inId = null\n if (sIn && !tIn){ inId=ed.source; outId=ed.target }\n else if (!sIn && tIn){ outId=ed.source; inId=ed.target }\n else {\n var s = geo[ed.source], t = geo[ed.target]\n if (!s || !t) return null\n var mx = (s.x+t.x)/2, my = (s.y+t.y)/2\n var hit = null\n lfGroupNodes.forEach(function(b){\n if (mx > b.x-b._w/2 && mx < b.x+b._w/2 && my > b.y-b._h/2 && my < b.y+b._h/2) hit = b\n })\n if (!hit) return null\n return { x: Math.round(hit.x - hit._w/2 - LABEL_GAP), y: Math.round(my) }\n }\n var o = geo[outId], box = parentBox[inId] || geo[inId]\n var tgt = geo[ed.target] || geo[inId]\n if (!o || !box || !tgt) return null\n var bias = LABEL_BIAS, obias = 1 - LABEL_BIAS\n if (P.rankDir === 'LR'){\n var boxEdgeX = (o.x < box.x) ? (box.x - box.w/2) : (box.x + box.w/2)\n var oInnerX = o.x + (o.x<box.x ? o.w/2 : -o.w/2)\n var lx = boxEdgeX*bias + oInnerX*obias\n return { x: Math.round(lx), y: Math.round(tgt.y) }\n } else {\n var boxEdgeY = (o.y < box.y) ? (box.y - box.h/2) : (box.y + box.h/2)\n var oInnerY = o.y + (o.y<box.y ? o.h/2 : -o.h/2)\n var ly = boxEdgeY*bias + oInnerY*obias\n return { x: Math.round(tgt.x), y: Math.round(ly) }\n }\n }\n\n // 邊 + 標籤框(供偵測). 標籤框尺寸依文字量測。\n var lfEdges = [], labelBoxes = []\n edges.forEach(function(ed, i){\n var lp = labelPos(ed)\n var txt = ed.label ? (lp ? { value: ed.label, x: lp.x, y: lp.y } : ed.label) : ''\n lfEdges.push({ id:'e'+i, sourceNodeId: ed.source, targetNodeId: ed.target, text: txt, _dashed: ed.dashed })\n if (ed.label){\n // 標籤未指定座標時, 取兩端中點估其位置(供偵測)\n var lx, ly\n if (lp){ lx = lp.x; ly = lp.y }\n else { var s=geo[ed.source], t=geo[ed.target]; if(!s||!t) return; lx=(s.x+t.x)/2; ly=(s.y+t.y)/2 }\n var lw = measure(ed.label, FONT) + 14, lh = LINE_H\n labelBoxes.push({ x: lx, y: ly, w: lw, h: lh, _label: true })\n }\n })\n\n return { lfNodes: lfNodes, lfGroupNodes: lfGroupNodes, lfEdges: lfEdges, labelBoxes: labelBoxes,\n byId: byId, parentBox: parentBox }\n}\n\n// ---- 偵測重疊: leaf↔leaf、group↔group(非巢狀)、leaf↔group(非歸屬)、邊標籤↔節點/其他標籤 ----\n// 回傳 { collisions, minGap(零碰撞時計區塊間最小可見間隙) }\nfunction detect(L){\n var byId = L.byId\n // 取某 leaf/group 的祖先群組鏈(含自身), 用於排除「容器內含其成員」的合法重疊\n function ancestors(id){ var arr=[id], p=byId[id]&&byId[id].parent; while(p){ arr.push(p); p=byId[p]&&byId[p].parent } return arr }\n // 判 a 是否為 b 之祖先或同一(容器包含成員 → 非碰撞)\n function related(idA, idB){\n var ca = ancestors(idA), cb = ancestors(idB)\n return ca.indexOf(idB) >= 0 || cb.indexOf(idA) >= 0\n }\n // 待偵測之矩形集合(節點 + 群組框, 各帶 id; 標籤無 id)\n var rects = []\n L.lfNodes.forEach(function(n){ rects.push({ id:n.id, x:n.x, y:n.y, w:n._w, h:n._h, kind:'node' }) })\n L.lfGroupNodes.forEach(function(b){ rects.push({ id:b.id, x:b.x, y:b.y, w:b._w, h:b._h, kind:'group' }) })\n var labels = L.labelBoxes.map(function(l){ return { x:l.x, y:l.y, w:l.w, h:l.h, kind:'label' } })\n\n var collisions = 0, minGap = Infinity\n // 節點/群組 兩兩(排除容器-成員關係)\n for (var i=0;i<rects.length;i++){\n for (var j=i+1;j<rects.length;j++){\n var A=rects[i], B=rects[j]\n if (related(A.id, B.id)) continue\n var ov = overlap(A, B)\n if (ov.ix > COLLIDE_TOL && ov.iy > COLLIDE_TOL){ collisions++ }\n else {\n // 未重疊: 取較近軸向之間隙(僅在另一軸有投影重疊時才計, 否則為斜對角不相鄰)\n if (ov.ix > 0) minGap = Math.min(minGap, -ov.iy) // 水平有交集 → 垂直間隙\n else if (ov.iy > 0) minGap = Math.min(minGap, -ov.ix) // 垂直有交集 → 水平間隙\n }\n }\n }\n // 邊標籤 ↔ 節點/群組(群組框為淺底大容器, 標籤落其內屬正常 → 僅與 leaf 節點及其他標籤計碰撞)\n for (var li=0;li<labels.length;li++){\n var lab = labels[li]\n for (var ri=0;ri<rects.length;ri++){\n if (rects[ri].kind !== 'node') continue\n var ovl = overlap(lab, rects[ri])\n if (ovl.ix > COLLIDE_TOL && ovl.iy > COLLIDE_TOL) collisions++\n }\n for (var lj=li+1;lj<labels.length;lj++){\n var ovll = overlap(lab, labels[lj])\n if (ovll.ix > COLLIDE_TOL && ovll.iy > COLLIDE_TOL) collisions++\n }\n }\n if (!isFinite(minGap)) minGap = 0 // 無相鄰對(單一節點等)\n return { collisions: collisions, minGap: minGap }\n}\n\n// ---- 把一份 layout 結果實際畫到 LF 並截圖前準備(回傳畫布尺寸) ----\nfunction renderLayout(L){\n var lfNodes = L.lfNodes, lfGroupNodes = L.lfGroupNodes, lfEdges = L.lfEdges\n\n // 整體 bbox → 畫布尺寸\n var minX=1e9,minY=1e9,maxX=-1e9,maxY=-1e9\n lfGroupNodes.concat(lfNodes).forEach(function(nd){\n minX=Math.min(minX, nd.x-nd._w/2); maxX=Math.max(maxX, nd.x+nd._w/2)\n minY=Math.min(minY, nd.y-nd._h/2); maxY=Math.max(maxY, nd.y+nd._h/2)\n })\n var M = CANVAS_MARGIN\n var W = Math.ceil(maxX-minX+M*2), H = Math.ceil(maxY-minY+M*2)\n var app = document.getElementById('app')\n app.innerHTML = ''\n app.style.width = W+'px'; app.style.height = H+'px'\n\n var G = window.Core || window.LogicFlow\n var LF = G.default || G.LogicFlow || G\n var RectNode = G.RectNode, RectNodeModel = G.RectNodeModel\n var DiamondNode = G.DiamondNode, DiamondNodeModel = G.DiamondNodeModel\n\n var lf = new LF({ container: app, grid:false, background:false, isSilentMode:true, stopScrollGraph:true, stopZoomGraph:true, adjustEdge:false,\n style: {\n rect: { rx: 7, ry: 7 },\n polyline: { stroke: EDGE.line, strokeWidth: 1.8 },\n nodeText: { color:'#1c2b36', fontFamily: FONT_FAMILY, fontSize: FONT_SIZE, overflowMode:'autoWrap', lineHeight: 1.4 },\n },\n })\n\n function makeRect(name){\n lf.register({ type: name, view: RectNode, model: class extends RectNodeModel {\n setAttributes(){ var p=this.properties||{}; if(p.width)this.width=p.width; if(p.height)this.height=p.height; this.radius=7 }\n getNodeStyle(){ var s=super.getNodeStyle(); var pal=this.properties._pal||PAL.blue; s.fill=pal.fill; s.stroke=pal.stroke; s.strokeWidth=this.properties.isGroup?2:1.8; if(this.properties.isGroup){s.fillOpacity=0.45} return s }\n getTextStyle(){ var s=super.getTextStyle(); var pal=this.properties._pal||PAL.blue; s.color=pal.text; s.fontFamily=FONT_FAMILY; s.fontSize=this.properties.isGroup?15:FONT_SIZE; if(this.properties.isGroup){s.fontWeight=700} s.overflowMode='autoWrap'; s.lineHeight=1.35; if(this.width)s.wrapPadding='0px'; return s }\n }})\n }\n function makeDiamond(name){\n lf.register({ type: name, view: DiamondNode, model: class extends DiamondNodeModel {\n setAttributes(){ var p=this.properties||{}; if(p.width)this.rx=p.width/2; if(p.height)this.ry=p.height/2 }\n getNodeStyle(){ var s=super.getNodeStyle(); var pal=this.properties._pal||PAL.diamond; s.fill=pal.fill; s.stroke=pal.stroke; s.strokeWidth=1.8; return s }\n getTextStyle(){ var s=super.getTextStyle(); var pal=this.properties._pal||PAL.diamond; s.color=pal.text; s.fontFamily=FONT_FAMILY; s.fontSize=FONT_SIZE; s.overflowMode='autoWrap'; return s }\n }})\n }\n makeRect('cRect'); makeDiamond('cDiamond')\n\n // 邊標籤樣式(全域修正 A): 背景透明(不遮轉折線) + 文字白色光暈(paint-order:stroke + stroke=白)\n function edgeText(s){\n s.color=EDGE.text; s.fontFamily=FONT_FAMILY; s.fontSize=12.5; s.overflowMode='default'; s.textWidth=400\n s.background={ fill:'transparent', stroke:'none' } // 透明底: 線段能透出, 不被白底遮斷\n s.stroke=EDGE.haloColor; s.strokeWidth=EDGE.haloWidth; s.paintOrder='stroke' // 白光暈描邊(描在文字下方)\n return s\n }\n var PolylineEdge=G.PolylineEdge, PolylineEdgeModel=G.PolylineEdgeModel\n lf.register({ type:'cDash', view:PolylineEdge, model: class extends PolylineEdgeModel {\n getEdgeStyle(){ var s=super.getEdgeStyle(); s.stroke='#7a8690'; s.strokeWidth=1.8; s.strokeDasharray='6 4'; return s }\n getTextStyle(){ return edgeText(super.getTextStyle()) }\n }})\n lf.register({ type:'cLine', view:PolylineEdge, model: class extends PolylineEdgeModel {\n getEdgeStyle(){ var s=super.getEdgeStyle(); s.stroke=EDGE.line; s.strokeWidth=1.8; return s }\n getTextStyle(){ return edgeText(super.getTextStyle()) }\n }})\n\n var graphNodes = []\n lfGroupNodes.forEach(function(b){\n graphNodes.push({ id:b.id, type:'cRect', x:b.x, y:b.y,\n text:{ value: b.label, x: b.x, y: Math.round(b.y - b._h/2 + GROUP_TITLE/2 + 4) },\n properties:{ width:b._w, height:b._h, isGroup:true, _pal:b._pal } })\n })\n lfNodes.forEach(function(nd){\n graphNodes.push({ id:nd.id, type: nd.type==='diamond'?'cDiamond':'cRect', x:nd.x, y:nd.y, text:nd.text, properties:{ width:nd._w, height:nd._h, _pal:nd._pal } })\n })\n var graphEdges = lfEdges.map(function(ed){\n return { id:ed.id, type: ed._dashed?'cDash':'cLine', sourceNodeId:ed.sourceNodeId, targetNodeId:ed.targetNodeId, text: ed.text||'' }\n })\n\n lf.render({ nodes: graphNodes, edges: graphEdges })\n\n // ---- 繪製順序(z-order, 原則2) ----\n // LF 預設把邊畫在節點下方, 群組(node)與節點同層; 文字會被節點/群組底色蓋住。\n // 以 setElementZIndex 數值分層強制: 群組容器=底(Z_GROUP) → 節點=中(Z_NODE) → 邊與邊標籤=頂(Z_EDGE),\n // 確保任何文字(節點文字 / 群組標題 / 邊標籤)都不被容器填色或其他區塊遮住。\n var Z_GROUP = 1, Z_NODE = 10, Z_EDGE = 100\n lfGroupNodes.forEach(function(b){ lf.setElementZIndex(b.id, Z_GROUP) }) // 半透明容器墊最底\n lfNodes.forEach(function(nd){ lf.setElementZIndex(nd.id, Z_NODE) }) // 節點居中\n graphEdges.forEach(function(ed){ lf.setElementZIndex(ed.id, Z_EDGE) }) // 邊與邊標籤置頂\n\n return { w: W, h: H, nNodes: graphNodes.length, nEdges: graphEdges.length }\n}\n\n// ---- 對外: 解析 → 以單一緊湊固定間距排版 → 實際渲染(z-order 處理遮蔽, 不掃描放大間距) ----\nwindow.renderFig = function(fig){ return new Promise(function(resolve){\n try {\n var P = prepare(fig)\n var sep = (P.rankDir === 'LR') ? SEP_LR : SEP_TB\n var L = layout(P, sep[0], sep[1])\n var d = detect(L) // 僅供回報(碰撞數/最小間隙), 不驅動任何放大\n var info = renderLayout(L)\n setTimeout(function(){ resolve({ ok:true, w:info.w, h:info.h, nNodes:info.nNodes, nEdges:info.nEdges,\n nodeSep:sep[0], rankSep:sep[1], collisions:d.collisions, minGap:Math.round(d.minGap) }) }, 200)\n } catch(e){ resolve({ ok:false, err: String(e && (e.stack||e.message||e)) }) }\n})}\nwindow.__ready = true\n<\/script></body></html>`;async function lr(n){const t=await e.chromium.launch();try{const e=await t.newPage({deviceScaleFactor:2}),r=[];e.on("pageerror",e=>r.push(e.message)),e.on("console",e=>{"error"===e.type()&&r.push("console:"+e.text())}),await e.setContent(sr,{waitUntil:"load"}),await e.waitForFunction(()=>window.__ready,{timeout:3e4}).catch(()=>{}),await e.evaluate(()=>document.fonts&&document.fonts.ready);const o=function(e){const n=[];for(const t of e.nodes)n.push({id:t.id,label:t.label,cls:t.cls||"blue",parent:t.group});for(const t of e.edges)n.push({source:t.from,target:t.to,label:t.label||"",dashed:"dashed"===t.kind});return{rankDir:e.dir,els:n}}(n),a=await e.evaluate(e=>window.renderFig(e),o);if(!a.ok)throw new Error(a.err||"renderFig failed");return await e.evaluate(()=>document.fonts&&document.fonts.ready),{browser:t,page:e,res:a}}catch(e){throw await t.close(),e}}var cr=Object.freeze({__proto__:null,genPng:async function(e,n={}){const{browser:t,page:r}=await lr(e);try{return await r.locator("#app").screenshot()}finally{await t.close()}},genSvg:async function(e,n={}){const{browser:t,page:r,res:o}=await lr(e);try{let e=await r.evaluate(()=>document.querySelector("svg.lf-canvas-overlay").outerHTML);const n=e.match(/^<svg[^>]*>/);if(!n)throw new Error("genSvg: 找不到圖面 svg 根元素");let t=n[0].replace('width="100%"',`width="${o.w}"`).replace('height="100%"',`height="${o.h}"`);t=t.replace(/>$/,` viewBox="0 0 ${o.w} ${o.h}">`),e=t+e.slice(n[0].length);const a=`<style>${nr}\n* { font-family: ${et}; }</style><rect x="0" y="0" width="${o.w}" height="${o.h}" fill="#fff"></rect>`;return e=e.replace("<g transform",a+"<g transform"),e=e.replace(/&nbsp;/g,"&#160;"),e}finally{await t.close()}}});const dr=r.dirname(t.fileURLToPath("undefined"==typeof document&&"undefined"==typeof location?require("url").pathToFileURL(__filename).href:"undefined"==typeof document?location.href:o&&"SCRIPT"===o.tagName.toUpperCase()&&o.src||new URL("w-flowchart.umd.js",document.baseURI).href));let pr=null;var fr=Object.freeze({__proto__:null,genPng:async function(t,r={}){const o=await e.chromium.launch();try{const e=await o.newPage({deviceScaleFactor:2}),r=[];e.on("pageerror",e=>r.push(e.message)),e.on("console",e=>{"error"===e.type()&&r.push("c:"+e.text().slice(0,200))}),await e.setContent((null===pr&&(pr=n.readFileSync(`${dr}/page9.html`,"utf8").replace("/*__XYFLOW_CSS__*/",()=>it("@xyflow/react/dist/style.css")).replace("/*__REACT_JS__*/",()=>at("react/umd/react.production.min.js")).replace("/*__REACT_DOM_JS__*/",()=>at("react-dom/umd/react-dom.production.min.js")).replace("/*__XYFLOW_JS__*/",()=>at("@xyflow/react/dist/umd/index.js")).replace("/*__DAGRE_JS__*/",()=>at("@dagrejs/dagre/dist/dagre.min.js"))),pr),{waitUntil:"load"}),await e.waitForFunction(()=>"READY"===document.title||"FAIL"===document.title,{timeout:6e4}).catch(()=>{});const a=await e.evaluate(()=>({t:document.title,err:window.__err}));if("READY"!==a.t)throw new Error("BOOT FAIL :: "+a.err+" | "+r.slice(0,6).join(" | "));const i=function(e){const n=e.nodes.map(e=>{const n={id:e.id,label:e.label,kind:e.cls};return e.group&&(n.parent=e.group),n}),t=e.edges.map(e=>{const n={s:e.from,t:e.to};return e.label&&(n.l=e.label),"dashed"===e.kind&&(n.dashed=!0),n});return{layout:"dagrec",rankDir:e.dir||"TB",nodesep:38,ranksep:44,nodes:n,edges:t}}(t),s=await e.evaluate(e=>window.renderFig(e),i);if(!s.ok)throw new Error("FAIL :: "+s.err);return await e.evaluate(()=>document.fonts&&document.fonts.ready),await e.waitForTimeout(250),await e.locator("#shot").screenshot()}finally{await o.close()}}});function ur(e){const n=e.nodes.filter(e=>nt(e.cls)).map(n=>{const t=tt(n.cls);return{id:n.id,label:n.label,parent:n.group||null,align:n.align,mi:e.nodes.indexOf(n),fill:t.fill,stroke:t.stroke,font:t.text}}),t=e.nodes.filter(e=>!nt(e.cls)).map(n=>{const t=tt(n.cls);return{id:n.id,label:n.label,title:n.title,items:n.items,parent:n.group||null,mi:e.nodes.indexOf(n),diamond:"diamond"===t.shape,fill:t.fill,stroke:t.stroke,font:t.text}}),r=e.edges.map((e,n)=>({id:"e"+n,from:e.from,to:e.to,label:e.label||"",dashed:"dashed"===e.kind})),o={};n.forEach(e=>o[e.id]=e.parent),t.forEach(e=>o[e.id]=e.parent);const a=e=>{const n=[];let t=o[e];for(;t;)n.push(t),t=o[t];return n};return r.forEach(e=>{const n=new Set(a(e.to));e.container=a(e.from).find(e=>n.has(e))||null}),{dir:e.dir||"TB",nodes:t,groups:n,edges:r}}const hr=`<!doctype html><html lang="zh-Hant"><head><meta charset="utf-8">\n<script>${at("elkjs/lib/elk.bundled.js")}<\/script>\n<style>body{margin:0;background:#fff} .lbl{font-family:'Microsoft JhengHei','微軟正黑體',sans-serif}</style>\n</head><body>\n<div id="meas" style="position:absolute;left:-99999px;top:0;font-family:'Microsoft JhengHei',sans-serif;visibility:hidden"></div>\n<div id="stage"></div>\n<script>\nconst FONT = "'Microsoft JhengHei','微軟正黑體',sans-serif"\nconst EDGE_LINE = '${Qn.line}', EDGE_TEXT = '${Qn.text}', HALO = '${Qn.haloColor}'\nconst FS = 14, GFS = 15, EFS = 12.5, MAXW = 200\n// 容器內矩形框之垂直對齊: 預設 top; 可於數據之群組節點設 align:'top'|'center'|'bottom' 個別覆寫(改 DEF_VALIGN 改全域預設)\nconst VALIGN = { top:'TOP', center:'CENTER', bottom:'BOTTOM' }, DEF_VALIGN = 'top'\nconst meas = document.getElementById('meas')\nfunction measure(label, fs, maxW){\n const d=document.createElement('div')\n // white-space:pre-line: label 內 \\n 為強制換行(其餘空白照常摺疊); 與渲染端 foreignObject div 一致\n d.style.cssText='display:inline-block;white-space:pre-line;overflow-wrap:break-word;word-break:normal;text-align:center;line-height:1.4;font-size:'+fs+'px;max-width:'+maxW+'px;padding:0;font-family:'+FONT\n d.textContent=label; meas.appendChild(d); const r=d.getBoundingClientRect(); meas.removeChild(d)\n return { w:Math.ceil(r.width), h:Math.ceil(r.height) }\n}\n// 量整塊 items 尺寸: 完全比照渲染(line-height 1.55、各項 nowrap)→ 直接得 items 區實際高度, 避免逐項估高(用錯 line-height)累積成底部多餘留白\nfunction measureItems(items, fs){\n const d=document.createElement('div')\n d.style.cssText='display:inline-block;font-size:'+fs+'px;line-height:1.55;font-family:'+FONT\n d.innerHTML=items.map(it=>'<div style="white-space:nowrap">•&#160;'+String(it).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')+'</div>').join('')\n meas.appendChild(d); const r=d.getBoundingClientRect(); meas.removeChild(d)\n return { w:Math.ceil(r.width), h:Math.ceil(r.height) }\n}\nconst esc = s => String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')\nconst mix = (a,b,t) => { const pa=[1,3,5].map(i=>parseInt(a.slice(i,i+2),16)), pb=[1,3,5].map(i=>parseInt(b.slice(i,i+2),16)); return '#'+pa.map((x,i)=>Math.round(x*(1-t)+pb[i]*t).toString(16).padStart(2,'0')).join('') }\n// 確保「起點後第一段」與「終點前最後一段」之直線長度 >= MIN(轉折不貼端點, 維持箭頭指向性)。\n// 做法: 末段過短時, 沿趨近軸向把「最後轉角」與「其前一轉角」一起後移(兩者同移=保正交); 需 >=4 點才不動端點。\n// ── 菱形端點貼齊(ELK 以外接矩形算端點, 落在矩形邊上非頂點處會與菱形斜邊「空接」) ──\n// 規則(共進共出, 每側獨立; 詳 p10/README):\n// R1 共出: 該側所有「出」一律由該側「頂點」出發, 多條共用首段後於各自高度 90 度轉出(分岔)。\n// R2 共進: 該側「無出」→ 全部匯入「頂點」(各自橫移至中軸, 共用末段, 箭頭重合);\n// 該側「有出」→ 依「半側」匯入該半側「斜邊中點」(頂點讓給出, 進出不同點)。\n// 正交線交叉允許存在(正交路由常態), 一致性優先。\n\n// 判定端點落在外接矩形哪一側; 回 { ax, sg, v }(離開軸/正負/該側頂點), 不在邊上回 null\nfunction diamondSide(p, box){\n const cx = box.x + box.w/2, cy = box.y + box.h/2, EPS = 1.5\n if (Math.abs(p.y - (box.y + box.h)) < EPS) return { ax: 'y', sg: 1, v: { x: cx, y: box.y + box.h } }\n if (Math.abs(p.y - box.y) < EPS) return { ax: 'y', sg: -1, v: { x: cx, y: box.y } }\n if (Math.abs(p.x - box.x) < EPS) return { ax: 'x', sg: -1, v: { x: box.x, y: cy } }\n if (Math.abs(p.x - (box.x + box.w)) < EPS) return { ax: 'x', sg: 1, v: { x: box.x + box.w, y: cy } }\n return null\n}\n\n// 該側某「半側」之斜邊中點(混合側的進匯點); half=+1 取座標較大半側, -1 反之\nfunction diamondSlantMid(box, side, half){\n const cx = box.x + box.w/2, cy = box.y + box.h/2, hw = box.w/2, hh = box.h/2\n if (side.ax === 'y') return { x: cx + half * hw / 2, y: cy + side.sg * hh / 2 }\n return { x: cx + side.sg * hw / 2, y: cy + half * hh / 2 }\n}\n\n// R1 共出: 出邊起點改自「頂點」出發\nfunction routeOutFromVertex(pts, side){\n const p0 = pts[0], p1 = pts[1]\n const EPS = 1.5, J = 18\n const v = side.v\n const off = side.ax === 'y' ? Math.abs(p0.x - v.x) : Math.abs(p0.y - v.y)\n if (off < EPS) return // 已在頂點(置中), 不動\n const alongAxis = side.ax === 'y' ? Math.abs(p1.x - p0.x) < EPS : Math.abs(p1.y - p0.y) < EPS\n if (!alongAxis) { pts.unshift(v); return } // 原首段即沿側邊橫走: 自頂點先橫走接回\n if (pts.length > 2) {\n // 有後續轉折: 頂點 → 沿軸行至原首個轉折高度 → 橫向接回(單一 L 型; 轉折可貼近頂點)\n const q1 = side.ax === 'y' ? { x: v.x, y: p1.y } : { x: p1.x, y: v.y }\n pts.splice(0, 1, v, q1)\n return\n }\n // 無轉折之直線邊: 於頂點外側作雙直角接回各自路線(距離取 J 與可用長度之小者)\n const segLen = side.ax === 'y' ? (p1.y - p0.y) * side.sg : (p1.x - p0.x) * side.sg\n const j = Math.min(J, Math.max(4, segLen / 2))\n const q1 = side.ax === 'y' ? { x: v.x, y: v.y + side.sg * j } : { x: v.x + side.sg * j, y: v.y }\n const q2 = side.ax === 'y' ? { x: p0.x, y: v.y + side.sg * j } : { x: v.x + side.sg * j, y: p0.y }\n pts.splice(0, 1, v, q1, q2)\n}\n\n// R2 決策: 進匯點 = 該側無出 → 頂點; 有出 → 依端點所在半側之斜邊中點\nfunction diamondInAnchor(box, side, hasOutOnSide, pE){\n if(!hasOutOnSide) return { x: side.v.x, y: side.v.y }\n const half = (side.ax === 'y' ? (pE.x - (box.x + box.w/2)) : (pE.y - (box.y + box.h/2))) >= 0 ? 1 : -1\n return diamondSlantMid(box, side, half)\n}\n\n// R2 共進: 進邊終點改匯入 anchor(頂點或半側斜邊中點)\n// 橫向差距小且末段前為可伸縮橫向段 → 整段末端直線「平移」對齊 anchor 軸線(零多餘轉折);\n// 否則於側外 J 處橫移 bus 再沿軸進入 anchor。\nfunction routeInToAnchor(pts, side, anchor){\n const pE = pts[pts.length - 1], pP = pts[pts.length - 2]\n const EPS = 1.5, J = 18, SLIDE = 40 // SLIDE: 可平移之最大橫向差距(過大恐撞鄰近元素, 改走 bus)\n const alongAxis = side.ax === 'y' ? Math.abs(pP.x - pE.x) < EPS : Math.abs(pP.y - pE.y) < EPS\n if (!alongAxis) return // 末段非沿進入軸(罕見), 保留原路由\n const lateral = side.ax === 'y' ? Math.abs(pE.x - anchor.x) : Math.abs(pE.y - anchor.y)\n if (lateral < EPS) {\n pts[pts.length - 1] = { x: anchor.x, y: anchor.y } // 已在 anchor 軸線上: 直接沿軸進入\n return\n }\n if (lateral <= SLIDE && pts.length >= 3) {\n const pP2 = pts[pts.length - 3]\n const prevHorizontal = side.ax === 'y' ? Math.abs(pP2.y - pP.y) < EPS : Math.abs(pP2.x - pP.x) < EPS\n if (prevHorizontal) {\n if (side.ax === 'y') pP.x = anchor.x; else pP.y = anchor.y\n pts[pts.length - 1] = { x: anchor.x, y: anchor.y }\n return\n }\n }\n const Lv = side.ax === 'y' ? side.v.y + side.sg * J : side.v.x + side.sg * J // 側外橫移層\n const segOK = side.ax === 'y' ? (pP.y - Lv) * side.sg > 2 : (pP.x - Lv) * side.sg > 2 // 原末段須延伸到橫移層之外\n if (!segOK) { pts[pts.length - 1] = { x: anchor.x, y: anchor.y }; return } // 空間不足: 直接斜線前最短接入(極罕見)\n const b1 = side.ax === 'y' ? { x: pE.x, y: Lv } : { x: Lv, y: pE.y }\n const b2 = side.ax === 'y' ? { x: anchor.x, y: Lv } : { x: Lv, y: anchor.y }\n pts.splice(pts.length - 1, 1, b1, b2, { x: anchor.x, y: anchor.y })\n}\n\nfunction ensureStub(pts, MIN, skipStart){\n function fixEnd(p){\n const n=p.length; if(n<4) return\n const C=p[n-1], B=p[n-2]\n const dx=C.x-B.x, dy=C.y-B.y, len=Math.abs(dx)+Math.abs(dy)\n if(len>=MIN || len<0.5) return\n const need=MIN-len, ux=dx/len, uy=dy/len\n p[n-2]={x:B.x-ux*need, y:B.y-uy*need}\n p[n-3]={x:p[n-3].x-ux*need, y:p[n-3].y-uy*need}\n }\n // 終點端(箭頭側)一律套最低門檻——含菱形貼齊後之路徑(bus 支線本給滿 J; 平移/保留支線之轉角平移不破壞正交)\n fixEnd(pts)\n // 起點端僅於「菱形頂點分岔」時跳過(分岔轉折貼近頂點屬預期; 後移轉角會與其後路徑段脫勾扭出斜線)\n if(!skipStart){ pts.reverse(); fixEnd(pts); pts.reverse() }\n return pts\n}\n\n// 供測試直接驗證菱形端點規則之純函式(不影響渲染)\nwindow.__dia = { diamondSide, diamondSlantMid, diamondInAnchor, routeOutFromVertex, routeInToAnchor }\n\nwindow.renderFig = async function(spec){\n try {\n const dir = spec.dir==='LR' ? 'RIGHT' : 'DOWN'\n const nById={}, gById={}\n spec.nodes.forEach(n=>nById[n.id]=n); spec.groups.forEach(g=>gById[g.id]=g)\n\n // 節點尺寸量測\n const size={}\n spec.nodes.forEach(n=>{\n if(n.items && n.items.length){ // 標題+項目節點(類 UML class box): 量標題與各項目 → 框寬=最寬列+邊距, 框高=標題列+各項目列\n const tm=measure(n.title||n.label, FS, 250), im=measureItems(n.items, FS-1)\n const headH=tm.h+12 // 標題列高(框頂到分隔線), 供分層繪製定位\n size[n.id]={ w:Math.max(tm.w, im.w)+30, h:headH+im.h+10, headH } // items 區高 = 實量塊高 + 上下 padding(6+4), 與渲染一致無多餘留白\n } else { const m=measure(n.label, FS, n.diamond?150:MAXW); if(n.diamond){ const d=Math.round(Math.max(m.w,m.h)*1.5+34); size[n.id]={w:d,h:d} } else size[n.id]={w:m.w+24,h:m.h+16} }\n })\n\n // 遞迴建 ELK 子樹(群組含 children)\n // 保序: 數據之節點順序=語義順序(選單/報告章節順序), 令 ELK 同層節點依 model order 排列, 不因交叉最小化重排(如 GIS2D/GIS3D 被拆開)。\n // considerModelOrder/forceNodeModelOrder 於 elkjs + INCLUDE_CHILDREN(巢狀)會內部崩潰, 故改用 semiInteractive:\n // crossingMinimization.semiInteractive + 各葉節點 elk.position 位置提示(以數據序為序), 同層節點依提示排序, 巢狀圖亦支援。\n // 例外: 含循環(如流程之退回邊)的圖, 位置提示會干擾分層使入口節點偏離頂端 → 根層不加提示(容器內仍保序)。\n const ORDER_OPTS = { 'elk.layered.crossingMinimization.semiInteractive':'true' }\n const hasCycle = (() => { const adj={}; spec.edges.forEach(e=>{ (adj[e.from]=adj[e.from]||[]).push(e.to) }); const st={}; const dfs=(u)=>{ st[u]=1; for(const v of (adj[u]||[])){ if(st[v]===1) return true; if(!st[v] && dfs(v)) return true } st[u]=2; return false }; return Object.keys(adj).some(u=>!st[u]&&dfs(u)) })()\n const gOpts = { 'elk.layered.nodePlacement.bk.fixedAlignment':'BALANCED', 'elk.padding':'[top=10,left=14,bottom=14,right=14]', 'elk.nodeLabels.placement':'[H_CENTER, V_TOP, INSIDE]', 'elk.algorithm':'layered', 'elk.direction':dir, 'elk.edgeRouting':'ORTHOGONAL', 'elk.hierarchyHandling':'INCLUDE_CHILDREN', 'elk.spacing.nodeNode':'28', 'elk.layered.spacing.nodeNodeBetweenLayers':'30', 'elk.spacing.edgeEdge':'22', 'elk.layered.spacing.edgeEdgeBetweenLayers':'24', ...ORDER_OPTS }\n function elkEdge(e){ let lab=[]; if(e.label){ const m=measure(e.label,EFS,300); lab=[{ text:e.label, width:m.w, height:m.h }] } return { id:e.id, sources:[e.from], targets:[e.to], labels:lab } }\n function build(parentId, padMap, wrapAR){\n const children=[], edges=[]\n spec.groups.filter(g=>g.parent===parentId).forEach(g=>{ const sub=build(g.id, padMap, wrapAR); const gm=measure(g.label,GFS,400)\n // 第二趟: 標題比容器寬時加大該容器左右 padding 撐寬(ELK 尊重複合容器 padding; 巢狀父容器自動跟著變寬, 不溢出又不過寬)\n const ex=(padMap&&padMap[g.id])||0\n const go={ ...gOpts, 'elk.padding':'[top=10,left='+(14+ex)+',bottom=14,right='+(14+ex)+']' }\n // A4 折排: 該容器被挑中時設 aspectRatio 觸發 ELK wrapping, 把線性鏈折成多欄/列(跨欄/列連接線由 ELK 自動正交路由)\n const ar=wrapAR&&wrapAR[g.id]; if(ar){ go['elk.aspectRatio']=String(ar); go['elk.layered.wrapping.strategy']='SINGLE_EDGE' }\n if(parentId || !hasCycle){ go['elk.position']='('+g.mi+','+g.mi+')' }\n children.push({ id:g.id, labels:[{text:g.label, width:gm.w+12, height:gm.h+6}], layoutOptions:go, children:sub.children, edges:sub.edges }) })\n spec.nodes.filter(n=>n.parent===parentId).forEach(n=>{ const va=VALIGN[(gById[parentId]&&gById[parentId].align)||DEF_VALIGN]||'TOP'; const lo={ 'elk.alignment':va }; if(parentId || !hasCycle){ lo['elk.position']='('+n.mi+','+n.mi+')' } children.push({ id:n.id, width:size[n.id].w, height:size[n.id].h, layoutOptions:lo }) })\n spec.edges.filter(e=>e.container===parentId).forEach(e=>{ edges.push(elkEdge(e)) })\n return { children, edges }\n }\n const rootOpts = { 'elk.layered.nodePlacement.bk.fixedAlignment':'BALANCED', 'elk.algorithm':'layered', 'elk.direction':dir, 'elk.edgeRouting':'ORTHOGONAL', 'elk.hierarchyHandling':'INCLUDE_CHILDREN', 'elk.spacing.nodeNode':'30', 'elk.layered.spacing.nodeNodeBetweenLayers':'34', 'elk.spacing.edgeNode':'24', 'elk.spacing.edgeEdge':'22', 'elk.layered.spacing.edgeEdgeBetweenLayers':'24', 'elk.layered.spacing.edgeNodeBetweenLayers':'26', ...(hasCycle ? { 'elk.layered.cycleBreaking.strategy':'MODEL_ORDER' } : ORDER_OPTS) }\n const elk = new ELK()\n const mkGraph = (pm, war) => { const rb = build(null, pm, war); return { id:'root', layoutOptions:rootOpts, children:rb.children, edges:rb.edges } }\n let res = await elk.layout(mkGraph({}, {}))\n // 第二趟: 偵測「標題寬 > 容器寬」之容器 → 加大其左右 padding 撐寬, 重新佈局(巢狀父容器自動跟進; 標題不溢出又不過寬)\n const padMap = {}\n ;(function collect(cell){ (cell.children||[]).forEach(ch=>{ if(gById[ch.id]){ const lw=(ch.labels&&ch.labels[0]&&ch.labels[0].width)||0; if(lw>(ch.width||0)) padMap[ch.id]=Math.ceil((lw-ch.width)/2)+6; collect(ch) } }) })(res)\n if(Object.keys(padMap).length) res = await elk.layout(mkGraph(padMap, {}))\n // ---- A4 長寬比自動折排 ----\n // A4 直式 h/w≈1.414、橫式≈0.707. 過高(R>HIGH)→挑最高之多子容器折成多欄縮高; 過寬(R<LOW)→挑最寬者折成多列增高.\n // 折排引擎=ELK wrapping(設 aspectRatio 觸發, 自動正交畫跨欄/列連接線); 逐次加深直到落入 A4 區間 / 無可折 / 達上限.\n const A4HIGH=1.45, A4LOW=0.70, wrapAR={}\n const foldable = () => { const out=[]; (function w(cell){ (cell.children||[]).forEach(ch=>{ if(gById[ch.id]){ if((ch.children||[]).length>=4) out.push({id:ch.id, w:ch.width||0, h:ch.height||0}); w(ch) } }) })(res); return out }\n for(let it=0; it<8; it++){\n const R=(res.height||1)/(res.width||1)\n if(R<=A4HIGH && R>=A4LOW) break\n const fs=foldable(); if(!fs.length) break\n let pick\n if(R>A4HIGH){ fs.sort((a,b)=>b.h-a.h); pick=fs.find(g=>(wrapAR[g.id]||0.6)<6) }\n else { fs.sort((a,b)=>b.w-a.w); pick=fs.find(g=>(wrapAR[g.id]||3.5)>0.22) }\n if(!pick) break\n wrapAR[pick.id] = R>A4HIGH ? (wrapAR[pick.id]||0.6)*1.9 : (wrapAR[pick.id]||3.5)/1.9\n res = await elk.layout(mkGraph(padMap, wrapAR))\n }\n\n // 遞迴收集: 容器(深度序)、節點(絕對座標)、邊(以容器絕對座標 + section 點)\n const groupsOut=[], nodesOut=[], edgesOut=[]\n function walk(cell, ox, oy, depth){\n ;(cell.children||[]).forEach(ch=>{\n const ax=ox+(ch.x||0), ay=oy+(ch.y||0)\n if (gById[ch.id]) { groupsOut.push({ g:gById[ch.id], x:ax, y:ay, w:ch.width, h:ch.height, depth, lbl:(ch.labels&&ch.labels[0])||null }); walk(ch, ax, ay, depth+1) }\n else if (nById[ch.id]) nodesOut.push({ n:nById[ch.id], x:ax, y:ay, w:ch.width, h:ch.height })\n })\n ;(cell.edges||[]).forEach(e=>{ edgesOut.push({ e:e, ox:ox, oy:oy }) })\n }\n walk(res, 0, 0, 0)\n\n const W=Math.ceil(res.width), H=Math.ceil(res.height), PAD=18\n let svg=''\n // 1) 容器(外層先畫=墊底; depth 小先畫)\n groupsOut.sort((a,b)=>a.depth-b.depth).forEach(o=>{\n svg+='<rect x="'+(o.x)+'" y="'+(o.y)+'" width="'+o.w+'" height="'+o.h+'" rx="8" fill="'+o.g.fill+'" fill-opacity="0.38" stroke="'+o.g.stroke+'" stroke-width="2"/>'\n const tw=o.lbl?o.lbl.width+12:o.w, th=o.lbl?o.lbl.height+6:24, tx=o.x+Math.max(0,(o.w-tw)/2), ty=o.y+(o.lbl?o.lbl.y:5)\n svg+='<foreignObject x="'+tx+'" y="'+ty+'" width="'+tw+'" height="'+th+'"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="text-align:center;font-weight:bold;font-size:'+GFS+'px;color:'+o.g.stroke+';line-height:1.3;white-space:pre-line;overflow-wrap:break-word;word-break:normal">'+esc(o.g.label)+'</div></foreignObject>'\n })\n // 2) 邊(在節點之下、容器之上)\n const nodeBoxById={}; nodesOut.forEach(o=>{ nodeBoxById[o.n.id]={ x:o.x, y:o.y, w:o.w, h:o.h, diamond:!!o.n.diamond } })\n const specEdgeById={}; spec.edges.forEach(se=>{ specEdgeById[se.id]=se })\n // 2a) 蒐集各 section 絕對路徑點 + 統計各菱形「各側」進出數(側=上/下/左/右, key: nodeId|軸|正負)。\n // 規則: 純出側→全部由頂點分岔; 純入側→各自貼斜邊; 混合側→全部貼斜邊\n // (混合側若連出走頂點+橫向接回, 橫向段必與夾在中間的連入直線交叉; 全貼斜邊則零共用點、零新增交叉)\n const drawList=[], sideMix={}\n edgesOut.forEach(o=>{\n const e=o.e, se=specEdgeById[e.id]\n const srcBox=se&&nodeBoxById[se.from], tgtBox=se&&nodeBoxById[se.to]\n const secs=(e.sections||[]).map(sec=>{\n const pts=[sec.startPoint].concat(sec.bendPoints||[]).concat([sec.endPoint]).map(p=>({x:o.ox+p.x, y:o.oy+p.y}))\n const item={ pts }\n if(srcBox&&srcBox.diamond){ item.srcSide=diamondSide(pts[0], srcBox); if(item.srcSide){ const k=se.from+'|'+item.srcSide.ax+item.srcSide.sg; (sideMix[k]=sideMix[k]||{in:0,out:0}).out++ } }\n if(tgtBox&&tgtBox.diamond){ item.tgtSide=diamondSide(pts[pts.length-1], tgtBox); if(item.tgtSide){ const k=se.to+'|'+item.tgtSide.ax+item.tgtSide.sg; (sideMix[k]=sideMix[k]||{in:0,out:0}).in++ } }\n return item\n })\n drawList.push({ o, e, se, srcBox, tgtBox, secs })\n })\n // 2a-2) 通用路徑化簡: ELK 階層邊(INCLUDE_CHILDREN)常見「先繞反方向再折返」之 S/U 形繞行,\n // 反覆對「三段式窗」(平行-垂直-平行)嘗試收斂成兩段, 僅在碰撞偵測全過才接受:\n // 不撞節點框/容器標題帶/邊標籤、不與他邊平行段貼齊併線; 端點段軸向與方向不可翻轉(維持節點進出方向,\n // 化簡先於菱形貼齊故不干擾貼齊規則); 本邊標籤若因化簡脫離路徑則整段還原(標籤座標由 ELK 依原路徑決定)。\n const obstacles=[]\n nodesOut.forEach(o=>{ obstacles.push({ x:o.x-8, y:o.y-8, w:o.w+16, h:o.h+16 }) })\n groupsOut.forEach(o=>{ if(!o.lbl) return; const tw=o.lbl.width+12, th=o.lbl.height+6, tx=o.x+Math.max(0,(o.w-tw)/2), ty=o.y+o.lbl.y; obstacles.push({ x:tx-4, y:ty-4, w:tw+8, h:th+8 }) })\n const labelBoxes=[]\n drawList.forEach(dd=>{ (dd.e.labels||[]).forEach(l=>{ if(!l.text) return; labelBoxes.push({ x:dd.o.ox+(l.x||0)-2, y:dd.o.oy+(l.y||0)-2, w:(l.width||40)+10, h:(l.height||18)+8 }) }) })\n function segHitsBox(a,b,box){ // 軸對齊線段 vs 矩形(線段必水平或垂直)\n const x1=Math.min(a.x,b.x), x2=Math.max(a.x,b.x), y1=Math.min(a.y,b.y), y2=Math.max(a.y,b.y)\n return x2>box.x && x1<box.x+box.w && y2>box.y && y1<box.y+box.h\n }\n function segOverlapsOther(a,b,selfItem){ // 與他邊平行段貼齊重疊(側距<6 且共線範圍>10)→ 視覺併線, 拒絕\n const horiz = Math.abs(a.y-b.y)<0.5\n for(const dd of drawList) for(const it of dd.secs){ if(it===selfItem) continue\n const q=it.pts\n for(let k=0;k+1<q.length;k++){ const c=q[k], d=q[k+1]\n if(horiz && Math.abs(c.y-d.y)<0.5 && Math.abs(c.y-a.y)<6){ const lo=Math.max(Math.min(a.x,b.x),Math.min(c.x,d.x)), hi=Math.min(Math.max(a.x,b.x),Math.max(c.x,d.x)); if(hi-lo>10) return true }\n if(!horiz && Math.abs(c.x-d.x)<0.5 && Math.abs(c.x-a.x)<6){ const lo=Math.max(Math.min(a.y,b.y),Math.min(c.y,d.y)), hi=Math.min(Math.max(a.y,b.y),Math.max(c.y,d.y)); if(hi-lo>10) return true }\n }\n }\n return false\n }\n // 標籤一律為硬障礙(含本邊自己的): ELK 邊標籤是「先佔位、路徑繞標籤走」, 原路的 jog 即標籤讓位,\n // 拉直會從自家標籤正中穿過(且「脫離才還原」守門對「穿過=距離變近」無感), 故不可豁免自家標籤\n function segClear(a,b,selfItem){\n for(const ob of obstacles) if(segHitsBox(a,b,ob)) return false\n for(const lb of labelBoxes) if(segHitsBox(a,b,lb)) return false\n return !segOverlapsOther(a,b,selfItem)\n }\n function mergeColl(p){ // 去重合點 + 併共線(含同線折返, 折返僅由化簡產生)\n for(let i=p.length-2;i>=0;i--) if(Math.abs(p[i].x-p[i+1].x)<0.5 && Math.abs(p[i].y-p[i+1].y)<0.5) p.splice(i,1)\n for(let i=p.length-2;i>=1;i--){ const A=p[i-1],B=p[i],C=p[i+1]\n if((Math.abs(A.x-B.x)<0.5 && Math.abs(B.x-C.x)<0.5) || (Math.abs(A.y-B.y)<0.5 && Math.abs(B.y-C.y)<0.5)) p.splice(i,1) }\n }\n function distToPoly(pt,p){ let d=Infinity\n for(let i=0;i+1<p.length;i++){ const a=p[i], b=p[i+1]\n const len2=Math.max(1,(b.x-a.x)*(b.x-a.x)+(b.y-a.y)*(b.y-a.y))\n const t=Math.max(0,Math.min(1, ((pt.x-a.x)*(b.x-a.x)+(pt.y-a.y)*(b.y-a.y))/len2 ))\n const qx=a.x+t*(b.x-a.x), qy=a.y+t*(b.y-a.y); d=Math.min(d, Math.hypot(pt.x-qx, pt.y-qy)) }\n return d\n }\n function simplifyOrtho(item, labels){\n const p=item.pts\n const before=p.map(q=>({x:q.x,y:q.y}))\n mergeColl(p)\n for(let pass=0; pass<20; pass++){\n let changed=false\n for(let i=0;i+3<p.length;i++){\n const isStart=(i===0), isEnd=(i+3===p.length-1)\n if(isStart && isEnd) continue // 4 點路徑已極簡, 兩端軸向無法同時保持\n const A=p[i], B=p[i+1], C=p[i+2], D=p[i+3]\n const s1h=Math.abs(A.y-B.y)<0.5\n // 取代點取窗之一角: 一般保持首段軸向; 末端窗改保持末段軸向(箭頭進入方向不可變)\n const P = isEnd ? (s1h ? {x:A.x,y:D.y} : {x:D.x,y:A.y}) : (s1h ? {x:D.x,y:A.y} : {x:A.x,y:D.y})\n if(isStart){ const sg0=s1h?Math.sign(B.x-A.x):Math.sign(B.y-A.y), sgN=s1h?Math.sign(P.x-A.x):Math.sign(P.y-A.y); if(sgN!==sg0) continue }\n if(isEnd){ const s3h=Math.abs(C.y-D.y)<0.5, sg0=s3h?Math.sign(D.x-C.x):Math.sign(D.y-C.y), sgN=s3h?Math.sign(D.x-P.x):Math.sign(D.y-P.y); if(sgN!==sg0) continue }\n if(!segClear(A,P,item) || !segClear(P,D,item)) continue\n p.splice(i+1, 2, P)\n mergeColl(p)\n changed=true\n break\n }\n if(!changed) break\n }\n // 標籤守門: 化簡若令本邊任一標籤脫離路徑(距離顯著變大), 整段還原\n if(labels.length && p.length !== before.length){\n for(const l of labels){\n if(distToPoly(l,p) > distToPoly(l,before)+2){ p.length=0; before.forEach(q=>p.push(q)); return }\n }\n }\n }\n drawList.forEach(dd=>{\n const labs=(dd.e.labels||[]).filter(l=>l.text).map(l=>({ x:dd.o.ox+(l.x||0)+(l.width||40)/2, y:dd.o.oy+(l.y||0)+(l.height||18)/2 }))\n dd.secs.forEach(item=>{ if(item.pts.length>=5) simplifyOrtho(item, labs) })\n })\n // 2b) 依側別規則貼齊並繪製\n const procSegsById={} // 實際繪製之路徑點(含菱形貼齊與 ensureStub), 供 geom 輸出一致\n drawList.forEach(dd=>{\n const o=dd.o, e=dd.e\n dd.secs.forEach(item=>{\n const pts=item.pts\n if(item.srcSide && pts.length >= 2) routeOutFromVertex(pts, item.srcSide) // R1: 出永遠自該側頂點\n if(item.tgtSide && pts.length >= 2){\n // R2: 純進側匯「頂點」; 混合側依「半側」匯「斜邊中點」(進出不同點)\n const m=sideMix[dd.se.to+'|'+item.tgtSide.ax+item.tgtSide.sg]\n const anchor=diamondInAnchor(dd.tgtBox, item.tgtSide, !!(m && m.out > 0), pts[pts.length-1])\n routeInToAnchor(pts, item.tgtSide, anchor)\n }\n // 分岔/匯入重排後清除共線折返: 頂點分岔之「接回原路第一轉折層」若跨過下一轉點, 會多走再折回疊出短勾, 併共線即塌縮為直接連線\n if(item.srcSide || item.tgtSide) mergeColl(pts)\n ensureStub(pts, 18, !!item.srcSide)\n ;(procSegsById[e.id]=procSegsById[e.id]||[]).push(pts)\n let d='M '+pts[0].x+' '+pts[0].y; for(let i=1;i<pts.length;i++) d+=' L '+pts[i].x+' '+pts[i].y\n const dash = (e.id && spec.edges.find(x=>x.id===e.id) && spec.edges.find(x=>x.id===e.id).dashed) ? ' stroke-dasharray="6 4"' : ''\n svg+='<path d="'+d+'" fill="none" stroke="'+EDGE_LINE+'" stroke-width="2"'+dash+'/>'\n // 箭頭(終點)\n const a=pts[pts.length-2], b=pts[pts.length-1], ang=Math.atan2(b.y-a.y,b.x-a.x), L=9, w=4\n const x1=b.x-L*Math.cos(ang)+w*Math.sin(ang), y1=b.y-L*Math.sin(ang)-w*Math.cos(ang)\n const x2=b.x-L*Math.cos(ang)-w*Math.sin(ang), y2=b.y-L*Math.sin(ang)+w*Math.cos(ang)\n svg+='<path d="M '+b.x+' '+b.y+' L '+x1+' '+y1+' L '+x2+' '+y2+' Z" fill="'+EDGE_LINE+'"/>'\n })\n // 邊標籤(白光暈)\n ;(e.labels||[]).forEach(l=>{ if(!l.text) return; const lx=o.ox+(l.x||0), ly=o.oy+(l.y||0), lw=l.width||40, lh=l.height||18\n svg+='<foreignObject x="'+lx+'" y="'+ly+'" width="'+(lw+6)+'" height="'+(lh+4)+'"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="font-size:'+EFS+'px;color:'+EDGE_TEXT+';text-shadow:0 0 3px '+HALO+',0 0 3px '+HALO+',0 0 3px '+HALO+',0 0 3px '+HALO+';white-space:pre-line">'+esc(l.text)+'</div></foreignObject>' })\n })\n // 3) 節點(最上)。各文字 div 之斷行 CSS(overflow-wrap:break-word + word-break:normal)須與 measure()/measureItems() 一致,\n // 否則長英文識別字量測時折行、渲染時不折 → 單行溢出 foreignObject 被裁切\n nodesOut.forEach(o=>{\n const n=o.n\n if(n.items && n.items.length){ // 標題+項目框: 依序分層(框背景→標題背景→框線→分隔線→標題→items), 使框線置頂不被標題背景遮蔽、線寬完整\n const headH=(size[n.id]&&size[n.id].headH)||26, r=7, sep=o.y+headH\n const lis=n.items.map(it=>'<div style="white-space:nowrap">•&#160;'+esc(it)+'</div>').join('')\n svg+='<rect x="'+o.x+'" y="'+o.y+'" width="'+o.w+'" height="'+o.h+'" rx="'+r+'" fill="'+n.fill+'"/>' // 1 框背景\n svg+='<path d="M '+(o.x+r)+' '+o.y+' H '+(o.x+o.w-r)+' A '+r+' '+r+' 0 0 1 '+(o.x+o.w)+' '+(o.y+r)+' V '+sep+' H '+o.x+' V '+(o.y+r)+' A '+r+' '+r+' 0 0 1 '+(o.x+r)+' '+o.y+' Z" fill="'+mix(n.fill,n.stroke,0.16)+'"/>' // 2 標題背景(上圓角下平)\n svg+='<rect x="'+o.x+'" y="'+o.y+'" width="'+o.w+'" height="'+o.h+'" rx="'+r+'" fill="none" stroke="'+n.stroke+'" stroke-width="1.6"/>' // 3 框線(頂層)\n svg+='<line x1="'+o.x+'" y1="'+sep+'" x2="'+(o.x+o.w)+'" y2="'+sep+'" stroke="'+n.stroke+'" stroke-width="1"/>' // 4 分隔線\n svg+='<foreignObject x="'+o.x+'" y="'+o.y+'" width="'+o.w+'" height="'+headH+'"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;text-align:center;font-weight:bold;font-size:'+FS+'px;color:'+n.font+'"><div style="width:100%;min-width:0;white-space:pre-line;overflow-wrap:break-word;word-break:normal">'+esc(n.title||n.label)+'</div></div></foreignObject>' // 5 標題文字\n svg+='<foreignObject x="'+o.x+'" y="'+sep+'" width="'+o.w+'" height="'+(o.h-headH)+'"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="font-size:'+(FS-1)+'px;line-height:1.55;padding:6px 9px 4px;text-align:left;color:'+n.font+'">'+lis+'</div></foreignObject>' // 6 items文字\n } else if(n.diamond){ const cx=o.x+o.w/2, cy=o.y+o.h/2\n svg+='<polygon points="'+cx+','+o.y+' '+(o.x+o.w)+','+cy+' '+cx+','+(o.y+o.h)+' '+o.x+','+cy+'" fill="'+n.fill+'" stroke="'+n.stroke+'" stroke-width="1.8"/>'\n svg+='<foreignObject x="'+o.x+'" y="'+o.y+'" width="'+o.w+'" height="'+o.h+'"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;text-align:center;font-size:'+FS+'px;color:'+n.font+';line-height:1.4;padding:0 4px;box-sizing:border-box"><div style="width:100%;min-width:0;white-space:pre-line;overflow-wrap:break-word;word-break:normal">'+esc(n.label)+'</div></div></foreignObject>'\n } else {\n svg+='<rect x="'+o.x+'" y="'+o.y+'" width="'+o.w+'" height="'+o.h+'" rx="7" fill="'+n.fill+'" stroke="'+n.stroke+'" stroke-width="1.6"/>'\n svg+='<foreignObject x="'+o.x+'" y="'+o.y+'" width="'+o.w+'" height="'+o.h+'"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;text-align:center;font-size:'+FS+'px;color:'+n.font+';line-height:1.4;padding:0 4px;box-sizing:border-box"><div style="width:100%;min-width:0;white-space:pre-line;overflow-wrap:break-word;word-break:normal">'+esc(n.label)+'</div></div></foreignObject>'\n }\n })\n\n const full='<svg xmlns="http://www.w3.org/2000/svg" width="'+(W+PAD*2)+'" height="'+(H+PAD*2)+'" viewBox="'+(-PAD)+' '+(-PAD)+' '+(W+PAD*2)+' '+(H+PAD*2)+'" style="background:#fff">'+svg+'</svg>'\n document.getElementById('stage').innerHTML = full\n // 幾何輸出(供 test 斷言: 存在性/包含/標題不溢出/重疊/A4比例/items框高); 不影響 SVG/PNG\n const geom = {\n W:W+PAD*2, H:H+PAD*2,\n nodes: nodesOut.map(o=>({ id:o.n.id, x:o.x, y:o.y, w:o.w, h:o.h, parent:o.n.parent||null, kind:(o.n.items&&o.n.items.length)?'items':(o.n.diamond?'diamond':'box'), headH:(size[o.n.id]&&size[o.n.id].headH)||null })),\n groups: groupsOut.map(o=>({ id:o.g.id, x:o.x, y:o.y, w:o.w, h:o.h, depth:o.depth, parent:o.g.parent||null, labelW:(o.lbl&&o.lbl.width)||0 })),\n edges: edgesOut.map(o=>({ id:o.e.id, segs: procSegsById[o.e.id]||[] }))\n }\n return { ok:true, w:W+PAD*2, h:H+PAD*2, geom }\n } catch(e){ return { ok:false, err:String(e&&(e.stack||e.message)||e).slice(0,400) } }\n}\nwindow.__ready = true\n<\/script></body></html>`;async function gr(n){const t=await e.chromium.launch();try{const e=await t.newPage({deviceScaleFactor:2});await e.setContent(hr,{waitUntil:"load"}),await e.waitForFunction(()=>window.__ready,{timeout:6e4}).catch(()=>{}),await e.evaluate(()=>document.fonts&&document.fonts.ready);const r=await e.evaluate(e=>window.renderFig(e),ur(n));if(!r.ok)throw new Error("p10 render failed: "+r.err);return await e.evaluate(()=>document.fonts&&document.fonts.ready),await e.waitForTimeout(150),{browser:t,page:e}}catch(e){throw await t.close(),e}}var yr=Object.freeze({__proto__:null,PAGE_HTML:hr,genPng:async function(e,n={}){const{browser:t,page:r}=await gr(e);try{return await r.locator("#stage svg").screenshot()}finally{await t.close()}},genSvg:async function(e,n={}){const{browser:t,page:r}=await gr(e);try{return(await r.evaluate(()=>document.getElementById("stage").innerHTML)).replace(/&nbsp;/g,"&#160;").trim()}finally{await t.close()}},translate:ur});return async function(e,n={}){let t=ie(n,"mode");Jn(t)||(t="p10");const r=t.trim().toLowerCase();if(!Vn[r])throw new Error(`invalid opt.mode[${t}], 須為 ${Object.keys(Vn).join(", ")} 之一`);let o=ie(n,"output");Jn(o)||(o="png");const a=o.trim().toLowerCase();if("png"!==a&&"svg"!==a)throw new Error(`invalid opt.output[${o}], 須為 png 或 svg`);if("svg"===a&&!Zn.includes(r))throw new Error(`mode[${r}] 不支援 svg 輸出, 支援: ${Zn.join(", ")}`);if(!function(e){if(Un(e)){for(let n in e)return!0;return!1}return!1}(e))throw new Error("invalid inp, 須為 { dir, nodes, edges } 正規化繪圖數據");if(!Xn(i=e.nodes)||0===i.length||1===i.length&&Yn(i[0]))throw new Error("invalid inp.nodes, 須為非空陣列");var i;if(!Xn(e.edges))throw new Error("invalid inp.edges, 須為陣列");const s=((l=Wn(e,5)).nodes.forEach(e=>{e.title&&null==e.label&&(e.label=e.title+(e.items&&e.items.length?" ("+e.items.join("/")+")":""))}),l);var l;const c=await Vn[r]();return await("svg"===a?c.genSvg:c.genPng)(s,n)}});
6
+ !function(e,n){"object"==typeof exports&&"undefined"!=typeof module?module.exports=n(require("playwright"),require("url"),require("path"),require("fs")):"function"==typeof define&&define.amd?define(["playwright","url","path","fs"],n):(e="undefined"!=typeof globalThis?globalThis:e||self)["w-flowchart"]=n(e.playwright,e.url,e.path,e.fs)}(this,function(e,n,t,r){"use strict";var o="undefined"!=typeof document?document.currentScript:null,a=Array.isArray,i="object"==typeof global&&global&&global.Object===Object&&global,s="object"==typeof self&&self&&self.Object===Object&&self,l=i||s||Function("return this")(),c=l.Symbol,d=Object.prototype,p=d.hasOwnProperty,f=d.toString,u=c?c.toStringTag:void 0;var h=Object.prototype.toString;var g=c?c.toStringTag:void 0;function y(e){return null==e?void 0===e?"[object Undefined]":"[object Null]":g&&g in Object(e)?function(e){var n=p.call(e,u),t=e[u];try{e[u]=void 0;var r=!0}catch(e){}var o=f.call(e);return r&&(n?e[u]=t:delete e[u]),o}(e):function(e){return h.call(e)}(e)}function b(e){return null!=e&&"object"==typeof e}function m(e){return"symbol"==typeof e||b(e)&&"[object Symbol]"==y(e)}var x=/\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/,w=/^\w*$/;function v(e){var n=typeof e;return null!=e&&("object"==n||"function"==n)}function k(e){if(!v(e))return!1;var n=y(e);return"[object Function]"==n||"[object GeneratorFunction]"==n||"[object AsyncFunction]"==n||"[object Proxy]"==n}var _,E=l["__core-js_shared__"],S=(_=/[^.]+$/.exec(E&&E.keys&&E.keys.IE_PROTO||""))?"Symbol(src)_1."+_:"";var O=Function.prototype.toString;function j(e){if(null!=e){try{return O.call(e)}catch(e){}try{return e+""}catch(e){}}return""}var I=/^\[object .+?Constructor\]$/,A=Function.prototype,M=Object.prototype,L=A.toString,P=M.hasOwnProperty,B=RegExp("^"+L.call(P).replace(/[\\^$.*+?()[\]{}|]/g,"\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g,"$1.*?")+"$");function F(e){return!(!v(e)||(n=e,S&&S in n))&&(k(e)?B:I).test(j(e));var n}function D(e,n){var t=function(e,n){return null==e?void 0:e[n]}(e,n);return F(t)?t:void 0}var N=D(Object,"create");var $=Object.prototype.hasOwnProperty;var G=Object.prototype.hasOwnProperty;function T(e){var n=-1,t=null==e?0:e.length;for(this.clear();++n<t;){var r=e[n];this.set(r[0],r[1])}}function C(e,n){return e===n||e!=e&&n!=n}function R(e,n){for(var t=e.length;t--;)if(C(e[t][0],n))return t;return-1}T.prototype.clear=function(){this.__data__=N?N(null):{},this.size=0},T.prototype.delete=function(e){var n=this.has(e)&&delete this.__data__[e];return this.size-=n?1:0,n},T.prototype.get=function(e){var n=this.__data__;if(N){var t=n[e];return"__lodash_hash_undefined__"===t?void 0:t}return $.call(n,e)?n[e]:void 0},T.prototype.has=function(e){var n=this.__data__;return N?void 0!==n[e]:G.call(n,e)},T.prototype.set=function(e,n){var t=this.__data__;return this.size+=this.has(e)?0:1,t[e]=N&&void 0===n?"__lodash_hash_undefined__":n,this};var H=Array.prototype.splice;function z(e){var n=-1,t=null==e?0:e.length;for(this.clear();++n<t;){var r=e[n];this.set(r[0],r[1])}}z.prototype.clear=function(){this.__data__=[],this.size=0},z.prototype.delete=function(e){var n=this.__data__,t=R(n,e);return!(t<0)&&(t==n.length-1?n.pop():H.call(n,t,1),--this.size,!0)},z.prototype.get=function(e){var n=this.__data__,t=R(n,e);return t<0?void 0:n[t][1]},z.prototype.has=function(e){return R(this.__data__,e)>-1},z.prototype.set=function(e,n){var t=this.__data__,r=R(t,e);return r<0?(++this.size,t.push([e,n])):t[r][1]=n,this};var W=D(l,"Map");function J(e,n){var t,r,o=e.__data__;return("string"==(r=typeof(t=n))||"number"==r||"symbol"==r||"boolean"==r?"__proto__"!==t:null===t)?o["string"==typeof n?"string":"hash"]:o.map}function q(e){var n=-1,t=null==e?0:e.length;for(this.clear();++n<t;){var r=e[n];this.set(r[0],r[1])}}q.prototype.clear=function(){this.size=0,this.__data__={hash:new T,map:new(W||z),string:new T}},q.prototype.delete=function(e){var n=J(this,e).delete(e);return this.size-=n?1:0,n},q.prototype.get=function(e){return J(this,e).get(e)},q.prototype.has=function(e){return J(this,e).has(e)},q.prototype.set=function(e,n){var t=J(this,e),r=t.size;return t.set(e,n),this.size+=t.size==r?0:1,this};function X(e,n){if("function"!=typeof e||null!=n&&"function"!=typeof n)throw new TypeError("Expected a function");var t=function(){var r=arguments,o=n?n.apply(this,r):r[0],a=t.cache;if(a.has(o))return a.get(o);var i=e.apply(this,r);return t.cache=a.set(o,i)||a,i};return t.cache=new(X.Cache||q),t}X.Cache=q;var Y,U,V,Z=/[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g,K=/\\(\\)?/g,Q=(Y=function(e){var n=[];return 46===e.charCodeAt(0)&&n.push(""),e.replace(Z,function(e,t,r,o){n.push(r?o.replace(K,"$1"):t||e)}),n},U=X(Y,function(e){return 500===V.size&&V.clear(),e}),V=U.cache,U),ee=Q;var ne=c?c.prototype:void 0,te=ne?ne.toString:void 0;function re(e){if("string"==typeof e)return e;if(a(e))return function(e,n){for(var t=-1,r=null==e?0:e.length,o=Array(r);++t<r;)o[t]=n(e[t],t,e);return o}(e,re)+"";if(m(e))return te?te.call(e):"";var n=e+"";return"0"==n&&1/e==-1/0?"-0":n}function oe(e,n){return a(e)?e:function(e,n){if(a(e))return!1;var t=typeof e;return!("number"!=t&&"symbol"!=t&&"boolean"!=t&&null!=e&&!m(e))||w.test(e)||!x.test(e)||null!=n&&e in Object(n)}(e,n)?[e]:ee(function(e){return null==e?"":re(e)}(e))}function ae(e){if("string"==typeof e||m(e))return e;var n=e+"";return"0"==n&&1/e==-1/0?"-0":n}function ie(e,n,t){var r=null==e?void 0:function(e,n){for(var t=0,r=(n=oe(n,e)).length;null!=e&&t<r;)e=e[ae(n[t++])];return t&&t==r?e:void 0}(e,n);return void 0===r?t:r}function se(e){var n=this.__data__=new z(e);this.size=n.size}se.prototype.clear=function(){this.__data__=new z,this.size=0},se.prototype.delete=function(e){var n=this.__data__,t=n.delete(e);return this.size=n.size,t},se.prototype.get=function(e){return this.__data__.get(e)},se.prototype.has=function(e){return this.__data__.has(e)},se.prototype.set=function(e,n){var t=this.__data__;if(t instanceof z){var r=t.__data__;if(!W||r.length<199)return r.push([e,n]),this.size=++t.size,this;t=this.__data__=new q(r)}return t.set(e,n),this.size=t.size,this};var le=function(){try{var e=D(Object,"defineProperty");return e({},"",{}),e}catch(e){}}(),ce=le;function de(e,n,t){"__proto__"==n&&ce?ce(e,n,{configurable:!0,enumerable:!0,value:t,writable:!0}):e[n]=t}var pe=Object.prototype.hasOwnProperty;function fe(e,n,t){var r=e[n];pe.call(e,n)&&C(r,t)&&(void 0!==t||n in e)||de(e,n,t)}function ue(e,n,t,r){var o=!t;t||(t={});for(var a=-1,i=n.length;++a<i;){var s=n[a],l=r?r(t[s],e[s],s,t,e):void 0;void 0===l&&(l=e[s]),o?de(t,s,l):fe(t,s,l)}return t}function he(e){return b(e)&&"[object Arguments]"==y(e)}var ge=Object.prototype,ye=ge.hasOwnProperty,be=ge.propertyIsEnumerable,me=he(function(){return arguments}())?he:function(e){return b(e)&&ye.call(e,"callee")&&!be.call(e,"callee")},xe=me;var we="object"==typeof exports&&exports&&!exports.nodeType&&exports,ve=we&&"object"==typeof module&&module&&!module.nodeType&&module,ke=ve&&ve.exports===we?l.Buffer:void 0,_e=(ke?ke.isBuffer:void 0)||function(){return!1},Ee=/^(?:0|[1-9]\d*)$/;function Se(e,n){var t=typeof e;return!!(n=null==n?9007199254740991:n)&&("number"==t||"symbol"!=t&&Ee.test(e))&&e>-1&&e%1==0&&e<n}function Oe(e){return"number"==typeof e&&e>-1&&e%1==0&&e<=9007199254740991}var je={};function Ie(e){return function(n){return e(n)}}je["[object Float32Array]"]=je["[object Float64Array]"]=je["[object Int8Array]"]=je["[object Int16Array]"]=je["[object Int32Array]"]=je["[object Uint8Array]"]=je["[object Uint8ClampedArray]"]=je["[object Uint16Array]"]=je["[object Uint32Array]"]=!0,je["[object Arguments]"]=je["[object Array]"]=je["[object ArrayBuffer]"]=je["[object Boolean]"]=je["[object DataView]"]=je["[object Date]"]=je["[object Error]"]=je["[object Function]"]=je["[object Map]"]=je["[object Number]"]=je["[object Object]"]=je["[object RegExp]"]=je["[object Set]"]=je["[object String]"]=je["[object WeakMap]"]=!1;var Ae="object"==typeof exports&&exports&&!exports.nodeType&&exports,Me=Ae&&"object"==typeof module&&module&&!module.nodeType&&module,Le=Me&&Me.exports===Ae&&i.process,Pe=function(){try{var e=Me&&Me.require&&Me.require("util").types;return e||Le&&Le.binding&&Le.binding("util")}catch(e){}}(),Be=Pe&&Pe.isTypedArray,Fe=Be?Ie(Be):function(e){return b(e)&&Oe(e.length)&&!!je[y(e)]},De=Object.prototype.hasOwnProperty;function Ne(e,n){var t=a(e),r=!t&&xe(e),o=!t&&!r&&_e(e),i=!t&&!r&&!o&&Fe(e),s=t||r||o||i,l=s?function(e,n){for(var t=-1,r=Array(e);++t<e;)r[t]=n(t);return r}(e.length,String):[],c=l.length;for(var d in e)!n&&!De.call(e,d)||s&&("length"==d||o&&("offset"==d||"parent"==d)||i&&("buffer"==d||"byteLength"==d||"byteOffset"==d)||Se(d,c))||l.push(d);return l}var $e=Object.prototype;function Ge(e){var n=e&&e.constructor;return e===("function"==typeof n&&n.prototype||$e)}function Te(e,n){return function(t){return e(n(t))}}var Ce=Te(Object.keys,Object),Re=Object.prototype.hasOwnProperty;function He(e){return null!=e&&Oe(e.length)&&!k(e)}function ze(e){return He(e)?Ne(e):function(e){if(!Ge(e))return Ce(e);var n=[];for(var t in Object(e))Re.call(e,t)&&"constructor"!=t&&n.push(t);return n}(e)}var We=Object.prototype.hasOwnProperty;function Je(e){if(!v(e))return function(e){var n=[];if(null!=e)for(var t in Object(e))n.push(t);return n}(e);var n=Ge(e),t=[];for(var r in e)("constructor"!=r||!n&&We.call(e,r))&&t.push(r);return t}function qe(e){return He(e)?Ne(e,!0):Je(e)}var Xe="object"==typeof exports&&exports&&!exports.nodeType&&exports,Ye=Xe&&"object"==typeof module&&module&&!module.nodeType&&module,Ue=Ye&&Ye.exports===Xe?l.Buffer:void 0,Ve=Ue?Ue.allocUnsafe:void 0;function Ze(){return[]}var Ke=Object.prototype.propertyIsEnumerable,Qe=Object.getOwnPropertySymbols,en=Qe?function(e){return null==e?[]:(e=Object(e),function(e,n){for(var t=-1,r=null==e?0:e.length,o=0,a=[];++t<r;){var i=e[t];n(i,t,e)&&(a[o++]=i)}return a}(Qe(e),function(n){return Ke.call(e,n)}))}:Ze,nn=en;function tn(e,n){for(var t=-1,r=n.length,o=e.length;++t<r;)e[o+t]=n[t];return e}var rn=Te(Object.getPrototypeOf,Object),on=Object.getOwnPropertySymbols?function(e){for(var n=[];e;)tn(n,nn(e)),e=rn(e);return n}:Ze,an=on;function sn(e,n,t){var r=n(e);return a(e)?r:tn(r,t(e))}function ln(e){return sn(e,ze,nn)}function cn(e){return sn(e,qe,an)}var dn=D(l,"DataView"),pn=D(l,"Promise"),fn=D(l,"Set"),un=D(l,"WeakMap"),hn="[object Map]",gn="[object Promise]",yn="[object Set]",bn="[object WeakMap]",mn="[object DataView]",xn=j(dn),wn=j(W),vn=j(pn),kn=j(fn),_n=j(un),En=y;(dn&&En(new dn(new ArrayBuffer(1)))!=mn||W&&En(new W)!=hn||pn&&En(pn.resolve())!=gn||fn&&En(new fn)!=yn||un&&En(new un)!=bn)&&(En=function(e){var n=y(e),t="[object Object]"==n?e.constructor:void 0,r=t?j(t):"";if(r)switch(r){case xn:return mn;case wn:return hn;case vn:return gn;case kn:return yn;case _n:return bn}return n});var Sn=En,On=Object.prototype.hasOwnProperty;var jn=l.Uint8Array;function In(e){var n=new e.constructor(e.byteLength);return new jn(n).set(new jn(e)),n}var An=/\w*$/;var Mn=c?c.prototype:void 0,Ln=Mn?Mn.valueOf:void 0;function Pn(e,n,t){var r,o=e.constructor;switch(n){case"[object ArrayBuffer]":return In(e);case"[object Boolean]":case"[object Date]":return new o(+e);case"[object DataView]":return function(e,n){var t=n?In(e.buffer):e.buffer;return new e.constructor(t,e.byteOffset,e.byteLength)}(e,t);case"[object Float32Array]":case"[object Float64Array]":case"[object Int8Array]":case"[object Int16Array]":case"[object Int32Array]":case"[object Uint8Array]":case"[object Uint8ClampedArray]":case"[object Uint16Array]":case"[object Uint32Array]":return function(e,n){var t=n?In(e.buffer):e.buffer;return new e.constructor(t,e.byteOffset,e.length)}(e,t);case"[object Map]":case"[object Set]":return new o;case"[object Number]":case"[object String]":return new o(e);case"[object RegExp]":return function(e){var n=new e.constructor(e.source,An.exec(e));return n.lastIndex=e.lastIndex,n}(e);case"[object Symbol]":return r=e,Ln?Object(Ln.call(r)):{}}}var Bn=Object.create,Fn=function(){function e(){}return function(n){if(!v(n))return{};if(Bn)return Bn(n);e.prototype=n;var t=new e;return e.prototype=void 0,t}}(),Dn=Fn;var Nn=Pe&&Pe.isMap,$n=Nn?Ie(Nn):function(e){return b(e)&&"[object Map]"==Sn(e)};var Gn=Pe&&Pe.isSet,Tn=Gn?Ie(Gn):function(e){return b(e)&&"[object Set]"==Sn(e)},Cn="[object Arguments]",Rn="[object Function]",Hn="[object Object]",zn={};function Wn(e,n,t,r,o,i){var s,l=1&n,c=2&n,d=4&n;if(t&&(s=o?t(e,r,o,i):t(e)),void 0!==s)return s;if(!v(e))return e;var p=a(e);if(p){if(s=function(e){var n=e.length,t=new e.constructor(n);return n&&"string"==typeof e[0]&&On.call(e,"index")&&(t.index=e.index,t.input=e.input),t}(e),!l)return function(e,n){var t=-1,r=e.length;for(n||(n=Array(r));++t<r;)n[t]=e[t];return n}(e,s)}else{var f=Sn(e),u=f==Rn||"[object GeneratorFunction]"==f;if(_e(e))return function(e,n){if(n)return e.slice();var t=e.length,r=Ve?Ve(t):new e.constructor(t);return e.copy(r),r}(e,l);if(f==Hn||f==Cn||u&&!o){if(s=c||u?{}:function(e){return"function"!=typeof e.constructor||Ge(e)?{}:Dn(rn(e))}(e),!l)return c?function(e,n){return ue(e,an(e),n)}(e,function(e,n){return e&&ue(n,qe(n),e)}(s,e)):function(e,n){return ue(e,nn(e),n)}(e,function(e,n){return e&&ue(n,ze(n),e)}(s,e))}else{if(!zn[f])return o?e:{};s=Pn(e,f,l)}}i||(i=new se);var h=i.get(e);if(h)return h;i.set(e,s),Tn(e)?e.forEach(function(r){s.add(Wn(r,n,t,r,e,i))}):$n(e)&&e.forEach(function(r,o){s.set(o,Wn(r,n,t,o,e,i))});var g=p?void 0:(d?c?cn:ln:c?qe:ze)(e);return function(e,n){for(var t=-1,r=null==e?0:e.length;++t<r&&!1!==n(e[t],t,e););}(g||e,function(r,o){g&&(r=e[o=r]),fe(s,o,Wn(r,n,t,o,e,i))}),s}zn[Cn]=zn["[object Array]"]=zn["[object ArrayBuffer]"]=zn["[object DataView]"]=zn["[object Boolean]"]=zn["[object Date]"]=zn["[object Float32Array]"]=zn["[object Float64Array]"]=zn["[object Int8Array]"]=zn["[object Int16Array]"]=zn["[object Int32Array]"]=zn["[object Map]"]=zn["[object Number]"]=zn[Hn]=zn["[object RegExp]"]=zn["[object Set]"]=zn["[object String]"]=zn["[object Symbol]"]=zn["[object Uint8Array]"]=zn["[object Uint8ClampedArray]"]=zn["[object Uint16Array]"]=zn["[object Uint32Array]"]=!0,zn["[object Error]"]=zn[Rn]=zn["[object WeakMap]"]=!1;function Jn(e){return"[object String]"===Object.prototype.toString.call(e)}function qn(e){return!(!Jn(e)||""===e)}function Xn(e){return"[object Object]"===Object.prototype.toString.call(e)}function Yn(e){return"[object Array]"===Object.prototype.toString.call(e)}function Un(e){return!!function(e){return"[object Undefined]"===Object.prototype.toString.call(e)}(e)||(!!function(e){return"[object Null]"===Object.prototype.toString.call(e)}(e)||(!!function(e){if(Xn(e)){for(let n in e)return!1;return!0}return!1}(e)||(!!function(e){return!(!Jn(e)||""!==e)}(e)||(!!function(e){return!!Yn(e)&&0===e.length}(e)||!!function(e){return e!=e}(e)))))}const Vn={p1:()=>Promise.resolve().then(function(){return ft}),p2:()=>Promise.resolve().then(function(){return yt}),p3:()=>Promise.resolve().then(function(){return vt}),p4:()=>Promise.resolve().then(function(){return St}),p5:()=>Promise.resolve().then(function(){return Nt}),p6:()=>Promise.resolve().then(function(){return Rt}),p7:()=>Promise.resolve().then(function(){return Jt}),p8:()=>Promise.resolve().then(function(){return Zt}),p9:()=>Promise.resolve().then(function(){return er}),p10:()=>Promise.resolve().then(function(){return or})},Zn=["p1","p2","p3","p4","p5","p6","p7","p8","p10"];const Kn={blue:{fill:"#eef4fb",stroke:"#3f6fb0",text:"#1c2b36",shape:"box"},green:{fill:"#eaf4ef",stroke:"#348a5c",text:"#1c2b36",shape:"box"},orange:{fill:"#fcf0e2",stroke:"#c46e1a",text:"#1c2b36",shape:"box"},purple:{fill:"#f4f0fa",stroke:"#8163ad",text:"#23303a",shape:"box"},red:{fill:"#fbecea",stroke:"#c0392b",text:"#23303a",shape:"box"},done:{fill:"#eaf4ef",stroke:"#256046",text:"#256046",shape:"box"},diamond:{fill:"#fcf0e2",stroke:"#c46e1a",text:"#23303a",shape:"diamond"},blueG:{fill:"#f5f9fe",stroke:"#3f6fb0",text:"#3f6fb0",group:!0},blueG2:{fill:"#eef4fb",stroke:"#3f6fb0",text:"#3f6fb0",group:!0},greenG:{fill:"#f0f7f3",stroke:"#348a5c",text:"#348a5c",group:!0},greenG2:{fill:"#eaf4ef",stroke:"#348a5c",text:"#348a5c",group:!0},orangeG:{fill:"#fdf6ec",stroke:"#c46e1a",text:"#c46e1a",group:!0},purpleG:{fill:"#faf7fd",stroke:"#8163ad",text:"#8163ad",group:!0}},Qn={line:"#44505a",text:"#4a5560",textBg:"transparent",haloColor:"#ffffff",haloWidth:3.5},et="'Microsoft JhengHei', sans-serif",nt=e=>/G2?$/.test(e||""),tt=e=>Kn[e]||Kn.blue,rt={mermaid10:"mermaid@10.9.6/dist/mermaid.min.js","mermaid11-esm":"mermaid@11.16.0/dist/mermaid.esm.min.mjs","layout-elk-esm":"@mermaid-js/layout-elk@0.2.2/dist/mermaid-layout-elk.esm.min.mjs",graphre:"graphre@0.1.3/dist/graphre.js",nomnoml:"nomnoml@1.7.0/dist/nomnoml.js",cytoscape:"cytoscape@3.34.0/dist/cytoscape.min.js","cytoscape-dagre":"cytoscape-dagre@4.0.0/dist/cytoscape-dagre.js","cytoscape-svg":"cytoscape-svg@0.4.0/cytoscape-svg.js",g6:"@antv/g6@5.1.1/dist/g6.min.js","g-lite":"@antv/g-lite@2.7.0/dist/index.umd.min.js","g-svg":"@antv/g-svg@2.1.1/dist/index.umd.min.js",joint:"@joint/core@4.3.0/dist/joint.min.js",logicflow:"@logicflow/core@2.2.4/dist/index.min.js","logicflow-css":"@logicflow/core@2.2.4/dist/index.css",react:"react@18.3.1/umd/react.production.min.js","react-dom":"react-dom@18.3.1/umd/react-dom.production.min.js",xyflow:"@xyflow/react@12.11.2/dist/umd/index.js","xyflow-css":"@xyflow/react@12.11.2/dist/style.css","dagrejs-dagre":"@dagrejs/dagre@1.1.8/dist/dagre.min.js",dagre:"dagre@0.8.5/dist/dagre.min.js",elkjs:"elkjs@0.11.1/lib/elk.bundled.js"};function ot(e){const n=rt[e];if(!n)throw new Error("未知 CDN 套件鍵: "+e);return"https://cdn.jsdelivr.net/npm/"+n}function at(e){return`<script src="${ot(e)}"><\/script>`}function it(e){return`<link rel="stylesheet" href="${ot(e)}">`}const st={};const lt=e=>String(e).replace(/\n/g,"<br/>");function ct(e,n){return"diamond"===e.cls?`${n(e.id)}{"${lt(e.label)}"}`:`${n(e.id)}["${lt(e.label)}"]`}function dt(e){const{dir:n,nodes:t,edges:r}=e,o=new Set,a={},i=e=>{if(a[e])return a[e];const n="n_"+String(e).replace(/[^A-Za-z0-9_]/g,"_");let t=n,r=2;for(;o.has(t);)t=n+"_"+r++;return o.add(t),a[e]=t,t},s={};for(const e of t)s[e.id]=e;new Set(t.filter(e=>nt(e.cls)).map(e=>e.id));const l=new Set(t.map(e=>e.cls)),c=[];for(const e of l){const n=Kn[e]||Kn.blue;nt(e)?c.push(` classDef ${e} fill:${n.fill},stroke:${n.stroke},stroke-width:1.8px,color:${n.text}`):"diamond"===e?c.push(` classDef ${e} fill:${n.fill},stroke:${n.stroke},stroke-width:1.6px,color:${n.text}`):c.push(` classDef ${e} fill:${n.fill},stroke:${n.stroke},stroke-width:1.5px,color:${n.text}`)}const d={};for(const e of t)d[e.cls]||(d[e.cls]=[]),d[e.cls].push(e.id);const p=[];for(const[e,n]of Object.entries(d))p.push(` class ${n.map(i).join(",")} ${e}`);const f=t.filter(e=>nt(e.cls)&&!e.group).map(e=>e.id),u=t.filter(e=>!nt(e.cls)&&!e.group);function h(e,n){const r=s[e],o=[];o.push(`${n}subgraph ${i(e)}["${String(r.label).replace(/\n/g," ")}"]`);const a=t.filter(n=>n.group===e&&!nt(n.cls));for(const e of a)o.push(`${n} ${ct(e,i)}`);const l=t.filter(n=>nt(n.cls)&&n.group===e);for(const e of l)o.push(...h(e.id,n+" "));return o.push(`${n}end`),o}const g=[];for(const e of r){const n="dashed"===e.kind?"-..->":"--\x3e",t=e.label?`|"${lt(e.label)}"| `:"";g.push(` ${i(e.from)} ${n} ${t}${i(e.to)}`)}const y=[`flowchart ${n}`];for(const e of u)y.push(` ${ct(e,i)}`);for(const e of f)y.push(...h(e," "));return y.push(...g),y.push(...c),y.push(...p),y.join("\n")}async function pt(n){const t=await e.chromium.launch();try{const e=await t.newPage({deviceScaleFactor:4});await e.setContent(function(e){return`<!doctype html><html lang="zh-Hant"><head><meta charset="utf-8">\n${at("mermaid10")}\n</head><body style="margin:0;background:#fff;font-family:'微軟正黑體','Microsoft JhengHei',sans-serif">\n<div id="box" style="display:inline-block;padding:24px"></div>\n<script>\nmermaid.initialize({ startOnLoad:false, theme:'base', themeVariables:{ fontFamily:"'Microsoft JhengHei','微軟正黑體',sans-serif", fontSize:'17px', primaryColor:'#eef4fb', primaryBorderColor:'#3f6fb0', lineColor:'#44505a', primaryTextColor:'#1c2b36' }, themeCSS: '.flowchart-link{stroke:#44505a !important;stroke-width:2.5px !important;} .edgePaths .path{stroke:#44505a !important;stroke-width:2.5px !important;} .marker,.marker path{fill:#44505a !important;stroke:#44505a !important;} .edgeLabel .label{background:transparent !important;} .edgeLabel{background:transparent !important;} .edgeLabel rect{fill:transparent !important;opacity:0 !important;} .label foreignObject{overflow:visible;} .cluster-label text,.cluster-label span,.cluster-label div,.edgeLabel span{font-family:"Microsoft JhengHei","微軟正黑體",sans-serif !important;}' });\n(async () => {\n try {\n const { svg } = await mermaid.render('g', ${JSON.stringify(e)})\n document.getElementById('box').innerHTML = svg\n const DK = '#333333', W = '3px'\n document.querySelectorAll('#box .edgePaths path, #box .edgePath path, #box path.flowchart-link, #box g.edgePaths path').forEach((p) => { p.style.setProperty('stroke', DK, 'important'); p.style.setProperty('stroke-width', W, 'important') })\n document.querySelectorAll('#box marker path, #box .marker, #box .marker path').forEach((m) => { m.style.setProperty('fill', DK, 'important'); m.style.setProperty('stroke', DK, 'important') })\n document.querySelectorAll('#box .node rect, #box .node polygon, #box .node circle, #box .node path').forEach((n) => { n.style.setProperty('stroke-width', '2px', 'important') })\n document.querySelectorAll('#box .cluster rect, #box .cluster polygon, #box .cluster path').forEach((c) => { c.style.setProperty('stroke', '#777777', 'important'); c.style.setProperty('stroke-width', '1.5px', 'important') })\n // 修正 A: 邊標籤背景透明 + 白色光暈(透過 foreignObject 內 span 的 CSS)\n document.querySelectorAll('#box .edgeLabel rect, #box .edgeLabel .label').forEach((el) => {\n el.style.setProperty('fill', 'transparent', 'important')\n el.style.setProperty('background', 'transparent', 'important')\n el.style.setProperty('opacity', '1', 'important')\n })\n document.querySelectorAll('#box .edgeLabel foreignObject').forEach((fo) => {\n fo.style.setProperty('overflow', 'visible', 'important')\n })\n document.querySelectorAll('#box .edgeLabel span, #box .edgeLabel .edgeLabel').forEach((sp) => {\n sp.style.setProperty('background', 'transparent', 'important')\n sp.style.setProperty('color', '#4a5560', 'important')\n // 字型覆蓋: 邊標籤 span 可能繼承系統字型, 強制指定\n sp.style.setProperty('font-family', "'Microsoft JhengHei', '微軟正黑體', sans-serif", 'important')\n // 白色光暈: CSS text-shadow 多向\n sp.style.setProperty('text-shadow', '-1.5px -1.5px 0 #fff, 1.5px -1.5px 0 #fff, -1.5px 1.5px 0 #fff, 1.5px 1.5px 0 #fff, 0 -2px 0 #fff, 0 2px 0 #fff, -2px 0 0 #fff, 2px 0 0 #fff', 'important')\n // 修正 B: 英數識別字不攔腰斷\n sp.style.setProperty('word-break', 'normal', 'important')\n sp.style.setProperty('overflow-wrap', 'break-word', 'important')\n })\n // 字型覆蓋: cluster(subgraph)標題文字 — mermaid@10 的 .cluster-label text/span 不繼承 fontFamily themeVariable\n document.querySelectorAll('#box .cluster-label text, #box .cluster-label span, #box .cluster-label div').forEach((el) => {\n el.style.setProperty('font-family', "'Microsoft JhengHei', '微軟正黑體', sans-serif", 'important')\n })\n document.title = 'DONE'\n }\n catch (e) {\n document.title = 'FAIL'\n console.log('mmErr', e.message)\n }\n})()\n<\/script></body></html>`}(n),{waitUntil:"load"}),await e.waitForFunction(()=>"DONE"===document.title||"FAIL"===document.title,{timeout:3e4});if("DONE"!==await e.title()){const n=await e.evaluate(()=>{const e=document.getElementById("box");return e?e.innerText:""});throw new Error(`mermaid render failed: ${n}`)}return await e.evaluate(()=>document.fonts.ready),{browser:t,page:e}}catch(e){throw await t.close(),e}}var ft=Object.freeze({__proto__:null,genPng:async function(e,n={}){const{browser:t,page:r}=await pt(dt(e));try{return await r.locator("#box svg").screenshot()}finally{await t.close()}},genSvg:async function(e,n={}){const{browser:t,page:r}=await pt(dt(e));try{return(await r.evaluate(()=>{const e=document.querySelector("#box svg"),n=e.viewBox.baseVal;return e.style.maxWidth="none",e.setAttribute("width",Math.ceil(n.width)),e.setAttribute("height",Math.ceil(n.height)),document.getElementById("box").innerHTML})).replace(/&nbsp;/g,"&#160;").trim()}finally{await t.close()}}});function ut(e){const n=e.dir||"TB",t="TB"===n?"LR":"TB",r=new Set,o={},a=e=>{if(o[e])return o[e];const n="n_"+String(e).replace(/[^A-Za-z0-9_]/g,"_");let t=n,a=2;for(;r.has(t);)t=n+"_"+a++;return r.add(t),o[e]=t,t},i={},s={};for(const n of e.nodes)s[n.id]=n,n.group&&(i[n.group]||(i[n.group]=[]),i[n.group].push(n.id));const l=e.nodes.filter(e=>!e.group),c=[];function d(e,n){const r=" ".repeat(n);if(nt(e.cls)){c.push(`${r}subgraph ${a(e.id)}["${e.label}"]`),c.push(`${r} direction ${t}`);const o=i[e.id]||[];for(const e of o)d(s[e],n+1);c.push(`${r}end`)}else{const n="diamond"===e.cls?`{${e.label}}`:`["${e.label}"]`;c.push(`${r}${a(e.id)}${n}`)}}c.push(`flowchart ${n}`);for(const e of l)d(e,1);for(const n of e.edges){const e="dashed"===n.kind?"-.->":"--\x3e",t=n.label?`|"${n.label}"|`:"";c.push(` ${a(n.from)} ${e}${t} ${a(n.to)}`)}const p=new Set(e.nodes.map(e=>e.cls)),f={};for(const n of e.nodes)f[n.cls]||(f[n.cls]=[]),f[n.cls].push(n.id);for(const e of p){const n=Kn[e]||Kn.blue,t=n.fill,r=n.stroke,o=n.text,a=(nt(e),"1.5px");c.push(` classDef ${e} fill:${t},stroke:${r},stroke-width:${a},color:${o};`)}for(const[e,n]of Object.entries(f))c.push(` class ${n.map(a).join(",")} ${e};`);return c.join("\n")}const ht=`<!doctype html><html lang="zh-Hant"><head><meta charset="utf-8">\n<style>\n /* 修正 A: 邊標籤光暈 — paint-order + stroke 白邊讓線段透出、文字仍清晰 */\n .edgeLabel text, .edgeLabel tspan {\n paint-order: stroke fill;\n stroke: #ffffff;\n stroke-width: 3.5px;\n stroke-linejoin: round;\n }\n /* 修正 A: 邊標籤背景框透明(移除不透明白底, 否則會遮斷轉折線) */\n .edgeLabel rect, .edgeLabel .label-container {\n fill: transparent !important;\n fill-opacity: 0 !important;\n stroke: none !important;\n }\n /* 修正 B: 節點標籤禁止英數識別字被攔腰斷行 */\n .node foreignObject div, .node foreignObject span,\n .label foreignObject div, .label foreignObject span {\n word-break: keep-all;\n overflow-wrap: break-word;\n white-space: normal;\n }\n /* 修正 C: 字型全覆蓋 — subgraph/容器標題(cluster-label)強制 Microsoft JhengHei */\n .cluster-label foreignObject div, .cluster-label foreignObject span,\n .cluster-label text, .cluster-label tspan,\n .nodeLabel, .nodeLabel span, text, tspan {\n font-family: 'Microsoft JhengHei','微軟正黑體',sans-serif !important;\n }\n</style>\n</head>\n<body style="margin:0;background:#fff;font-family:'微軟正黑體','Microsoft JhengHei',sans-serif">\n<div id="box" style="display:inline-block;padding:24px"></div>\n<script type="module">\ntry {\n const mermaid = (await import('${ot("mermaid11-esm")}')).default\n const elk = (await import('${ot("layout-elk-esm")}')).default\n mermaid.registerLayoutLoaders(elk)\n mermaid.initialize({ startOnLoad:false, theme:'base', flowchart:{ useMaxWidth:false }, themeVariables:{ fontFamily:"'微軟正黑體','Microsoft JhengHei',sans-serif", fontSize:'17px', primaryColor:'#eef4fb', primaryBorderColor:'#3f6fb0', lineColor:'#44505a', primaryTextColor:'#1c2b36' } })\n window.renderFig = async (code, idx) => {\n try {\n const { svg } = await mermaid.render('g'+idx, code)\n const box = document.getElementById('box'); box.innerHTML = svg\n const svgEl = box.querySelector('svg')\n try {\n const vb = svgEl.viewBox && svgEl.viewBox.baseVal\n let w = vb && vb.width, h = vb && vb.height\n if (!w || !h) { const bb = svgEl.getBBox(); w = bb.width; h = bb.height }\n svgEl.style.maxWidth = 'none'\n svgEl.setAttribute('width', Math.ceil(w))\n svgEl.setAttribute('height', Math.ceil(h))\n } catch(e){}\n const DK='#333333', W='3px'\n box.querySelectorAll('.edgePaths path, .edgePath path, path.flowchart-link, g.edgePaths path').forEach(p=>{p.style.setProperty('stroke',DK,'important');p.style.setProperty('stroke-width',W,'important')})\n box.querySelectorAll('marker path, .marker, .marker path').forEach(m=>{m.style.setProperty('fill',DK,'important');m.style.setProperty('stroke',DK,'important')})\n box.querySelectorAll('.node rect, .node polygon, .node circle, .node path').forEach(n=>n.style.setProperty('stroke-width','2px','important'))\n box.querySelectorAll('.cluster rect, .cluster polygon, .cluster path').forEach(c=>{c.style.setProperty('stroke','#777','important');c.style.setProperty('stroke-width','1.5px','important')})\n // 修正 A (JS 層): 邊標籤背景矩形透明 + 文字白色光暈(JS inline style 強制覆蓋 mermaid 注入的樣式)\n box.querySelectorAll('.edgeLabel rect, .edgeLabel .label-container').forEach(r=>{\n r.style.setProperty('fill','transparent','important')\n r.style.setProperty('fill-opacity','0','important')\n r.style.setProperty('stroke','none','important')\n })\n box.querySelectorAll('.edgeLabel text, .edgeLabel tspan').forEach(t=>{\n t.style.setProperty('paint-order','stroke fill','important')\n t.style.setProperty('stroke','#ffffff','important')\n t.style.setProperty('stroke-width','3.5px','important')\n t.style.setProperty('stroke-linejoin','round','important')\n })\n // 修正 C (JS 層): 字型全覆蓋 — subgraph/容器標題 cluster-label + 所有 SVG text/tspan\n const FF = "'Microsoft JhengHei','微軟正黑體',sans-serif"\n box.querySelectorAll('.cluster-label foreignObject div, .cluster-label foreignObject span').forEach(el=>{\n el.style.setProperty('font-family',FF,'important')\n })\n box.querySelectorAll('text, tspan').forEach(el=>{\n el.style.setProperty('font-family',FF,'important')\n })\n if (document.fonts && document.fonts.ready) await document.fonts.ready\n return { ok:true }\n } catch(e){ return { ok:false, err:(e&&(e.message||e.stack))||String(e) } }\n }\n window.__ready = true\n} catch(e){ window.__setupErr = (e&&(e.message||e.stack))||String(e) }\n<\/script></body></html>`;async function gt(n){const t="---\nconfig:\n layout: elk\n---\n"+ut(n),r=await e.chromium.launch();try{const e=await r.newPage({deviceScaleFactor:2});await e.setContent(ht,{waitUntil:"load"}),await e.waitForFunction(()=>window.__ready||window.__setupErr,{timeout:9e4}).catch(()=>{});const n=await e.evaluate(()=>window.__setupErr);if(n)throw new Error(n);const o=await e.evaluate(([e,n])=>window.renderFig(e,n),[t,0]);if(!o.ok)throw new Error(o.err);return await e.waitForTimeout(250),{browser:r,page:e}}catch(e){throw await r.close(),e}}var yt=Object.freeze({__proto__:null,genPng:async function(e,n={}){const{browser:t,page:r}=await gt(e);try{return await r.locator("#box svg").screenshot()}finally{await t.close()}},genSvg:async function(e,n={}){const{browser:t,page:r}=await gt(e);try{return(await r.evaluate(()=>{const e=document.querySelector("#box svg"),n=Array.from(document.querySelectorAll("head style")).map(e=>e.textContent).join("\n"),t=document.createElementNS("http://www.w3.org/2000/svg","style");t.textContent=n,e.insertBefore(t,e.firstChild);const r=document.getElementById("box").innerHTML;return t.remove(),r})).replace(/&nbsp;/g,"&#160;").trim()}finally{await t.close()}},translate:ut});function bt(e){return String(e).toLowerCase().replace(/[^a-z]/g,e=>"2"===e?"b":"g")}function mt(e){const n="LR"===e.dir?"right":"down",t=function(){const e=[];for(const[n,t]of Object.entries(Kn))"diamond"!==n&&e.push(`#.${bt(n)}: fill=${t.fill} stroke=${t.stroke}`);return e.join("\n")}(),r={};for(const n of e.nodes)r[n.id]=n;const o=e=>String(e).replace(/\n/g,";"),a=e=>String(e).replace(/\n/g," "),i=e=>"diamond"===e.cls?a(e.label):o(e.label);return`#direction: ${n}\n#font: Microsoft JhengHei\n${t}\n${e.nodes.map(e=>{return`[${n=e.cls,"diamond"===n?"<choice>":`<${bt(n)}>`}${i(e)}]`;var n}).join("\n")}\n${e.nodes.filter(e=>e.group&&r[e.group]).map(e=>`[${i(r[e.group])}] --\x3e [${i(e)}]`).join("\n")}\n${e.edges.map(e=>{const n=r[e.from]?i(r[e.from]):o(e.from),t=r[e.to]?i(r[e.to]):o(e.to),s="dashed"===e.kind?"--\x3e":"->";return`[${n}]${e.label?` ${a(e.label)} `:" "}${s} [${t}]`}).join("\n")}`}const xt=`<!doctype html><html lang="zh-Hant"><head><meta charset="utf-8">\n${at("graphre")}\n${at("nomnoml")}\n</head><body style="margin:0;background:#fff;font-family:'Microsoft JhengHei',sans-serif">\n<div id="box" style="display:inline-block;padding:20px"></div>\n<script>\nvar HALO_COLOR = ${JSON.stringify(Qn.haloColor)}, HALO_W = ${Qn.haloWidth}\nfunction applyEdgeLabelHalo(box){\n // 邊標籤 = <text> 無 data-name(節點內標籤皆帶 data-name)。\n var svg = box.querySelector('svg')\n var ts = Array.prototype.slice.call(box.querySelectorAll('svg text'))\n ts.forEach(function(t){\n if (t.hasAttribute('data-name')) return // 節點標籤, 不處理\n // (A) 白描邊 + paint-order:stroke 使描邊在字下方\n t.setAttribute('paint-order','stroke')\n t.setAttribute('stroke', HALO_COLOR)\n t.setAttribute('stroke-width', HALO_W)\n t.setAttribute('stroke-linejoin','round')\n // (B) z-order: 移至 svg 尾端 → 最後繪製, 永不被節點 rect/容器填色蓋住\n svg.appendChild(t)\n })\n}\nwindow.renderAndDetect = function(src){\n try {\n var svg = window.nomnoml.renderSvg(src)\n var box = document.getElementById('box'); box.innerHTML = svg\n applyEdgeLabelHalo(box)\n var texts = Array.prototype.slice.call(box.querySelectorAll('svg text'))\n var R = texts.map(function(t){ return t.getBoundingClientRect() })\n // 只計「節點×節點」文字重疊驅動間距(原則2/3): 節點標籤帶 data-name; 邊標籤無 data-name。\n // 凡牽涉邊標籤之重疊(邊標籤×節點 / 邊標籤×邊標籤)一律「不算碰撞」——其可讀性已由\n // halo(白描邊)+ z-order(提至最上層)保證, 不該用「加大間距」處理(否則 進度回報×Worker Thread\n // 這類邊標籤壓節點之 bbox 重疊會把圖撐爆)。唯有節點本體互疊才是 z-order 無法解、須間距處理者。\n var isNode = texts.map(function(t){ return t.hasAttribute('data-name') })\n var col = 0, details = []\n for (var i=0;i<R.length;i++) for (var j=i+1;j<R.length;j++){\n if (!(isNode[i] && isNode[j])) continue // 任一方為邊標籤 → 交給 halo+z-order, 不驅動間距\n var a=R[i], b=R[j]\n var ix = Math.min(a.right,b.right)-Math.max(a.left,b.left)\n var iy = Math.min(a.bottom,b.bottom)-Math.max(a.top,b.top)\n if (ix>2 && iy>2){ col++; if(details.length<3) details.push(((texts[i].textContent||'').trim().slice(0,6))+'×'+((texts[j].textContent||'').trim().slice(0,6))) }\n }\n return { ok:true, collisions: col, details: details }\n } catch(e){ return { ok:false, err:(e&&(e.message||e.stack))||String(e) } }\n}\nwindow.__ready = true\n<\/script></body></html>`;async function wt(n){const t=await e.chromium.launch();try{const e=await t.newPage({deviceScaleFactor:2});await e.setContent(xt,{waitUntil:"load"}),await e.waitForFunction(()=>window.__ready,{timeout:6e4}).catch(()=>{});const r=mt(n),o=await async function(e,n){const t=[40,60,85];let r=null;for(const o of t){const t=await e.evaluate(e=>window.renderAndDetect(e),"#spacing: "+o+"\n"+n);if(!t.ok)return{ok:!1,err:String(t.err).split("\n")[0]};if((null===r||t.collisions<r.collisions)&&(r={spacing:o,collisions:t.collisions,details:t.details}),0===t.collisions){r={spacing:o,collisions:0,details:[]};break}}return{ok:!0,...r}}(e,r);if(!o.ok)throw new Error(o.err);return await e.evaluate(e=>window.renderAndDetect(e),"#spacing: "+o.spacing+"\n"+r),await e.evaluate(()=>document.fonts&&document.fonts.ready),await e.waitForTimeout(180),{browser:t,page:e}}catch(e){throw await t.close(),e}}var vt=Object.freeze({__proto__:null,genPng:async function(e,n={}){const{browser:t,page:r}=await wt(e);try{return await r.locator("#box svg").screenshot()}finally{await t.close()}},genSvg:async function(e,n={}){const{browser:t,page:r}=await wt(e);try{return(await r.evaluate(()=>document.getElementById("box").innerHTML)).replace(/&nbsp;/g,"&#160;").trim()}finally{await t.close()}}});function kt(e){const n=[];for(const t of e.nodes)n.push({data:{id:t.id,label:t.label,parent:t.group},classes:t.cls});for(const t of e.edges)n.push({data:{source:t.from,target:t.to,label:t.label},classes:"dashed"===t.kind?"dashed":void 0});return n}const _t=`<!doctype html><html lang="zh-Hant"><head><meta charset="utf-8">\n${at("cytoscape")}\n${at("dagre")}\n${at("cytoscape-dagre")}\n${at("cytoscape-svg")}\n</head><body style="margin:0;background:#fff">\n<div id="cy" style="width:1400px;height:1400px;background:#fff"></div>\n<script>\nif (window.cytoscapeDagre) { try { cytoscape.use(window.cytoscapeDagre) } catch(e){} }\nif (window.cytoscapeSvg) { try { cytoscape.use(window.cytoscapeSvg) } catch(e){} }\n// mode: 'png'(預設) 或 'svg' — 版面/元素/樣式建置流程共用, 僅末端輸出格式不同(genPng 行為不變)\nwindow.renderFig = function(els, styleJson, rankDir, mode){ return new Promise(function(resolve){\n try {\n var cy = cytoscape({ container: document.getElementById('cy'), elements: els, style: JSON.parse(styleJson) })\n els.forEach(function(x){ if (x.classes && x.data && x.data.id) cy.getElementById(x.data.id).addClass(x.classes) })\n var l = cy.layout({ name:'dagre', rankDir:rankDir, nodeSep:42, rankSep:55, edgeSep:18 })\n l.promiseOn('layoutstop').then(function(){\n setTimeout(function(){\n try {\n var bb=cy.elements().boundingBox()\n if (mode === 'svg') { window.__svg = cy.svg({ full:true, scale:2, bg:'#ffffff' }) }\n else { window.__png = cy.png({ full:true, scale:2, bg:'#ffffff', output:'base64' }) }\n resolve({ ok:true, w:Math.round(bb.w), h:Math.round(bb.h) })\n }\n catch(e){ resolve({ ok:false, err:String(e.message||e) }) }\n }, 250)\n })\n l.run()\n } catch(e){ resolve({ ok:false, err:String(e.message||e) }) }\n})}\nwindow.__ready = true\n<\/script></body></html>`;async function Et(n,t){const r=await e.chromium.launch();try{const e=await r.newPage({deviceScaleFactor:1});await e.setContent(_t,{waitUntil:"load"}),await e.waitForFunction(()=>window.__ready,{timeout:6e4}).catch(()=>{}),await e.evaluate(()=>document.fonts&&document.fonts.ready);const o=await e.evaluate(([e,n,t,r])=>window.renderFig(e,n,t,r),[kt(n),'[\n { "selector":"node", "style":{ "shape":"round-rectangle","background-color":"#eef4fb","border-color":"#3f6fb0","border-width":1.8,"label":"data(label)","text-valign":"center","text-halign":"center","font-family":"Microsoft JhengHei, sans-serif","font-size":14,"color":"#1c2b36","width":"label","height":"label","padding":"10px","text-wrap":"wrap","text-max-width":"360px" } },\n { "selector":"node.green", "style":{ "background-color":"#eaf4ef","border-color":"#348a5c" } },\n { "selector":"node.orange", "style":{ "background-color":"#fcf0e2","border-color":"#c46e1a" } },\n { "selector":"node.purple", "style":{ "background-color":"#f4f0fa","border-color":"#8163ad" } },\n { "selector":"node.red", "style":{ "background-color":"#fbecea","border-color":"#c0392b" } },\n { "selector":"node.done", "style":{ "background-color":"#eaf4ef","border-color":"#256046","color":"#256046" } },\n { "selector":"node.diamond", "style":{ "shape":"diamond","background-color":"#fcf0e2","border-color":"#c46e1a","padding":"34px","text-max-width":"120px" } },\n { "selector":":parent", "style":{ "text-valign":"top","text-halign":"center","font-family":"Microsoft JhengHei, sans-serif","font-weight":"bold","padding":"16px","background-opacity":0.35,"border-width":2,"z-compound-depth":"bottom" } },\n { "selector":"node.blueG", "style":{ "background-color":"#f5f9fe","border-color":"#3f6fb0","color":"#3f6fb0" } },\n { "selector":"node.blueG2", "style":{ "background-color":"#eef4fb","border-color":"#3f6fb0","color":"#3f6fb0" } },\n { "selector":"node.greenG", "style":{ "background-color":"#f0f7f3","border-color":"#348a5c","color":"#348a5c" } },\n { "selector":"node.greenG2", "style":{ "background-color":"#eaf4ef","border-color":"#348a5c","color":"#348a5c" } },\n { "selector":"node.orangeG", "style":{ "background-color":"#fdf6ec","border-color":"#c46e1a","color":"#c46e1a" } },\n { "selector":"node.purpleG", "style":{ "background-color":"#faf7fd","border-color":"#8163ad","color":"#8163ad" } },\n { "selector":"edge", "style":{ "width":2.2,"line-color":"#44505a","target-arrow-color":"#44505a","target-arrow-shape":"triangle","curve-style":"bezier","label":"data(label)","font-family":"Microsoft JhengHei, sans-serif","font-size":12.5,"color":"#4a5560","text-background-opacity":0,"text-outline-color":"#ffffff","text-outline-width":3,"text-margin-y":-2,"text-wrap":"wrap" } },\n { "selector":"edge.dashed", "style":{ "line-style":"dashed" } }\n]',n.dir,t]);if(!o.ok)throw new Error(o.err);return await e.evaluate(e=>"svg"===e?window.__svg:window.__png,t)}finally{await r.close()}}var St=Object.freeze({__proto__:null,genPng:async function(e,n={}){const t=await Et(e,"png");return Buffer.from(t,"base64")},genSvg:async function(e,n={}){let t=await Et(e,"svg");return t=t.replace(/&nbsp;/g,"&#160;"),/\sxmlns=/.test(t)||(t=t.replace("<svg",'<svg xmlns="http://www.w3.org/2000/svg"')),/font-family/.test(t)||(t=t.replace(">","><style>text{font-family:'Microsoft JhengHei',sans-serif}</style>")),t}});const Ot='"Microsoft JhengHei","Microsoft YaHei",sans-serif',jt=`<style>text,.text-bold,.text-italic,.text-underline,tspan{font-family:${Ot} !important;}.text-bold{font-weight:bold !important;}</style>`;const It="<style>.text-italic{paint-order:stroke;stroke:#ffffff;stroke-width:3.5px;}</style>";const At=e=>String(e).replace(/\$/g,"\\$").replace(/\n/g,"\\n"),Mt={TB:"down",LR:"right",BT:"up",RL:"left"};function Lt(e,n){const t=tt(e),r=[];return r.push(` style.fill: "${t.fill}"`),r.push(` style.stroke: "${t.stroke}"`),t.text&&(n||"done"===e)&&r.push(` style.font-color: "${t.text}"`),n&&r.push(" style.bold: true"),"diamond"===t.shape&&r.push(" shape: diamond"),r}function Pt(e,n,t){const r=[];let o=e;for(;o;)r.unshift(o),o=n[o];return r.map(t).join(".")}function Bt(e,n,t,r,o,a){const i=" ".repeat(o),s=[];if(nt(e.cls)){s.push(`${i}${a(e.id)}: "${At(e.label)}" {`);const l=n[e.id]||[];l.length>0&&s.push(`${i} direction: ${Mt[r]||"down"}`);for(const n of Lt(e.cls,!0))s.push(`${i}${n}`);for(const e of l)s.push(Bt(e,n,t,r,o+2,a));s.push(`${i}}`)}else{s.push(`${i}${a(e.id)}: "${At(e.label)}" {`);for(const n of Lt(e.cls,!1))s.push(`${i}${n}`);s.push(`${i}}`)}return s.join("\n")}const Ft={layout:"dagre",pad:40,themeID:0};async function Dt(e){const{D2:n}=await import("@terrastruct/d2"),t=new n;try{const n=function(e){const n=new Set,t={},r=e=>{if(t[e])return t[e];const r="n_"+String(e).replace(/[^A-Za-z0-9_]/g,"_");let o=r,a=2;for(;n.has(o);)o=r+"_"+a++;return n.add(o),t[e]=o,o},o={},a={};for(const n of e.nodes)n.group&&(o[n.id]=n.group,a[n.group]||(a[n.group]=[]),a[n.group].push(n));const i=e.nodes.filter(e=>!e.group),s=[];s.push(`direction: ${Mt[e.dir]||"down"}`);for(const n of i)s.push(Bt(n,a,o,e.dir,0,r));for(const n of e.edges){let e=`${Pt(n.from,o,r)} -> ${Pt(n.to,o,r)}`;n.label&&(e+=`: "${At(n.label)}"`),"dashed"===n.kind&&(e+=" { style.stroke-dash: 4 }"),s.push(e)}return s.join("\n\n")}(e),r=await t.compile({fs:{index:n},options:Ft});let o=await t.render(r.diagram,r.renderOptions);return o=function(e){const n=(e=e.replace(/<mask\b([^>]*)>([\s\S]*?)<\/mask>/g,(e,n,t)=>`<mask${n}>${t.replace(/<rect[^>]*fill="black"[^>]*><\/rect>/g,"")}</mask>`)).indexOf("<style");if(n>=0)return e.slice(0,n)+It+e.slice(n);const t=e.indexOf(">");return e.slice(0,t+1)+It+e.slice(t+1)}(o),o=function(e){const n=(e=(e=e.replace(/font-family\s*:\s*[^;}<]+/gi,`font-family:${Ot}`)).replace(/font-family\s*=\s*"[^"]*"/gi,`font-family="${Ot}"`)).indexOf("<style");if(n>=0)return e.slice(0,n)+jt+e.slice(n);const t=e.indexOf(">");return e.slice(0,t+1)+jt+e.slice(t+1)}(o),o}finally{await t.ready.catch(()=>{}),t.worker&&t.worker.terminate&&await t.worker.terminate()}}var Nt=Object.freeze({__proto__:null,genPng:async function(e,n={}){const{default:t}=await import("sharp"),r=await Dt(e),o=r.match(/viewBox="0 0 (\d+(?:\.\d+)?) (\d+(?:\.\d+)?)"/),a=o?parseFloat(o[1]):1e3,i=96*Math.min(2.4,Math.max(1.4,1700/a));return await t(Buffer.from(r),{density:i}).png().toBuffer()},genSvg:async function(e,n={}){return await Dt(e)}});function $t(e){const n=String(e).split("\n");return[Math.round(Math.max(...n.map(e=>[...e].reduce((e,n)=>e+(/[一-鿿()/]/.test(n)?15.5:8.5),0)))+34),Math.round(26*n.length+20)]}function Gt(e){const n=[],t=[],r=new Set(e.nodes.filter(e=>nt(e.cls)).map(e=>e.id)),o={};for(const n of e.nodes)n.group&&(o[n.group]=o[n.group]||[]).push(n);const a=e=>{for(const n of o[e]||[]){if(!nt(n.cls))return n.id;const e=a(n.id);if(e)return e}return null},i=e=>r.has(e)&&a(e)||e;for(const r of e.nodes){const e=tt(r.cls);if(nt(r.cls)){t.push({id:r.id,combo:r.group,type:"rect",style:{zIndex:-10,labelText:r.label,labelPlacement:"top",labelFontFamily:et,labelFontSize:15,labelFontWeight:700,labelFill:e.text,fill:e.fill,fillOpacity:.3,stroke:e.stroke,lineWidth:2}});continue}const o="diamond"===e.shape;let[a,i]=$t(r.label);o&&(a+=60,i+=50),n.push({id:r.id,combo:r.group,type:o?"diamond":"rect",style:{size:[a,i],radius:o?0:6,lineWidth:1.8,fill:e.fill,stroke:e.stroke,labelText:r.label,labelPlacement:"center",labelFill:e.text,labelFontSize:14,labelFontFamily:et}})}let s=0;return{nodes:n,combos:t,edges:e.edges.map(e=>{s++;const n="dashed"===e.kind;return{id:"e"+s,source:i(e.from),target:i(e.to),style:{zIndex:100,lineWidth:2.2,endArrow:!0,stroke:n?tt("orange").stroke:Qn.line,lineDash:n?[7,5]:void 0,labelText:e.label||"",labelFontFamily:et,labelFontSize:12.5,labelFill:Qn.text,labelBackground:!1,labelStroke:Qn.haloColor,labelLineWidth:Qn.haloWidth,labelPaintOrder:"stroke",labelAutoRotate:!1}}})}}const Tt=`<!doctype html><html lang="zh-Hant"><head><meta charset="utf-8">\n${at("g6")}\n</head><body style="margin:0;background:#fff">\n<div id="g6" style="background:#fff"></div>\n<script>\nwindow.renderFig = async function(g, rankdir, nodesep, ranksep, canvas, font, comboZ, edgeZ){\n try {\n const el = document.getElementById('g6')\n el.style.width = canvas + 'px'; el.style.height = canvas + 'px'\n if (window.__graph) { try { window.__graph.destroy() } catch(e){} }\n const graph = new G6.Graph({\n container: el, width: canvas, height: canvas, autoFit:'view',\n data: { nodes: g.nodes, edges: g.edges, combos: g.combos },\n layout: { type:'antv-dagre', rankdir: rankdir, nodesep: nodesep, ranksep: ranksep, sortByCombo: (g.combos && g.combos.length>0) },\n // 原則2:combo 墊底、edge 最上層之 graph 預設(各元素 style 已設, 此處兜底)\n combo: { type:'rect', style:{ zIndex: comboZ, padding:[30,18,18,18], labelPlacement:'top', labelFontFamily: font } },\n edge: { type:'polyline', style:{ zIndex: edgeZ, lineWidth:2.2, endArrow:true, labelFontFamily: font } },\n // 原則1:node/edge/combo 三層 graph 預設字型統一(JhengHei), 全覆蓋兜底\n node: { style:{ labelFontFamily: font } },\n })\n window.__graph = graph\n await graph.render()\n await new Promise(r=>setTimeout(r,300))\n return { ok:true }\n } catch(e){ return { ok:false, err:String(e&&(e.stack||e.message)||e).slice(0,400) } }\n}\n// 以目前已渲染之圖截圖\nwindow.snap = async function(){\n try {\n await new Promise(r=>setTimeout(r,200))\n const png = await window.__graph.toDataURL({ mode:'overall' })\n return { ok:true, png }\n } catch(e){ return { ok:false, err:String(e&&(e.stack||e.message)||e).slice(0,400) } }\n}\nwindow.__ready=true\n<\/script></body></html>`;const Ct=`<!doctype html><html lang="zh-Hant"><head><meta charset="utf-8">\n${at("g-lite")}\n${at("g-svg")}\n${at("g6")}\n</head><body style="margin:0;background:#fff">\n<div id="g6" style="background:#fff"></div>\n<script>\nwindow.renderFigSvg = async function(g, rankdir, nodesep, ranksep, canvas, font, comboZ, edgeZ){\n try {\n const el = document.getElementById('g6')\n el.style.width = canvas + 'px'; el.style.height = canvas + 'px'\n if (window.__graph) { try { window.__graph.destroy() } catch(e){} }\n const graph = new G6.Graph({\n container: el, width: canvas, height: canvas, autoFit:'view',\n renderer: () => new window.G.SVG.Renderer(),\n data: { nodes: g.nodes, edges: g.edges, combos: g.combos },\n layout: { type:'antv-dagre', rankdir: rankdir, nodesep: nodesep, ranksep: ranksep, sortByCombo: (g.combos && g.combos.length>0) },\n combo: { type:'rect', style:{ zIndex: comboZ, padding:[30,18,18,18], labelPlacement:'top', labelFontFamily: font } },\n edge: { type:'polyline', style:{ zIndex: edgeZ, lineWidth:2.2, endArrow:true, labelFontFamily: font } },\n node: { style:{ labelFontFamily: font } },\n })\n window.__graph = graph\n await graph.render()\n await new Promise(r=>setTimeout(r,300))\n return { ok:true }\n } catch(e){ return { ok:false, err:String(e&&(e.stack||e.message)||e).slice(0,400) } }\n}\n// 取 main layer 之 SVG DOM(SVG renderer 下唯一承載節點/邊/combo 之 layer, background/label/transient 三層無內容),\n// 以 canvas.getBounds() 對齊 toDataURL({mode:'overall'}) 之緊裁切邊界改寫 viewBox/width/height, 補白底矩形。\nwindow.snapSvg = async function(){\n try {\n await new Promise(r=>setTimeout(r,200))\n const canvas = window.__graph.getCanvas()\n const bounds = canvas.getBounds()\n const minX = bounds.min[0], minY = bounds.min[1]\n const w = bounds.max[0] - minX, h = bounds.max[1] - minY\n const mainEl = canvas.getLayer('main').getContextService().getDomElement()\n const svg = mainEl.cloneNode(true)\n // canvas.getBounds() 回傳世界座標(排版原始座標, 未經 camera 縮放平移); 但複製之 DOM 內容掛在\n // #g-svg-camera 群組下, 帶有互動視圖之 camera transform(autoFit:'view' 縮放平移)。\n // 兩者座標系不同, 混用會裁切錯位, 故先歸零 camera transform, 使內容回到與 bounds 相同之世界座標。\n const cameraEl = svg.querySelector('#g-svg-camera')\n if (cameraEl) cameraEl.removeAttribute('transform')\n svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg')\n svg.setAttribute('viewBox', minX + ' ' + minY + ' ' + w + ' ' + h)\n svg.setAttribute('width', String(Math.ceil(w)))\n svg.setAttribute('height', String(Math.ceil(h)))\n svg.removeAttribute('tabindex')\n svg.style.background = ''\n svg.style.pointerEvents = ''\n svg.style.gridArea = ''\n svg.style.outline = ''\n const bgRect = document.createElementNS('http://www.w3.org/2000/svg', 'rect')\n bgRect.setAttribute('x', String(minX)); bgRect.setAttribute('y', String(minY))\n bgRect.setAttribute('width', String(w)); bgRect.setAttribute('height', String(h))\n bgRect.setAttribute('fill', '#fff')\n svg.insertBefore(bgRect, svg.firstChild)\n const out = svg.outerHTML.replace(/&nbsp;/g, '&#160;')\n return { ok:true, svg: out }\n } catch(e){ return { ok:false, err:String(e&&(e.stack||e.message)||e).slice(0,400) } }\n}\nwindow.__readySvg=true\n<\/script></body></html>`;var Rt=Object.freeze({__proto__:null,genPng:async function(n,t={}){const r=await e.chromium.launch();try{const e=await r.newPage({deviceScaleFactor:2}),t=[];e.on("pageerror",e=>t.push(e.message)),e.on("console",e=>{"error"===e.type()&&t.push("c:"+e.text())}),await e.setContent(Tt,{waitUntil:"load"}),await e.waitForFunction(()=>window.__ready,{timeout:6e4}).catch(()=>{}),await e.evaluate(()=>document.fonts&&document.fonts.ready);const o=Gt(n),a=await e.evaluate(([e,n,t,r,o,a,i,s])=>window.renderFig(e,n,t,r,o,a,i,s),[o,n.dir,28,60,2200,et,-10,100]);if(!a.ok)throw new Error("renderFig failed :: "+String(a.err).split("\n")[0]);const i=await e.evaluate(()=>window.snap());if(!i.ok)throw new Error("snap failed :: "+i.err);return Buffer.from(i.png.split(",")[1],"base64")}finally{await r.close()}},genSvg:async function(n,t={}){const r=await e.chromium.launch();try{const e=await r.newPage(),t=[];e.on("pageerror",e=>t.push(e.message)),e.on("console",e=>{"error"===e.type()&&t.push("c:"+e.text())}),await e.setContent(Ct,{waitUntil:"load"}),await e.waitForFunction(()=>window.__readySvg,{timeout:6e4}).catch(()=>{}),await e.evaluate(()=>document.fonts&&document.fonts.ready);const o=Gt(n),a=await e.evaluate(([e,n,t,r,o,a,i,s])=>window.renderFigSvg(e,n,t,r,o,a,i,s),[o,n.dir,28,60,2200,et,-10,100]);if(!a.ok)throw new Error("renderFigSvg failed :: "+String(a.err).split("\n")[0]);const i=await e.evaluate(()=>window.snapSvg());if(!i.ok)throw new Error("snapSvg failed :: "+i.err);return i.svg}finally{await r.close()}}});const Ht=Object.fromEntries(Object.entries(Kn).map(([e,n])=>{const t={fill:n.fill,stroke:n.stroke,color:n.text};return"diamond"===n.shape&&(t.shape="diamond"),n.group&&(t.group=!0),[e,t]}));const zt=`<!doctype html><html lang="zh-Hant"><head><meta charset="utf-8">\n${at("joint")}\n${at("dagre")}\n</head><body style="margin:0;background:#fff">\n<div id="paper" style="background:#fff"></div>\n<script>\nvar CLS = ${JSON.stringify(Ht)}\nvar FONT = 'Microsoft JhengHei, "Microsoft JhengHei", sans-serif'\nvar FS = 14, FSG = 14.5, FSL = 12.5\n\n// 量測文字寬度(canvas)\nvar __cv = document.createElement('canvas'); var __cx = __cv.getContext('2d')\nfunction measure(text, fs){ __cx.font = fs + 'px ' + FONT; return __cx.measureText(text).width }\n\n// 依字寬自動換行(回傳 lines 陣列 + 最大寬)\n// label 內 \\n 為強制換行: 先依 \\n 切段、各段再逐字寬斷行——渲染端以 join('\\n') 交給 JointJS,\n// 若量測不切 \\n 會令實際行數多於估算行數, 框高低估、文字上下溢出框\nfunction wrapText(text, fs, maxW){\n var lines = []\n String(text).split('\\n').forEach(function(seg){\n var cur = ''\n for (var i = 0; i < seg.length; i++){\n var ch = seg[i]\n var test = cur + ch\n if (measure(test, fs) > maxW && cur !== ''){ lines.push(cur); cur = ch }\n else cur = test\n }\n lines.push(cur) // 空段亦保留(連續 \\n 表空行)\n })\n if (lines.length === 1 && lines[0] === '') lines = [] // 空字串輸入維持原行為(零行)\n var w = 0; lines.forEach(function(l){ w = Math.max(w, measure(l, fs)) })\n return { lines: lines, w: w }\n}\n\nfunction renderFig(els, rankDir, sep){\n return new Promise(function(resolve){\n try {\n var NODESEP = (sep && sep.nodesep) || 32 // 同層節點間距(緊湊固定基準)\n var RANKSEP = (sep && sep.ranksep) || 48 // 層間距(緊湊固定基準)\n var nodes = els.filter(function(x){ return x.id !== undefined })\n var edges = els.filter(function(x){ return x.source !== undefined })\n var nodeById = {}; nodes.forEach(function(nn){ nodeById[nn.id] = nn })\n\n // 分類:群組(parent)與一般節點\n var groups = nodes.filter(function(nn){ return CLS[nn.cls] && CLS[nn.cls].group })\n var groupIds = {}; groups.forEach(function(g){ groupIds[g.id] = true })\n var leaves = nodes.filter(function(nn){ return !groupIds[nn.id] })\n\n // 量測 leaf 節點尺寸(含換行)\n var maxLeafW = 480\n var sizing = {}\n leaves.forEach(function(nn){\n var st = CLS[nn.cls] || CLS.blue\n var wr = wrapText(nn.label, FS, maxLeafW)\n var w, h\n if (st.shape === 'diamond'){\n var tw = wr.w + 24, th = wr.lines.length * (FS + 6) + 16\n w = Math.max(tw * 1.9, 130); h = Math.max(th * 1.9, 90)\n } else {\n w = Math.max(wr.w + 34, 90); h = wr.lines.length * (FS + 7) + 24\n }\n sizing[nn.id] = { w: Math.round(w), h: Math.round(h), lines: wr.lines }\n })\n\n // ── 兩段式 layout(避開 dagre compound 對「群組為邊端點」的 rank bug)──\n // 巢狀關係:childrenOf[parentId] = [子節點...];topLevel = 無 parent 者\n var GPAD = 16 // 群組內距(標題列高 titleH 依群組各自動態計算)\n var childrenOf = {}\n nodes.forEach(function(nn){ var p = nn.parent || '__root'; (childrenOf[p] = childrenOf[p] || []).push(nn) })\n var grpById = {}; groups.forEach(function(gp){ grpById[gp.id] = gp })\n var parentOf = {}; nodes.forEach(function(nn){ parentOf[nn.id] = nn.parent || '__root' })\n // 將節點往上抬升,直到其 parent 為指定容器(回傳該容器之直接子);無則回 null\n function liftTo(nodeId, containerId){\n var cur = nodeId\n while (cur != null){\n if (parentOf[cur] === containerId) return cur\n cur = (parentOf[cur] === '__root') ? null : parentOf[cur]\n }\n return null\n }\n\n // size[id] = {w,h}:leaf 直接取量測;group 需先排內部子圖才得知\n var size = {}\n leaves.forEach(function(nn){ size[nn.id] = { w: sizing[nn.id].w, h: sizing[nn.id].h } })\n // 群組內每個子節點相對群組內容左上角的偏移\n var innerPos = {} // innerPos[childId] = {x,y} 相對其 parent 內容區\n var groupContentW = {}, groupContentH = {}\n var titleH = {}, titleLines = {} // 每個群組標題列高度與換行\n\n // 對一個容器(root 或 group)內的直接子節點做 dagre 排版,回傳 {w,h} 內容尺寸\n function layoutContainer(containerId){\n var kids = (childrenOf[containerId] || [])\n // 先遞迴排子群組,取得其尺寸\n kids.forEach(function(k){ if (grpById[k.id]) { var s = layoutContainer(k.id); size[k.id] = { w: s.w, h: s.h } } })\n var dg = new dagre.graphlib.Graph()\n dg.setGraph({ rankdir: rankDir, nodesep: NODESEP, ranksep: RANKSEP, marginx: 0, marginy: 0, ranker: 'tight-tree' })\n dg.setDefaultEdgeLabel(function(){ return {} })\n var kidSet = {}; kids.forEach(function(k){ kidSet[k.id] = true; dg.setNode(k.id, { width: size[k.id].w, height: size[k.id].h }) })\n // 納入「抬升至本容器直接子」後兩端相異之邊(含跨群組邊),避免重複\n var seen = {}\n edges.forEach(function(ed){\n var s = liftTo(ed.source, containerId), t = liftTo(ed.target, containerId)\n if (s && t && s !== t && kidSet[s] && kidSet[t]){\n var key = s + '' + t\n if (!seen[key]){ seen[key] = true; dg.setEdge(s, t) }\n }\n })\n dagre.layout(dg)\n var minX = 1e9, minY = 1e9, maxX = -1e9, maxY = -1e9\n kids.forEach(function(k){\n var p = dg.node(k.id)\n minX = Math.min(minX, p.x - p.width/2); minY = Math.min(minY, p.y - p.height/2)\n maxX = Math.max(maxX, p.x + p.width/2); maxY = Math.max(maxY, p.y + p.height/2)\n })\n if (kids.length === 0){ minX = minY = maxX = maxY = 0 }\n kids.forEach(function(k){\n var p = dg.node(k.id)\n innerPos[k.id + '@' + containerId] = { x: p.x - p.width/2 - minX, y: p.y - p.height/2 - minY }\n })\n var cw = (maxX - minX), ch = (maxY - minY)\n if (containerId !== '__root'){\n cw += GPAD * 2\n // 標題依群組內寬換行,動態決定標題列高度\n var gp = grpById[containerId]\n var twr = wrapText(gp.label, FSG, cw - GPAD * 2)\n var th = twr.lines.length * (FSG + 4) + 12\n titleH[containerId] = th; titleLines[containerId] = twr.lines\n ch += GPAD * 2 + th // 群組外框含內距與標題列\n }\n groupContentW[containerId] = cw; groupContentH[containerId] = ch\n return { w: cw, h: ch }\n }\n layoutContainer('__root')\n\n // 由 root 內容起,遞迴攤平出每個節點的絕對 (x,y)=中心點\n var pos = {} // pos[id] = {x,y,w,h}(x,y 為中心)\n function place(containerId, originX, originY){\n // originX/originY = 此容器內容區左上角絕對座標\n var kids = (childrenOf[containerId] || [])\n kids.forEach(function(k){\n var off = innerPos[k.id + '@' + containerId]\n var w = size[k.id].w, h = size[k.id].h\n var topLeftX = originX + off.x, topLeftY = originY + off.y\n pos[k.id] = { x: topLeftX + w/2, y: topLeftY + h/2, w: w, h: h }\n if (grpById[k.id]){\n // 群組內容區左上角 = 群組左上角 + 內距 +(標題列)\n place(k.id, topLeftX + GPAD, topLeftY + GPAD + titleH[k.id])\n }\n })\n }\n place('__root', 0, 0)\n\n // 建 JointJS graph\n var graph = new joint.dia.Graph({}, { cellNamespace: joint.shapes })\n var cells = []\n var cellById = {}\n\n // 群組容器(先畫,置底);標題用獨立 TextBlock 置於頂部中央\n var groupTitleCells = []\n groups.forEach(function(gp){\n var p = pos[gp.id]\n var st = CLS[gp.cls]\n var x = p.x - p.w/2, y = p.y - p.h/2\n var r = new joint.shapes.standard.Rectangle()\n r.position(x, y); r.resize(p.w, p.h)\n r.attr({\n body: { fill: st.fill, stroke: st.stroke, strokeWidth: 2, rx: 8, ry: 8, fillOpacity: 0.5 },\n label: { text: '' }\n })\n cells.push(r); cellById[gp.id] = r\n // 標題(獨立元件,置頂中央)\n var th = titleH[gp.id]\n var tb = new joint.shapes.standard.TextBlock()\n tb.resize(p.w - 16, th)\n tb.position(x + 8, y + 4)\n tb.attr({\n body: { fill: 'none', stroke: 'none' },\n // TextBlock 以 foreignObject<div> 渲染: font-family 屬性對 HTML div 無效(會落回瀏覽器預設 Times New Roman),\n // 字型必須寫進 style(JointJS 將 style keys 複製為 div inline CSS)才會套用 → 群組標題與全圖統一 JhengHei\n label: { text: (titleLines[gp.id] || [gp.label]).join('\\n'), fontFamily: FONT, fontSize: FSG, fontWeight: 'bold', fill: st.color,\n style: { color: st.color, fontWeight: 'bold', fontFamily: FONT } }\n })\n groupTitleCells.push(tb)\n })\n\n // leaf 節點\n leaves.forEach(function(nn){\n var p = pos[nn.id]\n var st = CLS[nn.cls] || CLS.blue\n var sz = sizing[nn.id]\n var cell\n if (st.shape === 'diamond'){\n cell = new joint.shapes.standard.Polygon()\n cell.attr('body/refPoints', '50,0 100,50 50,100 0,50')\n } else {\n cell = new joint.shapes.standard.Rectangle()\n cell.attr('body/rx', 6); cell.attr('body/ry', 6)\n }\n cell.position(p.x - p.w/2, p.y - p.h/2)\n cell.resize(p.w, p.h)\n cell.attr('body/fill', st.fill)\n cell.attr('body/stroke', st.stroke)\n cell.attr('body/strokeWidth', 1.8)\n cell.attr('label/text', sz.lines.join('\\n'))\n cell.attr('label/fontFamily', FONT)\n cell.attr('label/fontSize', FS)\n cell.attr('label/fill', st.color)\n cell.attr('label/lineHeight', (FS + 7) + 'px')\n cells.push(cell); cellById[nn.id] = cell\n })\n\n // 邊\n edges.forEach(function(ed){\n var s = cellById[ed.source], t = cellById[ed.target]\n if (!s || !t) return\n var l = new joint.shapes.standard.Link()\n l.source(s); l.target(t)\n var dashed = ed.cls === 'dashed'\n l.attr({\n line: {\n stroke: '#44505a', strokeWidth: 2.2,\n strokeDasharray: dashed ? '6,4' : null,\n targetMarker: { type: 'path', d: 'M 10 -5 0 0 10 5 z', fill: '#44505a' }\n }\n })\n l.router('normal'); l.connector('rounded', { radius: 6 })\n if (ed.label){\n // 邊標籤距離: 預設置中(0.5); 虛線(跨群組/驗證類)靠來源端;\n // 跨群組邊(端點落在不同容器)把標籤推向「位於容器外」的一端, 避免疊到群組內節點。\n var srcIn = parentOf[ed.source] && parentOf[ed.source] !== '__root'\n var tgtIn = parentOf[ed.target] && parentOf[ed.target] !== '__root'\n var dist = 0.5\n if (dashed) dist = 0.32\n else if (tgtIn && !srcIn) dist = 0.26 // 進入群組: 標籤靠來源(群組外)\n else if (srcIn && !tgtIn) dist = 0.74 // 離開群組: 標籤靠目標(群組外)\n // 全域修正 A: 背景框透明(不遮轉折線), 文字以白色光暈/描邊保持清晰\n // SVG paint-order:stroke → 先描白邊再填字 → 線段能透出、文字仍清晰\n l.labels([{\n markup: [{ tagName: 'text', selector: 'lbl' }],\n attrs: {\n lbl: { text: ed.label, fontFamily: FONT, fontSize: FSL, fill: '#4a5560',\n stroke: '#ffffff', strokeWidth: 3.5, paintOrder: 'stroke', strokeLinejoin: 'round',\n textAnchor: 'middle', textVerticalAnchor: 'middle' }\n },\n position: { distance: dist }\n }])\n }\n cells.push(l)\n })\n\n // 群組標題置於最上層\n groupTitleCells.forEach(function(tb){ cells.push(tb) })\n\n var paper = document.getElementById('paper')\n var P = new joint.dia.Paper({\n el: paper, model: graph,\n width: 100, height: 100, background: { color: '#ffffff' },\n cellViewNamespace: joint.shapes\n })\n graph.resetCells(cells)\n // 確保群組在底層\n groups.forEach(function(gp){ cellById[gp.id].toBack() })\n P.fitToContent({ padding: 28, allowNewOrigin: 'any', useModelGeometry: true })\n\n // ── 碰撞偵測(通用機制, 對所有圖一致) ─────────────────────────────\n // 量測「節點 bounding box」與「邊標籤 bounding box」, 兩兩偵測重疊(>2px 計碰撞)。\n // 群組容器刻意包住其子節點, 故排除「容器 ↔ 其後代」之合法重疊; 其餘皆計入。\n // 座標一律取 getBoundingClientRect()(同一螢幕座標系, 跨節點/標籤可直接比對, paper 未縮放故相對關係即真實版面)。\n // 容器後代集合(供排除合法包含)\n var descOf = {}\n function collectDesc(gid){\n var set = {}; var stack = (childrenOf[gid] || []).slice()\n while (stack.length){ var c = stack.pop(); set[c.id] = true; if (grpById[c.id]) (childrenOf[c.id] || []).forEach(function(k){ stack.push(k) }) }\n return set\n }\n groups.forEach(function(gp){ descOf[gp.id] = collectDesc(gp.id) })\n function toRect(dom, kind, id){\n var r = dom.getBoundingClientRect()\n return { kind: kind, id: id, x: r.left, y: r.top, w: r.width, h: r.height }\n }\n // 收集矩形: 節點(cellView body 之 client rect), 群組亦納入(偵測容器 ↔ 無關節點/容器)\n var rects = []\n nodes.forEach(function(nn){\n var c = cellById[nn.id]; if (!c) return\n var cv = P.findViewByModel(c); if (!cv) return\n // 群組量「外框 body」, 不含其內子節點; 一般節點量整個 cell\n var dom = grpById[nn.id] ? (cv.el.querySelector('[joint-selector="body"], rect') || cv.el) : cv.el\n rects.push(toRect(dom, grpById[nn.id] ? 'group' : 'node', nn.id))\n })\n // 邊標籤: 取其 label <text>(同一連線之 halo+fill 為單一 text, 不重複計)\n var labelRects = []\n cells.forEach(function(c){\n if (!(c.isLink && c.isLink())) return\n if (!(c.attributes.labels && c.attributes.labels.length)) return\n var lv = P.findViewByModel(c); if (!lv) return\n var tnodes = lv.el.querySelectorAll('[joint-selector="lbl"]')\n if (!tnodes.length) tnodes = lv.el.querySelectorAll('.labels text, .label text')\n for (var ti = 0; ti < tnodes.length; ti++){\n var r = tnodes[ti].getBoundingClientRect()\n if (r.width < 1) continue\n labelRects.push({ kind: 'label', id: c.id + ':' + ti, x: r.left, y: r.top, w: r.width, h: r.height })\n }\n })\n function overlapPx(a, b){\n var ix = Math.min(a.x + a.w, b.x + b.w) - Math.max(a.x, b.x)\n var iy = Math.min(a.y + a.h, b.y + b.h) - Math.max(a.y, b.y)\n return { ix: ix, iy: iy }\n }\n var allRects = rects.concat(labelRects)\n var collisions = 0, minGap = 1e9, details = []\n for (var i = 0; i < allRects.length; i++) for (var j = i + 1; j < allRects.length; j++){\n var A = allRects[i], B = allRects[j]\n // 排除合法包含: 容器 ↔ 其後代節點\n if (A.kind === 'group' && descOf[A.id] && descOf[A.id][B.id]) continue\n if (B.kind === 'group' && descOf[B.id] && descOf[B.id][A.id]) continue\n // 排除「群組外框 ↔ 邊標籤」: 群組為半透明色塊背景(swimlane), 標籤帶白色光暈疊於其上仍清晰,\n // 且跨群組邊必然穿越群組邊界 → 不計為碰撞(真正要避免的是標籤疊到節點文字/其他標籤)\n if ((A.kind === 'group' && B.kind === 'label') || (B.kind === 'group' && A.kind === 'label')) continue\n var ov = overlapPx(A, B)\n if (ov.ix > 2 && ov.iy > 2){\n collisions++\n if (details.length < 4) details.push((A.kind[0]) + A.id.slice(0, 6) + '×' + (B.kind[0]) + B.id.slice(0, 6))\n } else if (ov.ix > -200 && ov.iy > -200){\n // 估「最近可見間隙」: 兩矩形在分離軸上的最小正向距離(僅在另一軸有重疊時才算同列/同行相鄰)\n if (ov.ix <= 2 && ov.iy > 2) minGap = Math.min(minGap, -ov.ix)\n if (ov.iy <= 2 && ov.ix > 2) minGap = Math.min(minGap, -ov.iy)\n }\n }\n if (!isFinite(minGap)) minGap = 999\n resolve({ ok: true, w: Math.round(P.options.width), h: Math.round(P.options.height),\n collisions: collisions, minGap: Math.round(minGap), details: details })\n } catch(e){ resolve({ ok: false, err: String(e.message || e), stack: String(e.stack) }) }\n })\n}\nwindow.renderFig = renderFig\nwindow.__ready = true\n<\/script></body></html>`;async function Wt(n){const t=await e.chromium.launch();try{const e=await t.newPage({deviceScaleFactor:2}),r=[];e.on("pageerror",e=>r.push(e.message)),e.on("console",e=>{"error"===e.type()&&r.push("c:"+e.text())}),await e.setContent(zt,{waitUntil:"load"}),await e.waitForFunction(()=>window.__ready,{timeout:6e4}).catch(()=>{}),await e.evaluate(()=>document.fonts&&document.fonts.ready);const o={nodesep:32,ranksep:48},{els:a,rankDir:i}=function(e){const n=[];for(const t of e.nodes)n.push({id:t.id,label:t.label,cls:t.cls||"blue",parent:t.group||null});for(const t of e.edges)n.push({source:t.from,target:t.to,label:t.label||"",cls:"dashed"===t.kind?"dashed":""});return{els:n,rankDir:e.dir}}(n),s=await e.evaluate(([e,n,t])=>window.renderFig(e,n,t),[a,i,o]);if(!s.ok)throw new Error(s.err+(s.stack?"\n"+s.stack:""));return{browser:t,page:e,res:s}}catch(e){throw await t.close(),e}}var Jt=Object.freeze({__proto__:null,genPng:async function(e,n={}){const{browser:t,page:r}=await Wt(e);try{const e=await r.$("#paper svg");return await e.screenshot()}finally{await t.close()}},genSvg:async function(e,n={}){const{browser:t,page:r,res:o}=await Wt(e);try{let e=await r.$eval("#paper svg",e=>e.outerHTML);return e=e.replace(/^<svg([^>]*)>/,(e,n)=>{let t=n;return/\bxmlns=/.test(t)||(t=' xmlns="http://www.w3.org/2000/svg"'+t),t=t.replace(/\swidth="100%"/,` width="${o.w}"`),t=t.replace(/\sheight="100%"/,` height="${o.h}"`),t=t.replace(/position:\s*absolute;\s*inset:\s*[^;]*;\s*/,""),`<svg${t}>`}),e.replace(/&nbsp;/g,"&#160;").trim()}finally{await t.close()}}});const qt=Object.fromEntries(Object.entries(Kn).map(([e,n])=>[e,{fill:n.fill,stroke:n.stroke,text:n.text}])),Xt=JSON.stringify(qt),Yt=JSON.stringify(Qn),Ut=`<!doctype html><html lang="zh-Hant"><head><meta charset="utf-8">\n${it("logicflow-css")}\n${at("logicflow")}\n${at("dagre")}\n<style>\n * { font-family: ${et}; }\n html,body{ margin:0; background:#fff; }\n #app { background:#fff; }\n .lf-grid { display:none !important; }\n</style>\n</head><body>\n<div id="app"></div>\n<script>\nvar PAL = ${Xt}\nvar EDGE = ${Yt}\nvar FONT = ${JSON.stringify(`14px ${et}`)} // canvas 量測用之 font 短語\nvar FONT_FAMILY = ${JSON.stringify(et)} // 純 font-family(供 SVG text 明確指定, 字型全覆蓋)\n\n// ---- 通用版面常數(非逐圖; 皆為與內容無關之全域預設) ----\nvar FONT_SIZE = 14, LINE_H = 22 // 節點字級 / 行高\nvar NODE_MAXW = 230, DIAMOND_MAXW = 110 // 換行最大寬度(矩形 / 菱形)\nvar PAD_X = 28, PAD_Y = 22 // 矩形內距\nvar DIA_PAD_X = 60, DIA_PAD_Y = 46 // 菱形內距(對角需較大留白)\nvar NODE_MINW = 120, NODE_MINH = 56 // 矩形最小尺寸\nvar DIA_MINW = 150, DIA_MINH = 110 // 菱形最小尺寸\nvar MARGIN = 20 // dagre 邊距\nvar GROUP_TITLE = 26 // 群組容器標題列高\nvar CANVAS_MARGIN = 60 // 畫布外緣留白\nvar LABEL_GAP = 28, LABEL_BIAS = 0.62 // 跨界邊標籤離容器框間隙 / 偏容器側權重\n\n// ---- 緊湊固定間距(通用機制, 對所有圖一致; 不做「掃描放大 until 零碰撞」) ----\n// 一律以單一緊湊間距排版, 維持報告可用的緊湊尺寸; 不因避碰撞而把圖撐大。\n// 文字遮蔽改由繪製順序(z-order)解決(群組墊底 / 節點居中 / 邊與邊標籤置頂), 不靠加大間距。\n// LR 層距略大於 TB(水平向標籤較長), 仍為固定值。\nvar SEP_TB = [36, 55] // [nodesep, ranksep] — 上下向緊湊間距\nvar SEP_LR = [36, 82] // [nodesep, ranksep] — 左右向緊湊間距\nvar COLLIDE_TOL = 2 // 重疊 >此像素 才計為碰撞(僅供回報用, 不再驅動間距放大)\n\n// 量測文字寬度\nvar _cv = document.createElement('canvas'); var _cx = _cv.getContext('2d')\nfunction measure(t, font){ _cx.font = font; return _cx.measureText(t).width }\n\n// 將長 label 依最大寬度斷行: 中文逐字可斷; 連續英數識別字(token)視為不可攔腰斷之整體, 整段超寬才獨佔一行。\nfunction wrap(label, maxW, font){\n _cx.font = font\n // 先切成 token: 連續英數(含 . _ / 等識別字常見字元)為一個 token, 其餘(中文/標點/空白)逐字\n var chars = Array.from(label)\n var tokens = [], buf = ''\n function isWordChar(c){ return /[0-9A-Za-z._/\\-]/.test(c) }\n for (var i=0;i<chars.length;i++){\n if (isWordChar(chars[i])){ buf += chars[i] }\n else { if (buf){ tokens.push(buf); buf='' } tokens.push(chars[i]) }\n }\n if (buf) tokens.push(buf)\n var lines = [], cur = ''\n for (var k=0;k<tokens.length;k++){\n var tk = tokens[k]\n var test = cur + tk\n if (_cx.measureText(test).width > maxW && cur.length>0){ lines.push(cur); cur = tk }\n else cur = test\n }\n if (cur) lines.push(cur)\n // 去除純空白造成的行首空白\n return lines.map(function(l){ return l.replace(/^\\s+/, '') }).filter(function(l){ return l.length>0 })\n}\n\n// 依 label 自動推算節點尺寸(寬: 依最寬一行字寬 + 內距; 高: 依行數 + 內距; 皆套最小值)\nfunction sizeOf(label, isDiamond){\n var maxW = isDiamond ? DIAMOND_MAXW : NODE_MAXW\n var lines = wrap(label, maxW, FONT)\n if (!lines.length) lines = [label]\n var w = 0\n lines.forEach(function(l){ w = Math.max(w, measure(l, FONT)) })\n var padX = isDiamond ? DIA_PAD_X : PAD_X, padY = isDiamond ? DIA_PAD_Y : PAD_Y\n var W = Math.ceil(w + padX*2)\n var H = Math.ceil(lines.length*LINE_H + padY*2)\n if (isDiamond){ W = Math.max(W, DIA_MINW); H = Math.max(H, DIA_MINH) }\n else { W = Math.max(W, NODE_MINW); H = Math.max(H, NODE_MINH) }\n return { w: W, h: H, text: lines.join('\\n') }\n}\n\n// 兩矩形重疊量(回傳 {ix,iy} 為水平/垂直交集像素, 皆>0 才重疊)\nfunction overlap(a, b){\n var ix = Math.min(a.x+a.w/2, b.x+b.w/2) - Math.max(a.x-a.w/2, b.x-b.w/2)\n var iy = Math.min(a.y+a.h/2, b.y+b.h/2) - Math.max(a.y-a.h/2, b.y-b.h/2)\n return { ix: ix, iy: iy }\n}\n\n// ---- 解析 fig 為內部結構 + 量測尺寸(只做一次, 不隨間距改變) ----\nfunction prepare(fig){\n var els = fig.els\n var nodes = els.filter(function(x){ return !('source' in x) })\n var edges = els.filter(function(x){ return ('source' in x) })\n var byId = {}; nodes.forEach(function(nd){ byId[nd.id] = nd })\n var groupIds = {}; nodes.forEach(function(nd){ if (nd.parent) groupIds[nd.parent] = true })\n nodes.forEach(function(nd){\n if (groupIds[nd.id]) return\n var sz = sizeOf(nd.label, nd.cls === 'diamond')\n nd._w = sz.w; nd._h = sz.h; nd._text = sz.text\n })\n return { nodes: nodes, edges: edges, byId: byId, groupIds: groupIds, rankDir: fig.rankDir }\n}\n\n// ---- 以給定間距做 dagre compound 排版, 回傳座標/群組框/邊標籤座標(供偵測與渲染) ----\nfunction layout(P, nodeSep, rankSep){\n var nodes = P.nodes, edges = P.edges, byId = P.byId, groupIds = P.groupIds\n var g = new dagre.graphlib.Graph({ compound:true })\n g.setGraph({ rankdir: P.rankDir, nodesep: nodeSep, ranksep: rankSep, marginx: MARGIN, marginy: MARGIN })\n g.setDefaultEdgeLabel(function(){ return {} })\n Object.keys(groupIds).forEach(function(gid){ g.setNode(gid, { label: gid }) })\n Object.keys(groupIds).forEach(function(gid){\n var gp = byId[gid] && byId[gid].parent\n if (gp && groupIds[gp]) g.setParent(gid, gp)\n })\n nodes.forEach(function(nd){\n if (groupIds[nd.id]) return\n g.setNode(nd.id, { width: nd._w, height: nd._h })\n if (nd.parent) g.setParent(nd.id, nd.parent)\n })\n function repLeaf(gid){\n var c = nodes.filter(function(x){ return x.parent === gid && !groupIds[x.id] })\n return c.length ? c[0].id : null\n }\n edges.forEach(function(ed){\n var sG = groupIds[ed.source], tG = groupIds[ed.target]\n if (!sG && !tG){ g.setEdge(ed.source, ed.target); return }\n var s = sG ? repLeaf(ed.source) : ed.source\n var t = tG ? repLeaf(ed.target) : ed.target\n if (s && t) g.setEdge(s, t)\n })\n dagre.layout(g)\n\n // leaf 節點座標\n var lfNodes = []\n nodes.forEach(function(nd){\n if (groupIds[nd.id]) return\n var p = g.node(nd.id)\n var pal = PAL[nd.cls] || PAL.blue\n var type = nd.cls === 'diamond' ? 'diamond' : 'rect'\n lfNodes.push({ id: nd.id, type: type, x: Math.round(p.x), y: Math.round(p.y), text: nd._text, _pal: pal, _w: nd._w, _h: nd._h })\n })\n // 群組容器框(dagre cluster bbox)\n function depth(gid){ var d=0,p=byId[gid]&&byId[gid].parent; while(p){ d++; p=byId[p]&&byId[p].parent } return d }\n var lfGroupNodes = []\n Object.keys(groupIds).sort(function(a,b){ return depth(a)-depth(b) }).forEach(function(gid){\n var gn = g.node(gid)\n var pal = PAL[byId[gid].cls] || PAL.blue\n // 群組標題帶為固定高(GROUP_TITLE), label 內 \\n 降級為空格(兩行標題會壓到成員節點)\n lfGroupNodes.push({ id: gid, label: String(byId[gid].label).replace(/\\n/g, ' '), _pal: pal,\n x: Math.round(gn.x), y: Math.round(gn.y), _w: Math.round(gn.width), _h: Math.round(gn.height) })\n })\n\n // 幾何查詢\n var geo = {}\n lfNodes.forEach(function(nd){ geo[nd.id]={ x:nd.x, y:nd.y, w:nd._w, h:nd._h } })\n lfGroupNodes.forEach(function(b){ geo[b.id]={ x:b.x, y:b.y, w:b._w, h:b._h } })\n var parentBox = {}\n nodes.forEach(function(nd){ if (nd.parent && geo[nd.parent]) parentBox[nd.id] = geo[nd.parent] })\n function inGroup(id){ return !!parentBox[id] }\n\n // 跨界邊標籤座標(落在外部節點與群組框之間淨空)\n function labelPos(ed){\n if (!ed.label) return null\n var sIn = inGroup(ed.source) || groupIds[ed.source]\n var tIn = inGroup(ed.target) || groupIds[ed.target]\n var outId = null, inId = null\n if (sIn && !tIn){ inId=ed.source; outId=ed.target }\n else if (!sIn && tIn){ outId=ed.source; inId=ed.target }\n else {\n var s = geo[ed.source], t = geo[ed.target]\n if (!s || !t) return null\n var mx = (s.x+t.x)/2, my = (s.y+t.y)/2\n var hit = null\n lfGroupNodes.forEach(function(b){\n if (mx > b.x-b._w/2 && mx < b.x+b._w/2 && my > b.y-b._h/2 && my < b.y+b._h/2) hit = b\n })\n if (!hit) return null\n return { x: Math.round(hit.x - hit._w/2 - LABEL_GAP), y: Math.round(my) }\n }\n var o = geo[outId], box = parentBox[inId] || geo[inId]\n var tgt = geo[ed.target] || geo[inId]\n if (!o || !box || !tgt) return null\n var bias = LABEL_BIAS, obias = 1 - LABEL_BIAS\n if (P.rankDir === 'LR'){\n var boxEdgeX = (o.x < box.x) ? (box.x - box.w/2) : (box.x + box.w/2)\n var oInnerX = o.x + (o.x<box.x ? o.w/2 : -o.w/2)\n var lx = boxEdgeX*bias + oInnerX*obias\n return { x: Math.round(lx), y: Math.round(tgt.y) }\n } else {\n var boxEdgeY = (o.y < box.y) ? (box.y - box.h/2) : (box.y + box.h/2)\n var oInnerY = o.y + (o.y<box.y ? o.h/2 : -o.h/2)\n var ly = boxEdgeY*bias + oInnerY*obias\n return { x: Math.round(tgt.x), y: Math.round(ly) }\n }\n }\n\n // 邊 + 標籤框(供偵測). 標籤框尺寸依文字量測。\n var lfEdges = [], labelBoxes = []\n edges.forEach(function(ed, i){\n var lp = labelPos(ed)\n var txt = ed.label ? (lp ? { value: ed.label, x: lp.x, y: lp.y } : ed.label) : ''\n lfEdges.push({ id:'e'+i, sourceNodeId: ed.source, targetNodeId: ed.target, text: txt, _dashed: ed.dashed })\n if (ed.label){\n // 標籤未指定座標時, 取兩端中點估其位置(供偵測)\n var lx, ly\n if (lp){ lx = lp.x; ly = lp.y }\n else { var s=geo[ed.source], t=geo[ed.target]; if(!s||!t) return; lx=(s.x+t.x)/2; ly=(s.y+t.y)/2 }\n var lw = measure(ed.label, FONT) + 14, lh = LINE_H\n labelBoxes.push({ x: lx, y: ly, w: lw, h: lh, _label: true })\n }\n })\n\n return { lfNodes: lfNodes, lfGroupNodes: lfGroupNodes, lfEdges: lfEdges, labelBoxes: labelBoxes,\n byId: byId, parentBox: parentBox }\n}\n\n// ---- 偵測重疊: leaf↔leaf、group↔group(非巢狀)、leaf↔group(非歸屬)、邊標籤↔節點/其他標籤 ----\n// 回傳 { collisions, minGap(零碰撞時計區塊間最小可見間隙) }\nfunction detect(L){\n var byId = L.byId\n // 取某 leaf/group 的祖先群組鏈(含自身), 用於排除「容器內含其成員」的合法重疊\n function ancestors(id){ var arr=[id], p=byId[id]&&byId[id].parent; while(p){ arr.push(p); p=byId[p]&&byId[p].parent } return arr }\n // 判 a 是否為 b 之祖先或同一(容器包含成員 → 非碰撞)\n function related(idA, idB){\n var ca = ancestors(idA), cb = ancestors(idB)\n return ca.indexOf(idB) >= 0 || cb.indexOf(idA) >= 0\n }\n // 待偵測之矩形集合(節點 + 群組框, 各帶 id; 標籤無 id)\n var rects = []\n L.lfNodes.forEach(function(n){ rects.push({ id:n.id, x:n.x, y:n.y, w:n._w, h:n._h, kind:'node' }) })\n L.lfGroupNodes.forEach(function(b){ rects.push({ id:b.id, x:b.x, y:b.y, w:b._w, h:b._h, kind:'group' }) })\n var labels = L.labelBoxes.map(function(l){ return { x:l.x, y:l.y, w:l.w, h:l.h, kind:'label' } })\n\n var collisions = 0, minGap = Infinity\n // 節點/群組 兩兩(排除容器-成員關係)\n for (var i=0;i<rects.length;i++){\n for (var j=i+1;j<rects.length;j++){\n var A=rects[i], B=rects[j]\n if (related(A.id, B.id)) continue\n var ov = overlap(A, B)\n if (ov.ix > COLLIDE_TOL && ov.iy > COLLIDE_TOL){ collisions++ }\n else {\n // 未重疊: 取較近軸向之間隙(僅在另一軸有投影重疊時才計, 否則為斜對角不相鄰)\n if (ov.ix > 0) minGap = Math.min(minGap, -ov.iy) // 水平有交集 → 垂直間隙\n else if (ov.iy > 0) minGap = Math.min(minGap, -ov.ix) // 垂直有交集 → 水平間隙\n }\n }\n }\n // 邊標籤 ↔ 節點/群組(群組框為淺底大容器, 標籤落其內屬正常 → 僅與 leaf 節點及其他標籤計碰撞)\n for (var li=0;li<labels.length;li++){\n var lab = labels[li]\n for (var ri=0;ri<rects.length;ri++){\n if (rects[ri].kind !== 'node') continue\n var ovl = overlap(lab, rects[ri])\n if (ovl.ix > COLLIDE_TOL && ovl.iy > COLLIDE_TOL) collisions++\n }\n for (var lj=li+1;lj<labels.length;lj++){\n var ovll = overlap(lab, labels[lj])\n if (ovll.ix > COLLIDE_TOL && ovll.iy > COLLIDE_TOL) collisions++\n }\n }\n if (!isFinite(minGap)) minGap = 0 // 無相鄰對(單一節點等)\n return { collisions: collisions, minGap: minGap }\n}\n\n// ---- 把一份 layout 結果實際畫到 LF 並截圖前準備(回傳畫布尺寸) ----\nfunction renderLayout(L){\n var lfNodes = L.lfNodes, lfGroupNodes = L.lfGroupNodes, lfEdges = L.lfEdges\n\n // 整體 bbox → 畫布尺寸\n var minX=1e9,minY=1e9,maxX=-1e9,maxY=-1e9\n lfGroupNodes.concat(lfNodes).forEach(function(nd){\n minX=Math.min(minX, nd.x-nd._w/2); maxX=Math.max(maxX, nd.x+nd._w/2)\n minY=Math.min(minY, nd.y-nd._h/2); maxY=Math.max(maxY, nd.y+nd._h/2)\n })\n var M = CANVAS_MARGIN\n var W = Math.ceil(maxX-minX+M*2), H = Math.ceil(maxY-minY+M*2)\n var app = document.getElementById('app')\n app.innerHTML = ''\n app.style.width = W+'px'; app.style.height = H+'px'\n\n var G = window.Core || window.LogicFlow\n var LF = G.default || G.LogicFlow || G\n var RectNode = G.RectNode, RectNodeModel = G.RectNodeModel\n var DiamondNode = G.DiamondNode, DiamondNodeModel = G.DiamondNodeModel\n\n var lf = new LF({ container: app, grid:false, background:false, isSilentMode:true, stopScrollGraph:true, stopZoomGraph:true, adjustEdge:false,\n style: {\n rect: { rx: 7, ry: 7 },\n polyline: { stroke: EDGE.line, strokeWidth: 1.8 },\n nodeText: { color:'#1c2b36', fontFamily: FONT_FAMILY, fontSize: FONT_SIZE, overflowMode:'autoWrap', lineHeight: 1.4 },\n },\n })\n\n function makeRect(name){\n lf.register({ type: name, view: RectNode, model: class extends RectNodeModel {\n setAttributes(){ var p=this.properties||{}; if(p.width)this.width=p.width; if(p.height)this.height=p.height; this.radius=7 }\n getNodeStyle(){ var s=super.getNodeStyle(); var pal=this.properties._pal||PAL.blue; s.fill=pal.fill; s.stroke=pal.stroke; s.strokeWidth=this.properties.isGroup?2:1.8; if(this.properties.isGroup){s.fillOpacity=0.45} return s }\n getTextStyle(){ var s=super.getTextStyle(); var pal=this.properties._pal||PAL.blue; s.color=pal.text; s.fontFamily=FONT_FAMILY; s.fontSize=this.properties.isGroup?15:FONT_SIZE; if(this.properties.isGroup){s.fontWeight=700} s.overflowMode='autoWrap'; s.lineHeight=1.35; if(this.width)s.wrapPadding='0px'; return s }\n }})\n }\n function makeDiamond(name){\n lf.register({ type: name, view: DiamondNode, model: class extends DiamondNodeModel {\n setAttributes(){ var p=this.properties||{}; if(p.width)this.rx=p.width/2; if(p.height)this.ry=p.height/2 }\n getNodeStyle(){ var s=super.getNodeStyle(); var pal=this.properties._pal||PAL.diamond; s.fill=pal.fill; s.stroke=pal.stroke; s.strokeWidth=1.8; return s }\n getTextStyle(){ var s=super.getTextStyle(); var pal=this.properties._pal||PAL.diamond; s.color=pal.text; s.fontFamily=FONT_FAMILY; s.fontSize=FONT_SIZE; s.overflowMode='autoWrap'; return s }\n }})\n }\n makeRect('cRect'); makeDiamond('cDiamond')\n\n // 邊標籤樣式(全域修正 A): 背景透明(不遮轉折線) + 文字白色光暈(paint-order:stroke + stroke=白)\n function edgeText(s){\n s.color=EDGE.text; s.fontFamily=FONT_FAMILY; s.fontSize=12.5; s.overflowMode='default'; s.textWidth=400\n s.background={ fill:'transparent', stroke:'none' } // 透明底: 線段能透出, 不被白底遮斷\n s.stroke=EDGE.haloColor; s.strokeWidth=EDGE.haloWidth; s.paintOrder='stroke' // 白光暈描邊(描在文字下方)\n return s\n }\n var PolylineEdge=G.PolylineEdge, PolylineEdgeModel=G.PolylineEdgeModel\n lf.register({ type:'cDash', view:PolylineEdge, model: class extends PolylineEdgeModel {\n getEdgeStyle(){ var s=super.getEdgeStyle(); s.stroke='#7a8690'; s.strokeWidth=1.8; s.strokeDasharray='6 4'; return s }\n getTextStyle(){ return edgeText(super.getTextStyle()) }\n }})\n lf.register({ type:'cLine', view:PolylineEdge, model: class extends PolylineEdgeModel {\n getEdgeStyle(){ var s=super.getEdgeStyle(); s.stroke=EDGE.line; s.strokeWidth=1.8; return s }\n getTextStyle(){ return edgeText(super.getTextStyle()) }\n }})\n\n var graphNodes = []\n lfGroupNodes.forEach(function(b){\n graphNodes.push({ id:b.id, type:'cRect', x:b.x, y:b.y,\n text:{ value: b.label, x: b.x, y: Math.round(b.y - b._h/2 + GROUP_TITLE/2 + 4) },\n properties:{ width:b._w, height:b._h, isGroup:true, _pal:b._pal } })\n })\n lfNodes.forEach(function(nd){\n graphNodes.push({ id:nd.id, type: nd.type==='diamond'?'cDiamond':'cRect', x:nd.x, y:nd.y, text:nd.text, properties:{ width:nd._w, height:nd._h, _pal:nd._pal } })\n })\n var graphEdges = lfEdges.map(function(ed){\n return { id:ed.id, type: ed._dashed?'cDash':'cLine', sourceNodeId:ed.sourceNodeId, targetNodeId:ed.targetNodeId, text: ed.text||'' }\n })\n\n lf.render({ nodes: graphNodes, edges: graphEdges })\n\n // ---- 繪製順序(z-order, 原則2) ----\n // LF 預設把邊畫在節點下方, 群組(node)與節點同層; 文字會被節點/群組底色蓋住。\n // 以 setElementZIndex 數值分層強制: 群組容器=底(Z_GROUP) → 節點=中(Z_NODE) → 邊與邊標籤=頂(Z_EDGE),\n // 確保任何文字(節點文字 / 群組標題 / 邊標籤)都不被容器填色或其他區塊遮住。\n var Z_GROUP = 1, Z_NODE = 10, Z_EDGE = 100\n lfGroupNodes.forEach(function(b){ lf.setElementZIndex(b.id, Z_GROUP) }) // 半透明容器墊最底\n lfNodes.forEach(function(nd){ lf.setElementZIndex(nd.id, Z_NODE) }) // 節點居中\n graphEdges.forEach(function(ed){ lf.setElementZIndex(ed.id, Z_EDGE) }) // 邊與邊標籤置頂\n\n return { w: W, h: H, nNodes: graphNodes.length, nEdges: graphEdges.length }\n}\n\n// ---- 對外: 解析 → 以單一緊湊固定間距排版 → 實際渲染(z-order 處理遮蔽, 不掃描放大間距) ----\nwindow.renderFig = function(fig){ return new Promise(function(resolve){\n try {\n var P = prepare(fig)\n var sep = (P.rankDir === 'LR') ? SEP_LR : SEP_TB\n var L = layout(P, sep[0], sep[1])\n var d = detect(L) // 僅供回報(碰撞數/最小間隙), 不驅動任何放大\n var info = renderLayout(L)\n setTimeout(function(){ resolve({ ok:true, w:info.w, h:info.h, nNodes:info.nNodes, nEdges:info.nEdges,\n nodeSep:sep[0], rankSep:sep[1], collisions:d.collisions, minGap:Math.round(d.minGap) }) }, 200)\n } catch(e){ resolve({ ok:false, err: String(e && (e.stack||e.message||e)) }) }\n})}\nwindow.__ready = true\n<\/script></body></html>`;async function Vt(n){const t=await e.chromium.launch();try{const e=await t.newPage({deviceScaleFactor:2}),r=[];e.on("pageerror",e=>r.push(e.message)),e.on("console",e=>{"error"===e.type()&&r.push("console:"+e.text())}),await e.setContent(Ut,{waitUntil:"load"}),await e.waitForFunction(()=>window.__ready,{timeout:3e4}).catch(()=>{}),await e.evaluate(()=>document.fonts&&document.fonts.ready);const o=function(e){const n=[];for(const t of e.nodes)n.push({id:t.id,label:t.label,cls:t.cls||"blue",parent:t.group});for(const t of e.edges)n.push({source:t.from,target:t.to,label:t.label||"",dashed:"dashed"===t.kind});return{rankDir:e.dir,els:n}}(n),a=await e.evaluate(e=>window.renderFig(e),o);if(!a.ok)throw new Error(a.err||"renderFig failed");return await e.evaluate(()=>document.fonts&&document.fonts.ready),{browser:t,page:e,res:a}}catch(e){throw await t.close(),e}}var Zt=Object.freeze({__proto__:null,genPng:async function(e,n={}){const{browser:t,page:r}=await Vt(e);try{return await r.locator("#app").screenshot()}finally{await t.close()}},genSvg:async function(e,n={}){const{browser:t,page:r,res:o}=await Vt(e);try{let e=await r.evaluate(()=>document.querySelector("svg.lf-canvas-overlay").outerHTML);const n=e.match(/^<svg[^>]*>/);if(!n)throw new Error("genSvg: 找不到圖面 svg 根元素");let t=n[0].replace('width="100%"',`width="${o.w}"`).replace('height="100%"',`height="${o.h}"`);t=t.replace(/>$/,` viewBox="0 0 ${o.w} ${o.h}">`),e=t+e.slice(n[0].length);const a=await async function(e){if(void 0===st[e]){const n=ot(e),t=await fetch(n);if(!t.ok)throw new Error(`CDN 取檔失敗 ${t.status}: ${n}`);st[e]=await t.text()}return st[e]}("logicflow-css"),i=`<style>${a}\n* { font-family: ${et}; }</style><rect x="0" y="0" width="${o.w}" height="${o.h}" fill="#fff"></rect>`;return e=e.replace("<g transform",i+"<g transform"),e=e.replace(/&nbsp;/g,"&#160;"),e}finally{await t.close()}}});const Kt=t.dirname(n.fileURLToPath("undefined"==typeof document&&"undefined"==typeof location?require("url").pathToFileURL(__filename).href:"undefined"==typeof document?location.href:o&&"SCRIPT"===o.tagName.toUpperCase()&&o.src||new URL("w-flowchart.umd.js",document.baseURI).href));let Qt=null;var er=Object.freeze({__proto__:null,genPng:async function(n,t={}){const o=await e.chromium.launch();try{const e=await o.newPage({deviceScaleFactor:2}),t=[];e.on("pageerror",e=>t.push(e.message)),e.on("console",e=>{"error"===e.type()&&t.push("c:"+e.text().slice(0,200))}),await e.setContent((null===Qt&&(Qt=r.readFileSync(`${Kt}/page9.html`,"utf8").replace("<style>/*__XYFLOW_CSS__*/</style>",()=>it("xyflow-css")).replace("<script>/*__REACT_JS__*/<\/script>",()=>at("react")).replace("<script>/*__REACT_DOM_JS__*/<\/script>",()=>at("react-dom")).replace("<script>/*__XYFLOW_JS__*/<\/script>",()=>at("xyflow")).replace("<script>/*__DAGRE_JS__*/<\/script>",()=>at("dagrejs-dagre"))),Qt),{waitUntil:"load"}),await e.waitForFunction(()=>"READY"===document.title||"FAIL"===document.title,{timeout:6e4}).catch(()=>{});const a=await e.evaluate(()=>({t:document.title,err:window.__err}));if("READY"!==a.t)throw new Error("BOOT FAIL :: "+a.err+" | "+t.slice(0,6).join(" | "));const i=function(e){const n=e.nodes.map(e=>{const n={id:e.id,label:e.label,kind:e.cls};return e.group&&(n.parent=e.group),n}),t=e.edges.map(e=>{const n={s:e.from,t:e.to};return e.label&&(n.l=e.label),"dashed"===e.kind&&(n.dashed=!0),n});return{layout:"dagrec",rankDir:e.dir||"TB",nodesep:38,ranksep:44,nodes:n,edges:t}}(n),s=await e.evaluate(e=>window.renderFig(e),i);if(!s.ok)throw new Error("FAIL :: "+s.err);return await e.evaluate(()=>document.fonts&&document.fonts.ready),await e.waitForTimeout(250),await e.locator("#shot").screenshot()}finally{await o.close()}}});function nr(e){const n=e.nodes.filter(e=>nt(e.cls)).map(n=>{const t=tt(n.cls);return{id:n.id,label:n.label,parent:n.group||null,align:n.align,mi:e.nodes.indexOf(n),fill:t.fill,stroke:t.stroke,font:t.text}}),t=e.nodes.filter(e=>!nt(e.cls)).map(n=>{const t=tt(n.cls);return{id:n.id,label:n.label,title:n.title,items:n.items,parent:n.group||null,mi:e.nodes.indexOf(n),diamond:"diamond"===t.shape,fill:t.fill,stroke:t.stroke,font:t.text}}),r=e.edges.map((e,n)=>({id:"e"+n,from:e.from,to:e.to,label:e.label||"",dashed:"dashed"===e.kind})),o={};n.forEach(e=>o[e.id]=e.parent),t.forEach(e=>o[e.id]=e.parent);const a=e=>{const n=[];let t=o[e];for(;t;)n.push(t),t=o[t];return n};return r.forEach(e=>{const n=new Set(a(e.to));e.container=a(e.from).find(e=>n.has(e))||null}),{dir:e.dir||"TB",nodes:t,groups:n,edges:r}}const tr=`<!doctype html><html lang="zh-Hant"><head><meta charset="utf-8">\n${at("elkjs")}\n<style>body{margin:0;background:#fff} .lbl{font-family:'Microsoft JhengHei','微軟正黑體',sans-serif}</style>\n</head><body>\n<div id="meas" style="position:absolute;left:-99999px;top:0;font-family:'Microsoft JhengHei',sans-serif;visibility:hidden"></div>\n<div id="stage"></div>\n<script>\nconst FONT = "'Microsoft JhengHei','微軟正黑體',sans-serif"\nconst EDGE_LINE = '${Qn.line}', EDGE_TEXT = '${Qn.text}', HALO = '${Qn.haloColor}'\nconst FS = 14, GFS = 15, EFS = 12.5, MAXW = 200\n// 容器內矩形框之垂直對齊: 預設 top; 可於數據之群組節點設 align:'top'|'center'|'bottom' 個別覆寫(改 DEF_VALIGN 改全域預設)\nconst VALIGN = { top:'TOP', center:'CENTER', bottom:'BOTTOM' }, DEF_VALIGN = 'top'\nconst meas = document.getElementById('meas')\nfunction measure(label, fs, maxW){\n const d=document.createElement('div')\n // white-space:pre-line: label 內 \\n 為強制換行(其餘空白照常摺疊); 與渲染端 foreignObject div 一致\n d.style.cssText='display:inline-block;white-space:pre-line;overflow-wrap:break-word;word-break:normal;text-align:center;line-height:1.4;font-size:'+fs+'px;max-width:'+maxW+'px;padding:0;font-family:'+FONT\n d.textContent=label; meas.appendChild(d); const r=d.getBoundingClientRect(); meas.removeChild(d)\n return { w:Math.ceil(r.width), h:Math.ceil(r.height) }\n}\n// 量整塊 items 尺寸: 完全比照渲染(line-height 1.55、各項 nowrap)→ 直接得 items 區實際高度, 避免逐項估高(用錯 line-height)累積成底部多餘留白\nfunction measureItems(items, fs){\n const d=document.createElement('div')\n d.style.cssText='display:inline-block;font-size:'+fs+'px;line-height:1.55;font-family:'+FONT\n d.innerHTML=items.map(it=>'<div style="white-space:nowrap">•&#160;'+String(it).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')+'</div>').join('')\n meas.appendChild(d); const r=d.getBoundingClientRect(); meas.removeChild(d)\n return { w:Math.ceil(r.width), h:Math.ceil(r.height) }\n}\nconst esc = s => String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')\nconst mix = (a,b,t) => { const pa=[1,3,5].map(i=>parseInt(a.slice(i,i+2),16)), pb=[1,3,5].map(i=>parseInt(b.slice(i,i+2),16)); return '#'+pa.map((x,i)=>Math.round(x*(1-t)+pb[i]*t).toString(16).padStart(2,'0')).join('') }\n// 確保「起點後第一段」與「終點前最後一段」之直線長度 >= MIN(轉折不貼端點, 維持箭頭指向性)。\n// 做法: 末段過短時, 沿趨近軸向把「最後轉角」與「其前一轉角」一起後移(兩者同移=保正交); 需 >=4 點才不動端點。\n// ── 菱形端點貼齊(ELK 以外接矩形算端點, 落在矩形邊上非頂點處會與菱形斜邊「空接」) ──\n// 規則(共進共出, 每側獨立; 詳 p10/README):\n// R1 共出: 該側所有「出」一律由該側「頂點」出發, 多條共用首段後於各自高度 90 度轉出(分岔)。\n// R2 共進: 該側「無出」→ 全部匯入「頂點」(各自橫移至中軸, 共用末段, 箭頭重合);\n// 該側「有出」→ 依「半側」匯入該半側「斜邊中點」(頂點讓給出, 進出不同點)。\n// 正交線交叉允許存在(正交路由常態), 一致性優先。\n\n// 判定端點落在外接矩形哪一側; 回 { ax, sg, v }(離開軸/正負/該側頂點), 不在邊上回 null\nfunction diamondSide(p, box){\n const cx = box.x + box.w/2, cy = box.y + box.h/2, EPS = 1.5\n if (Math.abs(p.y - (box.y + box.h)) < EPS) return { ax: 'y', sg: 1, v: { x: cx, y: box.y + box.h } }\n if (Math.abs(p.y - box.y) < EPS) return { ax: 'y', sg: -1, v: { x: cx, y: box.y } }\n if (Math.abs(p.x - box.x) < EPS) return { ax: 'x', sg: -1, v: { x: box.x, y: cy } }\n if (Math.abs(p.x - (box.x + box.w)) < EPS) return { ax: 'x', sg: 1, v: { x: box.x + box.w, y: cy } }\n return null\n}\n\n// 該側某「半側」之斜邊中點(混合側的進匯點); half=+1 取座標較大半側, -1 反之\nfunction diamondSlantMid(box, side, half){\n const cx = box.x + box.w/2, cy = box.y + box.h/2, hw = box.w/2, hh = box.h/2\n if (side.ax === 'y') return { x: cx + half * hw / 2, y: cy + side.sg * hh / 2 }\n return { x: cx + side.sg * hw / 2, y: cy + half * hh / 2 }\n}\n\n// R1 共出: 出邊起點改自「頂點」出發\nfunction routeOutFromVertex(pts, side){\n const p0 = pts[0], p1 = pts[1]\n const EPS = 1.5, J = 18\n const v = side.v\n const off = side.ax === 'y' ? Math.abs(p0.x - v.x) : Math.abs(p0.y - v.y)\n if (off < EPS) return // 已在頂點(置中), 不動\n const alongAxis = side.ax === 'y' ? Math.abs(p1.x - p0.x) < EPS : Math.abs(p1.y - p0.y) < EPS\n if (!alongAxis) { pts.unshift(v); return } // 原首段即沿側邊橫走: 自頂點先橫走接回\n if (pts.length > 2) {\n // 有後續轉折: 頂點 → 沿軸行至原首個轉折高度 → 橫向接回(單一 L 型; 轉折可貼近頂點)\n const q1 = side.ax === 'y' ? { x: v.x, y: p1.y } : { x: p1.x, y: v.y }\n pts.splice(0, 1, v, q1)\n return\n }\n // 無轉折之直線邊: 於頂點外側作雙直角接回各自路線(距離取 J 與可用長度之小者)\n const segLen = side.ax === 'y' ? (p1.y - p0.y) * side.sg : (p1.x - p0.x) * side.sg\n const j = Math.min(J, Math.max(4, segLen / 2))\n const q1 = side.ax === 'y' ? { x: v.x, y: v.y + side.sg * j } : { x: v.x + side.sg * j, y: v.y }\n const q2 = side.ax === 'y' ? { x: p0.x, y: v.y + side.sg * j } : { x: v.x + side.sg * j, y: p0.y }\n pts.splice(0, 1, v, q1, q2)\n}\n\n// R2 決策: 進匯點 = 該側無出 → 頂點; 有出 → 依端點所在半側之斜邊中點\nfunction diamondInAnchor(box, side, hasOutOnSide, pE){\n if(!hasOutOnSide) return { x: side.v.x, y: side.v.y }\n const half = (side.ax === 'y' ? (pE.x - (box.x + box.w/2)) : (pE.y - (box.y + box.h/2))) >= 0 ? 1 : -1\n return diamondSlantMid(box, side, half)\n}\n\n// R2 共進: 進邊終點改匯入 anchor(頂點或半側斜邊中點)\n// 橫向差距小且末段前為可伸縮橫向段 → 整段末端直線「平移」對齊 anchor 軸線(零多餘轉折);\n// 否則於側外 J 處橫移 bus 再沿軸進入 anchor。\nfunction routeInToAnchor(pts, side, anchor){\n const pE = pts[pts.length - 1], pP = pts[pts.length - 2]\n const EPS = 1.5, J = 18, SLIDE = 40 // SLIDE: 可平移之最大橫向差距(過大恐撞鄰近元素, 改走 bus)\n const alongAxis = side.ax === 'y' ? Math.abs(pP.x - pE.x) < EPS : Math.abs(pP.y - pE.y) < EPS\n if (!alongAxis) return // 末段非沿進入軸(罕見), 保留原路由\n const lateral = side.ax === 'y' ? Math.abs(pE.x - anchor.x) : Math.abs(pE.y - anchor.y)\n if (lateral < EPS) {\n pts[pts.length - 1] = { x: anchor.x, y: anchor.y } // 已在 anchor 軸線上: 直接沿軸進入\n return\n }\n if (lateral <= SLIDE && pts.length >= 3) {\n const pP2 = pts[pts.length - 3]\n const prevHorizontal = side.ax === 'y' ? Math.abs(pP2.y - pP.y) < EPS : Math.abs(pP2.x - pP.x) < EPS\n if (prevHorizontal) {\n if (side.ax === 'y') pP.x = anchor.x; else pP.y = anchor.y\n pts[pts.length - 1] = { x: anchor.x, y: anchor.y }\n return\n }\n }\n const Lv = side.ax === 'y' ? side.v.y + side.sg * J : side.v.x + side.sg * J // 側外橫移層\n const segOK = side.ax === 'y' ? (pP.y - Lv) * side.sg > 2 : (pP.x - Lv) * side.sg > 2 // 原末段須延伸到橫移層之外\n if (!segOK) { pts[pts.length - 1] = { x: anchor.x, y: anchor.y }; return } // 空間不足: 直接斜線前最短接入(極罕見)\n const b1 = side.ax === 'y' ? { x: pE.x, y: Lv } : { x: Lv, y: pE.y }\n const b2 = side.ax === 'y' ? { x: anchor.x, y: Lv } : { x: Lv, y: anchor.y }\n pts.splice(pts.length - 1, 1, b1, b2, { x: anchor.x, y: anchor.y })\n}\n\nfunction ensureStub(pts, MIN, skipStart){\n function fixEnd(p){\n const n=p.length; if(n<4) return\n const C=p[n-1], B=p[n-2]\n const dx=C.x-B.x, dy=C.y-B.y, len=Math.abs(dx)+Math.abs(dy)\n if(len>=MIN || len<0.5) return\n const need=MIN-len, ux=dx/len, uy=dy/len\n p[n-2]={x:B.x-ux*need, y:B.y-uy*need}\n p[n-3]={x:p[n-3].x-ux*need, y:p[n-3].y-uy*need}\n }\n // 終點端(箭頭側)一律套最低門檻——含菱形貼齊後之路徑(bus 支線本給滿 J; 平移/保留支線之轉角平移不破壞正交)\n fixEnd(pts)\n // 起點端僅於「菱形頂點分岔」時跳過(分岔轉折貼近頂點屬預期; 後移轉角會與其後路徑段脫勾扭出斜線)\n if(!skipStart){ pts.reverse(); fixEnd(pts); pts.reverse() }\n return pts\n}\n\n// 供測試直接驗證菱形端點規則之純函式(不影響渲染)\nwindow.__dia = { diamondSide, diamondSlantMid, diamondInAnchor, routeOutFromVertex, routeInToAnchor }\n\nwindow.renderFig = async function(spec){\n try {\n const dir = spec.dir==='LR' ? 'RIGHT' : 'DOWN'\n const nById={}, gById={}\n spec.nodes.forEach(n=>nById[n.id]=n); spec.groups.forEach(g=>gById[g.id]=g)\n\n // 節點尺寸量測\n const size={}\n spec.nodes.forEach(n=>{\n if(n.items && n.items.length){ // 標題+項目節點(類 UML class box): 量標題與各項目 → 框寬=最寬列+邊距, 框高=標題列+各項目列\n const tm=measure(n.title||n.label, FS, 250), im=measureItems(n.items, FS-1)\n const headH=tm.h+12 // 標題列高(框頂到分隔線), 供分層繪製定位\n size[n.id]={ w:Math.max(tm.w, im.w)+30, h:headH+im.h+10, headH } // items 區高 = 實量塊高 + 上下 padding(6+4), 與渲染一致無多餘留白\n } else { const m=measure(n.label, FS, n.diamond?150:MAXW); if(n.diamond){ const d=Math.round(Math.max(m.w,m.h)*1.5+34); size[n.id]={w:d,h:d} } else size[n.id]={w:m.w+24,h:m.h+16} }\n })\n\n // 遞迴建 ELK 子樹(群組含 children)\n // 保序: 數據之節點順序=語義順序(選單/報告章節順序), 令 ELK 同層節點依 model order 排列, 不因交叉最小化重排(如 GIS2D/GIS3D 被拆開)。\n // considerModelOrder/forceNodeModelOrder 於 elkjs + INCLUDE_CHILDREN(巢狀)會內部崩潰, 故改用 semiInteractive:\n // crossingMinimization.semiInteractive + 各葉節點 elk.position 位置提示(以數據序為序), 同層節點依提示排序, 巢狀圖亦支援。\n // 例外: 含循環(如流程之退回邊)的圖, 位置提示會干擾分層使入口節點偏離頂端 → 根層不加提示(容器內仍保序)。\n const ORDER_OPTS = { 'elk.layered.crossingMinimization.semiInteractive':'true' }\n const hasCycle = (() => { const adj={}; spec.edges.forEach(e=>{ (adj[e.from]=adj[e.from]||[]).push(e.to) }); const st={}; const dfs=(u)=>{ st[u]=1; for(const v of (adj[u]||[])){ if(st[v]===1) return true; if(!st[v] && dfs(v)) return true } st[u]=2; return false }; return Object.keys(adj).some(u=>!st[u]&&dfs(u)) })()\n const gOpts = { 'elk.layered.nodePlacement.bk.fixedAlignment':'BALANCED', 'elk.padding':'[top=10,left=14,bottom=14,right=14]', 'elk.nodeLabels.placement':'[H_CENTER, V_TOP, INSIDE]', 'elk.algorithm':'layered', 'elk.direction':dir, 'elk.edgeRouting':'ORTHOGONAL', 'elk.hierarchyHandling':'INCLUDE_CHILDREN', 'elk.spacing.nodeNode':'28', 'elk.layered.spacing.nodeNodeBetweenLayers':'30', 'elk.spacing.edgeEdge':'22', 'elk.layered.spacing.edgeEdgeBetweenLayers':'24', ...ORDER_OPTS }\n function elkEdge(e){ let lab=[]; if(e.label){ const m=measure(e.label,EFS,300); lab=[{ text:e.label, width:m.w, height:m.h }] } return { id:e.id, sources:[e.from], targets:[e.to], labels:lab } }\n function build(parentId, padMap, wrapAR){\n const children=[], edges=[]\n spec.groups.filter(g=>g.parent===parentId).forEach(g=>{ const sub=build(g.id, padMap, wrapAR); const gm=measure(g.label,GFS,400)\n // 第二趟: 標題比容器寬時加大該容器左右 padding 撐寬(ELK 尊重複合容器 padding; 巢狀父容器自動跟著變寬, 不溢出又不過寬)\n const ex=(padMap&&padMap[g.id])||0\n const go={ ...gOpts, 'elk.padding':'[top=10,left='+(14+ex)+',bottom=14,right='+(14+ex)+']' }\n // A4 折排: 該容器被挑中時設 aspectRatio 觸發 ELK wrapping, 把線性鏈折成多欄/列(跨欄/列連接線由 ELK 自動正交路由)\n const ar=wrapAR&&wrapAR[g.id]; if(ar){ go['elk.aspectRatio']=String(ar); go['elk.layered.wrapping.strategy']='SINGLE_EDGE' }\n if(parentId || !hasCycle){ go['elk.position']='('+g.mi+','+g.mi+')' }\n children.push({ id:g.id, labels:[{text:g.label, width:gm.w+12, height:gm.h+6}], layoutOptions:go, children:sub.children, edges:sub.edges }) })\n spec.nodes.filter(n=>n.parent===parentId).forEach(n=>{ const va=VALIGN[(gById[parentId]&&gById[parentId].align)||DEF_VALIGN]||'TOP'; const lo={ 'elk.alignment':va }; if(parentId || !hasCycle){ lo['elk.position']='('+n.mi+','+n.mi+')' } children.push({ id:n.id, width:size[n.id].w, height:size[n.id].h, layoutOptions:lo }) })\n spec.edges.filter(e=>e.container===parentId).forEach(e=>{ edges.push(elkEdge(e)) })\n return { children, edges }\n }\n const rootOpts = { 'elk.layered.nodePlacement.bk.fixedAlignment':'BALANCED', 'elk.algorithm':'layered', 'elk.direction':dir, 'elk.edgeRouting':'ORTHOGONAL', 'elk.hierarchyHandling':'INCLUDE_CHILDREN', 'elk.spacing.nodeNode':'30', 'elk.layered.spacing.nodeNodeBetweenLayers':'34', 'elk.spacing.edgeNode':'24', 'elk.spacing.edgeEdge':'22', 'elk.layered.spacing.edgeEdgeBetweenLayers':'24', 'elk.layered.spacing.edgeNodeBetweenLayers':'26', ...(hasCycle ? { 'elk.layered.cycleBreaking.strategy':'MODEL_ORDER' } : ORDER_OPTS) }\n const elk = new ELK()\n const mkGraph = (pm, war) => { const rb = build(null, pm, war); return { id:'root', layoutOptions:rootOpts, children:rb.children, edges:rb.edges } }\n let res = await elk.layout(mkGraph({}, {}))\n // 第二趟: 偵測「標題寬 > 容器寬」之容器 → 加大其左右 padding 撐寬, 重新佈局(巢狀父容器自動跟進; 標題不溢出又不過寬)\n const padMap = {}\n ;(function collect(cell){ (cell.children||[]).forEach(ch=>{ if(gById[ch.id]){ const lw=(ch.labels&&ch.labels[0]&&ch.labels[0].width)||0; if(lw>(ch.width||0)) padMap[ch.id]=Math.ceil((lw-ch.width)/2)+6; collect(ch) } }) })(res)\n if(Object.keys(padMap).length) res = await elk.layout(mkGraph(padMap, {}))\n // ---- A4 長寬比自動折排 ----\n // A4 直式 h/w≈1.414、橫式≈0.707. 過高(R>HIGH)→挑最高之多子容器折成多欄縮高; 過寬(R<LOW)→挑最寬者折成多列增高.\n // 折排引擎=ELK wrapping(設 aspectRatio 觸發, 自動正交畫跨欄/列連接線); 逐次加深直到落入 A4 區間 / 無可折 / 達上限.\n const A4HIGH=1.45, A4LOW=0.70, wrapAR={}\n const foldable = () => { const out=[]; (function w(cell){ (cell.children||[]).forEach(ch=>{ if(gById[ch.id]){ if((ch.children||[]).length>=4) out.push({id:ch.id, w:ch.width||0, h:ch.height||0}); w(ch) } }) })(res); return out }\n for(let it=0; it<8; it++){\n const R=(res.height||1)/(res.width||1)\n if(R<=A4HIGH && R>=A4LOW) break\n const fs=foldable(); if(!fs.length) break\n let pick\n if(R>A4HIGH){ fs.sort((a,b)=>b.h-a.h); pick=fs.find(g=>(wrapAR[g.id]||0.6)<6) }\n else { fs.sort((a,b)=>b.w-a.w); pick=fs.find(g=>(wrapAR[g.id]||3.5)>0.22) }\n if(!pick) break\n wrapAR[pick.id] = R>A4HIGH ? (wrapAR[pick.id]||0.6)*1.9 : (wrapAR[pick.id]||3.5)/1.9\n res = await elk.layout(mkGraph(padMap, wrapAR))\n }\n\n // 遞迴收集: 容器(深度序)、節點(絕對座標)、邊(以容器絕對座標 + section 點)\n const groupsOut=[], nodesOut=[], edgesOut=[]\n function walk(cell, ox, oy, depth){\n ;(cell.children||[]).forEach(ch=>{\n const ax=ox+(ch.x||0), ay=oy+(ch.y||0)\n if (gById[ch.id]) { groupsOut.push({ g:gById[ch.id], x:ax, y:ay, w:ch.width, h:ch.height, depth, lbl:(ch.labels&&ch.labels[0])||null }); walk(ch, ax, ay, depth+1) }\n else if (nById[ch.id]) nodesOut.push({ n:nById[ch.id], x:ax, y:ay, w:ch.width, h:ch.height })\n })\n ;(cell.edges||[]).forEach(e=>{ edgesOut.push({ e:e, ox:ox, oy:oy }) })\n }\n walk(res, 0, 0, 0)\n\n const W=Math.ceil(res.width), H=Math.ceil(res.height), PAD=18\n let svg=''\n // 1) 容器(外層先畫=墊底; depth 小先畫)\n groupsOut.sort((a,b)=>a.depth-b.depth).forEach(o=>{\n svg+='<rect x="'+(o.x)+'" y="'+(o.y)+'" width="'+o.w+'" height="'+o.h+'" rx="8" fill="'+o.g.fill+'" fill-opacity="0.38" stroke="'+o.g.stroke+'" stroke-width="2"/>'\n const tw=o.lbl?o.lbl.width+12:o.w, th=o.lbl?o.lbl.height+6:24, tx=o.x+Math.max(0,(o.w-tw)/2), ty=o.y+(o.lbl?o.lbl.y:5)\n svg+='<foreignObject x="'+tx+'" y="'+ty+'" width="'+tw+'" height="'+th+'"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="text-align:center;font-weight:bold;font-size:'+GFS+'px;color:'+o.g.stroke+';line-height:1.3;white-space:pre-line;overflow-wrap:break-word;word-break:normal">'+esc(o.g.label)+'</div></foreignObject>'\n })\n // 2) 邊(在節點之下、容器之上)\n const nodeBoxById={}; nodesOut.forEach(o=>{ nodeBoxById[o.n.id]={ x:o.x, y:o.y, w:o.w, h:o.h, diamond:!!o.n.diamond } })\n const specEdgeById={}; spec.edges.forEach(se=>{ specEdgeById[se.id]=se })\n // 2a) 蒐集各 section 絕對路徑點 + 統計各菱形「各側」進出數(側=上/下/左/右, key: nodeId|軸|正負)。\n // 規則: 純出側→全部由頂點分岔; 純入側→各自貼斜邊; 混合側→全部貼斜邊\n // (混合側若連出走頂點+橫向接回, 橫向段必與夾在中間的連入直線交叉; 全貼斜邊則零共用點、零新增交叉)\n const drawList=[], sideMix={}\n edgesOut.forEach(o=>{\n const e=o.e, se=specEdgeById[e.id]\n const srcBox=se&&nodeBoxById[se.from], tgtBox=se&&nodeBoxById[se.to]\n const secs=(e.sections||[]).map(sec=>{\n const pts=[sec.startPoint].concat(sec.bendPoints||[]).concat([sec.endPoint]).map(p=>({x:o.ox+p.x, y:o.oy+p.y}))\n const item={ pts }\n if(srcBox&&srcBox.diamond){ item.srcSide=diamondSide(pts[0], srcBox); if(item.srcSide){ const k=se.from+'|'+item.srcSide.ax+item.srcSide.sg; (sideMix[k]=sideMix[k]||{in:0,out:0}).out++ } }\n if(tgtBox&&tgtBox.diamond){ item.tgtSide=diamondSide(pts[pts.length-1], tgtBox); if(item.tgtSide){ const k=se.to+'|'+item.tgtSide.ax+item.tgtSide.sg; (sideMix[k]=sideMix[k]||{in:0,out:0}).in++ } }\n return item\n })\n drawList.push({ o, e, se, srcBox, tgtBox, secs })\n })\n // 2a-2) 通用路徑化簡: ELK 階層邊(INCLUDE_CHILDREN)常見「先繞反方向再折返」之 S/U 形繞行,\n // 反覆對「三段式窗」(平行-垂直-平行)嘗試收斂成兩段, 僅在碰撞偵測全過才接受:\n // 不撞節點框/容器標題帶/邊標籤、不與他邊平行段貼齊併線; 端點段軸向與方向不可翻轉(維持節點進出方向,\n // 化簡先於菱形貼齊故不干擾貼齊規則); 本邊標籤若因化簡脫離路徑則整段還原(標籤座標由 ELK 依原路徑決定)。\n const obstacles=[]\n nodesOut.forEach(o=>{ obstacles.push({ x:o.x-8, y:o.y-8, w:o.w+16, h:o.h+16 }) })\n groupsOut.forEach(o=>{ if(!o.lbl) return; const tw=o.lbl.width+12, th=o.lbl.height+6, tx=o.x+Math.max(0,(o.w-tw)/2), ty=o.y+o.lbl.y; obstacles.push({ x:tx-4, y:ty-4, w:tw+8, h:th+8 }) })\n const labelBoxes=[]\n drawList.forEach(dd=>{ (dd.e.labels||[]).forEach(l=>{ if(!l.text) return; labelBoxes.push({ x:dd.o.ox+(l.x||0)-2, y:dd.o.oy+(l.y||0)-2, w:(l.width||40)+10, h:(l.height||18)+8 }) }) })\n function segHitsBox(a,b,box){ // 軸對齊線段 vs 矩形(線段必水平或垂直)\n const x1=Math.min(a.x,b.x), x2=Math.max(a.x,b.x), y1=Math.min(a.y,b.y), y2=Math.max(a.y,b.y)\n return x2>box.x && x1<box.x+box.w && y2>box.y && y1<box.y+box.h\n }\n function segOverlapsOther(a,b,selfItem){ // 與他邊平行段貼齊重疊(側距<6 且共線範圍>10)→ 視覺併線, 拒絕\n const horiz = Math.abs(a.y-b.y)<0.5\n for(const dd of drawList) for(const it of dd.secs){ if(it===selfItem) continue\n const q=it.pts\n for(let k=0;k+1<q.length;k++){ const c=q[k], d=q[k+1]\n if(horiz && Math.abs(c.y-d.y)<0.5 && Math.abs(c.y-a.y)<6){ const lo=Math.max(Math.min(a.x,b.x),Math.min(c.x,d.x)), hi=Math.min(Math.max(a.x,b.x),Math.max(c.x,d.x)); if(hi-lo>10) return true }\n if(!horiz && Math.abs(c.x-d.x)<0.5 && Math.abs(c.x-a.x)<6){ const lo=Math.max(Math.min(a.y,b.y),Math.min(c.y,d.y)), hi=Math.min(Math.max(a.y,b.y),Math.max(c.y,d.y)); if(hi-lo>10) return true }\n }\n }\n return false\n }\n // 標籤一律為硬障礙(含本邊自己的): ELK 邊標籤是「先佔位、路徑繞標籤走」, 原路的 jog 即標籤讓位,\n // 拉直會從自家標籤正中穿過(且「脫離才還原」守門對「穿過=距離變近」無感), 故不可豁免自家標籤\n function segClear(a,b,selfItem){\n for(const ob of obstacles) if(segHitsBox(a,b,ob)) return false\n for(const lb of labelBoxes) if(segHitsBox(a,b,lb)) return false\n return !segOverlapsOther(a,b,selfItem)\n }\n function mergeColl(p){ // 去重合點 + 併共線(含同線折返, 折返僅由化簡產生)\n for(let i=p.length-2;i>=0;i--) if(Math.abs(p[i].x-p[i+1].x)<0.5 && Math.abs(p[i].y-p[i+1].y)<0.5) p.splice(i,1)\n for(let i=p.length-2;i>=1;i--){ const A=p[i-1],B=p[i],C=p[i+1]\n if((Math.abs(A.x-B.x)<0.5 && Math.abs(B.x-C.x)<0.5) || (Math.abs(A.y-B.y)<0.5 && Math.abs(B.y-C.y)<0.5)) p.splice(i,1) }\n }\n function distToPoly(pt,p){ let d=Infinity\n for(let i=0;i+1<p.length;i++){ const a=p[i], b=p[i+1]\n const len2=Math.max(1,(b.x-a.x)*(b.x-a.x)+(b.y-a.y)*(b.y-a.y))\n const t=Math.max(0,Math.min(1, ((pt.x-a.x)*(b.x-a.x)+(pt.y-a.y)*(b.y-a.y))/len2 ))\n const qx=a.x+t*(b.x-a.x), qy=a.y+t*(b.y-a.y); d=Math.min(d, Math.hypot(pt.x-qx, pt.y-qy)) }\n return d\n }\n function simplifyOrtho(item, labels){\n const p=item.pts\n const before=p.map(q=>({x:q.x,y:q.y}))\n mergeColl(p)\n for(let pass=0; pass<20; pass++){\n let changed=false\n for(let i=0;i+3<p.length;i++){\n const isStart=(i===0), isEnd=(i+3===p.length-1)\n if(isStart && isEnd) continue // 4 點路徑已極簡, 兩端軸向無法同時保持\n const A=p[i], B=p[i+1], C=p[i+2], D=p[i+3]\n const s1h=Math.abs(A.y-B.y)<0.5\n // 取代點取窗之一角: 一般保持首段軸向; 末端窗改保持末段軸向(箭頭進入方向不可變)\n const P = isEnd ? (s1h ? {x:A.x,y:D.y} : {x:D.x,y:A.y}) : (s1h ? {x:D.x,y:A.y} : {x:A.x,y:D.y})\n if(isStart){ const sg0=s1h?Math.sign(B.x-A.x):Math.sign(B.y-A.y), sgN=s1h?Math.sign(P.x-A.x):Math.sign(P.y-A.y); if(sgN!==sg0) continue }\n if(isEnd){ const s3h=Math.abs(C.y-D.y)<0.5, sg0=s3h?Math.sign(D.x-C.x):Math.sign(D.y-C.y), sgN=s3h?Math.sign(D.x-P.x):Math.sign(D.y-P.y); if(sgN!==sg0) continue }\n if(!segClear(A,P,item) || !segClear(P,D,item)) continue\n p.splice(i+1, 2, P)\n mergeColl(p)\n changed=true\n break\n }\n if(!changed) break\n }\n // 標籤守門: 化簡若令本邊任一標籤脫離路徑(距離顯著變大), 整段還原\n if(labels.length && p.length !== before.length){\n for(const l of labels){\n if(distToPoly(l,p) > distToPoly(l,before)+2){ p.length=0; before.forEach(q=>p.push(q)); return }\n }\n }\n }\n drawList.forEach(dd=>{\n const labs=(dd.e.labels||[]).filter(l=>l.text).map(l=>({ x:dd.o.ox+(l.x||0)+(l.width||40)/2, y:dd.o.oy+(l.y||0)+(l.height||18)/2 }))\n dd.secs.forEach(item=>{ if(item.pts.length>=5) simplifyOrtho(item, labs) })\n })\n // 2b) 依側別規則貼齊並繪製\n const procSegsById={} // 實際繪製之路徑點(含菱形貼齊與 ensureStub), 供 geom 輸出一致\n drawList.forEach(dd=>{\n const o=dd.o, e=dd.e\n dd.secs.forEach(item=>{\n const pts=item.pts\n if(item.srcSide && pts.length >= 2) routeOutFromVertex(pts, item.srcSide) // R1: 出永遠自該側頂點\n if(item.tgtSide && pts.length >= 2){\n // R2: 純進側匯「頂點」; 混合側依「半側」匯「斜邊中點」(進出不同點)\n const m=sideMix[dd.se.to+'|'+item.tgtSide.ax+item.tgtSide.sg]\n const anchor=diamondInAnchor(dd.tgtBox, item.tgtSide, !!(m && m.out > 0), pts[pts.length-1])\n routeInToAnchor(pts, item.tgtSide, anchor)\n }\n // 分岔/匯入重排後清除共線折返: 頂點分岔之「接回原路第一轉折層」若跨過下一轉點, 會多走再折回疊出短勾, 併共線即塌縮為直接連線\n if(item.srcSide || item.tgtSide) mergeColl(pts)\n ensureStub(pts, 18, !!item.srcSide)\n ;(procSegsById[e.id]=procSegsById[e.id]||[]).push(pts)\n let d='M '+pts[0].x+' '+pts[0].y; for(let i=1;i<pts.length;i++) d+=' L '+pts[i].x+' '+pts[i].y\n const dash = (e.id && spec.edges.find(x=>x.id===e.id) && spec.edges.find(x=>x.id===e.id).dashed) ? ' stroke-dasharray="6 4"' : ''\n svg+='<path d="'+d+'" fill="none" stroke="'+EDGE_LINE+'" stroke-width="2"'+dash+'/>'\n // 箭頭(終點)\n const a=pts[pts.length-2], b=pts[pts.length-1], ang=Math.atan2(b.y-a.y,b.x-a.x), L=9, w=4\n const x1=b.x-L*Math.cos(ang)+w*Math.sin(ang), y1=b.y-L*Math.sin(ang)-w*Math.cos(ang)\n const x2=b.x-L*Math.cos(ang)-w*Math.sin(ang), y2=b.y-L*Math.sin(ang)+w*Math.cos(ang)\n svg+='<path d="M '+b.x+' '+b.y+' L '+x1+' '+y1+' L '+x2+' '+y2+' Z" fill="'+EDGE_LINE+'"/>'\n })\n // 邊標籤(白光暈)\n ;(e.labels||[]).forEach(l=>{ if(!l.text) return; const lx=o.ox+(l.x||0), ly=o.oy+(l.y||0), lw=l.width||40, lh=l.height||18\n svg+='<foreignObject x="'+lx+'" y="'+ly+'" width="'+(lw+6)+'" height="'+(lh+4)+'"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="font-size:'+EFS+'px;color:'+EDGE_TEXT+';text-shadow:0 0 3px '+HALO+',0 0 3px '+HALO+',0 0 3px '+HALO+',0 0 3px '+HALO+';white-space:pre-line">'+esc(l.text)+'</div></foreignObject>' })\n })\n // 3) 節點(最上)。各文字 div 之斷行 CSS(overflow-wrap:break-word + word-break:normal)須與 measure()/measureItems() 一致,\n // 否則長英文識別字量測時折行、渲染時不折 → 單行溢出 foreignObject 被裁切\n nodesOut.forEach(o=>{\n const n=o.n\n if(n.items && n.items.length){ // 標題+項目框: 依序分層(框背景→標題背景→框線→分隔線→標題→items), 使框線置頂不被標題背景遮蔽、線寬完整\n const headH=(size[n.id]&&size[n.id].headH)||26, r=7, sep=o.y+headH\n const lis=n.items.map(it=>'<div style="white-space:nowrap">•&#160;'+esc(it)+'</div>').join('')\n svg+='<rect x="'+o.x+'" y="'+o.y+'" width="'+o.w+'" height="'+o.h+'" rx="'+r+'" fill="'+n.fill+'"/>' // 1 框背景\n svg+='<path d="M '+(o.x+r)+' '+o.y+' H '+(o.x+o.w-r)+' A '+r+' '+r+' 0 0 1 '+(o.x+o.w)+' '+(o.y+r)+' V '+sep+' H '+o.x+' V '+(o.y+r)+' A '+r+' '+r+' 0 0 1 '+(o.x+r)+' '+o.y+' Z" fill="'+mix(n.fill,n.stroke,0.16)+'"/>' // 2 標題背景(上圓角下平)\n svg+='<rect x="'+o.x+'" y="'+o.y+'" width="'+o.w+'" height="'+o.h+'" rx="'+r+'" fill="none" stroke="'+n.stroke+'" stroke-width="1.6"/>' // 3 框線(頂層)\n svg+='<line x1="'+o.x+'" y1="'+sep+'" x2="'+(o.x+o.w)+'" y2="'+sep+'" stroke="'+n.stroke+'" stroke-width="1"/>' // 4 分隔線\n svg+='<foreignObject x="'+o.x+'" y="'+o.y+'" width="'+o.w+'" height="'+headH+'"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;text-align:center;font-weight:bold;font-size:'+FS+'px;color:'+n.font+'"><div style="width:100%;min-width:0;white-space:pre-line;overflow-wrap:break-word;word-break:normal">'+esc(n.title||n.label)+'</div></div></foreignObject>' // 5 標題文字\n svg+='<foreignObject x="'+o.x+'" y="'+sep+'" width="'+o.w+'" height="'+(o.h-headH)+'"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="font-size:'+(FS-1)+'px;line-height:1.55;padding:6px 9px 4px;text-align:left;color:'+n.font+'">'+lis+'</div></foreignObject>' // 6 items文字\n } else if(n.diamond){ const cx=o.x+o.w/2, cy=o.y+o.h/2\n svg+='<polygon points="'+cx+','+o.y+' '+(o.x+o.w)+','+cy+' '+cx+','+(o.y+o.h)+' '+o.x+','+cy+'" fill="'+n.fill+'" stroke="'+n.stroke+'" stroke-width="1.8"/>'\n svg+='<foreignObject x="'+o.x+'" y="'+o.y+'" width="'+o.w+'" height="'+o.h+'"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;text-align:center;font-size:'+FS+'px;color:'+n.font+';line-height:1.4;padding:0 4px;box-sizing:border-box"><div style="width:100%;min-width:0;white-space:pre-line;overflow-wrap:break-word;word-break:normal">'+esc(n.label)+'</div></div></foreignObject>'\n } else {\n svg+='<rect x="'+o.x+'" y="'+o.y+'" width="'+o.w+'" height="'+o.h+'" rx="7" fill="'+n.fill+'" stroke="'+n.stroke+'" stroke-width="1.6"/>'\n svg+='<foreignObject x="'+o.x+'" y="'+o.y+'" width="'+o.w+'" height="'+o.h+'"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;text-align:center;font-size:'+FS+'px;color:'+n.font+';line-height:1.4;padding:0 4px;box-sizing:border-box"><div style="width:100%;min-width:0;white-space:pre-line;overflow-wrap:break-word;word-break:normal">'+esc(n.label)+'</div></div></foreignObject>'\n }\n })\n\n const full='<svg xmlns="http://www.w3.org/2000/svg" width="'+(W+PAD*2)+'" height="'+(H+PAD*2)+'" viewBox="'+(-PAD)+' '+(-PAD)+' '+(W+PAD*2)+' '+(H+PAD*2)+'" style="background:#fff">'+svg+'</svg>'\n document.getElementById('stage').innerHTML = full\n // 幾何輸出(供 test 斷言: 存在性/包含/標題不溢出/重疊/A4比例/items框高); 不影響 SVG/PNG\n const geom = {\n W:W+PAD*2, H:H+PAD*2,\n nodes: nodesOut.map(o=>({ id:o.n.id, x:o.x, y:o.y, w:o.w, h:o.h, parent:o.n.parent||null, kind:(o.n.items&&o.n.items.length)?'items':(o.n.diamond?'diamond':'box'), headH:(size[o.n.id]&&size[o.n.id].headH)||null })),\n groups: groupsOut.map(o=>({ id:o.g.id, x:o.x, y:o.y, w:o.w, h:o.h, depth:o.depth, parent:o.g.parent||null, labelW:(o.lbl&&o.lbl.width)||0 })),\n edges: edgesOut.map(o=>({ id:o.e.id, segs: procSegsById[o.e.id]||[] }))\n }\n return { ok:true, w:W+PAD*2, h:H+PAD*2, geom }\n } catch(e){ return { ok:false, err:String(e&&(e.stack||e.message)||e).slice(0,400) } }\n}\nwindow.__ready = true\n<\/script></body></html>`;async function rr(n){const t=await e.chromium.launch();try{const e=await t.newPage({deviceScaleFactor:2});await e.setContent(tr,{waitUntil:"load"}),await e.waitForFunction(()=>window.__ready,{timeout:6e4}).catch(()=>{}),await e.evaluate(()=>document.fonts&&document.fonts.ready);const r=await e.evaluate(e=>window.renderFig(e),nr(n));if(!r.ok)throw new Error("p10 render failed: "+r.err);return await e.evaluate(()=>document.fonts&&document.fonts.ready),await e.waitForTimeout(150),{browser:t,page:e}}catch(e){throw await t.close(),e}}var or=Object.freeze({__proto__:null,PAGE_HTML:tr,genPng:async function(e,n={}){const{browser:t,page:r}=await rr(e);try{return await r.locator("#stage svg").screenshot()}finally{await t.close()}},genSvg:async function(e,n={}){const{browser:t,page:r}=await rr(e);try{return(await r.evaluate(()=>document.getElementById("stage").innerHTML)).replace(/&nbsp;/g,"&#160;").trim()}finally{await t.close()}},translate:nr});return async function(e,n={}){let t=ie(n,"mode");qn(t)||(t="p10");const r=t.trim().toLowerCase();if(!Vn[r])throw new Error(`invalid opt.mode[${t}], 須為 ${Object.keys(Vn).join(", ")} 之一`);let o=ie(n,"output");qn(o)||(o="png");const a=o.trim().toLowerCase();if("png"!==a&&"svg"!==a)throw new Error(`invalid opt.output[${o}], 須為 png 或 svg`);if("svg"===a&&!Zn.includes(r))throw new Error(`mode[${r}] 不支援 svg 輸出, 支援: ${Zn.join(", ")}`);if(!function(e){if(Xn(e)){for(let n in e)return!0;return!1}return!1}(e))throw new Error("invalid inp, 須為 { dir, nodes, edges } 正規化繪圖數據");if(!Yn(i=e.nodes)||0===i.length||1===i.length&&Un(i[0]))throw new Error("invalid inp.nodes, 須為非空陣列");var i;if(!Yn(e.edges))throw new Error("invalid inp.edges, 須為陣列");const s=((l=Wn(e,5)).nodes.forEach(e=>{e.title&&null==e.label&&(e.label=e.title+(e.items&&e.items.length?" ("+e.items.join("/")+")":""))}),l);var l;const c=await Vn[r]();return await("svg"===a?c.genSvg:c.genPng)(s,n)}});
7
7
  //# sourceMappingURL=w-flowchart.umd.js.map