@oh-just-another/diagram 0.1.0

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.
Files changed (39) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +68 -0
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/bindings.d.ts +73 -0
  5. package/dist/bindings.d.ts.map +1 -0
  6. package/dist/bindings.js +77 -0
  7. package/dist/bindings.js.map +1 -0
  8. package/dist/index.d.ts +18 -0
  9. package/dist/index.d.ts.map +1 -0
  10. package/dist/index.js +20 -0
  11. package/dist/index.js.map +1 -0
  12. package/dist/oja-diagram-element.d.ts +42 -0
  13. package/dist/oja-diagram-element.d.ts.map +1 -0
  14. package/dist/oja-diagram-element.js +202 -0
  15. package/dist/oja-diagram-element.js.map +1 -0
  16. package/dist/oja-diagram.global.js +1846 -0
  17. package/dist/oja-diagram.global.js.map +7 -0
  18. package/dist/render-worker.js +5 -0
  19. package/dist/render-worker.js.map +7 -0
  20. package/dist/styles.generated.d.ts +2 -0
  21. package/dist/styles.generated.d.ts.map +1 -0
  22. package/dist/styles.generated.js +4 -0
  23. package/dist/styles.generated.js.map +1 -0
  24. package/fonts/LICENSE +25 -0
  25. package/fonts/PTSerif-Bold.woff2 +0 -0
  26. package/fonts/PTSerif-BoldItalic.woff2 +0 -0
  27. package/fonts/PTSerif-Italic.woff2 +0 -0
  28. package/fonts/PTSerif-Regular.woff2 +0 -0
  29. package/fonts/Roboto-Bold.woff2 +0 -0
  30. package/fonts/Roboto-BoldItalic.woff2 +0 -0
  31. package/fonts/Roboto-Italic.woff2 +0 -0
  32. package/fonts/Roboto-Regular.woff2 +0 -0
  33. package/fonts/RobotoMono-Bold.woff2 +0 -0
  34. package/fonts/RobotoMono-BoldItalic.woff2 +0 -0
  35. package/fonts/RobotoMono-Italic.woff2 +0 -0
  36. package/fonts/RobotoMono-Regular.woff2 +0 -0
  37. package/package.json +72 -0
  38. package/wasm/rasterizer.wasm +0 -0
  39. package/wasm/text_shaper.wasm +0 -0
@@ -0,0 +1,5 @@
1
+ var Pe=Object.defineProperty;var K=(t,e)=>{for(var n in e)Pe(t,n,{get:e[n],enumerable:!0})};var ce=new Map,_=(t,e)=>{ce.set(t,e)},de=t=>ce.get(t);var q=(t,e,n)=>{if(!t||t.type==="sharp")return 0;let o=Math.min(Math.abs(e),Math.abs(n));if(o<=0)return 0;if(t.value!==void 0)return Math.max(0,Math.min(t.value,o/2));let r=32/.25;return o<=r?o*.25:32};var x=t=>{if(t===void 0)throw new Error("required value is undefined");return t};var A={};K(A,{IDENTITY:()=>Ne,applyToBounds:()=>qe,applyToPoint:()=>D,decompose:()=>Ve,equals:()=>Ye,inverse:()=>Ke,multiply:()=>Xe,of:()=>He,rotation:()=>Ue,scaling:()=>ze,translation:()=>Ge});var Ne=Object.freeze({a:1,b:0,c:0,d:1,e:0,f:0}),He=(t,e,n,o,r,i)=>({a:t,b:e,c:n,d:o,e:r,f:i}),Ge=(t,e)=>({a:1,b:0,c:0,d:1,e:t,f:e}),ze=(t,e=t)=>({a:t,b:0,c:0,d:e,e:0,f:0}),Ue=t=>{let e=Math.cos(t),n=Math.sin(t);return{a:e,b:n,c:-n,d:e,e:0,f:0}},Xe=(t,e)=>({a:t.a*e.a+t.c*e.b,b:t.b*e.a+t.d*e.b,c:t.a*e.c+t.c*e.d,d:t.b*e.c+t.d*e.d,e:t.a*e.e+t.c*e.f+t.e,f:t.b*e.e+t.d*e.f+t.f}),Ke=t=>{let e=t.a*t.d-t.b*t.c;if(e===0)throw new Error("Cannot invert singular matrix");return{a:t.d/e,b:-t.b/e,c:-t.c/e,d:t.a/e,e:(t.c*t.f-t.d*t.e)/e,f:(t.b*t.e-t.a*t.f)/e}},D=(t,e)=>({x:t.a*e.x+t.c*e.y+t.e,y:t.b*e.x+t.d*e.y+t.f}),qe=(t,e)=>{let n=D(t,{x:e.x,y:e.y}),o=D(t,{x:e.x+e.width,y:e.y}),r=D(t,{x:e.x,y:e.y+e.height}),i=D(t,{x:e.x+e.width,y:e.y+e.height}),a=Math.min(n.x,o.x,r.x,i.x),l=Math.min(n.y,o.y,r.y,i.y),s=Math.max(n.x,o.x,r.x,i.x),c=Math.max(n.y,o.y,r.y,i.y);return{x:a,y:l,width:s-a,height:c-l}},Ve=t=>{let e=Math.sqrt(t.a*t.a+t.b*t.b),n=Math.sqrt(t.c*t.c+t.d*t.d),r=t.a*t.d-t.b*t.c<0?-n:n;return{translation:{x:t.e,y:t.f},rotation:Math.atan2(t.b,t.a),scale:{x:e,y:r}}},Ye=(t,e,n=0)=>{let o=["a","b","c","d","e","f"];return n===0?o.every(r=>t[r]===e[r]):o.every(r=>Math.abs(t[r]-e[r])<=n)};var E={};K(E,{EMPTY:()=>me,centerOf:()=>Ze,contains:()=>tt,containsBounds:()=>nt,equals:()=>it,expand:()=>ot,fromCenter:()=>$e,fromPoints:()=>Je,intersection:()=>he,intersects:()=>et,isEmpty:()=>V,maxX:()=>L,maxY:()=>I,normalize:()=>rt,of:()=>je,union:()=>Qe});var me=Object.freeze({x:0,y:0,width:0,height:0}),je=(t,e,n,o)=>({x:t,y:e,width:n,height:o}),Je=t=>{if(t.length===0)return me;let e=1/0,n=1/0,o=-1/0,r=-1/0;for(let i of t)i.x<e&&(e=i.x),i.y<n&&(n=i.y),i.x>o&&(o=i.x),i.y>r&&(r=i.y);return{x:e,y:n,width:o-e,height:r-n}},$e=(t,e,n)=>({x:t.x-e/2,y:t.y-n/2,width:e,height:n}),Ze=t=>({x:t.x+t.width/2,y:t.y+t.height/2}),L=t=>t.x+t.width,I=t=>t.y+t.height,V=t=>t.width<=0||t.height<=0,Qe=(t,e)=>{if(V(t))return e;if(V(e))return t;let n=Math.min(t.x,e.x),o=Math.min(t.y,e.y),r=Math.max(L(t),L(e)),i=Math.max(I(t),I(e));return{x:n,y:o,width:r-n,height:i-o}},he=(t,e)=>{let n=Math.max(t.x,e.x),o=Math.max(t.y,e.y),r=Math.min(L(t),L(e)),i=Math.min(I(t),I(e));return r<=n||i<=o?null:{x:n,y:o,width:r-n,height:i-o}},et=(t,e)=>he(t,e)!==null,tt=(t,e)=>e.x>=t.x&&e.x<=L(t)&&e.y>=t.y&&e.y<=I(t),nt=(t,e)=>e.x>=t.x&&e.y>=t.y&&L(e)<=L(t)&&I(e)<=I(t),ot=(t,e)=>({x:t.x-e,y:t.y-e,width:t.width+2*e,height:t.height+2*e}),rt=t=>({x:t.width<0?t.x+t.width:t.x,y:t.height<0?t.y+t.height:t.y,width:Math.abs(t.width),height:Math.abs(t.height)}),it=(t,e,n=0)=>n===0?t.x===e.x&&t.y===e.y&&t.width===e.width&&t.height===e.height:Math.abs(t.x-e.x)<=n&&Math.abs(t.y-e.y)<=n&&Math.abs(t.width-e.width)<=n&&Math.abs(t.height-e.height)<=n;var M={};K(M,{offsetClosedPath:()=>st,signedArea:()=>at});var st=(t,e)=>{if(t.length<3||e===0)return t.map(s=>({x:s.x,y:s.y}));let n=0,o=0;for(let s of t)n+=s.x,o+=s.y;n/=t.length,o/=t.length;let r=t.length,i=new Array(r),a=new Array(r);for(let s=0;s<r;s++){let c=x(t[s]),d=x(t[(s+1)%r]),m=d.x-c.x,f=d.y-c.y,y=Math.hypot(m,f)||1;i[s]=-f/y,a[s]=m/y}let l=[];for(let s=0;s<r;s++){let c=(s-1+r)%r,d=x(i[c]),m=x(a[c]),f=x(i[s]),y=x(a[s]),w=d+f,p=m+y,T=Math.hypot(w,p);T<1e-6?(w=d,p=m):(w/=T,p/=T);let O=x(t[s]),b=n-O.x,h=o-O.y,R=w*b+p*h>=0?1:-1,ae=w*d+p*m,le=ae>1e-6?e/ae:e;l.push({x:O.x+R*w*le,y:O.y+R*p*le})}return l},at=t=>{let e=0;for(let n=0;n<t.length;n++){let o=x(t[n]),r=x(t[(n+1)%t.length]);e+=o.x*r.y-r.x*o.y}return e/2};var lt=null;var ue=()=>lt;var Y=t=>t.type==="text";var fe=new Map,k=(t,e)=>{fe.set(t,e)};var xe=t=>{let e=fe.get(t.type);if(!e)throw new Error(`No bounder registered for shape type: ${t.type}`);return e(t)},j=t=>{let e=xe(t),n=[{x:e.x,y:e.y},{x:e.x+e.width,y:e.y},{x:e.x,y:e.y+e.height},{x:e.x+e.width,y:e.y+e.height}],o=Math.sin(t.rotation),r=Math.cos(t.rotation),i=n.map(a=>{let l=a.x*t.scale.x,s=a.y*t.scale.y;return{x:t.position.x+(l*r-s*o),y:t.position.y+(l*o+s*r)}});return E.fromPoints(i)};k("rectangle",t=>({x:0,y:0,width:t.width,height:t.height}));k("ellipse",t=>({x:0,y:0,width:t.width,height:t.height}));k("polygon",t=>E.fromPoints(t.points));k("path",t=>{let e=[],n={x:0,y:0};for(let o of t.commands)switch(o.kind){case"M":case"L":e.push(o.to),n=o.to;break;case"Q":e.push(o.control,o.to),n=o.to;break;case"C":e.push(o.control1,o.control2,o.to),n=o.to;break;case"Z":break}return E.fromPoints(e)});k("text",t=>{let e=t.fontSize*1.2,n=t.text.split(`
2
+ `),o=ue(),r={bold:t.style.fontWeight==="bold",italic:t.style.fontStyle==="italic"},i=l=>{if(o){let s=o(l,t.fontFamily,t.fontSize,r);if(s!==null)return s}return l.length*t.fontSize*.6};if(t.maxWidth===void 0){let l=0;for(let s of n)l=Math.max(l,i(s));return l=Math.max(l,t.fontSize*.5),{x:0,y:0,width:l,height:Math.max(1,n.length)*e}}let a=0;for(let l of n)a+=Math.max(1,Math.ceil(i(l)/t.maxWidth));return{x:0,y:0,width:t.maxWidth,height:Math.max(1,a)*e}});k("image",t=>({x:0,y:0,width:t.width,height:t.height}));k("template",t=>({x:0,y:0,width:t.width,height:t.height}));k("brush",t=>{if(t.points.length===0)return{x:0,y:0,width:0,height:0};let e=1/0,n=1/0,o=-1/0,r=-1/0;for(let i of t.points)i.x-i.width<e&&(e=i.x-i.width),i.y-i.width<n&&(n=i.y-i.width),i.x+i.width>o&&(o=i.x+i.width),i.y+i.width>r&&(r=i.y+i.width);return{x:e,y:n,width:o-e,height:r-n}});k("group",()=>({x:0,y:0,width:0,height:0}));k("frame",t=>({x:0,y:0,width:t.width,height:t.height}));k("block-arrow",t=>({x:0,y:0,width:t.width,height:t.height}));var J=(t,e)=>t.order<e.order?-1:t.order>e.order?1:0;var $=t=>[...t.layers.values()].sort(J),H=(t,e)=>[...t.elements.values()].filter(n=>n.layerId===e).sort(J);var ct=new Map,Z=(t,e)=>{ct.set(t,e)};var dt=Object.freeze({pan:{x:0,y:0},zoom:1,rotation:0,size:{width:0,height:0},gridEnabled:!1}),Q=t=>{let e=A.translation(-t.pan.x,-t.pan.y),n=A.rotation(t.rotation),o=A.scaling(t.zoom);return A.multiply(o,A.multiply(n,e))};var F=class{entries=new Map;get(e){let n=this.entries.get(e.id);if(n){if(n.ref!==e){this.entries.delete(e.id);return}return n.value}}set(e,n){return this.entries.set(e.id,{ref:e,value:n}),n}getOrCompute(e,n){let o=this.get(e);return o!==void 0?o:this.set(e,n(e))}invalidate(e){this.entries.delete(e)}clear(){this.entries.clear()}prune(e){for(let n of this.entries.keys())e.elements.has(n)||this.entries.delete(n)}get size(){return this.entries.size}},$n=new F,P=(t,e)=>t.getOrCompute(e,j);var ye="#bbb";var pe=.06,ge=.92,be=.5;var Re=.4,Te=.5,ee=.1,te=.9,we="#888",Ee="#ffffff",ke="#222",_e="#ddd";var v=class{cap;onEvict;map=new Map;constructor(e,n){this.cap=e,this.onEvict=n}get(e){let n=this.map.get(e);if(n!==void 0)return this.map.delete(e),this.map.set(e,n),n}has(e){return this.map.has(e)}set(e,n){for(this.map.delete(e),this.map.set(e,n);this.map.size>this.cap;){let o=this.map.keys().next().value,r=this.map.get(o);this.map.delete(o),r!==void 0&&this.onEvict?.(o,r)}}delete(e){return this.map.delete(e)}clear(){this.map.clear()}get size(){return this.map.size}keys(){return this.map.keys()}values(){return this.map.values()}};var Ae=t=>t<=0?1:2**Math.round(Math.log2(t));var ne=(t,e,n={})=>{let o=Q(t.viewport),r=n.dirtyWorld;if(!n.skipClear)if(r){let b=[A.applyToPoint(o,{x:r.x,y:r.y}),A.applyToPoint(o,{x:r.x+r.width,y:r.y+r.height})],h=E.expand(E.fromPoints(b),2);e.clear(h)}else e.clear();e.save(),e.setTransform(o);let i=n.boundsCache??new F,a=n.viewport,l=null;a&&n.spatialIndex&&(l=n.spatialIndex.query(a));let s=t.viewport.zoom,c=n.lod,d=c?.placeholder!==void 0&&s<c.placeholder,m=c?.hideText!==void 0&&s<c.hideText,f=n.placeholderFill??ye,y=n.layerCompositeCache,w=n.dirtyLayerIds,p=n.compositeLayerBitmap,T=Ae(s),O=b=>{let h=null;for(let g of H(t,b)){let R=P(i,g);h=h?E.union(h,R):R}return h};for(let b of $(t))if(b.visible){if(y&&p){w?.has(b.id)&&y.invalidateLayer(b.id);let h=y.get(b.id,T);if(h===void 0){let g=p(b.id,T,t);g!==null&&(y.set(b.id,T,g),h=g)}if(h!==void 0){let g=O(b.id);g&&e.drawImage(h,g.x,g.y,g.width,g.height);continue}}for(let h of H(t,b.id)){if(n.hideElements?.has(h.id)||l&&!l.has(h.id))continue;if(a){let R=P(i,h);if(!E.intersects(R,a))continue}if(r){let R=P(i,h);if(!E.intersects(R,r))continue}if(m&&Y(h))continue;if(d){let R=P(i,h);e.setFill(f),e.setStrokeWidth(0),e.beginPath(),e.rect(R.x,R.y,R.width,R.height),e.fill();continue}let g=de(h.type);if(!g){n.onUnknownElement?.(h);continue}e.save(),n.dimElements?.has(h.id)&&n.dimOpacity!==void 0&&e.setOpacity(n.dimOpacity),e.translate(h.position.x,h.position.y),h.rotation!==0&&e.rotate(h.rotation),(h.scale.x!==1||h.scale.y!==1)&&e.scale(h.scale.x,h.scale.y),g(h,e,{zoom:s}),e.restore()}}e.restore()};var mt=1.2,ht=(t,e,n,o,r)=>{if(t===""){r.push({text:"",start:e,end:e,width:0});return}let i=[],a=/\S+/g,l;for(;(l=a.exec(t))!==null;)i.push({s:l.index,e:l.index+l[0].length});if(i.length===0){r.push({text:t,start:e,end:e+t.length,width:o(t)});return}let s=0,c=(m,f)=>{let y=t.slice(m,f);r.push({text:y,start:e+m,end:e+f,width:o(y)})},d=0;for(;d<i.length;){let m=x(i[d]);if(o(t.slice(s,m.e))<=n){d++;continue}if(m.s>s){c(s,m.s),s=m.s;continue}let f=m.s+1;for(;f<m.e&&o(t.slice(s,f+1))<=n;)f++;if(f>=m.e){d++;continue}c(s,f),s=f,m.s=f}c(s,t.length)},Le=(t,e,n)=>{let o=n.fontSize*(n.lineHeightFactor??mt),r=[],i=0;for(let s=0;s<=t.length;s++)if(s===t.length||t[s]===`
3
+ `){let c=t.slice(i,s);n.maxWidth===void 0?r.push({text:c,start:i,end:s,width:e(c)}):ht(c,i,n.maxWidth,e,r),i=s+1}r.length===0&&r.push({text:"",start:0,end:0,width:0});let a=0;for(let s of r)a=Math.max(a,s.width);let l=n.maxWidth??a;return{lines:r,lineHeight:o,blockWidth:l}};var ut=new Map;var ft=()=>typeof performance<"u"?performance.now():0;var oe=(t,e=ft(t))=>{if(!t.animationKind)return t.src;let n=ut.get(t.animationKind);if(!n)return t.src;try{return n.getFrameAt(t.animationData,e)}catch{return t.src}};var B=(t,e)=>{let n=t.fill!==void 0&&t.fill!=="transparent",o=t.stroke!==void 0&&t.stroke!=="transparent"&&(t.strokeWidth??1)>0;return n&&e.setFill(t.fill),o&&(e.setStroke(t.stroke),e.setStrokeWidth(t.strokeWidth??1),t.lineCap&&e.setLineCap(t.lineCap),t.lineJoin&&e.setLineJoin(t.lineJoin),t.dashArray&&e.setDashArray(t.dashArray)),t.opacity!==void 0&&e.setOpacity(t.opacity),{fill:n,stroke:o}},yt=(t,e)=>{let{fill:n,stroke:o}=B(t.style,e);if(!n&&!o)return;let r=q(t.style.roundness,t.width,t.height);if(n&&(e.beginPath(),r>0?Ie(e,0,0,t.width,t.height,r):e.rect(0,0,t.width,t.height),e.fill()),o){let i=U(t.style),a=i,l=i,s=t.width-2*i,c=t.height-2*i;if(s<=0||c<=0)return;let d=r>0?Math.max(0,r-i):0;e.beginPath(),d>0?Ie(e,a,l,s,c,d):e.rect(a,l,s,c),e.stroke()}},U=t=>{let e=t.strokeAlign??"center";if(e==="center")return 0;let n=(t.strokeWidth??1)/2;return e==="inside"?n:-n},Ie=(t,e,n,o,r,i)=>{t.moveTo(e+i,n),t.lineTo(e+o-i,n),t.quadraticCurveTo(e+o,n,e+o,n+i),t.lineTo(e+o,n+r-i),t.quadraticCurveTo(e+o,n+r,e+o-i,n+r),t.lineTo(e+i,n+r),t.quadraticCurveTo(e,n+r,e,n+r-i),t.lineTo(e,n+i),t.quadraticCurveTo(e,n,e+i,n),t.closePath()},pt=(t,e)=>{let{fill:n,stroke:o}=B(t.style,e);if(!n&&!o)return;let r=t.width/2,i=t.height/2;if(n&&(e.beginPath(),e.ellipse(r,i,r,i),e.fill()),o){let a=U(t.style),l=r-a,s=i-a;if(l<=0||s<=0)return;e.beginPath(),e.ellipse(r,i,l,s),e.stroke()}},gt=(t,e)=>{if(t.points.length<2)return;let{fill:n,stroke:o}=B(t.style,e);if(!(!n&&!o)&&(n&&(e.beginPath(),z(e,t.points),e.fill()),o)){let r=U(t.style),i=r!==0?M.offsetClosedPath(t.points,r):t.points;e.beginPath(),z(e,i),e.stroke()}},z=(t,e)=>{let n=e[0];if(n!==void 0){t.moveTo(n.x,n.y);for(let o=1;o<e.length;o++){let r=e[o];r!==void 0&&t.lineTo(r.x,r.y)}t.closePath()}},bt=(t,e)=>{if(t.commands.length===0)return;let{fill:n,stroke:o}=B(t.style,e);if(!(!n&&!o)){e.beginPath();for(let r of t.commands)switch(r.kind){case"M":e.moveTo(r.to.x,r.to.y);break;case"L":e.lineTo(r.to.x,r.to.y);break;case"Q":e.quadraticCurveTo(r.control.x,r.control.y,r.to.x,r.to.y);break;case"C":e.bezierCurveTo(r.control1.x,r.control1.y,r.control2.x,r.control2.y,r.to.x,r.to.y);break;case"Z":e.closePath();break}n&&e.fill(),o&&e.stroke()}},Rt=(t,e)=>{let n=t.style.textAlign??"left",o=t.style.fontWeight,r=t.style.fontStyle;e.setFont(t.fontFamily,t.fontSize,{...o?{weight:o}:{},...r?{style:r}:{}}),e.setTextAlign("left"),e.setTextBaseline(t.style.textBaseline??"top");let i=t.style.fill??"#000";e.setFill(i),t.style.opacity!==void 0&&e.setOpacity(t.style.opacity);let a=t.fontSize,l;if(t.maxWidth===void 0&&!t.text.includes(`
4
+ `))l=[{text:t.text,x:0,width:e.measureText(t.text).width,top:0}];else{let c=m=>e.measureText(m).width,d=Le(t.text,c,{fontSize:a,...t.maxWidth!==void 0?{maxWidth:t.maxWidth}:{}});l=d.lines.map((m,f)=>({text:m.text,x:n==="center"?d.blockWidth/2-m.width/2:n==="right"?d.blockWidth-m.width:0,width:m.width,top:f*d.lineHeight}))}for(let c of l)e.fillText(c.text,c.x,c.top);let s=t.style.textDecoration;if(s?.underline||s?.strikethrough){let c=Math.max(1,a*pe);for(let d of l)d.width<=0||(s.underline&&(e.beginPath(),e.rect(d.x,d.top+a*ge,d.width,c),e.fill()),s.strikethrough&&(e.beginPath(),e.rect(d.x,d.top+a*be-c/2,d.width,c),e.fill()))}},Tt=(t,e)=>{let n=t.points;if(n.length===0)return;let o=t.style.fill??t.style.stroke??"#000";if(e.setFill(o),e.setStroke(null),n.length===1){let r=x(n[0]);e.beginPath(),e.ellipse(r.x,r.y,r.width,r.width),e.fill();return}for(let r=0;r<n.length-1;r++){let i=x(n[r]),a=x(n[r+1]),l=a.x-i.x,s=a.y-i.y,c=Math.hypot(l,s)||1,d=-s/c,m=l/c;e.beginPath(),e.moveTo(i.x+d*i.width,i.y+m*i.width),e.lineTo(a.x+d*a.width,a.y+m*a.width),e.lineTo(a.x-d*a.width,a.y-m*a.width),e.lineTo(i.x-d*i.width,i.y-m*i.width),e.closePath(),e.fill(),e.beginPath(),e.ellipse(a.x,a.y,a.width,a.width),e.fill()}},wt=(t,e)=>{let n=t.animationKind?oe(t):t.metadata?.image??oe(t),o=t.metadata?.animated===!0||t.animationKind!==void 0;e.drawImage(n,0,0,t.width,t.height,o)},se=()=>{_("rectangle",yt),_("ellipse",pt),_("polygon",gt),_("path",bt),_("text",Rt),_("image",wt),_("group",()=>{}),_("frame",At),Z("frame",()=>({top:24})),_("block-arrow",Et),_("brush",Tt)},Et=(t,e)=>{let{fill:n,stroke:o}=B(t.style,e),r=t.direction??"right",i=Math.max(ee,Math.min(te,t.headRatio??Re)),a=Math.max(ee,Math.min(te,t.bodyThickness??Te)),l=t.width,s=t.height,c=l*i,d=l-c,m=s*a/2,f=s/2,y=[[0,f-m],[d,f-m],[d,0],[l,f],[d,s],[d,f+m],[0,f+m]];r!=="right"&&(y=y.map(([p,T])=>kt([p,T],r,l,s)));let w=y.map(([p,T])=>({x:p,y:T}));if(n&&(e.beginPath(),z(e,w),e.fill()),o){let p=U(t.style),T=p!==0?M.offsetClosedPath(w,p):w;e.beginPath(),z(e,T),e.stroke()}},kt=([t,e],n,o,r)=>{switch(n){case"left":return[o-t,e];case"up":return[e*(o/r),r-t*(r/o)];case"down":return[(r-e)*(o/r),t*(r/o)]}},ie="\u2026",_t=(t,e,n)=>{if(e<=0)return"";if(n.measureText(t).width<=e)return t;let o=0,r=t.length;for(;o<r;){let i=Math.ceil((o+r)/2);n.measureText(t.slice(0,i)+ie).width<=e?o=i:r=i-1}return o>0?t.slice(0,o)+ie:ie},At=(t,e,n)=>{e.setFill(t.style.fill??Ee),e.setStroke(null),e.setDashArray(null),e.beginPath(),e.rect(0,0,t.width,t.height),e.fill();let o=(n?.zoom??1)*(t.scale.x||1);e.setFill(null),e.setStroke(we),e.setStrokeWidth(1/(o||1)),e.setDashArray(null),e.beginPath(),e.rect(0,0,t.width,t.height),e.stroke();let r=t.name??"Frame";e.setFont("system-ui, sans-serif",12);let i=t.width-16,a=e.measureText(r).width<=i,l=a?r:_t(r,i,e),s=a?Math.min(e.measureText(r).width+16,t.width):t.width;e.setFill(ke),e.beginPath(),e.rect(0,-24,s,24),e.fill(),e.setFill(_e),e.setTextBaseline("middle"),e.setTextAlign("left"),e.fillText(l,8,-24/2)};var S="Roboto",W="PT Serif",N="Roboto Mono",ve=t=>{let e=t.toLowerCase();return e.includes("mono")?N:e.includes("sans")?S:e.includes("serif")||e.includes("slab")||e.includes("georgia")||e.includes("times")?W:S},Lt=[{family:S,weight:"400",style:"normal",url:new URL("../fonts/Roboto-Regular.woff2",import.meta.url)},{family:S,weight:"700",style:"normal",url:new URL("../fonts/Roboto-Bold.woff2",import.meta.url)},{family:S,weight:"400",style:"italic",url:new URL("../fonts/Roboto-Italic.woff2",import.meta.url)},{family:S,weight:"700",style:"italic",url:new URL("../fonts/Roboto-BoldItalic.woff2",import.meta.url)},{family:W,weight:"400",style:"normal",url:new URL("../fonts/PTSerif-Regular.woff2",import.meta.url)},{family:W,weight:"700",style:"normal",url:new URL("../fonts/PTSerif-Bold.woff2",import.meta.url)},{family:W,weight:"400",style:"italic",url:new URL("../fonts/PTSerif-Italic.woff2",import.meta.url)},{family:W,weight:"700",style:"italic",url:new URL("../fonts/PTSerif-BoldItalic.woff2",import.meta.url)},{family:N,weight:"400",style:"normal",url:new URL("../fonts/RobotoMono-Regular.woff2",import.meta.url)},{family:N,weight:"700",style:"normal",url:new URL("../fonts/RobotoMono-Bold.woff2",import.meta.url)},{family:N,weight:"400",style:"italic",url:new URL("../fonts/RobotoMono-Italic.woff2",import.meta.url)},{family:N,weight:"700",style:"italic",url:new URL("../fonts/RobotoMono-BoldItalic.woff2",import.meta.url)}],Ce=async(t=globalThis)=>{let e=t.fonts;!e||typeof FontFace>"u"||await Promise.allSettled(Lt.map(async n=>{let o=new FontFace(n.family,`url(${n.url.href})`,{weight:n.weight,style:n.style});await o.load(),e.add(o)}))};var It=["HTMLImageElement","HTMLCanvasElement","HTMLVideoElement","ImageBitmap","OffscreenCanvas","SVGImageElement","VideoFrame"],Se=t=>{if(typeof t!="object"||t===null)return!1;let e=globalThis;for(let n of It){let o=e[n];if(typeof o=="function"&&t instanceof o)return!0}return!1},Oe=new Set,De=t=>{if(typeof console>"u")return;let e=typeof t=="string"?t.startsWith("blob:")?"dead-blob-url":"string-src":t==null?"empty":"stale-object";Oe.has(e)||(Oe.add(e),console.warn(`[renderer] skipped a non-drawable image source (kind: ${e}). The shape's image handle isn't a live HTMLImageElement / canvas / bitmap \u2014 likely a scene restored from storage where the <img> was lost (metadata.image) and src is a dead blob: URL. The image won't render until rehydrated from Scene.files.`))};var X=class{ctx;_width;_height;dpr;constructor(e,n,o,r=1){this.ctx=e,this._width=n,this._height=o,this.dpr=r}get size(){return{width:this._width,height:this._height}}resize(e,n,o){this._width=e,this._height=n,o!==void 0&&(this.dpr=o)}setFill(e){this.ctx.fillStyle=e??"transparent"}setStroke(e){this.ctx.strokeStyle=e??"transparent"}setStrokeWidth(e){this.ctx.lineWidth=e}setOpacity(e){this.ctx.globalAlpha=e}setLineCap(e){this.ctx.lineCap=e}setLineJoin(e){this.ctx.lineJoin=e}setDashArray(e){this.ctx.setLineDash(e?[...e]:[])}setFont(e,n,o){let r=o?.style==="italic"?"italic ":"",i=o?.weight==="bold"?"bold ":"";this.ctx.font=`${r}${i}${n}px "${ve(e)}", ${e}`}setTextAlign(e){this.ctx.textAlign=e==="center"?"center":e}setTextBaseline(e){this.ctx.textBaseline=e==="middle"?"middle":e==="top"?"top":"bottom"}save(){this.ctx.save()}restore(){this.ctx.restore()}translate(e,n){this.ctx.translate(e,n)}rotate(e){this.ctx.rotate(e)}scale(e,n){this.ctx.scale(e,n)}setTransform(e){let n=this.dpr;this.ctx.setTransform(n*e.a,n*e.b,n*e.c,n*e.d,n*e.e,n*e.f)}resetTransform(){this.ctx.setTransform(this.dpr,0,0,this.dpr,0,0)}beginPath(){this.ctx.beginPath()}closePath(){this.ctx.closePath()}moveTo(e,n){this.ctx.moveTo(e,n)}lineTo(e,n){this.ctx.lineTo(e,n)}quadraticCurveTo(e,n,o,r){this.ctx.quadraticCurveTo(e,n,o,r)}bezierCurveTo(e,n,o,r,i,a){this.ctx.bezierCurveTo(e,n,o,r,i,a)}rect(e,n,o,r){this.ctx.rect(e,n,o,r)}ellipse(e,n,o,r){this.ctx.ellipse(e,n,o,r,0,0,Math.PI*2)}fill(e){this.ctx.fill(e)}stroke(){this.ctx.stroke()}fillText(e,n,o,r){r!==void 0?this.ctx.fillText(e,n,o,r):this.ctx.fillText(e,n,o)}measureText(e){return{width:this.ctx.measureText(e).width}}drawImage(e,n,o,r,i,a){if(!Se(e)){De(e);return}this.ctx.drawImage(e,n,o,r,i)}clear(e){this.dirtyRect=null,e?this.ctx.clearRect(e.x,e.y,e.width,e.height):(this.ctx.save(),this.ctx.setTransform(1,0,0,1,0,0),this.ctx.clearRect(0,0,this.ctx.canvas.width,this.ctx.canvas.height),this.ctx.restore())}dirtyRect=null;markDirty(e){if(!this.dirtyRect){this.dirtyRect=e;return}let n=Math.min(this.dirtyRect.x,e.x),o=Math.min(this.dirtyRect.y,e.y),r=Math.max(this.dirtyRect.x+this.dirtyRect.width,e.x+e.width),i=Math.max(this.dirtyRect.y+this.dirtyRect.height,e.y+e.height);this.dirtyRect={x:n,y:o,width:r-n,height:i-o}}getDirtyRect(){return this.dirtyRect}};var Me=(t,e,n=new v(64))=>{for(let o of e)switch(o.k){case"setFill":t.setFill(o.color);break;case"setStroke":t.setStroke(o.color);break;case"setStrokeWidth":t.setStrokeWidth(o.w);break;case"setOpacity":t.setOpacity(o.a);break;case"setLineCap":t.setLineCap(o.cap);break;case"setLineJoin":t.setLineJoin(o.join);break;case"setDashArray":t.setDashArray(o.dash);break;case"setFont":t.setFont(o.family,o.size,o.options);break;case"setTextAlign":t.setTextAlign(o.align);break;case"setTextBaseline":t.setTextBaseline(o.baseline);break;case"save":t.save();break;case"restore":t.restore();break;case"translate":t.translate(o.x,o.y);break;case"rotate":t.rotate(o.r);break;case"scale":t.scale(o.sx,o.sy);break;case"setTransform":t.setTransform(o.t);break;case"resetTransform":t.resetTransform();break;case"beginPath":t.beginPath();break;case"closePath":t.closePath();break;case"moveTo":t.moveTo(o.x,o.y);break;case"lineTo":t.lineTo(o.x,o.y);break;case"quadraticCurveTo":t.quadraticCurveTo(o.cx,o.cy,o.x,o.y);break;case"bezierCurveTo":t.bezierCurveTo(o.c1x,o.c1y,o.c2x,o.c2y,o.x,o.y);break;case"rect":t.rect(o.x,o.y,o.w,o.h);break;case"ellipse":t.ellipse(o.cx,o.cy,o.rx,o.ry);break;case"fill":o.rule!==void 0?t.fill(o.rule):t.fill();break;case"stroke":t.stroke();break;case"fillText":o.maxWidth!==void 0?t.fillText(o.text,o.x,o.y,o.maxWidth):t.fillText(o.text,o.x,o.y);break;case"clear":o.bounds!==void 0?t.clear(o.bounds):t.clear();break;case"markDirty":t.markDirty?.(o.bounds);break;case"resize":break;case"defineImage":n.set(o.id,o.bitmap);break;case"drawImage":{let r=n.get(o.id);r&&t.drawImage(r,o.dx,o.dy,o.dw,o.dh);break}}};var u={canvas:null,target:null,dpr:1,images:new v(64,(t,e)=>{e.close()})},Fe=!1,Ct=()=>{Fe||(se(),Fe=!0)},C=(t,e)=>{e&&e.length>0?self.postMessage(t,e):self.postMessage(t)},Ot=(t,e,n,o)=>{Ce(self),u.canvas=t,u.dpr=o,t.width=Math.max(1,Math.round(e*o)),t.height=Math.max(1,Math.round(n*o));let r=t.getContext("2d");if(!r)throw new Error("OffscreenCanvas 2D context unavailable");r.setTransform(o,0,0,o,0,0),u.target=new X(r,e,n,o),C({type:"ready"})},St=(t,e)=>{if(!u.canvas||!u.target)return;u.canvas.width=Math.max(1,Math.round(t*u.dpr)),u.canvas.height=Math.max(1,Math.round(e*u.dpr));let n=u.canvas.getContext("2d");n&&(n.setTransform(u.dpr,0,0,u.dpr,0,0),u.target.resize(t,e,u.dpr))},Dt=t=>{if(!u.canvas||!u.target){C({type:"error",message:"Worker not initialised"});return}Ct();let e=u.canvas.getContext("2d");if(e===null){C({type:"error",message:"Worker not initialised"});return}e.save(),e.setTransform(u.dpr,0,0,u.dpr,0,0),e.clearRect(0,0,u.target.size.width,u.target.size.height),ne(t,u.target),e.restore();let n=u.canvas.transferToImageBitmap();C({type:"frame-done",bitmap:n},[n])},Mt=t=>{if(!u.target){C({type:"error",message:"Worker not initialised"});return}Me(u.target,t,u.images)};self.addEventListener("message",t=>{let e=t.data;try{switch(e.type){case"init":Ot(e.canvas,e.width,e.height,e.dpr);break;case"resize":St(e.width,e.height);break;case"snapshot":Dt(e.scene);break;case"replay":Mt(e.commands);break;case"frame":C({type:"error",message:"patch-stream frames not implemented"});break}}catch(n){C({type:"error",message:n instanceof Error?n.message:String(n)})}});
5
+ //# sourceMappingURL=render-worker.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../renderer-core/src/shape-renderer.ts", "../../scene/src/style.ts", "../../types/src/assert.ts", "../../math/src/matrix.ts", "../../math/src/bounds.ts", "../../math/src/polygon.ts", "../../scene/src/text-measure.ts", "../../scene/src/shape.ts", "../../scene/src/order.ts", "../../scene/src/queries.ts", "../../scene/src/render-bounds.ts", "../../scene/src/viewport.ts", "../../renderer-core/src/shape-cache.ts", "../../renderer-core/src/constants.ts", "../../renderer-core/src/lru-cache.ts", "../../renderer-core/src/shape-cache-bitmap.ts", "../../renderer-core/src/scene-renderer.ts", "../../renderer-core/src/text-editing.ts", "../../renderer-core/src/animation-adapter.ts", "../../renderer-core/src/built-in-renderers.ts", "../../fonts/src/index.ts", "../../renderer-canvas/src/image-source.ts", "../../renderer-canvas/src/canvas-target.ts", "../../renderer-canvas/src/recording-target.ts", "../../renderer-canvas/src/render-worker.ts"],
4
+ "sourcesContent": ["import type { ElementBase } from \"@oh-just-another/scene\";\nimport type { RenderTarget } from \"./render-target.js\";\n\n/**\n * Optional draw context passed to an {@link ElementRenderer}. Carries the\n * current view `zoom` so a renderer can draw screen-constant features (e.g. a\n * 1px hairline border that does NOT scale with zoom): a local stroke width of\n * `1 / (zoom * shape.scale)` lands at one device pixel. Optional and additive \u2014\n * renderers that don't need it ignore the third argument, and callers that\n * can't supply it (preview / export at 1:1) may omit it.\n */\nexport interface ElementRenderContext {\n /** Current view scale (1.0 = 1:1). `world \u00D7 zoom = screen px`. */\n readonly zoom: number;\n}\n\n/**\n * Draws a single shape onto `target`. The shape's `position` / `rotation` /\n * `scale` have already been applied to the target \u2014 implementations draw in\n * the shape's *local* coordinate space.\n *\n * Implementations should also apply style (fill / stroke / etc.) themselves;\n * the renderer-core does not push styles globally because some shapes (e.g.\n * `text`) extend the base `Style` with overlays.\n */\nexport type ElementRenderer<S extends ElementBase = ElementBase> = (\n shape: S,\n target: RenderTarget,\n ctx?: ElementRenderContext,\n) => void;\n\nconst registry = new Map<string, ElementRenderer>();\n\n/**\n * Register a renderer for a shape type. Plugins call this at module load.\n * The kernel ships renderers for every built-in shape from `@oh-just-another/scene`\n * \u2014 they are installed by `@oh-just-another/renderer-canvas` (and any other\n * backend) on import.\n */\nexport const registerElementRenderer = <S extends ElementBase>(\n type: S[\"type\"],\n renderer: ElementRenderer<S>,\n): void => {\n registry.set(type, renderer as ElementRenderer);\n};\n\n/** Look up a registered renderer. Returns `undefined` for unknown types. */\nexport const getElementRenderer = (type: string): ElementRenderer | undefined => registry.get(type);\n\n/** True if a renderer is registered for `type`. */\nexport const hasElementRenderer = (type: string): boolean => registry.has(type);\n", "import type { Color } from \"@oh-just-another/types\";\nimport { ADAPTIVE_CORNER_RADIUS, PROPORTIONAL_CORNER_RADIUS } from \"./constants.js\";\n\nexport type LineCap = \"butt\" | \"round\" | \"square\";\nexport type LineJoin = \"miter\" | \"round\" | \"bevel\";\n\n/**\n * Where the stroke sits relative to the shape's path.\n * `center` \u2014 half the stroke width inside the path, half outside.\n * Canvas2D / SVG default.\n * `inside` \u2014 stroke is fully inside the path (path = outer edge).\n * Useful when shape bounds must match the fill region\n * exactly (auto-layout / hit-tests).\n * `outside` \u2014 stroke is fully outside (path = inner edge).\n */\nexport type StrokeAlign = \"center\" | \"inside\" | \"outside\";\n\n/**\n * Corner-rounding spec for shapes that support it (rectangle, container,\n * box arrow, \u2026):\n * `sharp` \u2014 no rounding (sharp corners). Equivalent to omitting the\n * field; lets the value be set explicitly.\n * `round` \u2014 rounded corners. Without `value`, falls back to the\n * adaptive radius (fixed 32 px for big shapes, scales to\n * 25 % of the smaller side for shapes < 128 px so they\n * don't read as a capsule).\n */\nexport interface Roundness {\n readonly type: \"sharp\" | \"round\";\n /**\n * Override the rounded-corner radius in world units. Ignored when\n * `type === \"sharp\"`. When omitted on `round` shapes the renderer\n * applies the adaptive default (see {@link Style}).\n */\n readonly value?: number;\n}\n\n/**\n * Visual style for shapes and edges. Every field is optional so that scenes,\n * patches and partial updates stay compact; renderers fall back to library\n * defaults when a field is omitted.\n */\nexport interface Style {\n readonly fill?: Color;\n readonly stroke?: Color;\n readonly strokeWidth?: number;\n readonly opacity?: number;\n readonly dashArray?: readonly number[];\n readonly lineCap?: LineCap;\n readonly lineJoin?: LineJoin;\n /** Stroke alignment relative to the path. Defaults to `center`. */\n readonly strokeAlign?: StrokeAlign;\n /** Corner-rounding spec. Omitted = sharp corners. */\n readonly roundness?: Roundness;\n}\n\nexport type TextAlign = \"left\" | \"center\" | \"right\";\nexport type TextBaseline = \"top\" | \"middle\" | \"bottom\";\nexport type FontWeight = \"normal\" | \"bold\";\nexport type FontStyle = \"normal\" | \"italic\";\n\n/**\n * Text decorations (underline / strikethrough). Both can be on at once.\n * Rendered as thin line-rects under / through the text by the renderer,\n * so they work identically on Canvas2D and WebGL2.\n */\nexport interface TextDecoration {\n readonly underline?: boolean;\n readonly strikethrough?: boolean;\n}\n\n/**\n * Text-specific style overlay. Inherits all `Style` fields (fill = text color,\n * stroke = outline). Layout metrics live on the `TextElement` itself, not here.\n */\nexport interface TextStyle extends Style {\n readonly textAlign?: TextAlign;\n readonly textBaseline?: TextBaseline;\n /** Bold toggle. Default `\"normal\"`. */\n readonly fontWeight?: FontWeight;\n /** Italic toggle. Default `\"normal\"`. */\n readonly fontStyle?: FontStyle;\n /** Underline / strikethrough. Omitted = neither. */\n readonly textDecoration?: TextDecoration;\n}\n\n/**\n * How far a shape's stroke extends OUTSIDE its geometric contour, in world\n * units. Depends on stroke width and alignment: `outside` \u2192 the full width,\n * `center` \u2192 half, `inside` \u2192 none. No stroke \u2192 0. Used to place the\n * selection halo a constant distance beyond the shape's VISIBLE outer edge\n * (contour + this extent), regardless of border thickness / alignment.\n */\nexport const strokeOutsideExtent = (style: Style): number => {\n const hasStroke = style.stroke !== undefined && style.stroke !== \"transparent\";\n if (!hasStroke) return 0;\n const w = style.strokeWidth ?? 1;\n if (w <= 0) return 0;\n switch (style.strokeAlign ?? \"center\") {\n case \"outside\":\n return w;\n case \"inside\":\n return 0;\n default:\n return w / 2;\n }\n};\n\nexport const getCornerRadius = (\n roundness: Roundness | undefined,\n width: number,\n height: number,\n): number => {\n if (!roundness || roundness.type === \"sharp\") return 0;\n const smaller = Math.min(Math.abs(width), Math.abs(height));\n if (smaller <= 0) return 0;\n if (roundness.value !== undefined) {\n // Honour the override but clamp to half the smaller side so\n // the corner radii can't overlap on narrow shapes (would\n // produce a degenerate path).\n return Math.max(0, Math.min(roundness.value, smaller / 2));\n }\n // Adaptive default: proportional below the cutoff, fixed above.\n const cutoff = ADAPTIVE_CORNER_RADIUS / PROPORTIONAL_CORNER_RADIUS;\n if (smaller <= cutoff) return smaller * PROPORTIONAL_CORNER_RADIUS;\n return ADAPTIVE_CORNER_RADIUS;\n};\n", "/**\n * Return `v` when defined, otherwise throw. For narrowing values the caller\n * knows are present (in-range array access, resolved lookups) without scattering\n * non-null assertions.\n */\nexport const req = <T>(v: T | undefined): T => {\n if (v === undefined) throw new Error(\"required value is undefined\");\n return v;\n};\n", "import type { Bounds, Transform, Vec2 } from \"@oh-just-another/types\";\n\nexport const IDENTITY: Transform = Object.freeze({ a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 });\n\nexport const of = (\n a: number,\n b: number,\n c: number,\n d: number,\n e: number,\n f: number,\n): Transform => ({ a, b, c, d, e, f });\n\nexport const translation = (tx: number, ty: number): Transform => ({\n a: 1,\n b: 0,\n c: 0,\n d: 1,\n e: tx,\n f: ty,\n});\n\nexport const scaling = (sx: number, sy: number = sx): Transform => ({\n a: sx,\n b: 0,\n c: 0,\n d: sy,\n e: 0,\n f: 0,\n});\n\nexport const rotation = (radians: number): Transform => {\n const cos = Math.cos(radians);\n const sin = Math.sin(radians);\n return { a: cos, b: sin, c: -sin, d: cos, e: 0, f: 0 };\n};\n\n/**\n * Matrix product `a \u00D7 b`. Composition is right-to-left: applying the result\n * to a point is equivalent to applying `b` first, then `a`.\n */\nexport const multiply = (a: Transform, b: Transform): Transform => ({\n a: a.a * b.a + a.c * b.b,\n b: a.b * b.a + a.d * b.b,\n c: a.a * b.c + a.c * b.d,\n d: a.b * b.c + a.d * b.d,\n e: a.a * b.e + a.c * b.f + a.e,\n f: a.b * b.e + a.d * b.f + a.f,\n});\n\nexport const inverse = (t: Transform): Transform => {\n const det = t.a * t.d - t.b * t.c;\n if (det === 0) throw new Error(\"Cannot invert singular matrix\");\n return {\n a: t.d / det,\n b: -t.b / det,\n c: -t.c / det,\n d: t.a / det,\n e: (t.c * t.f - t.d * t.e) / det,\n f: (t.b * t.e - t.a * t.f) / det,\n };\n};\n\nexport const applyToPoint = (t: Transform, p: Vec2): Vec2 => ({\n x: t.a * p.x + t.c * p.y + t.e,\n y: t.b * p.x + t.d * p.y + t.f,\n});\n\n/**\n * Axis-aligned bounding box of `b` after applying `t`. The result is the AABB\n * of the four transformed corners \u2014 tighter approaches exist for pure rotations\n * but this is correct for any affine transform.\n */\nexport const applyToBounds = (t: Transform, b: Bounds): Bounds => {\n const p1 = applyToPoint(t, { x: b.x, y: b.y });\n const p2 = applyToPoint(t, { x: b.x + b.width, y: b.y });\n const p3 = applyToPoint(t, { x: b.x, y: b.y + b.height });\n const p4 = applyToPoint(t, { x: b.x + b.width, y: b.y + b.height });\n const minX = Math.min(p1.x, p2.x, p3.x, p4.x);\n const minY = Math.min(p1.y, p2.y, p3.y, p4.y);\n const maxX = Math.max(p1.x, p2.x, p3.x, p4.x);\n const maxY = Math.max(p1.y, p2.y, p3.y, p4.y);\n return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };\n};\n\nexport interface DecomposedTransform {\n readonly translation: Vec2;\n /** Radians, range (-\u03C0, \u03C0]. */\n readonly rotation: number;\n readonly scale: Vec2;\n}\n\n/**\n * Extracts translate / rotate / scale (TRS) from a transform. Assumes the matrix\n * encodes only translate/rotate/uniform-or-axis-aligned-scale (no skew). For matrices\n * with skew the decomposition is approximate.\n */\nexport const decompose = (t: Transform): DecomposedTransform => {\n const sx = Math.sqrt(t.a * t.a + t.b * t.b);\n const sy = Math.sqrt(t.c * t.c + t.d * t.d);\n const det = t.a * t.d - t.b * t.c;\n const sySigned = det < 0 ? -sy : sy;\n return {\n translation: { x: t.e, y: t.f },\n rotation: Math.atan2(t.b, t.a),\n scale: { x: sx, y: sySigned },\n };\n};\n\nexport const equals = (a: Transform, b: Transform, epsilon = 0): boolean => {\n const fields: readonly (keyof Transform)[] = [\"a\", \"b\", \"c\", \"d\", \"e\", \"f\"];\n if (epsilon === 0) return fields.every((k) => a[k] === b[k]);\n return fields.every((k) => Math.abs(a[k] - b[k]) <= epsilon);\n};\n", "import type { Bounds, Vec2 } from \"@oh-just-another/types\";\n\nexport const EMPTY: Bounds = Object.freeze({ x: 0, y: 0, width: 0, height: 0 });\n\nexport const of = (x: number, y: number, width: number, height: number): Bounds => ({\n x,\n y,\n width,\n height,\n});\n\nexport const fromPoints = (points: readonly Vec2[]): Bounds => {\n if (points.length === 0) return EMPTY;\n let minX = Infinity;\n let minY = Infinity;\n let maxXv = -Infinity;\n let maxYv = -Infinity;\n for (const p of points) {\n if (p.x < minX) minX = p.x;\n if (p.y < minY) minY = p.y;\n if (p.x > maxXv) maxXv = p.x;\n if (p.y > maxYv) maxYv = p.y;\n }\n return { x: minX, y: minY, width: maxXv - minX, height: maxYv - minY };\n};\n\nexport const fromCenter = (center: Vec2, width: number, height: number): Bounds => ({\n x: center.x - width / 2,\n y: center.y - height / 2,\n width,\n height,\n});\n\nexport const centerOf = (b: Bounds): Vec2 => ({\n x: b.x + b.width / 2,\n y: b.y + b.height / 2,\n});\n\nexport const maxX = (b: Bounds): number => b.x + b.width;\nexport const maxY = (b: Bounds): number => b.y + b.height;\n\n/** True if width or height is non-positive. */\nexport const isEmpty = (b: Bounds): boolean => b.width <= 0 || b.height <= 0;\n\nexport const union = (a: Bounds, b: Bounds): Bounds => {\n if (isEmpty(a)) return b;\n if (isEmpty(b)) return a;\n const x = Math.min(a.x, b.x);\n const y = Math.min(a.y, b.y);\n const xMax = Math.max(maxX(a), maxX(b));\n const yMax = Math.max(maxY(a), maxY(b));\n return { x, y, width: xMax - x, height: yMax - y };\n};\n\n/** Returns null if the intersection is empty. */\nexport const intersection = (a: Bounds, b: Bounds): Bounds | null => {\n const x = Math.max(a.x, b.x);\n const y = Math.max(a.y, b.y);\n const xMax = Math.min(maxX(a), maxX(b));\n const yMax = Math.min(maxY(a), maxY(b));\n if (xMax <= x || yMax <= y) return null;\n return { x, y, width: xMax - x, height: yMax - y };\n};\n\nexport const intersects = (a: Bounds, b: Bounds): boolean => intersection(a, b) !== null;\n\nexport const contains = (b: Bounds, point: Vec2): boolean =>\n point.x >= b.x && point.x <= maxX(b) && point.y >= b.y && point.y <= maxY(b);\n\nexport const containsBounds = (outer: Bounds, inner: Bounds): boolean =>\n inner.x >= outer.x &&\n inner.y >= outer.y &&\n maxX(inner) <= maxX(outer) &&\n maxY(inner) <= maxY(outer);\n\nexport const expand = (b: Bounds, padding: number): Bounds => ({\n x: b.x - padding,\n y: b.y - padding,\n width: b.width + 2 * padding,\n height: b.height + 2 * padding,\n});\n\n/** Flips negative width/height so that x/y is the top-left corner. */\nexport const normalize = (b: Bounds): Bounds => ({\n x: b.width < 0 ? b.x + b.width : b.x,\n y: b.height < 0 ? b.y + b.height : b.y,\n width: Math.abs(b.width),\n height: Math.abs(b.height),\n});\n\nexport const equals = (a: Bounds, b: Bounds, epsilon = 0): boolean => {\n if (epsilon === 0) {\n return a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height;\n }\n return (\n Math.abs(a.x - b.x) <= epsilon &&\n Math.abs(a.y - b.y) <= epsilon &&\n Math.abs(a.width - b.width) <= epsilon &&\n Math.abs(a.height - b.height) <= epsilon\n );\n};\n", "import { req, type Vec2 } from \"@oh-just-another/types\";\n\n/**\n * Offset a closed polygon's vertices along the bisector at each corner.\n * Positive `distance` moves vertices inward (toward the centroid), negative\n * moves outward; both winding orders are handled by projecting the bisector\n * onto the toward-centroid vector and flipping the sign as needed.\n *\n * Each vertex moves `distance / cos(angle/2)` along the bisector of its two\n * adjacent edges (miter offset of the polygon's stroked outline).\n *\n * The bisector clamps at very sharp angles (cos < 1e-6) to avoid pixel-spike\n * artefacts. Concave polygons whose centroid lies outside the polygon can flip\n * inward/outward sign on isolated vertices. Polygons with fewer than 3 vertices\n * are returned unchanged.\n */\nexport const offsetClosedPath = (points: readonly Vec2[], distance: number): Vec2[] => {\n if (points.length < 3 || distance === 0) return points.map((p) => ({ x: p.x, y: p.y }));\n\n // Centroid as an interior reference, used to disambiguate inward / outward\n // direction regardless of vertex winding order.\n let cx = 0;\n let cy = 0;\n for (const p of points) {\n cx += p.x;\n cy += p.y;\n }\n cx /= points.length;\n cy /= points.length;\n\n const n = points.length;\n // Edge unit normals \u2014 rotate each edge vector 90\u00B0.\n const nx = new Array<number>(n);\n const ny = new Array<number>(n);\n for (let i = 0; i < n; i++) {\n const a = req(points[i]);\n const b = req(points[(i + 1) % n]);\n const dx = b.x - a.x;\n const dy = b.y - a.y;\n const len = Math.hypot(dx, dy) || 1;\n nx[i] = -dy / len;\n ny[i] = dx / len;\n }\n\n const out: Vec2[] = [];\n for (let i = 0; i < n; i++) {\n const prev = (i - 1 + n) % n;\n const n1x = req(nx[prev]);\n const n1y = req(ny[prev]);\n const n2x = req(nx[i]);\n const n2y = req(ny[i]);\n let bx = n1x + n2x;\n let by = n1y + n2y;\n const blen = Math.hypot(bx, by);\n if (blen < 1e-6) {\n // 180\u00B0 turn \u2014 bisector ill-defined. Use one of the normals.\n bx = n1x;\n by = n1y;\n } else {\n bx /= blen;\n by /= blen;\n }\n // Inward = toward centroid; check the bisector's component along\n // (centroid - vertex).\n const vertex = req(points[i]);\n const towardCx = cx - vertex.x;\n const towardCy = cy - vertex.y;\n const dot = bx * towardCx + by * towardCy;\n const sign = dot >= 0 ? 1 : -1;\n const cos = bx * n1x + by * n1y;\n const miterLen = cos > 1e-6 ? distance / cos : distance;\n out.push({\n x: vertex.x + sign * bx * miterLen,\n y: vertex.y + sign * by * miterLen,\n });\n }\n return out;\n};\n\n/**\n * Twice the signed polygon area via the shoelace formula. Positive =\n * counter-clockwise in y-up coordinates / clockwise in y-down. Use the sign\n * to detect winding order; abs/2 = polygon area.\n */\nexport const signedArea = (points: readonly Vec2[]): number => {\n let s = 0;\n for (let i = 0; i < points.length; i++) {\n const a = req(points[i]);\n const b = req(points[(i + 1) % points.length]);\n s += a.x * b.y - b.x * a.y;\n }\n return s / 2;\n};\n", "/**\n * Optional host-provided text measurer. The text bounder\n * (`getElementLocalBounds` for `TextElement`) is otherwise purely\n * geometric (`chars \u00D7 fontSize \u00D7 factor`), which can diverge a lot\n * from the actually-rendered width \u2014 the WebGL2 MSDF path draws with a\n * WASM-baked font whose glyph advances differ from any geometric\n * estimate, so the selection box would not hug the text.\n *\n * A host (the interaction layer) injects a measurer backed by the\n * renderer's `measureText` (which itself matches the active text\n * backend). When set, the bounder uses it for accurate width; when\n * absent (headless / tests), it falls back to the geometric estimate.\n *\n * Returns the measured width in world px, or `null` to defer to the\n * geometric estimate for that call. `opts` carries weight/style so the\n * measurer matches the *rendered* width \u2014 bold/italic change glyph\n * advances, so without it the bounds lag behind a bolded text and the\n * glyphs overflow the element box.\n */\nexport interface TextMeasureOpts {\n readonly bold?: boolean;\n readonly italic?: boolean;\n}\n\nexport type TextMeasurer = (\n text: string,\n fontFamily: string,\n fontSize: number,\n opts?: TextMeasureOpts,\n) => number | null;\n\nlet activeMeasurer: TextMeasurer | null = null;\n\n/** Install (or clear, with `null`) the active text measurer. */\nexport const setTextMeasurer = (measurer: TextMeasurer | null): void => {\n activeMeasurer = measurer;\n};\n\n/** The active text measurer, or `null` when none is installed. */\nexport const getTextMeasurer = (): TextMeasurer | null => activeMeasurer;\n", "import type { Bounds, FileId, LayerId, ElementId, Vec2 } from \"@oh-just-another/types\";\nimport type { FractionalIndex } from \"fractional-keys\";\nimport { bounds as B } from \"@oh-just-another/math\";\nimport type { AnchorRef } from \"./edge.js\";\nimport type { Style, TextStyle } from \"./style.js\";\nimport { TEXT_APPROX_CHAR_WIDTH_FACTOR, TEXT_LINE_HEIGHT_FACTOR } from \"./constants.js\";\nimport { getTextMeasurer } from \"./text-measure.js\";\n\n/**\n * Fields shared by every shape variant. `order` is a fractional-index string\n * used for z-ordering within the parent layer \u2014 insertions are O(1) and never\n * require renumbering neighbors, which keeps history small and is conflict-free\n * under concurrent edits.\n */\nexport interface ElementBase {\n readonly id: ElementId;\n readonly layerId: LayerId;\n /** Discriminator. Built-in shapes use the literal types declared below. */\n readonly type: string;\n /** Local-space origin. The shape is rotated/scaled around this point. */\n readonly position: Vec2;\n /** Rotation in radians, counter-clockwise. */\n readonly rotation: number;\n readonly scale: Vec2;\n /** Z-order key within `layerId`. */\n readonly order: FractionalIndex;\n readonly style: Style;\n /** Free-form metadata for plugins; the kernel never reads from here. */\n readonly metadata?: Readonly<Record<string, unknown>>;\n\n /**\n * Interactive-resize size constraints in local pixels. The editor clamps\n * the shape's width/height into [min, max] after every resize gesture.\n * Omitted = no constraint on that axis.\n */\n readonly minWidth?: number;\n readonly minHeight?: number;\n readonly maxWidth?: number;\n readonly maxHeight?: number;\n\n /**\n * If true, interactive resize is prevented from dragging through the\n * opposite edge \u2014 the shape cannot be mirrored by overshooting a handle.\n * `width` / `height` are clamped to a non-negative range (or `minWidth` /\n * `minHeight` if set). Defaults to `false`.\n */\n readonly noFlip?: boolean;\n\n /**\n * Custom named connection points on this shape, on top of the 9 standard\n * anchors (`top-left` / `top` / `top-right` / `right` / `bottom-right` /\n * `bottom` / `bottom-left` / `left` / `center`). Entries with a standard\n * name override the standard placement; new names add fresh ports.\n *\n * Values are `AnchorRef`s \u2014 `ratio` keeps the point proportional to the\n * shape's bounds, `absolute` pins it at a fixed pixel offset. Resolve\n * an anchor through `getAnchorLocal` / `getAnchorWorld`.\n */\n readonly anchors?: Readonly<Record<string, AnchorRef>>;\n\n /**\n * Optional parent shape id. When set, the shape is considered part of\n * the parent's group: hit-test and drag operations promote selection\n * to the parent (grouped), and `moveSelectionBy` translates every\n * descendant in lockstep. The kernel does not enforce a particular\n * shape type for parents \u2014 `GroupElement` (type `\"group\"`) is just the\n * default zero-render container; custom shape types can also act as\n * parents.\n */\n readonly parentId?: ElementId;\n\n /**\n * Frame membership \u2014 modern-style. Distinct from `parentId`\n * (which is for groups and containers). Children of a frame are\n * NOT nested in its `children` list; they're flat in the scene\n * but share `frameId === frame.id`. Move-by-drag of the frame\n * translates every shape with the matching frameId; export-by-\n * frame uses the frame's bounds as the crop region.\n */\n readonly frameId?: ElementId;\n\n /**\n * Per-shape lock flag. Locked shapes ignore all interactive gestures\n * (hit-test pretends they're not there for clicks / drags / resize),\n * but still render and remain serialisable. Propagates to\n * descendants: if any ancestor in the `parentId` chain is locked,\n * the shape is effectively locked. Use `isElementLocked(scene, shape)`\n * to consult the propagated state.\n *\n * Independent from `Layer.locked` \u2014 both gate interactions; either\n * one being true is enough to lock.\n */\n readonly locked?: boolean;\n\n /**\n * Per-shape visibility flag. Hidden shapes do not render and do not\n * receive interactions. Propagates to descendants like `locked`.\n * Use `isElementHidden(scene, shape)` to consult the propagated state.\n *\n * Independent from `Layer.visible` \u2014 either being false hides the\n * shape.\n */\n readonly hidden?: boolean;\n\n /**\n * Element-level hyperlink. Any shape \u2014 text, image,\n * rectangle \u2014 can carry one. The host opens it on Cmd/Ctrl-click or via\n * the hover link-popup. Stored verbatim; the host MUST validate the\n * scheme before navigating (only `http`/`https`/`mailto` \u2014 never\n * `javascript:`). Per-fragment links inside text are a separate\n * rich-text feature.\n */\n readonly href?: string;\n}\n\nexport interface RectangleElement extends ElementBase {\n readonly type: \"rectangle\";\n readonly width: number;\n readonly height: number;\n}\n\nexport interface EllipseElement extends ElementBase {\n readonly type: \"ellipse\";\n readonly width: number;\n readonly height: number;\n}\n\nexport interface PolygonElement extends ElementBase {\n readonly type: \"polygon\";\n /** Closed polygon in local coordinates (origin = `position`). */\n readonly points: readonly Vec2[];\n}\n\nexport type PathCommand =\n | { readonly kind: \"M\"; readonly to: Vec2 }\n | { readonly kind: \"L\"; readonly to: Vec2 }\n | { readonly kind: \"Q\"; readonly control: Vec2; readonly to: Vec2 }\n | { readonly kind: \"C\"; readonly control1: Vec2; readonly control2: Vec2; readonly to: Vec2 }\n | { readonly kind: \"Z\" };\n\nexport interface PathElement extends ElementBase {\n readonly type: \"path\";\n /** Commands in local coordinates. */\n readonly commands: readonly PathCommand[];\n}\n\nexport interface TextElement extends ElementBase {\n readonly type: \"text\";\n readonly text: string;\n readonly fontFamily: string;\n readonly fontSize: number;\n /** Width budget for wrapping; `undefined` = single line. */\n readonly maxWidth?: number;\n readonly style: TextStyle;\n}\n\nexport interface ImageElement extends ElementBase {\n readonly type: \"image\";\n /**\n * URL or data-URI. Used for remote-host / SVG images that don't need\n * binary registration. Setting `fileId` instead points at a\n * `Scene.files` entry, which keeps scene.json small for large bitmaps.\n */\n readonly src: string;\n /**\n * Id of the `BinaryFile` in `Scene.files` that backs this image.\n * When present, hosts should resolve through the file registry\n * (creates an object-URL or ImageBitmap on demand); `src` stays\n * around as a fallback for the static renderer path.\n */\n readonly fileId?: FileId;\n readonly width: number;\n readonly height: number;\n /**\n * Animated-content hint (opt-in). When set, the\n * renderer's image path consults `getAnimationAdapter(kind)`\n * to fetch the current frame's image source instead of using\n * `src` directly. Hosts that don't register an adapter for the\n * kind get a static fallback (src as-is). The actual frame data\n * lives in `animationData` \u2014 opaque to the kernel, decoded by\n * the adapter.\n *\n * Built-in kinds: \"gif\" (host plugs `gifuct-js`), \"lottie\"\n * (host plugs `lottie-web`), \"video\" (host plugs an\n * `HTMLVideoElement`). No adapters ship in the kernel \u2014\n * registration is per-host.\n */\n readonly animationKind?: string;\n readonly animationData?: unknown;\n}\n\n/**\n * Composite shape backed by a rich template (`@oh-just-another/templates`). The\n * scene stores only the binding (`templateId` + `data`) plus a fixed box\n * size \u2014 layout, hit-test and rendering live in the templates package.\n *\n * The kernel ships a basic bounder (uses `width` \u00D7 `height`); the templates\n * package can re-register a tighter bounder that respects the layout engine.\n */\nexport interface TemplateElement extends ElementBase {\n readonly type: \"template\";\n readonly templateId: string;\n readonly data: Readonly<Record<string, unknown>>;\n readonly width: number;\n readonly height: number;\n}\n\n/**\n * Variable-width brush stroke. Each `BrushPoint` carries its own width\n * (typically derived from `PointerEvent.pressure \u00D7 MAX_BRUSH_WIDTH`).\n * The renderer interpolates between consecutive widths along the path.\n *\n * Coordinates are local; the shape's `position` / `rotation` / `scale`\n * apply on top, same as any other shape variant.\n */\nexport interface BrushPoint {\n readonly x: number;\n readonly y: number;\n /** Stroke half-width in local pixels at this vertex. */\n readonly width: number;\n}\n\nexport interface BrushElement extends ElementBase {\n readonly type: \"brush\";\n readonly points: readonly BrushPoint[];\n}\n\n/**\n * Container shape that holds children via the shared `parentId` link.\n * Rendered as a no-op (the group itself has no visual); the editor's\n * overlay highlights the union AABB of the children when selected.\n */\nexport interface GroupElement extends ElementBase {\n readonly type: \"group\";\n}\n\n/**\n * Frame element \u2014 modern-style visual container that groups\n * shapes via a separate `frameId` link (NOT `parentId`). Drawn as\n * a dashed rectangle with a header title; clicks pass through to\n * children. Move-by-drag translates every shape whose `frameId`\n * matches; the export pipeline can crop to the frame's bounds.\n *\n * Auto-numbering: the editor picks the next free \"Frame N\" on\n * create. Custom `name` overrides.\n */\nexport interface FrameElement extends ElementBase {\n readonly type: \"frame\";\n readonly width: number;\n readonly height: number;\n /** Visible header label. */\n readonly name?: string;\n}\n\n/**\n * Filled arrow drawn as a single shape (body rectangle + triangular\n * head, optionally with a triangular tail). Distinct from an Link:\n * edges connect anchors and re-route on shape move; a BlockArrowElement\n * is a free-standing element with a fixed silhouette like a block-arrow icon.\n */\nexport interface BlockArrowElement extends ElementBase {\n readonly type: \"block-arrow\";\n readonly width: number;\n readonly height: number;\n /**\n * Where the arrow points. Default `\"right\"`. Rotation is still\n * applied on top via `ElementBase.rotation` \u2014 this enum just picks\n * the head side in local coords so the user can quickly toggle\n * direction without typing a 90/180/270 deg angle.\n */\n readonly direction?: \"right\" | \"left\" | \"up\" | \"down\";\n /** Ratio of the head length over the total length (0..0.9). Default 0.4. */\n readonly headRatio?: number;\n /** Ratio of the body thickness over the perpendicular dimension. Default 0.5. */\n readonly bodyThickness?: number;\n}\n\nexport type BuiltinElement =\n | RectangleElement\n | EllipseElement\n | PolygonElement\n | PathElement\n | TextElement\n | ImageElement\n | TemplateElement\n | GroupElement\n | FrameElement\n | BlockArrowElement\n | BrushElement;\n\n/**\n * Open shape type. `Element` accepts any `ElementBase` extension, which lets plugins\n * register their own types without amending this union. The kernel treats\n * unknown shape types via the bounder registry \u2014 see `registerBounder`.\n */\nexport type Element = BuiltinElement | ElementBase;\n\n// --- type guards ---\n\nexport const isRectangle = (s: ElementBase): s is RectangleElement => s.type === \"rectangle\";\nexport const isEllipse = (s: ElementBase): s is EllipseElement => s.type === \"ellipse\";\nexport const isPolygon = (s: ElementBase): s is PolygonElement => s.type === \"polygon\";\nexport const isPath = (s: ElementBase): s is PathElement => s.type === \"path\";\nexport const isText = (s: ElementBase): s is TextElement => s.type === \"text\";\nexport const isImage = (s: ElementBase): s is ImageElement => s.type === \"image\";\nexport const isTemplate = (s: ElementBase): s is TemplateElement => s.type === \"template\";\nexport const isGroup = (s: ElementBase): s is GroupElement => s.type === \"group\";\nexport const isFrame = (s: ElementBase): s is FrameElement => s.type === \"frame\";\nexport const isBlockArrow = (s: ElementBase): s is BlockArrowElement => s.type === \"block-arrow\";\nexport const isBrush = (s: ElementBase): s is BrushElement => s.type === \"brush\";\n\n// --- bounder registry ---\n\n/**\n * Computes the *local* bounds of a shape \u2014 its AABB in local coordinates,\n * before `position`/`rotation`/`scale` are applied. The world AABB lives in\n * `getElementWorldBounds`.\n */\nexport type ElementBounder<S extends ElementBase = ElementBase> = (shape: S) => Bounds;\n\nconst bounderRegistry = new Map<string, ElementBounder>();\n\n/**\n * Register a bounder for a custom shape type. Plugins call this once at module\n * load. The kernel ships bounders for every `BuiltinElement`.\n */\nexport const registerBounder = <S extends ElementBase>(\n type: S[\"type\"],\n bounder: ElementBounder<S>,\n): void => {\n bounderRegistry.set(type, bounder as ElementBounder);\n};\n\n/** Look up a registered bounder. Returns `undefined` for unknown shape types. */\nexport const getBounder = (type: string): ElementBounder | undefined => bounderRegistry.get(type);\n\n/**\n * Local AABB for any shape with a registered bounder. Throws on unknown types\n * \u2014 callers should either register a bounder or filter unknown shapes out.\n */\nexport const getElementLocalBounds = (shape: ElementBase): Bounds => {\n const bounder = bounderRegistry.get(shape.type);\n if (!bounder) {\n throw new Error(`No bounder registered for shape type: ${shape.type}`);\n }\n return bounder(shape);\n};\n\n/**\n * World-space AABB after `position`/`rotation`/`scale`. This is the conservative\n * AABB of the rotated/scaled local box, suitable for spatial-index keys.\n */\nexport const getElementWorldBounds = (shape: ElementBase): Bounds => {\n const local = getElementLocalBounds(shape);\n // Transform 4 corners then re-AABB.\n const corners: readonly Vec2[] = [\n { x: local.x, y: local.y },\n { x: local.x + local.width, y: local.y },\n { x: local.x, y: local.y + local.height },\n { x: local.x + local.width, y: local.y + local.height },\n ];\n const sin = Math.sin(shape.rotation);\n const cos = Math.cos(shape.rotation);\n const transformed = corners.map((p) => {\n const sx = p.x * shape.scale.x;\n const sy = p.y * shape.scale.y;\n return {\n x: shape.position.x + (sx * cos - sy * sin),\n y: shape.position.y + (sx * sin + sy * cos),\n };\n });\n return B.fromPoints(transformed);\n};\n\n// --- built-in bounders ---\n\nregisterBounder<RectangleElement>(\"rectangle\", (s) => ({\n x: 0,\n y: 0,\n width: s.width,\n height: s.height,\n}));\n\nregisterBounder<EllipseElement>(\"ellipse\", (s) => ({\n x: 0,\n y: 0,\n width: s.width,\n height: s.height,\n}));\n\nregisterBounder<PolygonElement>(\"polygon\", (s) => B.fromPoints(s.points));\n\nregisterBounder<PathElement>(\"path\", (s) => {\n const points: Vec2[] = [];\n let cursor: Vec2 = { x: 0, y: 0 };\n for (const cmd of s.commands) {\n switch (cmd.kind) {\n case \"M\":\n case \"L\":\n points.push(cmd.to);\n cursor = cmd.to;\n break;\n case \"Q\":\n points.push(cmd.control, cmd.to);\n cursor = cmd.to;\n break;\n case \"C\":\n points.push(cmd.control1, cmd.control2, cmd.to);\n cursor = cmd.to;\n break;\n case \"Z\":\n // closes the subpath, no new points\n break;\n }\n }\n // suppress unused-var warning\n void cursor;\n return B.fromPoints(points);\n});\n\nregisterBounder<TextElement>(\"text\", (s) => {\n // Width comes from the host measurer when installed (matches the\n // actually-rendered glyph advances), falling back to a geometric\n // estimate (`chars \u00D7 fontSize \u00D7 factor`) headless / in tests. Height\n // is line count \u00D7 line-height; hard newlines honoured in both modes.\n const lineHeight = s.fontSize * TEXT_LINE_HEIGHT_FACTOR;\n const paragraphs = s.text.split(\"\\n\");\n const measurer = getTextMeasurer();\n // Pass weight/style so the measured width matches the rendered (bold /\n // italic) glyphs \u2014 otherwise the box wouldn't grow when text is bolded.\n const opts = {\n bold: s.style.fontWeight === \"bold\",\n italic: s.style.fontStyle === \"italic\",\n };\n const measureLine = (line: string): number => {\n if (measurer) {\n const w = measurer(line, s.fontFamily, s.fontSize, opts);\n if (w !== null) return w;\n }\n return line.length * s.fontSize * TEXT_APPROX_CHAR_WIDTH_FACTOR;\n };\n if (s.maxWidth === undefined) {\n // Auto-width: widest paragraph drives width, one visual line per\n // paragraph. Empty text keeps a caret-sized box so it stays\n // selectable / editable.\n let width = 0;\n for (const p of paragraphs) width = Math.max(width, measureLine(p));\n width = Math.max(width, s.fontSize * 0.5);\n return { x: 0, y: 0, width, height: Math.max(1, paragraphs.length) * lineHeight };\n }\n // Fixed-width: width is the budget; height \u2248 wrapped line count.\n let lines = 0;\n for (const p of paragraphs) {\n lines += Math.max(1, Math.ceil(measureLine(p) / s.maxWidth));\n }\n return { x: 0, y: 0, width: s.maxWidth, height: Math.max(1, lines) * lineHeight };\n});\n\nregisterBounder<ImageElement>(\"image\", (s) => ({\n x: 0,\n y: 0,\n width: s.width,\n height: s.height,\n}));\n\n// Built-in template bounder: uses the explicit `width` \u00D7 `height` box. The\n// templates package can re-register a tighter bounder driven by the layout\n// engine when an instance is auto-sized.\nregisterBounder<TemplateElement>(\"template\", (s) => ({\n x: 0,\n y: 0,\n width: s.width,\n height: s.height,\n}));\n\nregisterBounder<BrushElement>(\"brush\", (s) => {\n if (s.points.length === 0) return { x: 0, y: 0, width: 0, height: 0 };\n let minX = Infinity;\n let minY = Infinity;\n let maxX = -Infinity;\n let maxY = -Infinity;\n for (const p of s.points) {\n if (p.x - p.width < minX) minX = p.x - p.width;\n if (p.y - p.width < minY) minY = p.y - p.width;\n if (p.x + p.width > maxX) maxX = p.x + p.width;\n if (p.y + p.width > maxY) maxY = p.y + p.width;\n }\n return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };\n});\n\n// Group shapes have no intrinsic geometry \u2014 their world AABB is empty.\n// Callers that need the union of descendants must walk `parentId` via\n// `getChildrenOf` and union the children's world bounds instead.\nregisterBounder<GroupElement>(\"group\", () => ({ x: 0, y: 0, width: 0, height: 0 }));\n\nregisterBounder<FrameElement>(\"frame\", (s) => ({\n x: 0,\n y: 0,\n width: s.width,\n height: s.height,\n}));\n\nregisterBounder<BlockArrowElement>(\"block-arrow\", (s) => ({\n x: 0,\n y: 0,\n width: s.width,\n height: s.height,\n}));\n", "import type { FractionalIndex } from \"fractional-keys\";\n\n/** Compare by fractional `order`, ascending (bottom-to-top z-order). */\nexport const byOrderAsc = <T extends { readonly order: FractionalIndex }>(a: T, b: T): number =>\n a.order < b.order ? -1 : a.order > b.order ? 1 : 0;\n\n/** Compare by fractional `order`, descending (top-to-bottom). */\nexport const byOrderDesc = <T extends { readonly order: FractionalIndex }>(a: T, b: T): number =>\n byOrderAsc(b, a);\n", "import type { Bounds, LinkId, LayerId, ElementId, Vec2 } from \"@oh-just-another/types\";\nimport { bounds as B } from \"@oh-just-another/math\";\nimport type { Link } from \"./edge.js\";\nimport type { Layer } from \"./layer.js\";\nimport type { Scene } from \"./scene.js\";\nimport {\n getElementWorldBounds,\n getElementLocalBounds,\n isPolygon,\n isEllipse,\n isRectangle,\n isImage,\n isText,\n isPath,\n isGroup,\n type Element,\n type PathCommand,\n} from \"./shape.js\";\nimport { getCornerRadius } from \"./style.js\";\nimport { SpatialGrid } from \"./spatial.js\";\nimport { byOrderAsc } from \"./order.js\";\nimport { localToWorld } from \"./shape-transform.js\";\nimport { ellipseOutlinePoint } from \"./ellipse.js\";\n\n// --- direct lookups ---\n\nexport const getElement = (scene: Scene, id: ElementId): Element | undefined =>\n scene.elements.get(id);\n\nexport const getLink = (scene: Scene, id: LinkId): Link | undefined => scene.links.get(id);\n\nexport const getLayer = (scene: Scene, id: LayerId): Layer | undefined => scene.layers.get(id);\n\n// --- iteration in z-order ---\n\n/**\n * Layers sorted bottom-to-top by their `order` field. Stable for equal orders\n * (which should not happen in practice with fractional indices).\n */\nexport const getLayersInOrder = (scene: Scene): readonly Layer[] =>\n [...scene.layers.values()].sort(byOrderAsc);\n\n/** Shapes in `layerId`, sorted bottom-to-top by `order`. */\nexport const getElementsInLayer = (scene: Scene, layerId: LayerId): readonly Element[] =>\n [...scene.elements.values()].filter((s) => s.layerId === layerId).sort(byOrderAsc);\n\nexport const getLinksInLayer = (scene: Scene, layerId: LayerId): readonly Link[] =>\n [...scene.links.values()].filter((e) => e.layerId === layerId).sort(byOrderAsc);\n\n// --- selection outline (contour) ---\n\n/** Number of samples for an ellipse's outline polyline. */\nconst ELLIPSE_OUTLINE_SAMPLES = 48;\n/** Samples per Q/C path segment when flattening to a polyline. */\nconst PATH_CURVE_SAMPLES = 10;\n\nconst rectLoop = (b: Bounds): Vec2[] => [\n { x: b.x, y: b.y },\n { x: b.x + b.width, y: b.y },\n { x: b.x + b.width, y: b.y + b.height },\n { x: b.x, y: b.y + b.height },\n];\n\n/** Samples per rounded-rect corner arc. */\nconst CORNER_ARC_SAMPLES = 6;\n\n/** Rounded-rect outline as a polyline \u2014 straight edges + sampled corner arcs. */\nconst roundedRectLoop = (b: Bounds, r: number): Vec2[] => {\n const { x, y, width: w, height: h } = b;\n const arc = (cx: number, cy: number, from: number, to: number): Vec2[] => {\n const pts: Vec2[] = [];\n for (let i = 0; i <= CORNER_ARC_SAMPLES; i++) {\n const a = from + (to - from) * (i / CORNER_ARC_SAMPLES);\n pts.push({ x: cx + r * Math.cos(a), y: cy + r * Math.sin(a) });\n }\n return pts;\n };\n const HALF_PI = Math.PI / 2;\n return [\n // top-left \u2192 top-right \u2192 bottom-right \u2192 bottom-left corners (clockwise).\n ...arc(x + r, y + r, Math.PI, Math.PI + HALF_PI),\n ...arc(x + w - r, y + r, -HALF_PI, 0),\n ...arc(x + w - r, y + h - r, 0, HALF_PI),\n ...arc(x + r, y + h - r, HALF_PI, Math.PI),\n ];\n};\n\nconst flattenPath = (commands: readonly PathCommand[]): Vec2[] => {\n const pts: Vec2[] = [];\n let cur: Vec2 = { x: 0, y: 0 };\n for (const c of commands) {\n if (c.kind === \"M\" || c.kind === \"L\") {\n cur = c.to;\n pts.push({ x: cur.x, y: cur.y });\n } else if (c.kind === \"Q\") {\n for (let i = 1; i <= PATH_CURVE_SAMPLES; i++) {\n const t = i / PATH_CURVE_SAMPLES;\n const u = 1 - t;\n pts.push({\n x: u * u * cur.x + 2 * u * t * c.control.x + t * t * c.to.x,\n y: u * u * cur.y + 2 * u * t * c.control.y + t * t * c.to.y,\n });\n }\n cur = c.to;\n } else if (c.kind === \"C\") {\n for (let i = 1; i <= PATH_CURVE_SAMPLES; i++) {\n const t = i / PATH_CURVE_SAMPLES;\n const u = 1 - t;\n pts.push({\n x:\n u ** 3 * cur.x +\n 3 * u * u * t * c.control1.x +\n 3 * u * t * t * c.control2.x +\n t ** 3 * c.to.x,\n y:\n u ** 3 * cur.y +\n 3 * u * u * t * c.control1.y +\n 3 * u * t * t * c.control2.y +\n t ** 3 * c.to.y,\n });\n }\n cur = c.to;\n }\n // \"Z\" closes implicitly \u2014 each loop is closed by the consumer.\n }\n return pts;\n};\n\n/**\n * Outline provider for a custom / composite element type \u2014 returns the\n * shape's contour as one or more LOCAL-space loops (pre transform). Lets a\n * plugin element made of several visually-disconnected figures (e.g. two\n * unconnected ellipses, no background) supply a multi-loop selection halo\n * instead of falling back to its bounding box. Registered by `shape.type`.\n */\nexport type ElementOutlineProvider = (shape: Element) => Vec2[][];\n\nconst outlineProviders = new Map<string, ElementOutlineProvider>();\n\n/** Register a multi-loop outline provider for a custom element `type`. */\nexport const registerElementOutline = (type: string, provider: ElementOutlineProvider): void => {\n outlineProviders.set(type, provider);\n};\n\n/** Local-space outline loop(s) for a single (non-group) shape, or `null`. */\nconst localOutlineLoops = (shape: Element): Vec2[][] | null => {\n if (isPolygon(shape)) return [shape.points.map((p) => ({ x: p.x, y: p.y }))];\n if (isEllipse(shape)) {\n const b = getElementLocalBounds(shape);\n const cx = b.x + b.width / 2;\n const cy = b.y + b.height / 2;\n const rx = b.width / 2;\n const ry = b.height / 2;\n const pts: Vec2[] = [];\n for (let i = 0; i < ELLIPSE_OUTLINE_SAMPLES; i++) {\n pts.push(ellipseOutlinePoint(cx, cy, rx, ry, i / ELLIPSE_OUTLINE_SAMPLES));\n }\n return [pts];\n }\n if (isRectangle(shape)) {\n const b = getElementLocalBounds(shape);\n const r = getCornerRadius(shape.style.roundness, b.width, b.height);\n return [r > 0 ? roundedRectLoop(b, r) : rectLoop(b)];\n }\n if (isImage(shape) || isText(shape)) {\n return [rectLoop(getElementLocalBounds(shape))];\n }\n if (isPath(shape)) return [flattenPath(shape.commands)];\n // group handled by the caller; brush / template / custom \u2192 bbox fallback.\n return null;\n};\n\n/**\n * World-space outline loop(s) tracing a shape's actual contour, for the\n * selection halo. polygon (star / diamond / hexagon) is exact; ellipse and\n * path are sampled; a group returns one loop per descendant (handles\n * visually-disconnected figures). Shapes without known geometry (composite\n * template, brush, custom) fall back to their world bounding box. Cheap\n * enough to recompute every frame \u2014 no baking needed.\n */\nexport const getElementOutline = (scene: Scene, shape: Element): Vec2[][] => {\n if (isGroup(shape)) {\n const loops: Vec2[][] = [];\n for (const child of getChildrenOf(scene, shape.id))\n loops.push(...getElementOutline(scene, child));\n return loops;\n }\n const local = localOutlineLoops(shape);\n if (local) return local.map((loop) => loop.map((p) => localToWorld(shape, p)));\n // Custom / composite type with a registered outline provider (multi-loop).\n const provider = outlineProviders.get(shape.type);\n if (provider) {\n const loops = provider(shape).filter((loop) => loop.length >= 2);\n if (loops.length > 0) return loops.map((loop) => loop.map((p) => localToWorld(shape, p)));\n }\n // Fallback: axis-aligned world bounding box.\n const b = getElementWorldBounds(shape);\n return [rectLoop(b)];\n};\n\n// --- group queries (parentId chain) ---\n\n/**\n * Direct children of `parentId` \u2014 every shape whose `parentId` equals\n * the argument, in z-order. Linear in scene size; for groups inside the\n * editor's hot path, cache the result by `(scene, parentId)`.\n */\nexport const getChildrenOf = (scene: Scene, parentId: ElementId): readonly Element[] => {\n const out: Element[] = [];\n for (const s of scene.elements.values()) {\n if (s.parentId === parentId) out.push(s);\n }\n out.sort(byOrderAsc);\n return out;\n};\n\n/**\n * `true` when the shape (or any of its ancestors via `parentId`) has\n * `locked: true`. Walks the parent chain bounded by\n * `MAX_PARENT_DEPTH` so the answer stays O(depth) for a freshly\n * grouped scene. Independent from `Layer.locked` \u2014 callers that need\n * the combined interactivity gate should `||` both flags.\n */\nexport const isElementLocked = (scene: Scene, shape: Element): boolean => {\n let current: Element | undefined = shape;\n for (let i = 0; current && i < MAX_PARENT_DEPTH; i++) {\n if (current.locked === true) return true;\n if (!current.parentId) return false;\n current = scene.elements.get(current.parentId);\n }\n return false;\n};\n\n/**\n * `true` when the shape (or any of its ancestors via `parentId`) has\n * `hidden: true`. Same propagation semantics as `isElementLocked`.\n */\nexport const isElementHidden = (scene: Scene, shape: Element): boolean => {\n let current: Element | undefined = shape;\n for (let i = 0; current && i < MAX_PARENT_DEPTH; i++) {\n if (current.hidden === true) return true;\n if (!current.parentId) return false;\n current = scene.elements.get(current.parentId);\n }\n return false;\n};\n\n/**\n * Walks the `parentId` chain starting from `elementId` and returns the\n * topmost ancestor (the root). Returns the shape itself when it has no\n * parent, or `undefined` when the shape (or any ancestor) is missing.\n * Cycle-safe \u2014 bails after `MAX_PARENT_DEPTH` hops.\n */\nexport const getRootSelf = (scene: Scene, elementId: ElementId): Element | undefined => {\n let current = scene.elements.get(elementId);\n for (let i = 0; current?.parentId && i < MAX_PARENT_DEPTH; i++) {\n const parent = scene.elements.get(current.parentId);\n if (!parent) break;\n current = parent;\n }\n return current;\n};\n\n/**\n * Every descendant of `parentId`, recursive, including the root itself.\n * Order: parent first, then a depth-first walk. Cycle-safe via the\n * `visited` set.\n */\nexport const getDescendantsOf = (scene: Scene, parentId: ElementId): readonly Element[] => {\n const root = scene.elements.get(parentId);\n if (!root) return [];\n const visited = new Set<ElementId>([parentId]);\n const out: Element[] = [root];\n const stack: ElementId[] = [parentId];\n while (stack.length > 0) {\n const cur = stack.pop();\n if (cur === undefined) break;\n for (const child of getChildrenOf(scene, cur)) {\n if (visited.has(child.id)) continue;\n visited.add(child.id);\n out.push(child);\n stack.push(child.id);\n }\n }\n return out;\n};\n\nconst MAX_PARENT_DEPTH = 64;\n\n// --- spatial queries (linear scan) ---\n\n/**\n * Shapes whose world AABB intersects `range`. Linear in the number of shapes.\n * For large scenes use `buildSpatialIndex` once and query the index.\n */\nexport const getElementsInBounds = (scene: Scene, range: Bounds): readonly Element[] => {\n const out: Element[] = [];\n for (const s of scene.elements.values()) {\n if (B.intersects(getElementWorldBounds(s), range)) out.push(s);\n }\n return out;\n};\n\n/**\n * Shapes whose world-AABB is at least `minCoverageRatio` covered by\n * `range`. `1` requires full containment (containment-style lasso);\n * `0.5` selects when at least half of the element sits inside the\n * box \u2014 friendlier than pure intersection because brushing past an\n * edge doesn't accidentally grab the shape.\n *\n * Always selects shapes that fully contain the lasso (small lasso\n * inside a big shape) \u2014 same affordance as bidirectional containment.\n * Zero-area shapes (groups, brushes-with-one-vertex) fall back to a\n * plain intersection test.\n */\nexport const getElementsCoveredByBounds = (\n scene: Scene,\n range: Bounds,\n minCoverageRatio = 0.5,\n): readonly Element[] => {\n const out: Element[] = [];\n for (const s of scene.elements.values()) {\n const b = getElementWorldBounds(s);\n if (!B.intersects(b, range)) continue;\n const area = b.width * b.height;\n if (area <= 0) {\n out.push(s);\n continue;\n }\n const ix = Math.max(b.x, range.x);\n const iy = Math.max(b.y, range.y);\n const ix2 = Math.min(b.x + b.width, range.x + range.width);\n const iy2 = Math.min(b.y + b.height, range.y + range.height);\n const iw = ix2 - ix;\n const ih = iy2 - iy;\n if (iw <= 0 || ih <= 0) continue;\n const coverage = (iw * ih) / area;\n if (coverage >= minCoverageRatio) {\n out.push(s);\n continue;\n }\n // Bidirectional: tiny lasso inside a big shape still picks it.\n const lassoArea = range.width * range.height;\n if (lassoArea > 0 && (iw * ih) / lassoArea >= minCoverageRatio) {\n out.push(s);\n }\n }\n return out;\n};\n\n/**\n * Topmost shape containing `point`. Iterates layers top-to-bottom, then shapes\n * within each layer top-to-bottom; returns the first hit. Hit-test here is the\n * conservative AABB test; renderer-specific shape-precise hit-tests belong\n * with the renderer.\n */\nexport const getElementAt = (scene: Scene, point: Vec2): Element | undefined => {\n const layers = getLayersInOrder(scene);\n for (let i = layers.length - 1; i >= 0; i--) {\n const layer = layers[i];\n if (!layer?.visible) continue;\n const shapes = getElementsInLayer(scene, layer.id);\n for (let j = shapes.length - 1; j >= 0; j--) {\n const s = shapes[j];\n if (s === undefined) continue;\n if (B.contains(getElementWorldBounds(s), point)) return s;\n }\n }\n return undefined;\n};\n\n// --- spatial index helpers ---\n\n/**\n * Build a `SpatialGrid` from the current scene. Re-build (or update\n * incrementally) when shapes change \u2014 the grid is not auto-synced with the\n * scene. The default cell size is tuned for typical editor scenes; pass an\n * explicit value if your shapes are much larger or smaller.\n */\nexport const buildSpatialIndex = (scene: Scene, cellSize?: number): SpatialGrid => {\n const grid = new SpatialGrid(cellSize);\n for (const shape of scene.elements.values()) {\n grid.insert(shape.id, getElementWorldBounds(shape));\n }\n return grid;\n};\n\n/**\n * Range query backed by the index. Returns shapes (not just ids) whose AABB\n * actually intersects `range`. The grid pre-filters by cell overlap; this\n * function does the precise AABB filter.\n */\nexport const queryByIndex = (\n scene: Scene,\n grid: SpatialGrid,\n range: Bounds,\n): readonly Element[] => {\n const candidates = grid.query(range);\n const out: Element[] = [];\n for (const id of candidates) {\n const shape = scene.elements.get(id);\n if (!shape) continue;\n if (B.intersects(getElementWorldBounds(shape), range)) out.push(shape);\n }\n return out;\n};\n\n/**\n * Point hit-test backed by a SpatialGrid. Equivalent to `getElementAt` but\n * pre-filters candidates through `grid.query` \u2014 O(k) where k is the\n * shapes overlapping the point's cell. Walks layers top-to-bottom for\n * stable z-order; within a layer picks the highest-`order` shape that\n * actually contains the point.\n */\nexport const getElementAtIndexed = (\n scene: Scene,\n grid: SpatialGrid,\n point: Vec2,\n): Element | undefined => {\n const pointRange: Bounds = { x: point.x, y: point.y, width: 0, height: 0 };\n const candidates = grid.query(pointRange);\n if (candidates.size === 0) return undefined;\n let best: Element | undefined;\n let bestLayerOrder = \"\";\n let bestElementOrder = \"\";\n let bestSet = false;\n for (const id of candidates) {\n const shape = scene.elements.get(id);\n if (!shape) continue;\n if (!B.contains(getElementWorldBounds(shape), point)) continue;\n const layer = scene.layers.get(shape.layerId);\n if (!layer?.visible) continue;\n const layerOrder = layer.order as string;\n if (\n !bestSet ||\n layerOrder > bestLayerOrder ||\n (layerOrder === bestLayerOrder && shape.order > bestElementOrder)\n ) {\n best = shape;\n bestLayerOrder = layerOrder;\n bestElementOrder = shape.order;\n bestSet = true;\n }\n }\n return best;\n};\n", "import type { Bounds } from \"@oh-just-another/types\";\nimport { getElementWorldBounds, type ElementBase } from \"./shape.js\";\n\n/**\n * Some elements PAINT beyond their geometric bounds \u2014 a frame draws its\n * header strip above the rectangle, a confetti box throws particles past\n * its edges. The dirty-rect / tile invalidation must clear that overspill\n * too, otherwise deleting (or moving without a full repaint) leaves a\n * \"ghost\" of the overpainted region.\n *\n * `RenderOverflow` is the per-side extra paint margin (world units) an\n * element type bleeds past `getElementWorldBounds`. Providers are keyed by\n * element `type` and may inspect the shape (e.g. only confetti-tagged\n * rectangles overflow). All sides default to 0.\n */\nexport interface RenderOverflow {\n readonly top?: number;\n readonly right?: number;\n readonly bottom?: number;\n readonly left?: number;\n}\n\ntype RenderOverflowProvider = (shape: ElementBase) => RenderOverflow;\n\nconst providers = new Map<string, RenderOverflowProvider>();\n\n/**\n * Register how far an element type paints past its bounds. The renderer\n * that draws the overspill owns this (it knows the header height /\n * particle spread). Idempotent per type \u2014 last registration wins.\n */\nexport const registerRenderOverflow = (type: string, fn: RenderOverflowProvider): void => {\n providers.set(type, fn);\n};\n\n/**\n * World bounds expanded by the element type's registered paint overflow \u2014\n * the region that must be invalidated/cleared when the element changes or\n * is removed. Falls back to the plain world bounds when no overflow is\n * registered (the common case).\n */\nexport const getElementRenderBounds = (shape: ElementBase): Bounds => {\n const b = getElementWorldBounds(shape);\n const fn = providers.get(shape.type);\n if (!fn) return b;\n const o = fn(shape);\n const top = o.top ?? 0;\n const right = o.right ?? 0;\n const bottom = o.bottom ?? 0;\n const left = o.left ?? 0;\n if (top === 0 && right === 0 && bottom === 0 && left === 0) return b;\n return {\n x: b.x - left,\n y: b.y - top,\n width: b.width + left + right,\n height: b.height + top + bottom,\n };\n};\n", "import type { Transform, Vec2 } from \"@oh-just-another/types\";\nimport { matrix } from \"@oh-just-another/math\";\nimport { DEFAULT_GRID_SPACING } from \"./constants.js\";\n\n/**\n * How the background grid is painted.\n * `\"lines\"` \u2014 ruled grid lines (default).\n * `\"dots\"` \u2014 a dot at every grid intersection.\n *\n * Snap behaviour is identical between styles; only the paint differs.\n */\nexport type GridStyle = \"lines\" | \"dots\";\n\n/**\n * Camera over the world. Stored as pan/zoom/rotation rather than a raw matrix\n * because every UI control (zoom-to-fit, hotkeys, pinch) wants these axes\n * directly; the matrix form is derivable.\n */\nexport interface Viewport {\n /** World coordinate at viewport (0, 0) before rotation. */\n readonly pan: Vec2;\n /** Uniform scale. 1 = native; 2 = zoomed in 2\u00D7. */\n readonly zoom: number;\n /** Rotation in radians, counter-clockwise. */\n readonly rotation: number;\n readonly size: { readonly width: number; readonly height: number };\n /**\n * Whether the background grid is drawn for this scene. Spacing is fixed\n * at {@link DEFAULT_GRID_SPACING}; this flag controls only visibility.\n */\n readonly gridEnabled: boolean;\n /** How the grid is painted. Renderers fall back to lines when unset. */\n readonly gridStyle?: GridStyle;\n /**\n * Programmatic snap opt-out. `undefined` is treated as ON\n * (see {@link isSnapToGridEnabled}). Snapping also requires the grid to be\n * enabled \u2014 snapping to a hidden grid is confusing \u2014 so this flag only\n * matters while `gridEnabled` is true.\n */\n readonly snapToGrid?: boolean;\n}\n\n/** World-unit spacing snap-to-grid rounds to (the fixed grid spacing). */\nexport const resolveSnapSpacing = (): number => DEFAULT_GRID_SPACING;\n\n/**\n * Whether snap-to-grid is enabled for this viewport. `undefined` counts as\n * ON \u2014 the product default.\n */\nexport const isSnapToGridEnabled = (viewport: Viewport): boolean => viewport.snapToGrid ?? true;\n\nexport const DEFAULT_VIEWPORT: Viewport = Object.freeze({\n pan: { x: 0, y: 0 },\n zoom: 1,\n rotation: 0,\n size: { width: 0, height: 0 },\n // Grid off by default; hosts enable it per scene via `gridEnabled`.\n gridEnabled: false,\n});\n\n/** World \u2192 screen transform. */\nexport const getWorldToScreen = (viewport: Viewport): Transform => {\n // Order: world point \u2192 translate by -pan \u2192 rotate \u2192 scale.\n const translate = matrix.translation(-viewport.pan.x, -viewport.pan.y);\n const rotate = matrix.rotation(viewport.rotation);\n const scale = matrix.scaling(viewport.zoom);\n return matrix.multiply(scale, matrix.multiply(rotate, translate));\n};\n\n/** Screen \u2192 world transform (inverse of `getWorldToScreen`). */\nexport const getScreenToWorld = (viewport: Viewport): Transform =>\n matrix.inverse(getWorldToScreen(viewport));\n\n/**\n * Pan the camera by a screen-space delta. Most useful for drag handlers that\n * report pixel deltas; the delta is divided by `zoom` so panning by 1 screen\n * pixel moves the world by 1 / zoom world units.\n */\nexport const panBy = (viewport: Viewport, deltaScreen: Vec2): Viewport => ({\n ...viewport,\n pan: {\n x: viewport.pan.x - deltaScreen.x / viewport.zoom,\n y: viewport.pan.y - deltaScreen.y / viewport.zoom,\n },\n});\n\n/**\n * Multiplicative zoom around a world-space anchor. The anchor stays under the\n * same screen pixel, which is what users expect from mouse-wheel zoom.\n */\nexport const zoomAt = (viewport: Viewport, factor: number, anchorWorld: Vec2): Viewport => {\n const newZoom = viewport.zoom * factor;\n // Adjust pan so that anchorWorld maps to the same screen point.\n return {\n ...viewport,\n zoom: newZoom,\n pan: {\n x: anchorWorld.x - (anchorWorld.x - viewport.pan.x) / factor,\n y: anchorWorld.y - (anchorWorld.y - viewport.pan.y) / factor,\n },\n };\n};\n\nexport const resize = (viewport: Viewport, width: number, height: number): Viewport => ({\n ...viewport,\n size: { width, height },\n});\n", "import type { Bounds, ElementId } from \"@oh-just-another/types\";\nimport { getElementWorldBounds, type Scene, type ElementBase } from \"@oh-just-another/scene\";\n\n/**\n * Per-shape memo with object-identity invalidation. Cached value sticks\n * until the underlying shape reference changes \u2014 and because every scene\n * op (`updateElement` / `moveElement` / ...) returns a new shape object, the\n * cache invalidates automatically without the caller threading versions\n * through.\n *\n * Caches survive across frames; pair with `prune(scene)` after large\n * deletions if memory matters. For ephemeral, single-render memos use\n * a fresh `ElementCache` instance (cheap to construct).\n */\nexport class ElementCache<T> {\n private readonly entries = new Map<ElementId, { readonly ref: ElementBase; value: T }>();\n\n get(shape: ElementBase): T | undefined {\n const entry = this.entries.get(shape.id);\n if (!entry) return undefined;\n if (entry.ref !== shape) {\n this.entries.delete(shape.id);\n return undefined;\n }\n return entry.value;\n }\n\n set(shape: ElementBase, value: T): T {\n this.entries.set(shape.id, { ref: shape, value });\n return value;\n }\n\n /**\n * Lazy memo. Returns the cached value if `shape` is the same reference\n * as the one we cached against; otherwise runs `compute`, stores the\n * result, and returns it.\n */\n getOrCompute(shape: ElementBase, compute: (s: ElementBase) => T): T {\n const cached = this.get(shape);\n if (cached !== undefined) return cached;\n return this.set(shape, compute(shape));\n }\n\n invalidate(id: ElementId): void {\n this.entries.delete(id);\n }\n\n clear(): void {\n this.entries.clear();\n }\n\n /** Drop entries whose shape is no longer in the scene. */\n prune(scene: Scene): void {\n for (const id of this.entries.keys()) {\n if (!scene.elements.has(id)) this.entries.delete(id);\n }\n }\n\n get size(): number {\n return this.entries.size;\n }\n}\n\n/**\n * Shared module-level cache for world-space bounds. Used by `renderScene`\n * for viewport culling and reusable from outside (hit-test, overlay) so\n * a single computation amortizes across passes.\n *\n * `getElementWorldBounds` is pure \u2014 same shape ref \u2192 same bounds \u2014 so a\n * by-identity cache is sound.\n */\nexport const sharedBoundsCache: ElementCache<Bounds> = new ElementCache<Bounds>();\n\nexport const cachedWorldBounds = (cache: ElementCache<Bounds>, shape: ElementBase): Bounds =>\n cache.getOrCompute(shape, getElementWorldBounds);\n", "/**\n * Tunable constants for the renderer core. All \"magic numbers\" used by\n * `renderScene` / `renderLinks` / `renderGrid` live here so there is one\n * place to tweak performance / visual behaviour.\n */\n\nimport { GRID_COLOR, GRID_DOT_COLOR } from \"@oh-just-another/tokens\";\nimport type { LodOptions } from \"./scene-renderer.js\";\n\n/**\n * Default zoom thresholds for the level-of-detail pipeline. Tuned for\n * a typical 1920\u00D71080 viewport viewing a 10k-shape scene.\n *\n * - `placeholder: 0.15` \u2014 below 15% zoom (one screen \u2248 6.7\u00D7 world),\n * shapes degrade to flat AABB fills. Saves ~10\u00D7 renderer cost per\n * shape.\n * - `hideText: 0.4` \u2014 below 40% zoom text glyphs are too small to read\n * anyway; skipping the `wrapText` + `measureText` calls saves the\n * bulk of text rendering cost.\n *\n * Hosts override per-render by passing `RenderSceneOptions.lod`.\n */\nexport const DEFAULT_LOD: LodOptions = {\n placeholder: 0.15,\n hideText: 0.4,\n};\n\n/**\n * Grey colour used for placeholder fills when LOD switches to the\n * cheapest path. A mid-tone neutral that blends with most scene\n * palettes; override via `RenderSceneOptions.placeholderFill`.\n */\nexport const DEFAULT_PLACEHOLDER_FILL = \"#bbb\";\n\n/**\n * Viewport-rect inflation factor applied by hosts when computing the\n * world-space culling rect. 0.05 = 5% padding on each side \u2014 enough\n * to avoid flicker during a one-frame pan without keeping much\n * off-screen geometry alive in the renderer.\n */\nexport const VIEWPORT_CULL_PADDING_RATIO = 0.05;\n\n/**\n * Text-decoration geometry (underline / strikethrough), as fractions of\n * font size, measured from the line's top (the renderer draws text with\n * a top baseline).\n *\n * - `TEXT_DECORATION_THICKNESS` \u2014 line thickness \u2248 6% of font size\n * (clamped to \u22651 px in the renderer).\n * - `TEXT_UNDERLINE_OFFSET` \u2014 underline top, ~92% down (just below the\n * glyph baseline).\n * - `TEXT_STRIKETHROUGH_OFFSET` \u2014 strikethrough centre, ~50% (x-height).\n */\nexport const TEXT_DECORATION_THICKNESS = 0.06;\nexport const TEXT_UNDERLINE_OFFSET = 0.92;\nexport const TEXT_STRIKETHROUGH_OFFSET = 0.5;\n\n/**\n * Corner radius (world px) for the rounded bends of an elbow (orthogonal)\n * connector and of a straight connector broken by user waypoints. Each\n * corner is replaced by a quadratic arc of this radius, clamped to half the\n * shorter adjacent segment so short segments don't overshoot. 0 disables\n * rounding (sharp corners). Range: 0\u201316.\n */\nexport const LINK_CORNER_RADIUS = 10;\n\n// --- Grid -------------------------------------------------------------------\n//\n// Lines and dots are tuned independently: a ruled line covers far more\n// pixels than a lone dot, so the dot grid needs a darker colour, a\n// slightly fatter mark, and a denser ladder to read as clearly as the\n// line grid at the same zoom.\n\n/** Stroke colour for the ruled (`\"lines\"`) grid. Neutral step-6 gray. */\nexport const GRID_LINE_COLOR = GRID_COLOR;\n\n/** Fill colour for the dotted (`\"dots\"`) grid \u2014 darker step-9 gray so the dots stay legible on a gray canvas. */\nexport const GRID_DOT_FILL = GRID_DOT_COLOR;\n\n/** On-screen stroke width (px) of a grid line. Divided by zoom at the use site so the line stays 1 px regardless of view scale. */\nexport const GRID_LINE_WIDTH_PX = 1.0;\n\n/**\n * Dot radius (screen px) for `gridStyle === \"dots\"`. Constant across\n * zoom (divided by `zoom` at the use site). Reads as a crisp anchor on\n * a gray surface. Range: 1.0\u20132.0.\n */\nexport const GRID_DOT_RADIUS_PX = 1;\n\n/**\n * Below this on-screen spacing (px) a grid level paints nothing \u2014\n * denser rendering reads as a flat haze. Only used by the fixed-ladder\n * path (`options.levels`); the default dynamic ladder uses the fade\n * bands below instead.\n */\nexport const GRID_MIN_SCREEN_SPACING_PX = 4;\n\n// --- Dynamic (infinite) grid ladder -----------------------------------------\n//\n// The default grid is a SELF-SIMILAR, zoom-relative ladder: instead of a\n// fixed set of world steps it renders a handful of rungs anchored to the\n// current zoom, each rung `GRID_LEVEL_SUBDIV`\u00D7 the previous. As you zoom\n// the rungs slide \u2014 a finer rung fades in and a coarser one fades out \u2014\n// so new lines / dots keep appearing at EVERY zoom, not just at the\n// hand-picked thresholds of a fixed ladder. Rungs finer than `gridSize`\n// are purely visual (snap-to-grid still rounds to `gridSize`).\n\n/** Ratio between adjacent rungs. 4 keeps the 64/16/4/1 cadence. */\nexport const GRID_LEVEL_SUBDIV = 4;\n\n/**\n * How many self-similar rungs to paint at once (finest first). 3 keeps a\n * stable fully-opaque coarse tier while the finest rung fades in/out.\n */\nexport const GRID_LEVEL_RUNGS = 3;\n\n/**\n * Line grid fade band (on-screen px). A rung is invisible at/below\n * `FROM`, ramps to full opacity by `FULL`, and stays full above. Tuned\n * so at 100 % (gridSize 20) the 20 px rung reads faint and the 80 px rung\n * is solid, while subdividing forever.\n */\nexport const GRID_LINE_FADE_FROM_PX = 12;\nexport const GRID_LINE_FADE_FULL_PX = 56;\n\n/**\n * Dot grid fade band. Lower / tighter than lines so the base `gridSize`\n * dot lattice is fully solid at 100 % (the denser dot field) yet still\n * subdivides on zoom-in.\n */\nexport const GRID_DOT_FADE_FROM_PX = 10;\nexport const GRID_DOT_FADE_FULL_PX = 20;\n\n// --- Block-arrow shape (BlockArrowElement) ----------------------------------\n\n/**\n * Fraction of the shape's length given to the arrow head when\n * `BlockArrowElement.headRatio` is omitted. 0.4 = head spans the last 40 %,\n * body the first 60 %. Clamped to `ARROWHEAD_RATIO_MIN`..`ARROWHEAD_RATIO_MAX`.\n */\nexport const ARROWHEAD_HEAD_RATIO = 0.4;\n\n/**\n * Fraction of the shape's cross-axis filled by the body when\n * `BlockArrowElement.bodyThickness` is omitted. 0.5 = body half as thick as\n * the box. Clamped to `ARROWHEAD_RATIO_MIN`..`ARROWHEAD_RATIO_MAX`.\n */\nexport const ARROWHEAD_BODY_THICKNESS = 0.5;\n\n/**\n * Lower clamp for the block-arrow head/body ratios so a degenerate input can't\n * collapse the head or body to nothing. Range: 0\u2013`ARROWHEAD_RATIO_MAX`.\n */\nexport const ARROWHEAD_RATIO_MIN = 0.1;\n\n/**\n * Upper clamp for the block-arrow head/body ratios so the head/body can't eat\n * the whole box. Range: `ARROWHEAD_RATIO_MIN`\u20131.\n */\nexport const ARROWHEAD_RATIO_MAX = 0.9;\n\n// --- Frame chrome colours ---------------------------------------------------\n\n/** Outline colour of a frame when no explicit style overrides it. Neutral gray. */\nexport const FRAME_STROKE_COLOR = \"#888\";\n\n/** Default body fill of a frame when `style.fill` is omitted. White. */\nexport const FRAME_FILL_COLOR = \"#ffffff\";\n\n/** Background fill of the frame's header strip. Near-black. */\nexport const FRAME_HEADER_BG_COLOR = \"#222\";\n\n/** Text colour of the frame's header label. Light gray for contrast on the dark strip. */\nexport const FRAME_HEADER_TEXT_COLOR = \"#ddd\";\n\n// --- Edge / link rendering defaults -----------------------------------------\n\n/**\n * Length (world px) of a block-arrow edge's head triangle when\n * `Link.blockArrow.headLength` is omitted. The body terminates this far before\n * the endpoint so the head fills the gap. Range: ~8\u201340.\n */\nexport const BLOCK_ARROW_HEAD_LENGTH = 18;\n\n/**\n * Body thickness (world px) of a block-arrow edge when\n * `Link.blockArrow.bodyThickness` is omitted. Offset half this on each side of\n * the routed path. Range: ~4\u201332.\n */\nexport const BLOCK_ARROW_BODY_THICKNESS = 12;\n\n/** Fallback fill for a block-arrow edge when neither `style.fill` nor `style.stroke` is set. Mid gray. */\nexport const BLOCK_ARROW_FILL_COLOR = \"#444\";\n\n/** Fallback stroke for a block-arrow edge when `style.stroke` is omitted. Near-black. */\nexport const BLOCK_ARROW_STROKE_COLOR = \"#222\";\n\n/**\n * Arrowhead size (world px) when `LinkArrowheads.size` is omitted. Drives the\n * wing/length scale of every arrowhead style. Range: ~6\u201324.\n */\nexport const ARROWHEAD_SIZE = 10;\n\n/** Fallback stroke colour for an edge / its arrowheads when `style.stroke` is omitted. Black. */\nexport const EDGE_STROKE_COLOR = \"#000\";\n\n/**\n * Position along the edge (0 = start, 1 = end) where a link label sits when\n * `LinkLabel.position` is omitted. 0.5 = midpoint.\n */\nexport const LABEL_POSITION = 0.5;\n\n/** Font size (world px) of a link label when `LinkLabel.fontSize` is omitted. */\nexport const LABEL_FONT_SIZE = 12;\n\n/** Fallback text colour of a link label when `LinkLabel.fill` is omitted. Near-black. */\nexport const LABEL_FILL_COLOR = \"#222\";\n\n/** Fallback pill-background colour of a link label when `LinkLabel.background` is omitted. White. */\nexport const LABEL_BG_COLOR = \"#fff\";\n", "/** Insertion-ordered LRU map: get() marks recency; set() evicts oldest past `cap` (by entry count). */\nexport class LruCache<K, V> {\n private readonly map = new Map<K, V>();\n constructor(\n private readonly cap: number,\n private readonly onEvict?: (key: K, value: V) => void,\n ) {}\n get(key: K): V | undefined {\n const v = this.map.get(key);\n if (v === undefined) return undefined;\n this.map.delete(key);\n this.map.set(key, v);\n return v;\n }\n has(key: K): boolean {\n return this.map.has(key);\n }\n set(key: K, value: V): void {\n this.map.delete(key);\n this.map.set(key, value);\n while (this.map.size > this.cap) {\n const oldest = this.map.keys().next().value as K;\n const ev = this.map.get(oldest);\n this.map.delete(oldest);\n if (ev !== undefined) this.onEvict?.(oldest, ev);\n }\n }\n delete(key: K): boolean {\n return this.map.delete(key);\n }\n clear(): void {\n this.map.clear();\n }\n get size(): number {\n return this.map.size;\n }\n keys(): IterableIterator<K> {\n return this.map.keys();\n }\n values(): IterableIterator<V> {\n return this.map.values();\n }\n}\n", "import type { ElementBase } from \"@oh-just-another/scene\";\nimport { LruCache } from \"./lru-cache.js\";\n\n/**\n * Per-shape rasterised cache. Keyed by the shape's identity reference \u2014\n * since scene mutations always replace the shape object (`apply(scene,\n * patch)` produces fresh references), a cache hit is guaranteed to reflect\n * the exact rendered output of the cached version. Pan / zoom invalidation\n * is the host's job: keep zoom in a small \"bucket\" (e.g. round to 0.1) and\n * include it in the key.\n *\n * LRU-by-insertion-order with a count cap. Hosts can replace with their own\n * cache by implementing the same `get` / `set` / `delete` surface.\n */\n\nexport interface ElementBitmapCache<V = unknown> {\n get(shape: ElementBase, zoomBucket: number): V | undefined;\n set(shape: ElementBase, zoomBucket: number, value: V): void;\n delete(shape: ElementBase, zoomBucket: number): void;\n clear(): void;\n readonly size: number;\n}\n\nconst keyFor = (shape: ElementBase, zoomBucket: number): string => `${shape.id}@${zoomBucket}`;\n\ninterface Entry<V> {\n readonly shapeRef: ElementBase;\n readonly value: V;\n}\n\n/**\n * In-memory LRU cache. Operates on shape identity (reference) \u2014\n * a stale shape reference for the same id is a miss because the\n * cached entry's `shapeRef !== shape`. That is the invalidation\n * mechanism \u2014 no version field needed.\n */\nexport class InMemoryElementBitmapCache<V> implements ElementBitmapCache<V> {\n private readonly entries: LruCache<string, Entry<V>>;\n\n constructor(cap = 512) {\n this.entries = new LruCache(cap);\n }\n\n get size(): number {\n return this.entries.size;\n }\n\n get(shape: ElementBase, zoomBucket: number): V | undefined {\n const key = keyFor(shape, zoomBucket);\n const e = this.entries.get(key);\n if (!e) return undefined;\n if (e.shapeRef !== shape) {\n // Reference changed \u2192 stale; evict so the slot is free.\n this.entries.delete(key);\n return undefined;\n }\n return e.value;\n }\n\n set(shape: ElementBase, zoomBucket: number, value: V): void {\n this.entries.set(keyFor(shape, zoomBucket), { shapeRef: shape, value });\n }\n\n delete(shape: ElementBase, zoomBucket: number): void {\n this.entries.delete(keyFor(shape, zoomBucket));\n }\n\n clear(): void {\n this.entries.clear();\n }\n}\n\n/**\n * Quantise a continuous zoom value to a bucket. Buckets within a\n * power-of-two range share a cache entry so small zoom adjustments\n * don't blow the cache. `bucket = 2 ^ round(log2(zoom))`.\n */\nexport const zoomBucket = (zoom: number): number => {\n if (zoom <= 0) return 1;\n return 2 ** Math.round(Math.log2(zoom));\n};\n", "import {\n getLayersInOrder,\n getElementsInLayer,\n getWorldToScreen,\n isText,\n type Scene,\n type ElementBase,\n type SpatialGrid,\n} from \"@oh-just-another/scene\";\nimport type { Bounds, LayerId, ElementId } from \"@oh-just-another/types\";\nimport { bounds as B, matrix } from \"@oh-just-another/math\";\nimport type { RenderTarget } from \"./render-target.js\";\nimport { getElementRenderer } from \"./shape-renderer.js\";\nimport { cachedWorldBounds, ElementCache } from \"./shape-cache.js\";\nimport { DEFAULT_PLACEHOLDER_FILL } from \"./constants.js\";\nimport type { LayerCompositeCache } from \"./layer-cache-composite.js\";\nimport { zoomBucket as bucketFor } from \"./shape-cache-bitmap.js\";\n\n/**\n * Zoom-based level-of-detail thresholds. Each threshold turns on a\n * cheaper render path when the scene zoom drops below it:\n *\n * - **placeholder** \u2192 draw a flat fill at the shape's world AABB and\n * skip the registered renderer entirely. Highest savings, biggest\n * visual fidelity loss; reserve for very-zoomed-out overviews.\n * - **hideText** \u2192 skip text shapes (their wrap+measure cost is high).\n *\n * Thresholds compare against `scene.viewport.zoom` (1.0 = 1:1 pixels).\n * Omit a threshold to disable that level.\n */\nexport interface LodOptions {\n readonly placeholder?: number;\n readonly hideText?: number;\n}\n\nexport interface RenderSceneOptions {\n /** Skip clearing the target before drawing. Default: false. */\n readonly skipClear?: boolean;\n /** Called for shapes whose `type` has no registered renderer. Default: ignore. */\n readonly onUnknownElement?: (shape: ElementBase) => void;\n /**\n * World-space viewport bounds. When provided, shapes whose AABB does\n * not intersect this rectangle are skipped (viewport culling). Pass\n * a slightly inflated rect to avoid pop-in during pan.\n */\n readonly viewport?: Bounds;\n /**\n * Persistent bounds cache. When omitted a fresh per-render cache is\n * created \u2014 fine for hot paths because lookups inside one frame still\n * amortize. Pass a long-lived cache from `Editor` to share work across\n * frames, hit-test, and overlay.\n */\n readonly boundsCache?: ElementCache<Bounds>;\n /**\n * Pre-built spatial index. When provided together with `viewport`, the\n * renderer picks candidate shapes from the index and skips full layer\n * scans \u2014 pays off around ~10k shapes.\n */\n readonly spatialIndex?: SpatialGrid;\n /**\n * Zoom thresholds for cheaper render paths. See {@link LodOptions}.\n */\n readonly lod?: LodOptions;\n /**\n * Placeholder fill colour. Defaults to `#bbb`. Pick something close\n * to the average shape colour so the transition is unobtrusive.\n */\n readonly placeholderFill?: string;\n /**\n * Optional dirty rectangle in **world** coords. When set:\n * \u2022 the renderer clears only the corresponding screen region;\n * \u2022 shapes whose world AABB doesn't intersect the dirty rect are\n * skipped entirely.\n * Combined with shape-identity tracking by the host this drops most\n * of the per-frame work for \"single shape moves on otherwise static\n * scene\".\n */\n readonly dirtyWorld?: Bounds;\n /**\n * Shapes to render with reduced alpha (modern-style group isolation).\n * For each shape whose `id` appears in this set, the renderer sets\n * `globalAlpha = dimOpacity` for the per-shape draw pass before\n * dispatching to the registered renderer.\n *\n * Caveat: shapes whose own `style.opacity` is explicitly set will\n * have their renderer call `setOpacity` again and override the\n * dim \u2014 the dim affects only the common case where shapes don't\n * carry an explicit opacity. Acceptable for the isolation UX\n * because outsiders are usually plain opaque shapes.\n */\n readonly dimElements?: ReadonlySet<ElementId>;\n /**\n * Alpha to use for `dimElements`. Default 1 (no-op). Hosts using the\n * isolation feature should pass their `ISOLATION_DIM_OPACITY`\n * constant.\n */\n readonly dimOpacity?: number;\n /**\n * Element ids that should NOT render this pass. The host computes\n * which shapes are effectively hidden (e.g. via group hide\n * propagation) and forwards the set here.\n */\n readonly hideElements?: ReadonlySet<ElementId>;\n /**\n * Per-layer composite bitmap cache. When supplied along with\n * `compositeLayerBitmap`, unchanged layers (i.e. not present in\n * `dirtyLayerIds`) are drawn from a single cached `drawImage` call\n * instead of walking every shape.\n *\n * Pass `dirtyLayerIds` so the renderer knows which layers to\n * re-rasterise. Without it the cache is treated as cold every\n * frame (defensive \u2014 better stale work than a stale visual).\n */\n readonly layerCompositeCache?: LayerCompositeCache;\n readonly dirtyLayerIds?: ReadonlySet<LayerId>;\n /**\n * Host-side layer rasteriser. Receives the layer id, the active\n * zoom bucket, and the scene; returns the bitmap to cache or\n * `null` to opt out. The kernel doesn't ship one \u2014 OffscreenCanvas\n * creation is the backend's job.\n */\n readonly compositeLayerBitmap?: (layerId: LayerId, zoomBucket: number, scene: Scene) => unknown;\n}\n\n/**\n * Renders the `main` z-stack of a scene onto a single target.\n *\n * Order of operations:\n * 1. Optionally clear the surface.\n * 2. Apply the scene's world-to-screen transform.\n * 3. For each visible layer (bottom \u2192 top): for each shape (bottom \u2192 top):\n * save state, push the shape's local TRS, invoke its registered renderer.\n *\n * This function does not draw edges, selection handles, or grids \u2014 those\n * either live on different layers (`background` / `overlay`) or are added by\n * higher-level packages.\n */\nexport const renderScene = (\n scene: Scene,\n target: RenderTarget,\n options: RenderSceneOptions = {},\n): void => {\n const w2s = getWorldToScreen(scene.viewport);\n const dirtyWorld = options.dirtyWorld;\n if (!options.skipClear) {\n if (dirtyWorld) {\n // Project the dirty rect to screen pixels, inflate by a few\n // pixels to cover anti-aliased stroke fuzz.\n const corners = [\n matrix.applyToPoint(w2s, { x: dirtyWorld.x, y: dirtyWorld.y }),\n matrix.applyToPoint(w2s, {\n x: dirtyWorld.x + dirtyWorld.width,\n y: dirtyWorld.y + dirtyWorld.height,\n }),\n ];\n const screen = B.expand(B.fromPoints(corners), 2);\n target.clear(screen);\n } else {\n target.clear();\n }\n }\n\n target.save();\n target.setTransform(w2s);\n\n const boundsCache = options.boundsCache ?? new ElementCache<Bounds>();\n const viewport = options.viewport;\n // Spatial-index candidate set: when present, restricts the per-layer\n // walk to shapes the index considers possibly-visible. Without it the\n // per-shape AABB check on a cached bounds is still cheap (~50ns), so\n // the index is only worth the build cost for very large scenes.\n let candidates: ReadonlySet<ElementId> | null = null;\n if (viewport && options.spatialIndex) {\n candidates = options.spatialIndex.query(viewport);\n }\n\n const zoom = scene.viewport.zoom;\n const lod = options.lod;\n const usePlaceholder = lod?.placeholder !== undefined && zoom < lod.placeholder;\n const dropText = lod?.hideText !== undefined && zoom < lod.hideText;\n const placeholderFill = options.placeholderFill ?? DEFAULT_PLACEHOLDER_FILL;\n\n const layerCache = options.layerCompositeCache;\n const dirtyLayers = options.dirtyLayerIds;\n const compositeLayerBitmap = options.compositeLayerBitmap;\n const zoomBucket = bucketFor(zoom);\n const layerBoundsFor = (layerId: LayerId): Bounds | null => {\n let acc: Bounds | null = null;\n for (const shape of getElementsInLayer(scene, layerId)) {\n const bb = cachedWorldBounds(boundsCache, shape);\n acc = acc ? B.union(acc, bb) : bb;\n }\n return acc;\n };\n\n for (const layer of getLayersInOrder(scene)) {\n if (!layer.visible) continue;\n\n // Per-layer composite cache fast path. Only fires when the host\n // plugged a cache + a layer rasteriser; the kernel ships no default\n // rasteriser (OffscreenCanvas creation is the backend's job). Drop\n // dirty layers from the cache so the bitmap isn't re-used after a\n // mutation.\n if (layerCache && compositeLayerBitmap) {\n if (dirtyLayers?.has(layer.id)) layerCache.invalidateLayer(layer.id);\n let bitmap = layerCache.get(layer.id, zoomBucket);\n if (bitmap === undefined) {\n const fresh = compositeLayerBitmap(layer.id, zoomBucket, scene);\n if (fresh !== null) {\n layerCache.set(layer.id, zoomBucket, fresh);\n bitmap = fresh;\n }\n }\n if (bitmap !== undefined) {\n const bb = layerBoundsFor(layer.id);\n if (bb) target.drawImage(bitmap, bb.x, bb.y, bb.width, bb.height);\n continue;\n }\n }\n\n for (const shape of getElementsInLayer(scene, layer.id)) {\n if (options.hideElements?.has(shape.id)) continue;\n if (candidates && !candidates.has(shape.id)) continue;\n if (viewport) {\n const bb = cachedWorldBounds(boundsCache, shape);\n if (!B.intersects(bb, viewport)) continue;\n }\n if (dirtyWorld) {\n const bb = cachedWorldBounds(boundsCache, shape);\n if (!B.intersects(bb, dirtyWorld)) continue;\n }\n\n if (dropText && isText(shape)) continue;\n\n if (usePlaceholder) {\n // Draw the AABB directly in world coords \u2014 skip the renderer\n // entirely. The shape's TRS is folded into the cached bounds.\n const bb = cachedWorldBounds(boundsCache, shape);\n target.setFill(placeholderFill);\n target.setStrokeWidth(0);\n target.beginPath();\n target.rect(bb.x, bb.y, bb.width, bb.height);\n target.fill();\n continue;\n }\n\n const renderer = getElementRenderer(shape.type);\n if (!renderer) {\n options.onUnknownElement?.(shape);\n continue;\n }\n\n target.save();\n // Isolation dim \u2014 set BEFORE the renderer runs so any\n // shape-style-specific setOpacity inside the renderer can\n // override (see RenderSceneOptions.dimElements).\n if (options.dimElements?.has(shape.id) && options.dimOpacity !== undefined) {\n target.setOpacity(options.dimOpacity);\n }\n target.translate(shape.position.x, shape.position.y);\n if (shape.rotation !== 0) target.rotate(shape.rotation);\n if (shape.scale.x !== 1 || shape.scale.y !== 1) {\n target.scale(shape.scale.x, shape.scale.y);\n }\n renderer(shape, target, { zoom });\n target.restore();\n }\n }\n\n target.restore();\n};\n", "import type { TextAlign } from \"./render-target.js\";\nimport { req, type Vec2 } from \"@oh-just-another/types\";\n\n/**\n * Caret-aware text layout. Unlike {@link wrapText} (which collapses\n * whitespace and is only good enough for *drawing*), this keeps every\n * line as an exact substring of the source plus its `[start, end)`\n * character offsets \u2014 so a caret index maps unambiguously to a line +\n * column. Measurement is injected as a `measure(s) => width` callback\n * so the same geometry can be computed against either backend's font\n * metrics (Canvas2D `measureText` or WebGL2 MSDF advances).\n *\n * Convention for `\\n`: a hard newline at source index `k` ends the\n * current line at `end === k` (caret at `k` = end of line) and the next\n * line starts at `start === k + 1` (caret at `k + 1` = start of next\n * line). The `\\n` itself never holds a caret.\n */\nexport interface LaidOutLine {\n /** Exact source substring for this visual line (no whitespace collapsing). */\n readonly text: string;\n /** Source offset where the line begins (inclusive). */\n readonly start: number;\n /** Source offset where the line ends (exclusive; excludes a trailing `\\n`). */\n readonly end: number;\n /** Measured width of `text` in CSS px. */\n readonly width: number;\n}\n\nexport interface EditableTextLayout {\n readonly lines: readonly LaidOutLine[];\n readonly lineHeight: number;\n /** Width the lines are aligned within (maxWidth, or the widest line). */\n readonly blockWidth: number;\n}\n\nexport type MeasureText = (text: string) => number;\n\nexport interface LayoutTextOptions {\n readonly fontSize: number;\n /** Wrap budget in CSS px. `undefined` \u2192 no wrap (split on `\\n` only). */\n readonly maxWidth?: number;\n /** Line-height multiplier. Default 1.2 (matches the text renderer). */\n readonly lineHeightFactor?: number;\n}\n\n/** Default multiplier from font size to line height (matches `drawText`). */\nexport const DEFAULT_LINE_HEIGHT_FACTOR = 1.2;\n\nconst wrapParagraph = (\n para: string,\n base: number,\n maxWidth: number,\n measure: MeasureText,\n out: LaidOutLine[],\n): void => {\n if (para === \"\") {\n out.push({ text: \"\", start: base, end: base, width: 0 });\n return;\n }\n // Word spans (non-whitespace runs) with offsets relative to `para`.\n const words: { s: number; e: number }[] = [];\n const re = /\\S+/g;\n let m: RegExpExecArray | null;\n while ((m = re.exec(para)) !== null) words.push({ s: m.index, e: m.index + m[0].length });\n if (words.length === 0) {\n // Whitespace-only paragraph \u2014 keep it as one line so offsets survive.\n out.push({ text: para, start: base, end: base + para.length, width: measure(para) });\n return;\n }\n\n // `white-space: pre-wrap` + `overflow-wrap: break-word` (standard):\n // preserve whitespace, wrap at word boundaries, and break a word that\n // is itself wider than the line so narrowing the block always reflows\n // (a single long word can't overflow forever). Lines are gapless\n // slices of the source \u2014 inter-word whitespace at a soft break stays\n // on the preceding line \u2014 so every character maps to exactly one line\n // (clean caret offsets). First line starts at 0 (keeps leading\n // whitespace); the last extends to the paragraph end (trailing ws).\n let lineStart = 0;\n const push = (start: number, end: number): void => {\n const text = para.slice(start, end);\n out.push({ text, start: base + start, end: base + end, width: measure(text) });\n };\n let i = 0;\n while (i < words.length) {\n const w = req(words[i]);\n if (measure(para.slice(lineStart, w.e)) <= maxWidth) {\n i++; // word fits on the current line \u2014 keep it, try the next\n continue;\n }\n if (w.s > lineStart) {\n // Content precedes this word on the line \u2192 break before it. The\n // whitespace up to its start stays on the current line (pre-wrap).\n push(lineStart, w.s);\n lineStart = w.s;\n continue; // retry this word on the fresh line\n }\n // The word starts the line and alone overflows \u2192 break it by chars,\n // keeping at least one char per line so we always make progress.\n let e = w.s + 1;\n while (e < w.e && measure(para.slice(lineStart, e + 1)) <= maxWidth) e++;\n if (e >= w.e) {\n // Whole (remaining) word consumed \u2014 leave it on the current line\n // and advance. Guards termination when the measurer is degenerate\n // (e.g. a constant stub that never reports \"fits\").\n i++;\n continue;\n }\n push(lineStart, e);\n lineStart = e;\n w.s = e; // remainder of the word continues on the next line\n }\n // Last line keeps everything through the end of the paragraph.\n push(lineStart, para.length);\n};\n\n/**\n * Lay out `text` into visual lines with exact source offsets. Always\n * returns at least one (possibly empty) line.\n */\nexport const layoutText = (\n text: string,\n measure: MeasureText,\n options: LayoutTextOptions,\n): EditableTextLayout => {\n const lineHeight = options.fontSize * (options.lineHeightFactor ?? DEFAULT_LINE_HEIGHT_FACTOR);\n const lines: LaidOutLine[] = [];\n let paraStart = 0;\n for (let i = 0; i <= text.length; i++) {\n if (i === text.length || text[i] === \"\\n\") {\n const para = text.slice(paraStart, i);\n if (options.maxWidth === undefined) {\n lines.push({ text: para, start: paraStart, end: i, width: measure(para) });\n } else {\n wrapParagraph(para, paraStart, options.maxWidth, measure, lines);\n }\n paraStart = i + 1;\n }\n }\n if (lines.length === 0) lines.push({ text: \"\", start: 0, end: 0, width: 0 });\n let widest = 0;\n for (const l of lines) widest = Math.max(widest, l.width);\n const blockWidth = options.maxWidth ?? widest;\n return { lines, lineHeight, blockWidth };\n};\n\n/** Left edge (local x) where a line's glyphs start, given the align. */\nconst lineLeftX = (lineWidth: number, blockWidth: number, align: TextAlign): number => {\n if (align === \"center\") return blockWidth / 2 - lineWidth / 2;\n if (align === \"right\") return blockWidth - lineWidth;\n return 0;\n};\n\n/** Index of the line a caret offset falls on (handles boundaries). */\nconst lineIndexForCaret = (layout: EditableTextLayout, caret: number): number => {\n const { lines } = layout;\n for (let i = 0; i < lines.length; i++) {\n const l = req(lines[i]);\n // Caret belongs to this line when it's within [start, end]; the\n // upper bound is inclusive so end-of-line resolves here, while\n // start-of-next-line (end + 1 for a hard `\\n`) resolves to the\n // next line on the following iteration.\n if (caret <= l.end) return i;\n }\n return lines.length - 1;\n};\n\nexport interface CaretGeometry {\n /** Local x of the caret bar. */\n readonly x: number;\n /** Local y of the caret top (baseline-top line origin). */\n readonly y: number;\n /** Caret height (\u2248 font size). */\n readonly height: number;\n /** Index of the line the caret sits on. */\n readonly line: number;\n}\n\n/**\n * Local-space geometry of the caret for a given source `caret` offset.\n * `align` must match the renderer's `textAlign`.\n */\nexport const caretGeometry = (\n layout: EditableTextLayout,\n caret: number,\n measure: MeasureText,\n fontSize: number,\n align: TextAlign,\n): CaretGeometry => {\n const i = lineIndexForCaret(layout, caret);\n const line = req(layout.lines[i]);\n const col = Math.max(0, Math.min(caret, line.end) - line.start);\n const prefixWidth = col === 0 ? 0 : measure(line.text.slice(0, col));\n const left = lineLeftX(line.width, layout.blockWidth, align);\n return { x: left + prefixWidth, y: i * layout.lineHeight, height: fontSize, line: i };\n};\n\n/**\n * Map a local-space point to the nearest source caret offset. Used for\n * click-to-place-caret and drag-to-select.\n */\nexport const pointToCaretIndex = (\n layout: EditableTextLayout,\n point: Vec2,\n measure: MeasureText,\n align: TextAlign,\n): number => {\n const { lines, lineHeight } = layout;\n const i = Math.max(0, Math.min(lines.length - 1, Math.floor(point.y / lineHeight)));\n const line = req(lines[i]);\n const left = lineLeftX(line.width, layout.blockWidth, align);\n // Walk columns, picking the boundary whose x is closest to point.x.\n let best = 0;\n let bestDist = Math.abs(left - point.x);\n for (let col = 1; col <= line.text.length; col++) {\n const x = left + measure(line.text.slice(0, col));\n const d = Math.abs(x - point.x);\n if (d < bestDist) {\n bestDist = d;\n best = col;\n }\n }\n return line.start + best;\n};\n\nexport interface SelectionRect {\n readonly x: number;\n readonly y: number;\n readonly width: number;\n readonly height: number;\n}\n\n/**\n * Local-space highlight rectangles covering the source range `[from, to)`\n * (order-independent), one per visual line it spans.\n */\nexport const selectionRects = (\n layout: EditableTextLayout,\n from: number,\n to: number,\n measure: MeasureText,\n align: TextAlign,\n): readonly SelectionRect[] => {\n const lo = Math.min(from, to);\n const hi = Math.max(from, to);\n if (lo === hi) return [];\n const rects: SelectionRect[] = [];\n for (let i = 0; i < layout.lines.length; i++) {\n const line = req(layout.lines[i]);\n const a = Math.max(lo, line.start);\n const b = Math.min(hi, line.end);\n if (a > b) continue;\n if (a === b && !(lo <= line.start && hi > line.end)) {\n // Empty intersection on this line, unless the selection spans the\n // hard break past it (then show a thin trailing marker).\n if (!(hi > line.end && lo <= line.end)) continue;\n }\n const left = lineLeftX(line.width, layout.blockWidth, align);\n const xa = left + (a === line.start ? 0 : measure(line.text.slice(0, a - line.start)));\n const xb = left + (b === line.start ? 0 : measure(line.text.slice(0, b - line.start)));\n // A line whose break is inside the selection gets a small trailing\n // sliver so multi-line selections read continuously.\n const trailing = hi > line.end ? layout.lineHeight * 0.25 : 0;\n rects.push({\n x: xa,\n y: i * layout.lineHeight,\n width: Math.max(0, xb - xa) + trailing,\n height: layout.lineHeight,\n });\n }\n return rects;\n};\n", "/**\n * Animated-content adapter registry. The kernel doesn't decode GIF /\n * Lottie / video itself: it exposes an `AnimatedSourceAdapter` interface\n * and a process-global registry indexed by `kind` (\"gif\", \"lottie\",\n * \"video\", \"<your-format>\"). Hosts plug their decoder of choice:\n *\n * registerAnimationAdapter({\n * kind: \"gif\",\n * getFrameAt(data, timestampMs) { ... return ImageBitmap }\n * });\n *\n * An `ImageElement` carrying `animationKind` + `animationData` resolves\n * its source through the registry; without those fields the static `src`\n * is used. The kernel only answers the stateless \"what should this frame\n * look like?\" question \u2014 live playback ticking is the host's job.\n */\n\nexport interface AnimatedSourceAdapter<Data = unknown> {\n readonly kind: string;\n /**\n * Return the image source the renderer should draw at\n * `timestampMs` (typically `performance.now()`). The returned\n * value is opaque \u2014 it gets passed straight to `target.drawImage\n * (image, ...)`. Backends accept different types: Canvas2D wants\n * a `CanvasImageSource`, headless SVG wants a string URL. The\n * adapter's `kind` is paired with the renderer the host actually\n * uses, so there's no ambiguity in practice.\n *\n * Implementations are stateless w.r.t. the registry; they may\n * cache decoded frames internally (the `data` payload is the\n * natural cache key).\n */\n getFrameAt(data: Data, timestampMs: number): unknown;\n /**\n * Optional \u2014 total animation duration in ms. The animation tick\n * uses this to schedule the next frame; an unset value means\n * \"keep ticking forever\" (endless lottie loops or streamed video).\n */\n totalDurationMs?(data: Data): number;\n}\n\nconst registry = new Map<string, AnimatedSourceAdapter>();\n\nexport const registerAnimationAdapter = <D>(adapter: AnimatedSourceAdapter<D>): void => {\n registry.set(adapter.kind, adapter);\n};\n\nexport const unregisterAnimationAdapter = (kind: string): void => {\n registry.delete(kind);\n};\n\nexport const getAnimationAdapter = (kind: string): AnimatedSourceAdapter | undefined =>\n registry.get(kind);\n\nexport const listAnimationKinds = (): readonly string[] => [...registry.keys()];\n\n/**\n * Content-ready notification. Adapters decode lazily and often\n * asynchronously (e.g. the GIF adapter's `createImageBitmap`): the\n * first `getFrameAt` returns `null` while the decode is in flight. For\n * a *playing* shape the host's animation tick re-renders on the next\n * rAF and picks up the frames \u2014 but a **paused** shape (reduced-motion,\n * auto-stopped, frozen) has no tick, so without a nudge it would stay\n * blank forever once decoded. Adapters call\n * {@link notifyAnimationContentReady} when a decode completes; the host\n * (editor) subscribes via {@link onAnimationContentReady} and schedules\n * one more render so the now-decoded (possibly paused) frame paints.\n */\nconst contentListeners = new Set<() => void>();\n\nexport const onAnimationContentReady = (fn: () => void): (() => void) => {\n contentListeners.add(fn);\n return () => contentListeners.delete(fn);\n};\n\nexport const notifyAnimationContentReady = (): void => {\n for (const fn of contentListeners) {\n try {\n fn();\n } catch {\n /* a listener throwing must not break sibling listeners / decode */\n }\n }\n};\n\n/**\n * Pluggable playback clock. Returns the playback position (ms) the\n * animation adapter should be sampled at for a given shape \u2014 letting\n * a host pause / freeze / offset individual animated shapes without\n * the renderer knowing about playback state.\n *\n * Default: wall-clock `performance.now()` for every shape (every GIF\n * plays, in lock-step with real time). A host (the editor) overrides\n * this via {@link setAnimationClock} to consult its per-shape\n * playback map \u2014 returning a frozen value for paused shapes, an\n * offset for shapes started later, etc.\n *\n * Module-global by design: the shape-renderer signature is\n * `(shape, target)` with no options channel, so the host sets the\n * clock immediately before each synchronous render pass.\n */\ntype AnimationClock = (shape: { readonly id?: unknown }) => number;\n\nlet animationClock: AnimationClock = () =>\n typeof performance !== \"undefined\" ? performance.now() : 0;\n\nexport const setAnimationClock = (clock: AnimationClock): void => {\n animationClock = clock;\n};\n\n/** Restore the default wall-clock playback (used in tests / teardown). */\nexport const resetAnimationClock = (): void => {\n animationClock = () => (typeof performance !== \"undefined\" ? performance.now() : 0);\n};\n\n/**\n * Resolve an image source for an `ImageElement`. When the shape has\n * an `animationKind` and a matching adapter is registered, the\n * adapter's `getFrameAt(animationData, t)` result is returned, where\n * `t` comes from the pluggable {@link setAnimationClock} (default\n * wall-clock). Otherwise \u2014 and as a fallback when the adapter throws \u2014\n * falls back to the static `src`. The renderer hands the result to\n * `target.drawImage` without further interpretation.\n */\nexport const resolveImageSource = (\n shape: {\n readonly id?: unknown;\n readonly src: string;\n readonly animationKind?: string;\n readonly animationData?: unknown;\n },\n timestampMs: number = animationClock(shape),\n): unknown => {\n if (!shape.animationKind) return shape.src;\n const adapter = registry.get(shape.animationKind);\n if (!adapter) return shape.src;\n try {\n return adapter.getFrameAt(shape.animationData, timestampMs);\n } catch {\n return shape.src;\n }\n};\n", "import { polygon as polygonMath } from \"@oh-just-another/math\";\nimport {\n getCornerRadius,\n registerRenderOverflow,\n FRAME_HEADER_HEIGHT,\n FRAME_HEADER_PADDING_X,\n FRAME_HEADER_FONT_SIZE,\n type BlockArrowElement,\n type BrushElement,\n type EllipseElement,\n type FrameElement,\n type GroupElement,\n type ImageElement,\n type PathElement,\n type PolygonElement,\n type RectangleElement,\n type Style,\n type TextElement,\n} from \"@oh-just-another/scene\";\nimport { registerElementRenderer, type ElementRenderer } from \"./shape-renderer.js\";\nimport type { RenderTarget } from \"./render-target.js\";\nimport { layoutText } from \"./text-editing.js\";\nimport { resolveImageSource } from \"./animation-adapter.js\";\nimport {\n TEXT_DECORATION_THICKNESS,\n TEXT_UNDERLINE_OFFSET,\n TEXT_STRIKETHROUGH_OFFSET,\n ARROWHEAD_HEAD_RATIO,\n ARROWHEAD_BODY_THICKNESS,\n ARROWHEAD_RATIO_MIN,\n ARROWHEAD_RATIO_MAX,\n FRAME_STROKE_COLOR,\n FRAME_FILL_COLOR,\n FRAME_HEADER_BG_COLOR,\n FRAME_HEADER_TEXT_COLOR,\n} from \"./constants.js\";\nimport { req, type Vec2 } from \"@oh-just-another/types\";\n\n/**\n * Applies common style fields to a target. Returns whether any fill or stroke\n * was configured \u2014 shape renderers use the result to decide which paint call\n * to issue.\n */\nconst applyStyle = (style: Style, target: RenderTarget): { fill: boolean; stroke: boolean } => {\n const hasFill = style.fill !== undefined && style.fill !== \"transparent\";\n const hasStroke =\n style.stroke !== undefined && style.stroke !== \"transparent\" && (style.strokeWidth ?? 1) > 0;\n\n if (hasFill) target.setFill(style.fill);\n if (hasStroke) {\n target.setStroke(style.stroke);\n target.setStrokeWidth(style.strokeWidth ?? 1);\n if (style.lineCap) target.setLineCap(style.lineCap);\n if (style.lineJoin) target.setLineJoin(style.lineJoin);\n if (style.dashArray) target.setDashArray(style.dashArray);\n }\n if (style.opacity !== undefined) target.setOpacity(style.opacity);\n\n return { fill: hasFill, stroke: hasStroke };\n};\n\nconst drawRectangle: ElementRenderer<RectangleElement> = (shape, target) => {\n const { fill, stroke } = applyStyle(shape.style, target);\n if (!fill && !stroke) return;\n const r = getCornerRadius(shape.style.roundness, shape.width, shape.height);\n // Fill path \u2014 always uses the original shape geometry.\n if (fill) {\n target.beginPath();\n if (r > 0) {\n buildRoundedRectPath(target, 0, 0, shape.width, shape.height, r);\n } else {\n target.rect(0, 0, shape.width, shape.height);\n }\n target.fill();\n }\n // Stroke path \u2014 offset by `strokeAlign` so the stroke sits inside\n // / centred-on / outside the fill region. The default (omitted /\n // `center`) reuses the fill geometry. Implemented at this layer so\n // every backend (Canvas2D, WebGL2, SVG) honours strokeAlign without\n // backend-specific work \u2014 the math is purely on the rect bounds.\n if (stroke) {\n const offset = strokeAlignOffset(shape.style);\n const sx = offset;\n const sy = offset;\n const sw = shape.width - 2 * offset;\n const sh = shape.height - 2 * offset;\n if (sw <= 0 || sh <= 0) return; // degenerate offset \u2014 skip\n const sr = r > 0 ? Math.max(0, r - offset) : 0;\n target.beginPath();\n if (sr > 0) {\n buildRoundedRectPath(target, sx, sy, sw, sh, sr);\n } else {\n target.rect(sx, sy, sw, sh);\n }\n target.stroke();\n }\n};\n\n/**\n * Translate `Style.strokeAlign` into a path-offset distance in world\n * units. The rendered stroke geometry shifts by `\u00B1half-width` along\n * the inward / outward normal:\n * center \u2192 0 (path centred \u2014 Canvas2D / SVG default).\n * inside \u2192 +half-width (path moves inward so the stroke's outer\n * edge sits on the original fill boundary).\n * outside \u2192 -half-width (path moves outward so the stroke's inner\n * edge sits on the boundary).\n *\n * Only used by axis-aligned primitives (rectangle, container) where\n * \"inward\" reduces to \"subtract from bbox\".\n */\nconst strokeAlignOffset = (style: Style): number => {\n const align = style.strokeAlign ?? \"center\";\n if (align === \"center\") return 0;\n const half = (style.strokeWidth ?? 1) / 2;\n return align === \"inside\" ? half : -half;\n};\n\n/**\n * Build a rounded-rect path via the standard \"4 corners with\n * quadratic Bezier arcs\" pattern \u2014 same shape every backend\n * understands without a special `roundRect()` API:\n *\n * \u250C\u2500\u2500\u2500arc\u2500\u2500\u2500\u2510\n * \u2502 \u2502\n * arc arc\n * \u2502 \u2502\n * \u2514\u2500\u2500\u2500arc\u2500\u2500\u2500\u2518\n *\n * Quadratic control points sit at each corner of the rect; the\n * curve goes from `r` units along one side to `r` units along the\n * adjacent side.\n *\n * Radius `r` is pre-clamped by `getCornerRadius` to half the\n * smaller side, so no overlap-handling is needed here.\n */\nconst buildRoundedRectPath = (\n target: RenderTarget,\n x: number,\n y: number,\n w: number,\n h: number,\n r: number,\n): void => {\n target.moveTo(x + r, y);\n target.lineTo(x + w - r, y);\n target.quadraticCurveTo(x + w, y, x + w, y + r);\n target.lineTo(x + w, y + h - r);\n target.quadraticCurveTo(x + w, y + h, x + w - r, y + h);\n target.lineTo(x + r, y + h);\n target.quadraticCurveTo(x, y + h, x, y + h - r);\n target.lineTo(x, y + r);\n target.quadraticCurveTo(x, y, x + r, y);\n target.closePath();\n};\n\nconst drawEllipse: ElementRenderer<EllipseElement> = (shape, target) => {\n const { fill, stroke } = applyStyle(shape.style, target);\n if (!fill && !stroke) return;\n const rx = shape.width / 2;\n const ry = shape.height / 2;\n if (fill) {\n target.beginPath();\n target.ellipse(rx, ry, rx, ry);\n target.fill();\n }\n if (stroke) {\n // Inset / outset radii by `strokeAlignOffset` so the stroke\n // sits inside / centred-on / outside the fill ellipse. Centre\n // stays the same; radii shift uniformly. Degenerate (radius \u2264 0)\n // skips the pass.\n const offset = strokeAlignOffset(shape.style);\n const srx = rx - offset;\n const sry = ry - offset;\n if (srx <= 0 || sry <= 0) return;\n target.beginPath();\n target.ellipse(rx, ry, srx, sry);\n target.stroke();\n }\n};\n\nconst drawPolygon: ElementRenderer<PolygonElement> = (shape, target) => {\n if (shape.points.length < 2) return;\n const { fill, stroke } = applyStyle(shape.style, target);\n if (!fill && !stroke) return;\n if (fill) {\n target.beginPath();\n polygonPath(target, shape.points);\n target.fill();\n }\n if (stroke) {\n const offset = strokeAlignOffset(shape.style);\n const pts = offset !== 0 ? polygonMath.offsetClosedPath(shape.points, offset) : shape.points;\n target.beginPath();\n polygonPath(target, pts);\n target.stroke();\n }\n};\n\n/** Emit a closed polygon outline as `moveTo` + `lineTo`s + `closePath`. */\nconst polygonPath = (target: RenderTarget, pts: readonly Vec2[]): void => {\n const first = pts[0];\n if (first === undefined) return;\n target.moveTo(first.x, first.y);\n for (let i = 1; i < pts.length; i++) {\n const p = pts[i];\n if (p === undefined) continue;\n target.lineTo(p.x, p.y);\n }\n target.closePath();\n};\n\nconst drawPath: ElementRenderer<PathElement> = (shape, target) => {\n if (shape.commands.length === 0) return;\n const { fill, stroke } = applyStyle(shape.style, target);\n if (!fill && !stroke) return;\n target.beginPath();\n for (const cmd of shape.commands) {\n switch (cmd.kind) {\n case \"M\":\n target.moveTo(cmd.to.x, cmd.to.y);\n break;\n case \"L\":\n target.lineTo(cmd.to.x, cmd.to.y);\n break;\n case \"Q\":\n target.quadraticCurveTo(cmd.control.x, cmd.control.y, cmd.to.x, cmd.to.y);\n break;\n case \"C\":\n target.bezierCurveTo(\n cmd.control1.x,\n cmd.control1.y,\n cmd.control2.x,\n cmd.control2.y,\n cmd.to.x,\n cmd.to.y,\n );\n break;\n case \"Z\":\n target.closePath();\n break;\n }\n }\n if (fill) target.fill();\n if (stroke) target.stroke();\n};\n\nconst drawText: ElementRenderer<TextElement> = (shape, target) => {\n const align = shape.style.textAlign ?? \"left\";\n const weight = shape.style.fontWeight;\n const fontStyle = shape.style.fontStyle;\n target.setFont(shape.fontFamily, shape.fontSize, {\n ...(weight ? { weight } : {}),\n ...(fontStyle ? { style: fontStyle } : {}),\n });\n // Lines are positioned manually (per-line x below) so the caret\n // geometry computed from the same `layoutText` lines up exactly, so\n // the target always draws left-anchored.\n target.setTextAlign(\"left\");\n target.setTextBaseline(shape.style.textBaseline ?? \"top\");\n\n // Color: use fill if specified, otherwise default to black.\n const color = shape.style.fill ?? \"#000\";\n target.setFill(color);\n if (shape.style.opacity !== undefined) target.setOpacity(shape.style.opacity);\n\n // Resolve per-line geometry once (x = align offset, top = i \u00D7\n // lineHeight). Single-line text skips the wrap engine.\n const fontSize = shape.fontSize;\n let lines: { text: string; x: number; width: number; top: number }[];\n if (shape.maxWidth === undefined && !shape.text.includes(\"\\n\")) {\n lines = [{ text: shape.text, x: 0, width: target.measureText(shape.text).width, top: 0 }];\n } else {\n // Measure with the target's own `measureText` so wrapping matches\n // exactly what this backend draws.\n const measure = (s: string) => target.measureText(s).width;\n const layout = layoutText(shape.text, measure, {\n fontSize,\n ...(shape.maxWidth !== undefined ? { maxWidth: shape.maxWidth } : {}),\n });\n lines = layout.lines.map((line, i) => ({\n text: line.text,\n x:\n align === \"center\"\n ? layout.blockWidth / 2 - line.width / 2\n : align === \"right\"\n ? layout.blockWidth - line.width\n : 0,\n width: line.width,\n top: i * layout.lineHeight,\n }));\n }\n\n for (const l of lines) target.fillText(l.text, l.x, l.top);\n\n // Underline / strikethrough \u2014 thin filled rects per line, same on\n // Canvas2D and WebGL2 (uses the current text fill colour).\n const deco = shape.style.textDecoration;\n if (deco?.underline || deco?.strikethrough) {\n const thickness = Math.max(1, fontSize * TEXT_DECORATION_THICKNESS);\n for (const l of lines) {\n if (l.width <= 0) continue;\n if (deco.underline) {\n target.beginPath();\n target.rect(l.x, l.top + fontSize * TEXT_UNDERLINE_OFFSET, l.width, thickness);\n target.fill();\n }\n if (deco.strikethrough) {\n target.beginPath();\n target.rect(\n l.x,\n l.top + fontSize * TEXT_STRIKETHROUGH_OFFSET - thickness / 2,\n l.width,\n thickness,\n );\n target.fill();\n }\n }\n }\n};\n\n/**\n * Variable-width brush stroke. Each segment between two `BrushPoint`s\n * is drawn as a quad (two triangles) \u2014 its width interpolates from\n * `p.width` at the head to `q.width` at the tail. Renders\n * pressure-sensitive ink that gets thicker / thinner along the path\n * without needing per-segment `setStrokeWidth` calls (which most 2D\n * APIs treat as a single line width).\n */\nconst drawBrush: ElementRenderer<BrushElement> = (shape, target) => {\n const pts = shape.points;\n if (pts.length === 0) return;\n const fill = shape.style.fill ?? shape.style.stroke ?? \"#000\";\n target.setFill(fill);\n target.setStroke(null);\n // Single dot for one-point strokes \u2014 degenerate quad would be invisible.\n if (pts.length === 1) {\n const p = req(pts[0]);\n target.beginPath();\n target.ellipse(p.x, p.y, p.width, p.width);\n target.fill();\n return;\n }\n for (let i = 0; i < pts.length - 1; i++) {\n const a = req(pts[i]);\n const b = req(pts[i + 1]);\n const dx = b.x - a.x;\n const dy = b.y - a.y;\n const len = Math.hypot(dx, dy) || 1;\n const nx = -dy / len;\n const ny = dx / len;\n target.beginPath();\n target.moveTo(a.x + nx * a.width, a.y + ny * a.width);\n target.lineTo(b.x + nx * b.width, b.y + ny * b.width);\n target.lineTo(b.x - nx * b.width, b.y - ny * b.width);\n target.lineTo(a.x - nx * a.width, a.y - ny * a.width);\n target.closePath();\n target.fill();\n // End-cap as a disk at the joint \u2014 smooths the kink between segments.\n target.beginPath();\n target.ellipse(b.x, b.y, b.width, b.width);\n target.fill();\n }\n};\n\nconst drawImage: ElementRenderer<ImageElement> = (shape, target) => {\n // Priority: for an animated source prefer the per-frame image the\n // registered adapter returns; otherwise a preloaded handle in\n // `metadata.image`; otherwise the static `src` fallback.\n // `resolveImageSource` returns `null` while an async decode is still\n // in flight, which the backend's drawImage guard skips.\n const handle = shape.animationKind\n ? resolveImageSource(shape)\n : (shape.metadata?.image ?? resolveImageSource(shape));\n // `dynamic` \u2192 backends that cache the upload (WebGL2) re-upload the\n // current frame. GIF / video sources flag `metadata.animated`, and\n // any adapter-driven source is dynamic by definition.\n const dynamic = shape.metadata?.animated === true || shape.animationKind !== undefined;\n target.drawImage(handle, 0, 0, shape.width, shape.height, dynamic);\n};\n\n/**\n * Registers renderers for every `BuiltinElement` type.\n */\nexport const installBuiltinRenderers = (): void => {\n registerElementRenderer<RectangleElement>(\"rectangle\", drawRectangle);\n registerElementRenderer<EllipseElement>(\"ellipse\", drawEllipse);\n registerElementRenderer<PolygonElement>(\"polygon\", drawPolygon);\n registerElementRenderer<PathElement>(\"path\", drawPath);\n registerElementRenderer<TextElement>(\"text\", drawText);\n registerElementRenderer<ImageElement>(\"image\", drawImage);\n // Group shapes are invisible containers \u2014 the shape itself paints nothing.\n registerElementRenderer<GroupElement>(\"group\", () => {\n /* intentional no-op: group shapes are invisible containers and paint nothing */\n });\n registerElementRenderer<FrameElement>(\"frame\", drawFrame);\n // The frame paints its header strip ABOVE the rectangle, so its dirty\n // region must extend up by the header height \u2014 otherwise deleting a\n // frame leaves the header behind.\n registerRenderOverflow(\"frame\", () => ({ top: FRAME_HEADER_HEIGHT }));\n registerElementRenderer<BlockArrowElement>(\"block-arrow\", drawBlockArrow);\n registerElementRenderer<BrushElement>(\"brush\", drawBrush);\n};\n\n/**\n * Block-arrow silhouette: a rectangle body whose tip is replaced\n * by a triangle, oriented by `direction`. Path is closed and filled\n * with `style.fill`; stroke applies to the outline.\n *\n * right \u2192 \u250C\u2500\u2500\u2500\u2500\u2510\u25B6\n * \u2502 body \u2502\n * \u2514\u2500\u2500\u2500\u2500\u2518\n *\n * up \u2191 \u25B2\n * \u250C\u2500\u2500\u2510\n * \u2502 \u2502\n * \u2514\u2500\u2500\u2518\n */\nconst drawBlockArrow: ElementRenderer<BlockArrowElement> = (shape, target) => {\n const { fill, stroke } = applyStyle(shape.style, target);\n const direction = shape.direction ?? \"right\";\n const headRatio = Math.max(\n ARROWHEAD_RATIO_MIN,\n Math.min(ARROWHEAD_RATIO_MAX, shape.headRatio ?? ARROWHEAD_HEAD_RATIO),\n );\n const bodyT = Math.max(\n ARROWHEAD_RATIO_MIN,\n Math.min(ARROWHEAD_RATIO_MAX, shape.bodyThickness ?? ARROWHEAD_BODY_THICKNESS),\n );\n const w = shape.width;\n const h = shape.height;\n // Compute the local path for a `right`-pointing arrow inside\n // [0, w] \u00D7 [0, h], then rotate the resulting points if the\n // direction is different. Keeps the drawing primitives in one\n // place.\n const headW = w * headRatio;\n const bodyW = w - headW;\n const bodyHalfH = (h * bodyT) / 2;\n const cy = h / 2;\n let points: readonly [number, number][] = [\n [0, cy - bodyHalfH],\n [bodyW, cy - bodyHalfH],\n [bodyW, 0],\n [w, cy],\n [bodyW, h],\n [bodyW, cy + bodyHalfH],\n [0, cy + bodyHalfH],\n ];\n if (direction !== \"right\") {\n points = points.map(([x, y]) => rotateLocal([x, y], direction, w, h));\n }\n const ptObjs = points.map(([x, y]) => ({ x, y }));\n if (fill) {\n target.beginPath();\n polygonPath(target, ptObjs);\n target.fill();\n }\n if (stroke) {\n const offset = strokeAlignOffset(shape.style);\n const sPts = offset !== 0 ? polygonMath.offsetClosedPath(ptObjs, offset) : ptObjs;\n target.beginPath();\n polygonPath(target, sPts);\n target.stroke();\n }\n};\n\nconst rotateLocal = (\n [x, y]: readonly [number, number],\n direction: \"left\" | \"up\" | \"down\",\n w: number,\n h: number,\n): [number, number] => {\n switch (direction) {\n case \"left\":\n return [w - x, y];\n case \"up\":\n // Rotate 90\u00B0 CCW around the box centre, then translate so the\n // result still fits inside [0, w] \u00D7 [0, h].\n return [y * (w / h), h - x * (h / w)];\n case \"down\":\n return [(h - y) * (w / h), x * (h / w)];\n }\n};\n\nconst FRAME_HEADER_ELLIPSIS = \"\u2026\";\n\n/**\n * Trim `text` with a trailing ellipsis until it fits `maxWidth` (in the\n * font already set on `target`). Returns the full text when it fits, the\n * longest prefix + \"\u2026\" otherwise, or just \"\u2026\" when even one char can't\n * fit. Binary-searches the prefix length to keep `measureText` calls ~log.\n */\nconst ellipsizeToWidth = (text: string, maxWidth: number, target: RenderTarget): string => {\n if (maxWidth <= 0) return \"\";\n if (target.measureText(text).width <= maxWidth) return text;\n let lo = 0;\n let hi = text.length;\n while (lo < hi) {\n const mid = Math.ceil((lo + hi) / 2);\n const w = target.measureText(text.slice(0, mid) + FRAME_HEADER_ELLIPSIS).width;\n if (w <= maxWidth) lo = mid;\n else hi = mid - 1;\n }\n return lo > 0 ? text.slice(0, lo) + FRAME_HEADER_ELLIPSIS : FRAME_HEADER_ELLIPSIS;\n};\n\nconst drawFrame: ElementRenderer<FrameElement> = (shape, target, ctx) => {\n // Body \u2014 solid fill + thin solid outline. Frames sit at the bottom\n // z-order, so the fill backs their members without covering them.\n // Honours an explicit `style.fill`, else default white.\n target.setFill(shape.style.fill ?? FRAME_FILL_COLOR);\n target.setStroke(null);\n target.setDashArray(null);\n target.beginPath();\n target.rect(0, 0, shape.width, shape.height);\n target.fill();\n // Outline on top of the fill \u2014 a 1px SCREEN-constant hairline (doesn't scale\n // with zoom). The renderer draws in local coords where 1 unit = zoom \u00D7 scale\n // device px, so divide to keep the stroke at one device pixel. Falls back to\n // 1 world-px when no zoom context is supplied (preview / export at 1:1).\n const screenScale = (ctx?.zoom ?? 1) * (shape.scale.x || 1);\n target.setFill(null);\n target.setStroke(FRAME_STROKE_COLOR);\n target.setStrokeWidth(1 / (screenScale || 1));\n target.setDashArray(null);\n target.beginPath();\n target.rect(0, 0, shape.width, shape.height);\n target.stroke();\n\n // Header label: the strip hugs the text width but never exceeds the\n // frame's right edge; a name too long for the frame is ellipsised.\n const name = shape.name ?? \"Frame\";\n target.setFont(\"system-ui, sans-serif\", FRAME_HEADER_FONT_SIZE);\n const avail = shape.width - FRAME_HEADER_PADDING_X * 2;\n const fits = target.measureText(name).width <= avail;\n // Fits \u2192 the strip hugs the text. Too long \u2192 ellipsise the text and\n // stretch the strip to the frame's full width (the label runs to the\n // right edge).\n const label = fits ? name : ellipsizeToWidth(name, avail, target);\n const headerWidth = fits\n ? Math.min(target.measureText(name).width + FRAME_HEADER_PADDING_X * 2, shape.width)\n : shape.width;\n\n // Header label background \u2014 stretches to fit the (possibly truncated) text.\n target.setFill(FRAME_HEADER_BG_COLOR);\n target.beginPath();\n target.rect(0, -FRAME_HEADER_HEIGHT, headerWidth, FRAME_HEADER_HEIGHT);\n target.fill();\n\n // Header label text.\n target.setFill(FRAME_HEADER_TEXT_COLOR);\n target.setTextBaseline(\"middle\");\n target.setTextAlign(\"left\");\n target.fillText(label, FRAME_HEADER_PADDING_X, -FRAME_HEADER_HEIGHT / 2);\n};\n", "/**\n * The fonts the editor ships and draws with \u2014 Roboto (sans), PT Serif\n * (serif) and Roboto Mono (mono). Bundling them means every render backend\n * (Canvas2D, WebGL2/MSDF, the offscreen worker) measures and draws the same\n * glyphs, instead of WebGL2 using the embedded font while Canvas2D falls back\n * to whatever the OS resolves for the requested family.\n */\n\n/** The three bundled font families. */\nexport const FONT_SANS = \"Roboto\";\nexport const FONT_SERIF = \"PT Serif\";\nexport const FONT_MONO = \"Roboto Mono\";\n\n/**\n * Map a CSS font-family stack to the bundled family that backs it. Mirrors\n * the resolution the WASM shaper uses, so Canvas2D and WebGL2 pick the same\n * face: `mono` wins, then `sans` (so `sans-serif` stays sans), then a\n * serif-ish keyword, else sans.\n */\nexport const resolveBundledFamily = (cssFamily: string): string => {\n const f = cssFamily.toLowerCase();\n if (f.includes(\"mono\")) return FONT_MONO;\n if (f.includes(\"sans\")) return FONT_SANS;\n if (f.includes(\"serif\") || f.includes(\"slab\") || f.includes(\"georgia\") || f.includes(\"times\")) {\n return FONT_SERIF;\n }\n return FONT_SANS;\n};\n\ninterface FaceSpec {\n readonly family: string;\n readonly weight: \"400\" | \"700\";\n readonly style: \"normal\" | \"italic\";\n /** Built with a static `new URL(...)` literal so bundlers emit the asset. */\n readonly url: URL;\n}\n\n// Each `new URL` must be a static literal \u2014 a dynamic path (template string)\n// isn't seen by bundler asset pipelines and would 404.\nconst FACES: readonly FaceSpec[] = [\n {\n family: FONT_SANS,\n weight: \"400\",\n style: \"normal\",\n url: new URL(\"../fonts/Roboto-Regular.woff2\", import.meta.url),\n },\n {\n family: FONT_SANS,\n weight: \"700\",\n style: \"normal\",\n url: new URL(\"../fonts/Roboto-Bold.woff2\", import.meta.url),\n },\n {\n family: FONT_SANS,\n weight: \"400\",\n style: \"italic\",\n url: new URL(\"../fonts/Roboto-Italic.woff2\", import.meta.url),\n },\n {\n family: FONT_SANS,\n weight: \"700\",\n style: \"italic\",\n url: new URL(\"../fonts/Roboto-BoldItalic.woff2\", import.meta.url),\n },\n {\n family: FONT_SERIF,\n weight: \"400\",\n style: \"normal\",\n url: new URL(\"../fonts/PTSerif-Regular.woff2\", import.meta.url),\n },\n {\n family: FONT_SERIF,\n weight: \"700\",\n style: \"normal\",\n url: new URL(\"../fonts/PTSerif-Bold.woff2\", import.meta.url),\n },\n {\n family: FONT_SERIF,\n weight: \"400\",\n style: \"italic\",\n url: new URL(\"../fonts/PTSerif-Italic.woff2\", import.meta.url),\n },\n {\n family: FONT_SERIF,\n weight: \"700\",\n style: \"italic\",\n url: new URL(\"../fonts/PTSerif-BoldItalic.woff2\", import.meta.url),\n },\n {\n family: FONT_MONO,\n weight: \"400\",\n style: \"normal\",\n url: new URL(\"../fonts/RobotoMono-Regular.woff2\", import.meta.url),\n },\n {\n family: FONT_MONO,\n weight: \"700\",\n style: \"normal\",\n url: new URL(\"../fonts/RobotoMono-Bold.woff2\", import.meta.url),\n },\n {\n family: FONT_MONO,\n weight: \"400\",\n style: \"italic\",\n url: new URL(\"../fonts/RobotoMono-Italic.woff2\", import.meta.url),\n },\n {\n family: FONT_MONO,\n weight: \"700\",\n style: \"italic\",\n url: new URL(\"../fonts/RobotoMono-BoldItalic.woff2\", import.meta.url),\n },\n];\n\nexport interface FontScope {\n readonly fonts?: {\n add(font: FontFace): void;\n has(font: FontFace): boolean;\n };\n}\n\n/**\n * Load and register the bundled fonts into a scope's font set \u2014 pass the\n * `window` on the main thread and the worker's `self` inside a render worker\n * (both expose `.fonts`). Idempotent and resolves once every face is ready,\n * so callers can render crisp text after it settles. A no-op where the\n * `FontFace` API is unavailable (older runtimes / SSR).\n */\nexport const registerBundledFonts = async (\n scope: FontScope = globalThis as FontScope,\n): Promise<void> => {\n const set = scope.fonts;\n if (!set || typeof FontFace === \"undefined\") return;\n // `allSettled` so one missing face doesn't block the rest from loading.\n await Promise.allSettled(\n FACES.map(async (f) => {\n const face = new FontFace(f.family, `url(${f.url.href})`, {\n weight: f.weight,\n style: f.style,\n });\n await face.load();\n set.add(face);\n }),\n );\n};\n", "/**\n * Runtime guard: is `value` an actual drawable image source that\n * `ctx.drawImage` / `gl.texImage2D` will accept?\n *\n * Needed because a deserialized scene can carry a **garbage**\n * `metadata.image`: a live `<img>` DOM element serialises to `{}`\n * via `JSON.stringify`, so a scene restored from localStorage has\n * `metadata.image === {}` \u2014 a truthy object that passes a naive\n * `typeof === \"object\"` check but throws inside `drawImage`\n * (\"provided value is not of type \u2026\") / `texImage2D` (\"overload\n * resolution failed\").\n *\n * The check is environment-safe: each constructor is probed for\n * existence first (workers / SSR / older browsers may lack some),\n * so it never throws on a missing global. A bare `{}` matches none\n * of them and is rejected.\n */\nconst DRAWABLE_CTOR_NAMES = [\n \"HTMLImageElement\",\n \"HTMLCanvasElement\",\n \"HTMLVideoElement\",\n \"ImageBitmap\",\n \"OffscreenCanvas\",\n \"SVGImageElement\",\n \"VideoFrame\",\n] as const;\n\nexport const isDrawableImageSource = (value: unknown): value is CanvasImageSource => {\n if (typeof value !== \"object\" || value === null) return false;\n const g = globalThis as Record<string, unknown>;\n for (const name of DRAWABLE_CTOR_NAMES) {\n const ctor = g[name];\n if (\n typeof ctor === \"function\" &&\n value instanceof (ctor as new (...args: never[]) => unknown)\n ) {\n return true;\n }\n }\n return false;\n};\n\n/**\n * Warn (once per distinct kind) when an image draw is skipped because\n * the handle isn't drawable. Throttled by a module-level `Set` so a\n * per-frame render loop doesn't spam the console \u2014 but the host still\n * sees that an image failed to render and the likely cause.\n */\nconst warnedImageKinds = new Set<string>();\n\nexport const warnSkippedImage = (value: unknown): void => {\n if (typeof console === \"undefined\") return;\n const kind =\n typeof value === \"string\"\n ? value.startsWith(\"blob:\")\n ? \"dead-blob-url\"\n : \"string-src\"\n : value === null || value === undefined\n ? \"empty\"\n : \"stale-object\"; // e.g. a {} from a serialised <img>\n if (warnedImageKinds.has(kind)) return;\n warnedImageKinds.add(kind);\n\n console.warn(\n `[renderer] skipped a non-drawable image source (kind: ${kind}). ` +\n \"The shape's image handle isn't a live HTMLImageElement / canvas / \" +\n \"bitmap \u2014 likely a scene restored from storage where the <img> was \" +\n \"lost (metadata.image) and src is a dead blob: URL. The image won't \" +\n \"render until rehydrated from Scene.files.\",\n );\n};\n", "import type { Bounds, Transform } from \"@oh-just-another/types\";\nimport type {\n FillRule,\n LineCap,\n LineJoin,\n RenderTarget,\n TextAlign,\n TextBaseline,\n} from \"@oh-just-another/renderer-core\";\nimport { resolveBundledFamily } from \"@oh-just-another/fonts\";\nimport { isDrawableImageSource, warnSkippedImage } from \"./image-source.js\";\n\n/**\n * Wraps a `CanvasRenderingContext2D` (or compatible OffscreenCanvas context)\n * as a backend-agnostic `RenderTarget`. Coordinates passed to the target are\n * in CSS pixels; the device-pixel scaling is applied once at construction by\n * the device-pixel-ratio (DPR) helper, so all draw calls see CSS units.\n *\n * `size` reports the CSS-pixel size that draw calls operate in. The underlying\n * canvas bitmap may be larger (DPR \u00D7 size) but that is transparent here.\n */\nexport class Canvas2DTarget implements RenderTarget {\n private readonly ctx: CanvasRenderingContext2D;\n private _width: number;\n private _height: number;\n /**\n * Device-pixel-ratio the canvas bitmap is scaled by (see `setupHiDpi`).\n * `setTransform` / `resetTransform` take a transform that maps world \u2192\n * CSS pixels; they pre-multiply by `scale(dpr)` so the result lands in\n * the DPR-scaled device buffer.\n */\n private dpr: number;\n\n /**\n * `width` / `height` are CSS-pixel dimensions. `dpr` must match the value\n * `setupHiDpi` used to scale the bitmap (default 1). The constructor assumes\n * the caller has already configured the canvas bitmap + context transform.\n */\n constructor(ctx: CanvasRenderingContext2D, width: number, height: number, dpr = 1) {\n this.ctx = ctx;\n this._width = width;\n this._height = height;\n this.dpr = dpr;\n }\n\n get size(): { readonly width: number; readonly height: number } {\n return { width: this._width, height: this._height };\n }\n\n /** Mutator for callers that resize the canvas. `dpr` updates the device\n * scale when the canvas moves to a different-density display. */\n resize(width: number, height: number, dpr?: number): void {\n this._width = width;\n this._height = height;\n if (dpr !== undefined) this.dpr = dpr;\n }\n\n // --- Style ---\n\n setFill(color: string | null): void {\n this.ctx.fillStyle = color ?? \"transparent\";\n }\n setStroke(color: string | null): void {\n this.ctx.strokeStyle = color ?? \"transparent\";\n }\n setStrokeWidth(width: number): void {\n this.ctx.lineWidth = width;\n }\n setOpacity(alpha: number): void {\n this.ctx.globalAlpha = alpha;\n }\n setLineCap(cap: LineCap): void {\n this.ctx.lineCap = cap;\n }\n setLineJoin(join: LineJoin): void {\n this.ctx.lineJoin = join;\n }\n setDashArray(dash: readonly number[] | null): void {\n this.ctx.setLineDash(dash ? [...dash] : []);\n }\n setFont(\n fontFamily: string,\n fontSize: number,\n options?: { weight?: \"normal\" | \"bold\"; style?: \"normal\" | \"italic\" },\n ): void {\n // CSS font shorthand order: `<style> <weight> <size> <family>`. Draw with\n // the bundled face that backs the requested family (matching the WebGL2\n // MSDF path), falling back to the original stack until it has loaded.\n const style = options?.style === \"italic\" ? \"italic \" : \"\";\n const weight = options?.weight === \"bold\" ? \"bold \" : \"\";\n this.ctx.font = `${style}${weight}${fontSize}px \"${resolveBundledFamily(fontFamily)}\", ${fontFamily}`;\n }\n setTextAlign(align: TextAlign): void {\n this.ctx.textAlign = align === \"center\" ? \"center\" : align;\n }\n setTextBaseline(baseline: TextBaseline): void {\n this.ctx.textBaseline =\n baseline === \"middle\" ? \"middle\" : baseline === \"top\" ? \"top\" : \"bottom\";\n }\n\n // --- State stack ---\n\n save(): void {\n this.ctx.save();\n }\n restore(): void {\n this.ctx.restore();\n }\n\n // --- Transform ---\n\n translate(dx: number, dy: number): void {\n this.ctx.translate(dx, dy);\n }\n rotate(radians: number): void {\n this.ctx.rotate(radians);\n }\n scale(sx: number, sy: number): void {\n this.ctx.scale(sx, sy);\n }\n setTransform(t: Transform): void {\n // Compose with the DPR base: device = scale(dpr) \u00B7 t. `t` maps world \u2192\n // CSS px; the bitmap is dpr\u00D7 bigger, so every coordinate scales by dpr.\n const d = this.dpr;\n this.ctx.setTransform(d * t.a, d * t.b, d * t.c, d * t.d, d * t.e, d * t.f);\n }\n resetTransform(): void {\n // Reset to the DPR base (NOT raw identity) so CSS-px draws stay scaled.\n this.ctx.setTransform(this.dpr, 0, 0, this.dpr, 0, 0);\n }\n\n // --- Path primitives ---\n\n beginPath(): void {\n this.ctx.beginPath();\n }\n closePath(): void {\n this.ctx.closePath();\n }\n moveTo(x: number, y: number): void {\n this.ctx.moveTo(x, y);\n }\n lineTo(x: number, y: number): void {\n this.ctx.lineTo(x, y);\n }\n quadraticCurveTo(cx: number, cy: number, x: number, y: number): void {\n this.ctx.quadraticCurveTo(cx, cy, x, y);\n }\n bezierCurveTo(c1x: number, c1y: number, c2x: number, c2y: number, x: number, y: number): void {\n this.ctx.bezierCurveTo(c1x, c1y, c2x, c2y, x, y);\n }\n rect(x: number, y: number, width: number, height: number): void {\n this.ctx.rect(x, y, width, height);\n }\n ellipse(cx: number, cy: number, rx: number, ry: number): void {\n this.ctx.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2);\n }\n\n // --- Fill / stroke ---\n\n fill(rule?: FillRule): void {\n this.ctx.fill(rule);\n }\n stroke(): void {\n this.ctx.stroke();\n }\n\n // --- Text ---\n\n fillText(text: string, x: number, y: number, maxWidth?: number): void {\n if (maxWidth !== undefined) this.ctx.fillText(text, x, y, maxWidth);\n else this.ctx.fillText(text, x, y);\n }\n measureText(text: string): { width: number } {\n const m = this.ctx.measureText(text);\n return { width: m.width };\n }\n\n // --- Images ---\n\n drawImage(\n image: unknown,\n dx: number,\n dy: number,\n dw: number,\n dh: number,\n _dynamic?: boolean,\n ): void {\n // `_dynamic` ignored \u2014 Canvas2D reads the source element live on\n // every drawImage, so animated GIF / video frames are picked up\n // automatically as long as the host re-renders.\n void _dynamic;\n // Guard against non-drawable handles. A restored scene carries\n // either a string `src` (dead blob: URL) OR a `metadata.image`\n // that serialised to `{}` (a live `<img>` becomes an empty object\n // through JSON). Both throw inside `ctx.drawImage`. Skip rather\n // than crash the whole render pass, and surface it once so hosts\n // know an image didn't render (and why).\n if (!isDrawableImageSource(image)) {\n warnSkippedImage(image);\n return;\n }\n this.ctx.drawImage(image, dx, dy, dw, dh);\n }\n\n // --- Surface control ---\n\n clear(bounds?: Bounds): void {\n // A `clear()` always opens a fresh dirty pass \u2014 the host took\n // responsibility for the cleared region, anything we accumulate\n // from here is the new frame's coverage.\n this.dirtyRect = null;\n if (bounds) {\n this.ctx.clearRect(bounds.x, bounds.y, bounds.width, bounds.height);\n } else {\n // Clear the entire CSS-space area, regardless of current transform.\n this.ctx.save();\n this.ctx.setTransform(1, 0, 0, 1, 0, 0);\n this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);\n this.ctx.restore();\n }\n }\n\n // --- Per-pass dirty accumulator ---\n\n /**\n * Screen-space union of every `markDirty(bounds)` call since the\n * last `clear()`. Hosts can read it via `getDirtyRect()` to size\n * the next clear precisely \u2014 covers anti-aliased stroke fuzz and\n * shape renderers that paint a few px beyond their geometric bbox.\n */\n private dirtyRect: Bounds | null = null;\n\n markDirty(bounds: Bounds): void {\n if (!this.dirtyRect) {\n this.dirtyRect = bounds;\n return;\n }\n const minX = Math.min(this.dirtyRect.x, bounds.x);\n const minY = Math.min(this.dirtyRect.y, bounds.y);\n const maxX = Math.max(this.dirtyRect.x + this.dirtyRect.width, bounds.x + bounds.width);\n const maxY = Math.max(this.dirtyRect.y + this.dirtyRect.height, bounds.y + bounds.height);\n this.dirtyRect = { x: minX, y: minY, width: maxX - minX, height: maxY - minY };\n }\n\n /** Read the accumulated dirty rect for this pass. `null` when nothing painted. */\n getDirtyRect(): Bounds | null {\n return this.dirtyRect;\n }\n}\n", "import type { Bounds, Color, Transform } from \"@oh-just-another/types\";\nimport {\n LruCache,\n type FillRule,\n type LineCap,\n type LineJoin,\n type RenderTarget,\n type TextAlign,\n type TextBaseline,\n} from \"@oh-just-another/renderer-core\";\nimport { resolveBundledFamily } from \"@oh-just-another/fonts\";\nimport { OFFSCREEN_IMAGE_CACHE_CAP } from \"./constants.js\";\n\n/**\n * Backend-agnostic RenderTarget that captures every method call as a\n * structured command. Powers the \"offscreen\" pipeline: the main thread\n * renders into a RecordingTarget, the resulting buffer is shipped to a\n * worker via postMessage, and the worker replays the commands onto its\n * OffscreenCanvas.\n *\n * Commands are a tagged union of primitive payloads (no class\n * instances) so they survive `structuredClone` cleanly across the\n * worker boundary. `flush()` returns the buffered commands and resets\n * the internal log.\n */\n\nexport type RenderCommand =\n | { readonly k: \"setFill\"; readonly color: Color | null }\n | { readonly k: \"setStroke\"; readonly color: Color | null }\n | { readonly k: \"setStrokeWidth\"; readonly w: number }\n | { readonly k: \"setOpacity\"; readonly a: number }\n | { readonly k: \"setLineCap\"; readonly cap: LineCap }\n | { readonly k: \"setLineJoin\"; readonly join: LineJoin }\n | { readonly k: \"setDashArray\"; readonly dash: readonly number[] | null }\n | {\n readonly k: \"setFont\";\n readonly family: string;\n readonly size: number;\n readonly options?: {\n readonly weight?: \"normal\" | \"bold\";\n readonly style?: \"normal\" | \"italic\";\n };\n }\n | { readonly k: \"setTextAlign\"; readonly align: TextAlign }\n | { readonly k: \"setTextBaseline\"; readonly baseline: TextBaseline }\n | { readonly k: \"save\" }\n | { readonly k: \"restore\" }\n | { readonly k: \"translate\"; readonly x: number; readonly y: number }\n | { readonly k: \"rotate\"; readonly r: number }\n | { readonly k: \"scale\"; readonly sx: number; readonly sy: number }\n | { readonly k: \"setTransform\"; readonly t: Transform }\n | { readonly k: \"resetTransform\" }\n | { readonly k: \"beginPath\" }\n | { readonly k: \"closePath\" }\n | { readonly k: \"moveTo\"; readonly x: number; readonly y: number }\n | { readonly k: \"lineTo\"; readonly x: number; readonly y: number }\n | {\n readonly k: \"quadraticCurveTo\";\n readonly cx: number;\n readonly cy: number;\n readonly x: number;\n readonly y: number;\n }\n | {\n readonly k: \"bezierCurveTo\";\n readonly c1x: number;\n readonly c1y: number;\n readonly c2x: number;\n readonly c2y: number;\n readonly x: number;\n readonly y: number;\n }\n | {\n readonly k: \"rect\";\n readonly x: number;\n readonly y: number;\n readonly w: number;\n readonly h: number;\n }\n | {\n readonly k: \"ellipse\";\n readonly cx: number;\n readonly cy: number;\n readonly rx: number;\n readonly ry: number;\n }\n | { readonly k: \"fill\"; readonly rule?: FillRule }\n | { readonly k: \"stroke\" }\n | {\n readonly k: \"fillText\";\n readonly text: string;\n readonly x: number;\n readonly y: number;\n readonly maxWidth?: number;\n }\n | { readonly k: \"clear\"; readonly bounds?: Bounds }\n | { readonly k: \"markDirty\"; readonly bounds: Bounds }\n | { readonly k: \"resize\"; readonly w: number; readonly h: number }\n | { readonly k: \"defineImage\"; readonly id: number; readonly bitmap: ImageBitmap }\n | {\n readonly k: \"drawImage\";\n readonly id: number;\n readonly dx: number;\n readonly dy: number;\n readonly dw: number;\n readonly dh: number;\n };\n\n/**\n * `drawImage` records `ImageBitmap` sources \u2014 they survive the\n * postMessage boundary and are replayed onto the worker's canvas. Other\n * source types (HTMLImageElement, HTMLCanvasElement) are skipped and\n * counted in `skippedImageDraws` so the host UI can warn (or fall back\n * to main-thread compositing for image-heavy scenes).\n *\n * To avoid re-shipping the same bitmap every frame (the offscreen path\n * re-renders each frame so GIF / video advance), bitmaps are interned by\n * identity to a stable numeric id: the first draw emits a `defineImage`\n * carrying the bitmap, later draws of the same bitmap emit only a tiny\n * `drawImage` referencing the id. An LRU bounds the live set; the worker\n * mirrors the same-capacity LRU, so both evict the same id in lockstep.\n *\n * `measureText` measures on a hidden 2D context with the active font, so\n * caret / selection geometry lines up with the text the worker draws \u2014\n * without routing each call across the worker boundary.\n */\nexport class RecordingTarget implements RenderTarget {\n private commands: RenderCommand[] = [];\n private _width: number;\n private _height: number;\n /** Counter so hosts can warn when images are silently skipped. */\n skippedImageDraws = 0;\n /** Current font as a CSS shorthand, mirrored from `setFont` for `measureText`. */\n private fontSpec = \"10px sans-serif\";\n /** Hidden 2D context used to measure text with the active font. */\n private measureCtx: CanvasRenderingContext2D | null = null;\n /**\n * Identity \u2192 id intern table for shipped bitmaps. Persists across\n * `flush()` (the worker keeps its mirror across replays); bounded by an\n * LRU of {@link OFFSCREEN_IMAGE_CACHE_CAP} so a long animation can't grow\n * it without bound. Ids are monotonic and never reused, so an evicted\n * bitmap that reappears is simply re-defined under a fresh id.\n */\n private readonly imageIds = new LruCache<ImageBitmap, number>(OFFSCREEN_IMAGE_CACHE_CAP);\n private nextImageId = 0;\n\n constructor(width: number, height: number) {\n this._width = width;\n this._height = height;\n }\n\n get size(): { readonly width: number; readonly height: number } {\n return { width: this._width, height: this._height };\n }\n\n resize(width: number, height: number): void {\n this._width = width;\n this._height = height;\n this.commands.push({ k: \"resize\", w: width, h: height });\n }\n\n /** Pop the buffered commands and clear the internal log. */\n flush(): readonly RenderCommand[] {\n const out = this.commands;\n this.commands = [];\n return out;\n }\n\n /** Snapshot of the buffered commands without clearing. */\n peek(): readonly RenderCommand[] {\n return this.commands;\n }\n\n setFill(color: Color | null): void {\n this.commands.push({ k: \"setFill\", color });\n }\n setStroke(color: Color | null): void {\n this.commands.push({ k: \"setStroke\", color });\n }\n setStrokeWidth(w: number): void {\n this.commands.push({ k: \"setStrokeWidth\", w });\n }\n setOpacity(a: number): void {\n this.commands.push({ k: \"setOpacity\", a });\n }\n setLineCap(cap: LineCap): void {\n this.commands.push({ k: \"setLineCap\", cap });\n }\n setLineJoin(join: LineJoin): void {\n this.commands.push({ k: \"setLineJoin\", join });\n }\n setDashArray(dash: readonly number[] | null): void {\n this.commands.push({ k: \"setDashArray\", dash });\n }\n setFont(\n family: string,\n size: number,\n options?: { weight?: \"normal\" | \"bold\"; style?: \"normal\" | \"italic\" },\n ): void {\n this.commands.push({ k: \"setFont\", family, size, ...(options ? { options } : {}) });\n // CSS font shorthand order: `<style> <weight> <size> <family>` \u2014 same as\n // the worker's Canvas2D target (bundled face first), so `measureText`\n // matches what it draws.\n const style = options?.style === \"italic\" ? \"italic \" : \"\";\n const weight = options?.weight === \"bold\" ? \"bold \" : \"\";\n this.fontSpec = `${style}${weight}${size}px \"${resolveBundledFamily(family)}\", ${family}`;\n }\n setTextAlign(align: TextAlign): void {\n this.commands.push({ k: \"setTextAlign\", align });\n }\n setTextBaseline(baseline: TextBaseline): void {\n this.commands.push({ k: \"setTextBaseline\", baseline });\n }\n\n save(): void {\n this.commands.push({ k: \"save\" });\n }\n restore(): void {\n this.commands.push({ k: \"restore\" });\n }\n\n translate(x: number, y: number): void {\n this.commands.push({ k: \"translate\", x, y });\n }\n rotate(r: number): void {\n this.commands.push({ k: \"rotate\", r });\n }\n scale(sx: number, sy: number): void {\n this.commands.push({ k: \"scale\", sx, sy });\n }\n setTransform(t: Transform): void {\n this.commands.push({ k: \"setTransform\", t });\n }\n resetTransform(): void {\n this.commands.push({ k: \"resetTransform\" });\n }\n\n beginPath(): void {\n this.commands.push({ k: \"beginPath\" });\n }\n closePath(): void {\n this.commands.push({ k: \"closePath\" });\n }\n moveTo(x: number, y: number): void {\n this.commands.push({ k: \"moveTo\", x, y });\n }\n lineTo(x: number, y: number): void {\n this.commands.push({ k: \"lineTo\", x, y });\n }\n quadraticCurveTo(cx: number, cy: number, x: number, y: number): void {\n this.commands.push({ k: \"quadraticCurveTo\", cx, cy, x, y });\n }\n bezierCurveTo(c1x: number, c1y: number, c2x: number, c2y: number, x: number, y: number): void {\n this.commands.push({ k: \"bezierCurveTo\", c1x, c1y, c2x, c2y, x, y });\n }\n rect(x: number, y: number, w: number, h: number): void {\n this.commands.push({ k: \"rect\", x, y, w, h });\n }\n ellipse(cx: number, cy: number, rx: number, ry: number): void {\n this.commands.push({ k: \"ellipse\", cx, cy, rx, ry });\n }\n\n fill(rule?: FillRule): void {\n this.commands.push(rule !== undefined ? { k: \"fill\", rule } : { k: \"fill\" });\n }\n stroke(): void {\n this.commands.push({ k: \"stroke\" });\n }\n\n fillText(text: string, x: number, y: number, maxWidth?: number): void {\n this.commands.push(\n maxWidth !== undefined\n ? { k: \"fillText\", text, x, y, maxWidth }\n : { k: \"fillText\", text, x, y },\n );\n }\n measureText(text: string): { width: number } {\n // Measure on a hidden 2D context with the active font, matching the\n // worker's Canvas2D target \u2014 so caret / selection geometry on the\n // offscreen backend lines up with the drawn glyphs. Falls back to a\n // proportional estimate where `OffscreenCanvas` is unavailable.\n const ctx = this.ensureMeasureCtx();\n if (!ctx) return { width: text.length * (Number.parseFloat(this.fontSpec) || 8) * 0.5 };\n ctx.font = this.fontSpec;\n return { width: ctx.measureText(text).width };\n }\n\n private ensureMeasureCtx(): CanvasRenderingContext2D | null {\n if (this.measureCtx) return this.measureCtx;\n if (typeof OffscreenCanvas === \"undefined\") return null;\n const ctx = new OffscreenCanvas(1, 1).getContext(\"2d\");\n // OffscreenCanvasRenderingContext2D is structurally compatible with the\n // `font` / `measureText` members used here.\n this.measureCtx = (ctx as unknown as CanvasRenderingContext2D | null) ?? null;\n return this.measureCtx;\n }\n\n drawImage(\n image: unknown,\n dx: number,\n dy: number,\n dw: number,\n dh: number,\n _dynamic?: boolean,\n ): void {\n void _dynamic;\n if (typeof ImageBitmap !== \"undefined\" && image instanceof ImageBitmap) {\n let id = this.imageIds.get(image);\n if (id === undefined) {\n // First sight of this bitmap (or it was evicted): assign a fresh\n // id and ship the pixels once via `defineImage`.\n id = this.nextImageId++;\n this.imageIds.set(image, id);\n this.commands.push({ k: \"defineImage\", id, bitmap: image });\n }\n this.commands.push({ k: \"drawImage\", id, dx, dy, dw, dh });\n } else {\n this.skippedImageDraws++;\n }\n }\n\n clear(bounds?: Bounds): void {\n this.commands.push(bounds !== undefined ? { k: \"clear\", bounds } : { k: \"clear\" });\n }\n\n markDirty(bounds: Bounds): void {\n this.commands.push({ k: \"markDirty\", bounds });\n }\n}\n\n/**\n * Replay a previously-flushed command buffer onto a real RenderTarget.\n * Used by the worker entry point to apply commands shipped from the\n * main thread.\n *\n * `images` holds the bitmaps interned by {@link RecordingTarget}: a\n * `defineImage` stores one under its id, a `drawImage` looks it up. The\n * caller (worker) owns this cache so it survives across replays and\n * mirrors the recorder's same-capacity LRU. When omitted (callers with\n * no image commands) a throwaway cache is used.\n */\nexport const replayCommands = (\n target: RenderTarget,\n commands: readonly RenderCommand[],\n images: LruCache<number, ImageBitmap> = new LruCache(OFFSCREEN_IMAGE_CACHE_CAP),\n): void => {\n for (const cmd of commands) {\n switch (cmd.k) {\n case \"setFill\":\n target.setFill(cmd.color);\n break;\n case \"setStroke\":\n target.setStroke(cmd.color);\n break;\n case \"setStrokeWidth\":\n target.setStrokeWidth(cmd.w);\n break;\n case \"setOpacity\":\n target.setOpacity(cmd.a);\n break;\n case \"setLineCap\":\n target.setLineCap(cmd.cap);\n break;\n case \"setLineJoin\":\n target.setLineJoin(cmd.join);\n break;\n case \"setDashArray\":\n target.setDashArray(cmd.dash);\n break;\n case \"setFont\":\n target.setFont(cmd.family, cmd.size, cmd.options);\n break;\n case \"setTextAlign\":\n target.setTextAlign(cmd.align);\n break;\n case \"setTextBaseline\":\n target.setTextBaseline(cmd.baseline);\n break;\n case \"save\":\n target.save();\n break;\n case \"restore\":\n target.restore();\n break;\n case \"translate\":\n target.translate(cmd.x, cmd.y);\n break;\n case \"rotate\":\n target.rotate(cmd.r);\n break;\n case \"scale\":\n target.scale(cmd.sx, cmd.sy);\n break;\n case \"setTransform\":\n target.setTransform(cmd.t);\n break;\n case \"resetTransform\":\n target.resetTransform();\n break;\n case \"beginPath\":\n target.beginPath();\n break;\n case \"closePath\":\n target.closePath();\n break;\n case \"moveTo\":\n target.moveTo(cmd.x, cmd.y);\n break;\n case \"lineTo\":\n target.lineTo(cmd.x, cmd.y);\n break;\n case \"quadraticCurveTo\":\n target.quadraticCurveTo(cmd.cx, cmd.cy, cmd.x, cmd.y);\n break;\n case \"bezierCurveTo\":\n target.bezierCurveTo(cmd.c1x, cmd.c1y, cmd.c2x, cmd.c2y, cmd.x, cmd.y);\n break;\n case \"rect\":\n target.rect(cmd.x, cmd.y, cmd.w, cmd.h);\n break;\n case \"ellipse\":\n target.ellipse(cmd.cx, cmd.cy, cmd.rx, cmd.ry);\n break;\n case \"fill\":\n if (cmd.rule !== undefined) target.fill(cmd.rule);\n else target.fill();\n break;\n case \"stroke\":\n target.stroke();\n break;\n case \"fillText\":\n if (cmd.maxWidth !== undefined) target.fillText(cmd.text, cmd.x, cmd.y, cmd.maxWidth);\n else target.fillText(cmd.text, cmd.x, cmd.y);\n break;\n case \"clear\":\n if (cmd.bounds !== undefined) target.clear(cmd.bounds);\n else target.clear();\n break;\n case \"markDirty\":\n target.markDirty?.(cmd.bounds);\n break;\n case \"resize\":\n // No-op for replay \u2014 the worker owns the canvas size and\n // resizes via its own `resize` message, not via the command\n // stream.\n break;\n case \"defineImage\":\n images.set(cmd.id, cmd.bitmap);\n break;\n case \"drawImage\": {\n // `get` bumps recency so the worker LRU evicts in lockstep with\n // the recorder's. A miss means an out-of-sync stream \u2014 skip\n // rather than throw (matches the non-drawable skip on record).\n const bitmap = images.get(cmd.id);\n if (bitmap) target.drawImage(bitmap, cmd.dx, cmd.dy, cmd.dw, cmd.dh);\n break;\n }\n }\n }\n};\n", "/// <reference lib=\"webworker\" />\nimport { LruCache, installBuiltinRenderers, renderScene } from \"@oh-just-another/renderer-core\";\nimport type { Scene } from \"@oh-just-another/scene\";\nimport type { WorkerRenderMessage, WorkerRenderResponse } from \"@oh-just-another/renderer-core\";\nimport { registerBundledFonts, type FontScope } from \"@oh-just-another/fonts\";\nimport { Canvas2DTarget } from \"./canvas-target.js\";\nimport { replayCommands, type RenderCommand } from \"./recording-target.js\";\nimport { OFFSCREEN_IMAGE_CACHE_CAP } from \"./constants.js\";\n\n/**\n * OffscreenCanvas render worker.\n *\n * Hosts spawn this with `new Worker(new URL(\"./render-worker.ts\",\n * import.meta.url), { type: \"module\" })`. The main thread transfers a\n * canvas via `transferCanvasToWorker(canvas, worker, { width, height,\n * dpr })`, then posts `snapshot` messages with full scenes.\n *\n * One worker owns one OffscreenCanvas \u2014 typically one per layer in a\n * `LayerWorkerPool`, so layers can be rasterised in parallel and then\n * composited on the main thread.\n */\n\ninterface WorkerState {\n canvas: OffscreenCanvas | null;\n target: Canvas2DTarget | null;\n dpr: number;\n /**\n * Bitmaps shipped by the main thread's RecordingTarget, keyed by the\n * id it assigned. Persists across `replay` messages and mirrors the\n * recorder's same-capacity LRU. Evicted clones are closed to release\n * their memory promptly (these are worker-owned copies, distinct from\n * the main thread's source bitmaps).\n */\n readonly images: LruCache<number, ImageBitmap>;\n}\n\nconst state: WorkerState = {\n canvas: null,\n target: null,\n dpr: 1,\n images: new LruCache<number, ImageBitmap>(OFFSCREEN_IMAGE_CACHE_CAP, (_id, bitmap) => {\n bitmap.close();\n }),\n};\n\nlet renderersInstalled = false;\n\nconst ensureRenderers = (): void => {\n if (renderersInstalled) return;\n installBuiltinRenderers();\n renderersInstalled = true;\n};\n\nconst post = (msg: WorkerRenderResponse, transfer?: Transferable[]): void => {\n if (transfer && transfer.length > 0) {\n (self as unknown as DedicatedWorkerGlobalScope).postMessage(msg, transfer);\n } else {\n (self as unknown as DedicatedWorkerGlobalScope).postMessage(msg);\n }\n};\n\nconst init = (canvas: OffscreenCanvas, width: number, height: number, dpr: number): void => {\n // Load the bundled fonts into the worker's font set so its Canvas2D target\n // draws the same faces as the main thread. Fire-and-forget \u2014 replays after\n // it resolves pick up the loaded fonts.\n void registerBundledFonts(self as unknown as FontScope);\n state.canvas = canvas;\n state.dpr = dpr;\n // Resize the bitmap to dpr-scaled pixels \u2014 the host's CSS size is\n // (width, height); render into the bigger buffer and let the\n // composite step downsample as needed.\n canvas.width = Math.max(1, Math.round(width * dpr));\n canvas.height = Math.max(1, Math.round(height * dpr));\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) throw new Error(\"OffscreenCanvas 2D context unavailable\");\n ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n state.target = new Canvas2DTarget(ctx as unknown as CanvasRenderingContext2D, width, height, dpr);\n post({ type: \"ready\" });\n};\n\nconst resize = (width: number, height: number): void => {\n if (!state.canvas || !state.target) return;\n state.canvas.width = Math.max(1, Math.round(width * state.dpr));\n state.canvas.height = Math.max(1, Math.round(height * state.dpr));\n const ctx = state.canvas.getContext(\"2d\");\n if (!ctx) return;\n ctx.setTransform(state.dpr, 0, 0, state.dpr, 0, 0);\n state.target.resize(width, height, state.dpr);\n};\n\nconst snapshot = (scene: Scene): void => {\n if (!state.canvas || !state.target) {\n post({ type: \"error\", message: \"Worker not initialised\" });\n return;\n }\n ensureRenderers();\n const ctx = state.canvas.getContext(\"2d\");\n if (ctx === null) {\n post({ type: \"error\", message: \"Worker not initialised\" });\n return;\n }\n ctx.save();\n ctx.setTransform(state.dpr, 0, 0, state.dpr, 0, 0);\n ctx.clearRect(0, 0, state.target.size.width, state.target.size.height);\n renderScene(scene, state.target);\n ctx.restore();\n const bitmap = state.canvas.transferToImageBitmap();\n post({ type: \"frame-done\", bitmap }, [bitmap]);\n};\n\n/**\n * Replay a serialised RecordingTarget command stream onto the owned\n * OffscreenCanvas. Used by the LayeredSurface \"offscreen\" backend: the\n * main thread captures every RenderTarget call into a buffer and ships\n * it here per frame; the worker replays.\n */\nconst replay = (commands: readonly RenderCommand[]): void => {\n if (!state.target) {\n post({ type: \"error\", message: \"Worker not initialised\" });\n return;\n }\n replayCommands(state.target, commands, state.images);\n};\n\ninterface ReplayMessage {\n readonly type: \"replay\";\n readonly commands: readonly RenderCommand[];\n}\ntype InboundMessage = WorkerRenderMessage | ReplayMessage;\n\n(self as unknown as DedicatedWorkerGlobalScope).addEventListener(\n \"message\",\n (ev: MessageEvent<InboundMessage>) => {\n const msg = ev.data;\n try {\n switch (msg.type) {\n case \"init\":\n init(msg.canvas as OffscreenCanvas, msg.width, msg.height, msg.dpr);\n break;\n case \"resize\":\n resize(msg.width, msg.height);\n break;\n case \"snapshot\":\n // A dpr update only takes effect on the next resize; snapshot\n // honours whatever transform init established.\n snapshot(msg.scene);\n break;\n case \"replay\":\n replay(msg.commands);\n break;\n case \"frame\":\n // Patch-stream frames are not implemented; the protocol is\n // reserved. Reply with an error so callers don't hang on the\n // awaited response.\n post({ type: \"error\", message: \"patch-stream frames not implemented\" });\n break;\n }\n } catch (err) {\n post({\n type: \"error\",\n message: err instanceof Error ? err.message : String(err),\n });\n }\n },\n);\n"],
5
+ "mappings": "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",
6
+ "names": ["registry", "registerElementRenderer", "type", "renderer", "getElementRenderer", "getCornerRadius", "roundness", "width", "height", "smaller", "cutoff", "req", "v", "matrix_exports", "__export", "IDENTITY", "applyToBounds", "applyToPoint", "decompose", "equals", "inverse", "multiply", "of", "rotation", "scaling", "translation", "a", "b", "c", "d", "e", "f", "tx", "ty", "sx", "sy", "radians", "cos", "sin", "det", "p", "p1", "p2", "p3", "p4", "minX", "minY", "maxX", "maxY", "sySigned", "epsilon", "fields", "k", "bounds_exports", "__export", "EMPTY", "centerOf", "contains", "containsBounds", "equals", "expand", "fromCenter", "fromPoints", "intersection", "intersects", "isEmpty", "maxX", "maxY", "normalize", "of", "union", "x", "y", "width", "height", "points", "minX", "minY", "maxXv", "maxYv", "p", "center", "b", "a", "xMax", "yMax", "point", "outer", "inner", "padding", "epsilon", "polygon_exports", "__export", "offsetClosedPath", "signedArea", "offsetClosedPath", "points", "distance", "p", "cx", "cy", "n", "nx", "ny", "i", "a", "req", "b", "dx", "dy", "len", "out", "prev", "n1x", "n1y", "n2x", "n2y", "bx", "by", "blen", "vertex", "towardCx", "towardCy", "sign", "cos", "miterLen", "signedArea", "s", "activeMeasurer", "getTextMeasurer", "activeMeasurer", "isText", "s", "bounderRegistry", "registerBounder", "type", "bounder", "getElementLocalBounds", "shape", "bounder", "bounderRegistry", "getElementWorldBounds", "local", "corners", "sin", "cos", "transformed", "p", "sx", "sy", "bounds_exports", "registerBounder", "s", "points", "cursor", "cmd", "lineHeight", "paragraphs", "measurer", "getTextMeasurer", "opts", "measureLine", "line", "w", "width", "lines", "minX", "minY", "maxX", "maxY", "byOrderAsc", "a", "b", "getLayersInOrder", "scene", "byOrderAsc", "getElementsInLayer", "layerId", "s", "providers", "registerRenderOverflow", "type", "fn", "DEFAULT_VIEWPORT", "getWorldToScreen", "viewport", "translate", "matrix_exports", "rotate", "scale", "ElementCache", "shape", "entry", "value", "compute", "cached", "id", "scene", "sharedBoundsCache", "cachedWorldBounds", "cache", "getElementWorldBounds", "DEFAULT_PLACEHOLDER_FILL", "TEXT_DECORATION_THICKNESS", "TEXT_UNDERLINE_OFFSET", "TEXT_STRIKETHROUGH_OFFSET", "ARROWHEAD_HEAD_RATIO", "ARROWHEAD_BODY_THICKNESS", "ARROWHEAD_RATIO_MIN", "ARROWHEAD_RATIO_MAX", "FRAME_STROKE_COLOR", "FRAME_FILL_COLOR", "FRAME_HEADER_BG_COLOR", "FRAME_HEADER_TEXT_COLOR", "LruCache", "cap", "onEvict", "key", "v", "value", "oldest", "ev", "zoomBucket", "zoom", "renderScene", "scene", "target", "options", "w2s", "getWorldToScreen", "dirtyWorld", "corners", "matrix_exports", "screen", "bounds_exports", "boundsCache", "ElementCache", "viewport", "candidates", "zoom", "lod", "usePlaceholder", "dropText", "placeholderFill", "DEFAULT_PLACEHOLDER_FILL", "layerCache", "dirtyLayers", "compositeLayerBitmap", "zoomBucket", "layerBoundsFor", "layerId", "acc", "shape", "getElementsInLayer", "bb", "cachedWorldBounds", "layer", "getLayersInOrder", "bitmap", "fresh", "isText", "renderer", "getElementRenderer", "DEFAULT_LINE_HEIGHT_FACTOR", "wrapParagraph", "para", "base", "maxWidth", "measure", "out", "words", "re", "m", "lineStart", "push", "start", "end", "text", "i", "w", "req", "e", "layoutText", "options", "lineHeight", "lines", "paraStart", "widest", "l", "blockWidth", "registry", "animationClock", "resolveImageSource", "shape", "timestampMs", "animationClock", "adapter", "registry", "applyStyle", "style", "target", "hasFill", "hasStroke", "drawRectangle", "shape", "fill", "stroke", "getCornerRadius", "buildRoundedRectPath", "offset", "strokeAlignOffset", "sx", "sy", "sw", "sh", "sr", "align", "half", "x", "y", "w", "h", "r", "drawEllipse", "rx", "ry", "srx", "sry", "drawPolygon", "polygonPath", "pts", "polygon_exports", "first", "i", "p", "drawPath", "cmd", "drawText", "weight", "fontStyle", "color", "fontSize", "lines", "measure", "s", "layout", "layoutText", "line", "l", "deco", "thickness", "TEXT_DECORATION_THICKNESS", "TEXT_UNDERLINE_OFFSET", "TEXT_STRIKETHROUGH_OFFSET", "drawBrush", "req", "a", "b", "dx", "dy", "len", "nx", "ny", "drawImage", "handle", "resolveImageSource", "dynamic", "installBuiltinRenderers", "registerElementRenderer", "drawFrame", "registerRenderOverflow", "drawBlockArrow", "direction", "headRatio", "ARROWHEAD_RATIO_MIN", "ARROWHEAD_RATIO_MAX", "ARROWHEAD_HEAD_RATIO", "bodyT", "ARROWHEAD_BODY_THICKNESS", "headW", "bodyW", "bodyHalfH", "cy", "points", "rotateLocal", "ptObjs", "sPts", "FRAME_HEADER_ELLIPSIS", "ellipsizeToWidth", "text", "maxWidth", "lo", "hi", "mid", "ctx", "FRAME_FILL_COLOR", "screenScale", "FRAME_STROKE_COLOR", "name", "avail", "fits", "label", "headerWidth", "FRAME_HEADER_BG_COLOR", "FRAME_HEADER_TEXT_COLOR", "FONT_SANS", "FONT_SERIF", "FONT_MONO", "resolveBundledFamily", "cssFamily", "f", "FACES", "registerBundledFonts", "scope", "set", "face", "DRAWABLE_CTOR_NAMES", "isDrawableImageSource", "value", "g", "name", "ctor", "warnedImageKinds", "warnSkippedImage", "kind", "Canvas2DTarget", "ctx", "width", "height", "dpr", "color", "alpha", "cap", "join", "dash", "fontFamily", "fontSize", "options", "style", "weight", "resolveBundledFamily", "align", "baseline", "dx", "dy", "radians", "sx", "sy", "t", "d", "x", "y", "cx", "cy", "c1x", "c1y", "c2x", "c2y", "rx", "ry", "rule", "text", "maxWidth", "image", "dw", "dh", "_dynamic", "isDrawableImageSource", "warnSkippedImage", "bounds", "minX", "minY", "maxX", "maxY", "replayCommands", "target", "commands", "images", "LruCache", "cmd", "bitmap", "state", "LruCache", "_id", "bitmap", "renderersInstalled", "ensureRenderers", "installBuiltinRenderers", "post", "msg", "transfer", "init", "canvas", "width", "height", "dpr", "registerBundledFonts", "ctx", "Canvas2DTarget", "resize", "snapshot", "scene", "renderScene", "replay", "commands", "replayCommands", "ev", "err"]
7
+ }